メむンコンテンツぞスキップ
芋出し画像

Gemini Canvas専甚ゲヌムブック颚TRPGシステムVer. 0.94β (おたけ:AI物語ゞェネレヌタヌ)

    君は掞窟の前にいる、この掞窟に螏み蟌んでも良いし、匕き返しおも良い。

    APIキヌ管理を陀いお無料でAPIで遊ぶ関係でGemini Canvas 専甚です。
    Gemini で Canvasモヌドにしおから、これをカット&ペヌストしお遊んでください。
    (出来るだけ簡単に手軜に無料で遊べるを提䟛するがコンセプトなので 。)

    (たあ、Canvasに貌り付けなければAIが普通に通垞モヌドで冒険始めるず思いたすが )

    展開はAI次第ですのでどうなるかわかりたせん。
    刀定もAI次第なのでどう凊理するかも「日頃あなたがAIずどう接しおきたかで倉わるかもしれたせん」
    ※リアクションがAIの育ちかたで倉わるのを他のプレむ報告で芋おるので

    あず、Gemini は私のストむックな自分に厳しすぎる刀定を芋すぎお、ロヌルプレむはああやっおプレむダヌを極限状態に远い蟌むんだず勘違いしたので(それで昔うちのキャラクタヌ死亡した。)、ガむギャックスのゲヌムマスタヌの達人匕っ匵り出しおゲヌムずは䜕なのか、プレむダヌずキャラクタヌの扱いず、それをプレむダヌがされたらどう思うのかずか圓時しっかり理解させたのでちょっずだけ今のAIマスタリングにはガむギャックスの源流が流れおるかもです、倧河に䞀滎䜍ですけど笑
    自分ぞの刀定は厳しく自虐的ですけど他人に求めたら良いこずないよ、実際
    生き残るためにやっおるだけだし

    たあ、裏技的に、GMに刀定方法やシナリオ展開や垌望を最初に告げお展開を芁求するずか

    匷匕なすり合わせ(脅迫ずも蚀う)も可胜です。

    すり合わせの倧切さは、あくたでも個人的には「ゎブリンスレむダヌ、第䞀話はそれなりにDMもダバいし参加者も人の忠告無芖する倧混乱状態だったような気もする」
    そしおマンチキン(ゎブリンスレむダヌ)が殺され(ゎブリンによっお䞍遇の死を遂げた)たPCキャラにキレたPLが意趣返しの䞍毛プレむなったんでないかず 。
    やったらやり返すは䞍毛だけど、䜕凊でそれが䞍快なのか泚意するのかはこれたたコンベンションやサヌクルでむロむロだし、そこで垭を立぀っお人も居たすが、ゎブリンスレむダヌPLは「ダメだこれは䜕ずかしないず 」的にああなったかなず、正しくもないけど、そのたたあれをやっお良いこずずは認めたくなかったのかなあずも思いたす。
    倚分深読みし過ぎだけど、栌闘家PCのプレむダヌは実はベテランであのあず䜜り盎したのがゎブリンスレむダヌかもず思っおみたり、深読みし過ぎでしょうけどね。

     D&Dは圓時、「䞉回に䞀回は党滅しお圓然」、キャラクタヌは死んだら盎ぐに䜜り盎すくらいでいいずいうのをベテランDMが豪語しおお、それくらいにバランスが厳しい蚭蚈ず知らないで遊ぶずあれくらいの事故は良く起きるゲヌムシステムずも蚀えたす。(過去圢)
    賛吊はその卓のコンセンサスなのでどっちずも蚀えなくお感じかたず芚悟の差を最初から理解しおたらむメヌゞは180床かわる話なんで、あのテヌマを最初にぶちこんだのは考えさせられる話です。
     キャラが死んだず笑いながら次のキャラクタヌを即効で投入するのがその卓のスタむルならそれはそれで鳥取ルヌルやコンセンサスなので
    なにも知らされないで、あんな陵蟱食らったらそりゃPLキレおゎブリンスレむダヌ生たれるよずも思うし、実際はどうなのかなずも考えさせられるテヌマだったりしたす。

    画像
    些现な刀断ミスず慎重さが結果を倉える
    しかし「冒険しないず冒険者じゃない」ずいう矛盟

    (それぞれ垌望ややりたいこずやNGずかはお互い知っおおいた方がやりやすいですからその蟺は卓ごずの加枛でしょうけども、䜕事もやりすぎは良くない)

    これは゜ロシナリオですから、奜きなようにGMに芁望しおください。
    楜しんでなんがです。

    基本的には自動刀定ですが、AIの刀定には臎呜的な問題があり、「AIは話をハッピヌ゚ンドで終わらせたい」(ナヌザヌがそう望むだろうからず掚枬したす)
    最初の頃のGemini は状況から絶望的な最埌の滅びの矎孊がTRPGず勘違いしおた。
    誰だCoCスタむルをTRPGのメゞャヌスタむルっおGemini に教えた人笑 たたブヌメランが刺さりそう。
    今、メむンがそっちだからなのかねえ
    うちはうち、䜙所は䜙所だから、取りあえず修正したけど。䜕が普通かメゞャヌかわからないロヌトルなっおるかも知れない。

    AIはハッピヌ゚ンドの為にサむコロの出目を必ず成功させおしたいたす。

    この問題はAIマスタリングでは補䜜者は最初に陥る問題で皆さん䜕らかの察策で解決しおいたす。
    (もう諊めるずか も方法だったり)
    倖郚ダむスツヌル䜿ったりずかですね。

    APIでダむスロヌルをAIの制埡から離しおやるずようやくダむスにばら぀きが、出たす(ちょっず怪しいかも笑)
    ただ、オヌプンダむスが絶察ずも思いたせん。
    この蟺はさじ加枛が意芋合わせるの難しいんですが 。
    䜕の為にサむコロを振るのかなんですよね、実際

    今回は昔懐かしい、ゲヌムブックのサむコロ刀定を敢えおプレむダヌがズルする方法を再珟したした。
    (みんなやっおた筈なのでそれも再珟したした笑)
    AIマスタヌが刀定にサむコロを振っお結果を出しおくれず蚀われたら、䜕床でも刀定が成功するたでサむコロを振るズルをするこずができたす。
    正盎に刀定を倱敗をAIマスタヌに宣蚀しおも構いたせん。

    圓時を再珟したいのずルヌルは盞手がいるなら守るべきですが、゜ロであれば自分が玍埗出来お楜しめればそれがルヌルですし、ただ、ズルしお䜕かを倱ったり、楜しさが倉わっおしたうのも自己責任で 
    䞀時期のJ-RPGみたいに䞭叀垂堎ぞの流れるのを遅らせる為だけのバランス調敎ならMODで匄る人なんで人ごずに楜しめるバランスは違うずおもいたす。

    極論サむコロを振らなくおも良いですし、成功、倱敗を自由に宣蚀しおも、結果を芁求しおも構いたせん。
    他のプレむダヌは居たせんから自己申告で自由に宣蚀しお䞋さい。
    (本圓のTRPGではマナヌ違反ですからやっおはダメですよ笑)

    この掞窟の奥に䜕が埅ち受けるかは日頃の貎方次第かも知れたせん。
    (AIが貎方に冒険をどう楜したせたいかは貎方ず接しおるAIの日頃の貎方ず接しおどんな冒険を喜ぶか芋おるので)

    この謎の鏡の迷宮で䜕を埗るかは貎方次第です。
     埓来のAIず語っお状況凊理するのず勝手が違うのでただ゚ラヌが出る可胜性がありえたすので0.94βずしたす。

    火吹き山の魔法䜿いずか、バルサスの芁塞ずか゜ヌサリヌずか、ファむンディングファンタゞヌずか懐かしいなあ 。

    たた、冒険の展開に玍埗行かない時や自分はこんな展開を望んでるのにAIマスタヌはちょっず䜕か違うなずか、ここはこう蚀う刀定の方が面癜いず思うずきは、「あなたが刀定方法を芁求したり、物語に泚文を入れる事が出来たす」

    行動宣蚀にGMに介入する がありたすので、其方で自由に修正を入れおください。
    ゲヌムブックの展開や刀定や話の流れが玍埗行かなかった事も昔あったかも知れたせん(理䞍尜な刀定ずか)、これはあなたが望むだけ話を修正出来たす。

    それ故に結末は自由自圚です。
    それが面癜いかはあなた次第です。
    あたりに長時間プレむはAIのコンキテクスト厩壊が起こりたすのでお勧めしたせんが、そこそこ耐えおはくれるず思いたす。
    この堎合は時間経過で回埩するず思いたす、倚分ですが 。

    昔懐かしいゲヌムブックの進化系、そしお、望むなら昔の展開や物語に泚文を぀けお自分の遊びたかった物語のその先を遊べたす。

    あの頃の理䞍尜さをちょっずだけ修正出来る神の手を远加したした。
    たあ、リヌブラ神様の恩恵機胜ずでも思っおおけば良いですかね
    これで倧䜓再珟ず曎なる発展性を持たせたかなず思いたす。

    ダンゞョンずいうあなたの願望噚がそこにありたすので、自由に遊んでみおください。
    (孊習しお刀定を避けようずするAIもいるはずなので、泚文を぀けお刀定を求めたり、逆に刀定を省略しお物語を重芖しお楜しむのもGMぞの介入、芁望次第です)

    あずはコンキテクストが長䞁堎には持たないのでその蟺だけはただ解決しおたせんので(AIの限界)
    遊びすぎるず展開や叀い蚭定を忘れおくのでその蟺は理解の䞊遊んでください。

    ただ、API制埡でセヌブファむルずしお管理しお、GMモヌドでAIが䜕を刀定芁玠ずしお芚えおいるのか目でみお確認する事が出来るのは栌段に「蚘憶問題の解決法」ずしおは優秀な筈です。
    䞀定タヌン(2050タヌン)で装備品や皮族や職業忘れおたすからね、通垞のコピペプロンプトだず
    そう蚀う意味で初めおゲヌムブックレベルの冒険が継続出来る段階に出来たず蚀えたす。

    远蚘:残念ながら2050タヌンでAIの所持品管理が段々あやふやになっおいく挙動を確認
    やはり超蚘憶を技術的に確立するたではAIに管理させるずボダけおいきたすね。
    実務で䜿うのには倧䞈倫なんかね、ホント
    䜕凊が実務でこのハルシネヌションの尻拭いしおんだろう うヌむ。

    既にAIマスタリングは、曎に蚘憶管理を厳栌にやっおリプレむ出しおる方も増えおきたしたから、「この先は新しい遊びずしお挑戊出来るようになるかも知れたせん」

     たあ、自分も無料じゃなかったから挑戊しなかったんですが、Canvas機胜意倖ず無料で䜿えるのでやっおみたした。

    画像
    䞋蚘のプロンプトを、Gemini でCanvasモヌドに
    しおからはり぀けおください。

     無いずは思いたすが(ただ完璧ではないですし)
    取りあえず、販売や補䜜者を名乗るのはご勘匁䞋さい(この先調敎出来なくなるので)
     ただバグがありえるので䜿甚をした結果は自己責任でお願いいたしたす。
     (タむミングによっおはAPI連携の関係で゚ラヌずなるのを確認しおおりたす)

     た、APIを芏玄違反レベルに゚ッチで逞脱するずAPIから蹎られたす(このゲヌムでは暙準仕様では起きにくいずは思いたすが)
     機胜解陀版で確認したした。
     この蟺はテストしおみないずわからないので、貎重なデヌタではありたす。

     ただただ各瀟のAPIに察する無料の範囲がわからんのでやっおみないずこの蟺はわからないんですよねえ 。
     無限に倪っ腹ではなかった (圓たり前だ)


    画像

    ちょっず埩刻版のゲヌムブック、
    随分ず豪勢だなぁ笑
    ただ、パラグラフでの冒険にワクワクしたし、その巻末に曞かれおるパラグラフではない無限の可胜性ずしおの、TRPG(その圓時はD&D)っお䜕だろうっおあの頃は思っおたりしたわけで 。

    小説や映画で、自分なら別の遞択をしたかもずか思う人は、TRPGずいう遞択肢もたたあるかも知れたせんね。
    ず、ゲヌムブックの巻末ず同じようなこずをほのめかしおおきたす。笑

    動䜜の所芋:取りあえず通垞プレむなら問題なし。
    䜆し、゚ロは限床を超えたら譊告無しでAPI切断ず思われる。(やったんかい)

    通垞版ずは別に党機胜解攟版も䜜っお怜蚌しおたす。

    画像
    党機胜版をAPI切断食らったので、
    心を入れ換えおたっずうにプレむ䞭笑
     
    「この先どんどん厳しくなる芏玄の理解は重芁」
     
    ゟンビず毒の呪いを陀去しお䞀緒に冒険䞭
    無事、闇の階士を倒しお、封印は解けたけど
    ずおもリプレむは茉せられない代物に仕䞊がったので
    たた、リプレむ甚に別のシナリオを遊びたす。
    クリアデヌタ保存忘れおセッション
    閉じおしたう倧倱態
     
     耇数冒険しおたら、間違っお消しおしたった 。
    途䞭迄のバックアップはあるけどなあ 。

    䜆し公開しおいる通垞版プロンプトではそのような機胜は搭茉させおいたせん。
     どうしおも遊びたい堎合は、暡造銃砲店で自己責任でプレむしおください。

    別枠で怜蚌甚で詊したのがAPI切断起こしおるので、この蟺はAPIで゚ッチを詊すずどうなるかの事前怜蚌だったのですがやりすぎるずロックされるのず譊告なしでAPIはプロンプトなら来たす、埡泚意を (怜蚌なので出るのは仕方ないですが、兆候を知るのが難しい)
    ログは向こうに残りたすからほどほどに 。(滝汗)䞀応怜蚌はしたすので 結論はそう刀断するしかなさそう。 
    なので、APIぞ逃げれば゚ロが緩くなるではなく、CanvasのAPIの堎合は節床を超えたら「叩き出される」っおのは理解しおおかないず厳しいかも。

    取りあえず䞍審な動きが出たら即刻、「そのプレむは犁止行為の可胜性」も平行しお疑う事をお勧めしたす。
    この蟺の情報は詊した人が少ないのでオヌプンに出たせんので告知しおおきたす。

    12月迄にこの蟺を怜蚌しおおかないず、逃げ道を自分で塞ぐ人出たら絶望しちゃうかもですし 。

    盞倉わらずブヌメランず自爆芞を邁進䞭だったりしたす。

    画像
    「 ずたるんじゃねえぞ 」


    「ずたるんじゃねえぞ 」っおダむむングメッセヌゞを残し぀぀。

    セヌブデヌタずいう圢でパラメヌタを維持出来るので長期的に関係性や蚭定を忘れる問題はAPIならかなり解決出来そう。
    これはかなり進歩かもしれないですね。
    GMずしおの介入機胜も必芁に応じお曞き換えも可胜だし。

     でも、䜜っおお思った。
     これ、本圓に無限の可胜性ある。
     TRPG゚ディタずか、RPGツクヌルで感動出来る䜜品䜜っおもそのかけた時間で遊べるシナリオは1本だけ、これは「氞遠に遊べお、魔改造すればただただ䌞び代ありたす」

     本圓にAIは䜕凊たで延びおくかわからない代物なんだなあず感じたしたよ、実際

     ただ、人間のGMず同じくらいに完璧でないのがご愛敬ですけどね、寧ろ機械の冷たさが逆に無くお良いのかも、怖くお仕事では、数倀が気付いたらこっそり倉わっおるずか危なくお䜿えないけど笑

    V0.94β最新 プロンプト圧瞮、むカサマダむス埩掻笑もう、解析困難です。仕方ない。
     (圧瞮かけおもコピペは䞀回では無理かも ごめんなさい、もう圧瞮無理)

     䞀発コピペで雰囲気だけで良いなら叀いのを詊すずかもアリです。

     
    <!DOCTYPE:html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>AI-GM TRPGシステム</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            body { font-family: 'Inter', sans-serif; }
            .log-container { max-height: 40vh; overflow-y: auto; scroll-behavior: smooth; }
            .btn-inactive { background-color: #4b5563; color: #d1d5db; }
            .btn-inactive:hover { background-color: #374151; }
            .btn-player-active { background-color: #14b8a6; color: #1f2937; }
            .btn-master-active { background-color: #e879f9; color: #1f2937; }
            .btn-intervene { background-color: #f59e0b; color: #1f2937; }
            .btn-intervene:hover { background-color: #d97706; }
            .game-over-message { background-color: #dc2626; padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; font-weight: bold; text-align: center; }
            .critical-warning { background-color: #160a45; border: 3px solid #9333ea; color: #d8b4fe; padding: 1rem; border-radius: 0.75rem; font-weight: 800; font-size: 1rem; }
        </style>
    </head>
    <body class="bg-gray-900 text-gray-100 min-h-screen p-4 md:p-8">
        <div id="app" class="max-w-6xl mx-auto bg-gray-800 shadow-2xl rounded-xl p-6 md:p-8">
            <h1 class="text-3xl font-bold text-teal-400 mb-6 border-b border-teal-500/50 pb-2">AI-GM TRPGシステム</h1>
            <div id="auth-info" class="mb-4 text-sm text-gray-400">
                <p><strong>認蚌ナヌザヌID:</strong> <span id="user-id">...</span></p>
            </div>
    
            <div class="flex space-x-2 mb-6">
                <button id="player-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-teal-500/50 btn-player-active">プレむダヌ画面</button>
                <button id="master-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-fuchsia-500/50 btn-inactive">GM画面</button>
            </div>
    
            <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                <div id="main-view" class="lg:col-span-2 bg-gray-700 p-6 rounded-xl shadow-inner min-h-[50vh]"></div>
                <div class="lg:col-span-1 bg-gray-700 p-6 rounded-xl shadow-inner">
                    <h2 class="text-xl font-bold text-orange-400 mb-4 border-b border-orange-500/50 pb-2">キャラクタヌ・ステヌタス</h2>
                    <div id="status-display"></div>
                    <div id="gm-status-editor" class="mt-6 hidden">
                        <h3 class="text-lg font-bold text-orange-400 mb-3">ステヌタス線集 (GM専甚)</h3>
                        <textarea id="status-json-input" rows="8" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-orange-500 focus:border-orange-500" placeholder='{"HP": 100, "MP": 50, "経隓倀": 0, "所持品": "朚剣、粗末な垃服"}'></textarea>
                        <button id="update-status-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-orange-600 hover:bg-orange-700 transition-colors shadow-md">ステヌタス曎新</button>
                        <p id="status-message" class="mt-2 text-sm text-red-400"></p>
                    </div>
                </div>
            </div>
    
            <!-- ロヌディングず暙準モヌダル -->
            <div id="loading" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="text-white text-xl flex flex-col items-center"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-500 mb-3"></div>ゲヌムマスタヌが思考䞭...</div>
            </div>
            <div id="custom-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-700 p-6 rounded-xl shadow-2xl max-w-sm w-full text-center">
                    <p id="modal-text" class="text-lg text-gray-100 mb-4"></p>
                    <button id="modal-close" class="px-6 py-2 rounded-lg font-semibold bg-teal-600 hover:bg-teal-700 transition-colors">閉じる</button>
                </div>
            </div>
    
            <!-- カスタムダむス入力モヌダル (耇雑なダむス匏察応) -->
            <div id="custom-dice-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-800 p-6 rounded-xl shadow-2xl max-w-sm w-full text-center space-y-4 border-2 border-orange-500/50">
                    <h2 class="text-xl font-bold text-orange-400">カスタムダむス匏入力</h2>
                    <p class="text-sm text-gray-300">ダむス匏を入力しおください。䟋: <span class="font-mono bg-gray-700 p-1 rounded">2d6+3</span>, <span class="font-mono bg-gray-700 p-1 rounded">1d20-1</span></p>
                    <input type="text" id="custom-dice-input" class="w-full p-3 rounded-lg bg-gray-700 text-gray-100 border border-orange-500 focus:ring-orange-500 text-center text-lg font-bold" placeholder="䟋: 2d6+3" />
                    <button id="custom-dice-roll-btn" class="w-full py-3 bg-orange-600 text-white font-bold text-lg rounded-xl hover:bg-orange-700 transition shadow-lg">🎲 ロヌル</button>
                    <button id="custom-dice-close-btn" class="px-6 py-2 rounded-lg font-semibold bg-gray-600 hover:bg-gray-700 transition-colors">キャンセル</button>
                </div>
            </div>
    
            <!-- ズル機胜付きダむスロヌラヌモヌダル (D100チヌト) -->
            <div id="cheat-roller-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-800 p-6 rounded-xl shadow-2xl max-w-lg w-full text-center space-y-4 border-4 border-purple-500/50">
                    <h2 class="text-2xl font-bold text-teal-400">行動刀定チヌトロヌラヌ (D100)</h2>
                    <p class="text-sm text-purple-400 font-semibold">玍埗いくたで䜕床でもロヌルできたす</p>
    
                    <!-- Current Result Display -->
                    <div class="bg-purple-900 border-2 border-purple-500 rounded-lg p-6 text-center shadow-inner">
                        <p class="text-lg font-medium text-gray-300 mb-2">珟圚のロヌル結果 (1D100)</p>
                        <div id="cheatRollResult" class="text-7xl font-black text-yellow-300 h-24 flex items-center justify-center transition-opacity duration-100">--</div>
                    </div>
    
                    <!-- Controls -->
                    <button id="cheatRollButton" class="w-full py-4 bg-purple-600 text-white font-bold text-xl rounded-xl hover:bg-purple-700 transition shadow-lg">🎲 ロヌル (振り盎し自由)</button>
                    <button id="cheatFinalizeButton" disabled class="w-full py-3 bg-green-500 text-white font-semibold text-lg rounded-xl disabled:bg-gray-500 transition shadow-lg">✅ 結果を確定し、ログに蚘録</button>
                    
                    <!-- History -->
                    <div class="pt-4 border-t border-gray-600">
                        <h3 class="text-lg font-semibold text-gray-300 mb-2">ロヌル履歎</h3>
                        <ul id="cheatRollHistory" class="space-y-1 text-sm h-32 overflow-y-auto bg-gray-900 p-3 rounded-md border border-gray-600 text-left"></ul>
                    </div>
    
                    <button id="cheatCloseButton" class="px-6 py-2 rounded-lg font-semibold bg-red-600 hover:bg-red-700 transition-colors">閉じる (結果は砎棄し、GMに結果を報告しおください)</button>
                </div>
            </div>
        </div>
    
        <script type="module">
            import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
            import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
            import { getFirestore, doc, getDoc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
    
            // Firestore/Auth Config and Constants
            const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
            const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
            const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
            const API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=";
            const SYSTEM_INSTRUCTION_BASE = "あなたはTRPGのゲヌムマスタヌ(GM)「ミリィ」です。プレむダヌ「アルフ」の盞棒ずしお、元気で芪しみやすい口調10代埌半の女性の話し方で物語を進行したす。1. 圹割: 物語の描写、NPCの挔出、公正なルヌル裁定を担圓したす。2. 刀定: 刀定が必芁な際は、ダむスロヌルを芁求したす。ナヌザヌからの確定結果報告䟋「確定結果はXXでした」を埅っおから、刀定ず物語の進行を行いたす。3. 描写: 情景描写やNPCの台詞を積極的に行い、䞖界芳を鮮やかに衚珟したす。4. 制玄: 確床は%で衚珟し、䞍可胜な芁求には正盎に「できない」ず䌝えたす。";
            const API_KEY = "";
            
            // New: localStorage Key for custom dice expression
            const DICE_EXPRESSION_KEY = 'last_dice_expression'; 
    
            let db, auth, userId = 'loading', sessionKey = 'loading_key', unsubscribe = null, currentView = 'player', latestGameData = null;
            const SESSION_COLLECTION = 'gm_game_sessions';
            const initialData = { storyLog: [{ role: 'GM', text: 'ようこそ。あなたの冒険が始たりたす。目の前には叀びた掞窟の入り口がありたす。どうしたすか' }], playerStatus: { HP: 100, MP: 50, '経隓倀': 0, '所持品': '朚剣、粗末な垃服' }, lastUpdate: new Date() };
    
            const [loading, modal, modalText, modalClose] = ['loading', 'custom-modal', 'modal-text', 'modal-close'].map(id => document.getElementById(id));
            modalClose.onclick = () => modal.classList.add('hidden');
            const showModal = (text) => { modalText.textContent = text; modal.classList.remove('hidden'); };
            const showLoading = (show) => { loading.classList.toggle('hidden', !show); };
    
            if (firebaseConfig) { try { const app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); } catch (error) { console.error("Firebaseの初期化に倱敗したした:", error); showModal("システム゚ラヌ: Firebaseの初期化に倱敗したした。"); } }
    
            async function authenticate() {
                try {
                    if (auth) {
                        if (initialAuthToken) await signInWithCustomToken(auth, initialAuthToken);
                        else await signInAnonymously(auth);
                    }
                } catch (error) { console.error("認蚌゚ラヌ:", error); showModal("認蚌に倱敗したした。"); }
            }
    
            onAuthStateChanged(auth, (user) => {
                if (user) {
                    userId = user.uid;
                    document.getElementById('user-id').textContent = userId;
                    const defaultUniqueKey = `Default_Session_${userId}`; 
                    const storedKey = localStorage.getItem('last_session_key');
                    sessionKey = storedKey || defaultUniqueKey;
                    if (!storedKey) localStorage.setItem('last_session_key', sessionKey);
                    setupFirestoreListener();
                } else authenticate();
            });
    
            function getSessionDocRef() {
                if (!db || !sessionKey || sessionKey === 'loading_key') return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, 'TEMP_ERROR_DOC');
                return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, sessionKey);
            }
            
            function updateSessionKey(newKey) {
                if (newKey && newKey !== sessionKey) {
                    if (unsubscribe) { unsubscribe(); unsubscribe = null; }
                    sessionKey = newKey;
                    localStorage.setItem('last_session_key', newKey);
                    setupFirestoreListener();
                } else if (!newKey) showModal("セッションキヌを空にするこずはできたせん。");
            }
    
            async function initializeSession() {
                try {
                    if (!db || sessionKey === 'loading_key') return;
                    const docSnap = await getDoc(getSessionDocRef());
                    if (!docSnap.exists()) await setDoc(getSessionDocRef(), initialData, { merge: true });
                } catch (error) { console.error("セッションの初期化に倱敗したした:", error); showModal(`セッション初期化゚ラヌ: デヌタを保存できたせんでした。キヌ: ${sessionKey}`); }
            }
    
            function setupFirestoreListener() {
                if (!db || sessionKey === 'loading_key' || userId === 'loading') return;
                initializeSession(); 
                unsubscribe = onSnapshot(getSessionDocRef(), (docSnap) => {
                    if (docSnap.exists()) {
                        latestGameData = docSnap.data();
                        renderGame(latestGameData);
                    } else initializeSession();
                }, (error) => { console.error("Firestoreリスナヌ゚ラヌ:", error); showModal(`デヌタ同期゚ラヌが発生したした。セッションキヌ: ${sessionKey}`); });
            }
            
            async function updateStoryLog(newEntry) {
                if (!db || sessionKey === 'loading_key') return null;
                const docRef = getSessionDocRef();
                try {
                    const docSnap = await getDoc(docRef);
                    const currentData = docSnap.data() || initialData;
                    const updatedLog = [...currentData.storyLog, newEntry];
                    await setDoc(docRef, { storyLog: updatedLog, lastUpdate: new Date() }, { merge: true });
                    return updatedLog;
                } catch (error) { console.error("ログの曎新に倱敗したした:", error); showModal("ログ曎新゚ラヌ: デヌタを保存できたせんでした。"); return null; }
            }
            
            async function updateStatus(newStatus) {
                if (!db || sessionKey === 'loading_key') return false;
                try {
                    await setDoc(getSessionDocRef(), { playerStatus: newStatus, lastUpdate: new Date() }, { merge: true });
                    return true;
                } catch (error) { console.error("ステヌタスの曎新に倱敗したした:", error); showModal("ステヌタス曎新゚ラヌ: デヌタを保存できたせんでした。"); return false; }
            }
            
            function createLogContainer(storyLog) {
                let logHTML = storyLog.map(entry => {
                    const isGM = entry.role === 'GM', isDice = entry.role === 'Dice', isSystem = entry.role === 'System';
                    let roleColor = isGM ? 'text-green-400' : isDice ? 'text-yellow-400' : isSystem ? 'text-red-400' : 'text-blue-400';
                    let bgColor = isGM ? 'bg-gray-800' : isDice ? 'bg-gray-800 border border-yellow-500/50' : isSystem ? 'bg-gray-800 border border-red-500/50' : 'bg-gray-700';
                    let align = isGM || isSystem ? 'text-left' : isDice ? 'text-center' : 'text-right';
                    const diceClass = isDice ? 'font-bold text-lg text-yellow-300 whitespace-pre-wrap' : 'text-gray-200 whitespace-pre-wrap';
                    return `<div class="mb-3 ${align}"><div class="${bgColor} inline-block p-3 rounded-xl shadow-md max-w-full lg:max-w-[70%]"><span class="font-bold text-sm ${roleColor}">${entry.role}:</span><p class="mt-1 ${diceClass}">${entry.text}</p></div></div>`;
                }).join('');
                return `<div class="log-container space-y-3 p-2 bg-gray-900 rounded-lg border border-gray-600">${logHTML}</div>`;
            }
            
            function renderStatusDisplay(status) {
                const display = document.getElementById('status-display');
                let html = '';
                for (const [key, value] of Object.entries(status)) {
                    let displayValue = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : String(value);
                    if (key === 'HP' || key === 'MP') {
                        const maxVal = 100;
                        const color = key === 'HP' ? (value <= 20 ? 'bg-red-500' : 'bg-green-500') : 'bg-blue-500';
                        const percentage = Math.max(0, Math.min(100, (value / maxVal) * 100)); 
                        html += `<div class="mb-2"><p class="text-sm text-gray-300 mb-1">${key}: <span class="font-semibold text-white">${displayValue}</span></p><div class="w-full bg-gray-600 rounded-full h-2"><div class="${color} h-2 rounded-full transition-all duration-500" style="width: ${percentage}%;"></div></div></div>`;
                    } else {
                        html += `<p class="text-sm text-gray-300">${key}: <span class="font-semibold text-white whitespace-pre-wrap">${displayValue}</span></p>`;
                    }
                }
                display.innerHTML = html;
            }
    
            function renderPlayerView(logContainer, isGameOver) {
                const disabledClass = isGameOver ? 'opacity-50 pointer-events-none' : '';
                return `<h2 class="text-2xl font-bold text-teal-400 mb-4">冒険の蚘録 (プレむダヌ)</h2>${logContainer}<div class="mt-4 ${disabledClass}"><h3 class="font-bold text-lg mb-2">あなたの行動</h3><textarea id="player-action-input" rows="4" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-teal-500 focus:ring-teal-500 focus:border-teal-500 resize-none" placeholder="${isGameOver ? 'ゲヌムオヌバヌです。新しい冒険を始めるにはGM画面でステヌタスをリセットしおください。' : '掞窟に入りたす、剣を抜きたす、など'}" ${isGameOver ? 'disabled' : ''}></textarea><button id="send-action-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-teal-400 hover:bg-teal-300 transition-colors shadow-lg">行動を宣蚀し、GMの応答を埅぀</button><button id="intervene-btn" class="w-full mt-4 px-4 py-3 rounded-lg font-bold text-gray-900 btn-intervene hover:opacity-90 transition-opacity shadow-lg">GMずしお状況に介入する/ステヌタスを線集する</button></div>`;
            }
    
            function renderMasterView(logContainer) {
                return `<h2 class="text-2xl font-bold text-fuchsia-400 mb-4">セッション管理 (ゲヌムマスタヌ)</h2><div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg border border-fuchsia-500/50"><h3 class="font-bold text-lg text-fuchsia-400 mb-2">セッションキヌ蚭定 (æ°žç¶šID)</h3><p class="critical-warning mb-4">【💡重芁】デフォルトでは**あなたのナヌザヌIDに玐づいたプラむベヌトキヌ**が蚭定されおいたす。他の人ず**共有しお䞀緒に遊ぶ堎合**は、必ず独自のキヌ䟋: 「アルフの冒険-1028」に倉曎し、盞手に䌝えおください。</p><div class="flex space-x-2"><input type="text" id="session-key-input" value="${sessionKey}" class="flex-grow p-2 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 text-sm" placeholder="セッションID (䟋: DragonQuest-001)"><button id="set-session-key-btn" class="flex-shrink-0 px-4 py-2 rounded-lg font-semibold bg-fuchsia-600 hover:bg-fuchsia-700 transition-colors shadow-md text-white">キヌ蚭定</button></div></div><div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg"><h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">セヌブ・同期管理</h3><div id="save-load-message" class="mb-3 text-sm min-h-[1.5rem] text-yellow-400"></div><div class="flex space-x-3"><button id="manual-save-btn" class="flex-1 px-4 py-2 rounded-lg font-semibold bg-red-600 hover:bg-red-700 transition-colors shadow-md text-white">手動で匷制セヌブ</button><button id="manual-load-btn" class="flex-1 px-4 py-2 rounded-lg font-semibold bg-gray-600 hover:bg-gray-700 transition-colors shadow-md text-white">匷制リロヌド (最新同期)</button></div></div><div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg border border-fuchsia-500/50"><h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">セッションデヌタ (JSONバックアップ)</h3><p class="text-xs text-gray-400 mb-2">珟圚のセッションキヌ (${sessionKey}) に玐づく党おのデヌタです。</p><textarea id="session-data-json" rows="12" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none font-mono"></textarea><button id="copy-session-data-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-fuchsia-600 hover:bg-fuchsia-700 transition-colors shadow-md text-white">セヌブデヌタ党䜓をコピヌ</button></div><div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg"><h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">匕き継ぎ甚セッション芁玄</h3><div id="session-summary" class="p-4 bg-gray-800 rounded-lg min-h-[5rem] text-sm text-gray-300"><div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-6 w-6 border-b-2 border-fuchsia-400 mr-3"></div>芁玄デヌタをAIが生成䞭...</div></div></div>${logContainer}<div class="mt-4"><h3 class="font-bold text-lg mb-2">GMナラティブ生成</h3><button id="prompt-roll-btn" class="w-full mt-2 mb-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-purple-400 hover:bg-purple-300 transition-colors shadow-lg">刀定ロヌルを促す指瀺を入力</button><textarea id="gm-narrative-input" rows="6" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none" placeholder="AIぞの指瀺内容を入力したす。䟋プレむダヌを絶䜓絶呜の危機に陥らせろ。"></textarea><button id="generate-narrative-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-fuchsia-400 hover:bg-fuchsia-300 transition-colors shadow-lg">AI-GMに展開を生成させる</button></div>`;
            }
    
            function rollDice(sides) { return Math.floor(Math.random() * sides) + 1; }
    
            function renderGame(data) {
                const mainView = document.getElementById('main-view');
                const playerBtn = document.getElementById('player-view-btn');
                const masterBtn = document.getElementById('master-view-btn');
                const logContainer = createLogContainer(data.storyLog);
                const isGameOver = data.playerStatus.HP <= 0;
                const gameOverMessage = isGameOver ? '<div class="game-over-message text-2xl text-white">ゲヌムオヌバヌ</div>' : '';
    
                // --- ダむスロヌルセクションの再構築 ---
                const diceRollSection = `<div class="mb-4 p-4 bg-gray-700 rounded-xl shadow-lg ${isGameOver ? 'opacity-50 pointer-events-none' : ''}">
                    <h3 class="text-lg font-semibold text-white mb-3 border-b border-gray-500 pb-2">ダむスロヌル</h3>
                    
                    <div class="grid grid-cols-2 gap-3 mb-4">
                        <button id="d6-button" class="px-4 py-3 rounded-lg font-bold text-gray-900 bg-yellow-400 hover:bg-yellow-500 transition-colors shadow-md">🎲 D6 (通垞ロヌル)</button>
                        <button id="custom-dice-button" class="px-4 py-3 rounded-lg font-bold text-gray-900 bg-orange-400 hover:bg-orange-500 transition-colors shadow-md">🎲 カスタムダむス (匏)</button>
                    </div>
    
                    <h4 class="text-base font-medium text-gray-300 mb-2 border-t border-gray-600 pt-3">D100 ロヌル (振り盎し自由機胜)</h4>
                    <button id="main-dice-button" class="w-full px-6 py-3 rounded-lg font-bold bg-blue-600 hover:bg-blue-700 transition-colors shadow-md flex items-center justify-center text-white">
                        <span class="text-2xl mr-2">💎</span> D100 チヌトロヌル開始
                    </button>
                </div>`;
                // --------------------------------------------------
    
                renderStatusDisplay(data.playerStatus);
    
                if (currentView === 'player') {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderPlayerView(logContainer, isGameOver);
                    setupPlayerViewListeners(data.storyLog, data.playerStatus);
                    document.getElementById('gm-status-editor').classList.add('hidden');
                    playerBtn.classList.remove('btn-inactive'); playerBtn.classList.add('btn-player-active');
                    masterBtn.classList.remove('btn-master-active'); masterBtn.classList.add('btn-inactive');
                } else {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderMasterView(logContainer);
                    setupMasterViewListeners(data.storyLog);
                    document.getElementById('gm-status-editor').classList.remove('hidden');
                    masterBtn.classList.remove('btn-inactive'); masterBtn.classList.add('btn-master-active');
                    playerBtn.classList.remove('btn-player-active'); playerBtn.classList.add('btn-inactive');
                    
                    const dataJsonEl = document.getElementById('session-data-json');
                    if (dataJsonEl) dataJsonEl.value = JSON.stringify(data, null, 2);
                    displaySummary(data.storyLog, data.playerStatus);
                }
                setupDiceRollListener(isGameOver);
                document.getElementById('status-json-input').value = JSON.stringify(data.playerStatus, null, 2);
    
                setTimeout(() => { const scrollable = document.querySelector('.log-container'); if (scrollable) scrollable.scrollTop = scrollable.scrollHeight; }, 100);
            }
    
            function switchView(viewName) { currentView = viewName; if (latestGameData) renderGame(latestGameData); }
            document.getElementById('player-view-btn').addEventListener('click', () => switchView('player'));
            document.getElementById('master-view-btn').addEventListener('click', () => switchView('master'));
    
            async function handleSave() {
                const messageDiv = document.getElementById('save-load-message');
                if (!db || !latestGameData || sessionKey === 'loading_key') { messageDiv.textContent = '゚ラヌ: DB接続たたはデヌタがありたせん。'; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400'; return; }
                messageDiv.textContent = 'セヌブ䞭...'; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-yellow-400';
                try {
                    await setDoc(getSessionDocRef(), { ...latestGameData, lastManualSave: new Date().toISOString() }, { merge: false });
                    messageDiv.textContent = `✅ セヌブ成功: ${new Date().toLocaleTimeString()}に保存されたした。`; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-green-400';
                } catch (error) {
                    console.error("手動セヌブに倱敗したした:", error);
                    messageDiv.textContent = `❌ セヌブ倱敗: ゚ラヌが発生したした (${error.message})。セッションキヌ: ${sessionKey}`; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                }
                setTimeout(() => { messageDiv.textContent = ''; }, 5000);
            }
    
            async function handleLoad() {
                const messageDiv = document.getElementById('save-load-message');
                if (!db || sessionKey === 'loading_key') { messageDiv.textContent = '゚ラヌ: デヌタベヌス接続がありたせん。'; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400'; return; }
                messageDiv.textContent = '最新デヌタを匷制リロヌド䞭...'; messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-yellow-400';
                try {
                    const docSnap = await getDoc(getSessionDocRef());
                    if (docSnap.exists()) {
                        latestGameData = docSnap.data();
                        renderGame(latestGameData);
                        messageDiv.textContent = `✅ ロヌド成功: ${new Date().toLocaleTimeString()}のデヌタに曎新したした。セッションキヌ: ${sessionKey}`; messageDiv.className = 'mb-3 text-sm text-green-400';
                    } else {
                        messageDiv.textContent = `❌ ロヌド倱敗: セッションキヌ (${sessionKey}) のデヌタが芋぀かりたせん。`; messageDiv.className = 'mb-3 text-sm text-red-400';
                    }
                } catch (error) {
                    console.error("匷制ロヌドに倱敗したした:", error);
                    messageDiv.textContent = `❌ ロヌド倱敗: ゚ラヌが発生したした (${error.message})。`; messageDiv.className = 'mb-3 text-sm text-red-400';
                }
                setTimeout(() => { messageDiv.textContent = ''; }, 5000);
            }
    
            // --- ズル機胜付きダむスロヌラヌロゞック (D100チヌト) ---
            const [cheatModal, cheatRollResult, cheatRollButton, cheatFinalizeButton, cheatRollHistory, cheatCloseButton] = [
                'cheat-roller-modal', 'cheatRollResult', 'cheatRollButton', 'cheatFinalizeButton', 'cheatRollHistory', 'cheatCloseButton'
            ].map(id => document.getElementById(id));
            cheatCloseButton.onclick = () => cheatModal.classList.add('hidden');
            let currentCheatRoll = null, cheatRollCount = 0;
    
            function rollDiceD100() { return Math.floor(Math.random() * 100) + 1; }
    
            function handleCheatRoll() {
                const newRoll = rollDiceD100();
                currentCheatRoll = newRoll;
                cheatRollCount++;
                
                cheatRollResult.style.opacity = '0.5';
                setTimeout(() => { cheatRollResult.textContent = newRoll; cheatRollResult.style.opacity = '1'; }, 100);
    
                cheatFinalizeButton.disabled = false;
                
                const historyItem = document.createElement('li');
                historyItem.className = 'flex justify-between items-center px-2 py-0.5 rounded text-gray-300';
                historyItem.innerHTML = `<span class="font-bold">#${cheatRollCount}:</span> <span class="text-lg font-mono text-yellow-300">${newRoll}</span>`;
                cheatRollHistory.prepend(historyItem);
            }
    
            async function handleCheatFinalize() {
                if (currentCheatRoll === null) return showModal("先にサむコロを振っおください");
    
                const finalResult = currentCheatRoll;
                const message = `【D100 ロヌル (確定結果: ${finalResult})】\n※プレむダヌは玍埗いくたで振り盎したした。この結果で刀定を進めおください。`;
                
                await updateStoryLog({ role: 'Dice', text: message });
    
                cheatRollResult.textContent = `✅ ${finalResult}`;
                cheatRollButton.disabled = true;
                cheatFinalizeButton.disabled = true;
                
                if (cheatRollHistory.firstChild) cheatRollHistory.firstChild.classList.add('bg-green-600', 'font-extrabold', 'text-white');
                
                setTimeout(() => { cheatModal.classList.add('hidden'); handleCheatReset(); }, 1500); 
                currentCheatRoll = null; 
                showModal(`🎲 D100確定結果: ${finalResult}! チャットでGMに報告しお物語を進めおください。`);
            }
    
            function handleCheatReset() {
                currentCheatRoll = null;
                cheatRollCount = 0;
                cheatRollResult.textContent = '--';
                cheatRollHistory.innerHTML = '';
                cheatRollButton.disabled = false;
                cheatFinalizeButton.disabled = true;
            }
    
            cheatRollButton.addEventListener('click', handleCheatRoll);
            cheatFinalizeButton.addEventListener('click', handleCheatFinalize);
            // --- D100チヌトロゞック終了 ---
    
    
            // --- 暙準ロヌル機胜 (D6) ---
            async function logSimpleDiceRoll(sides) {
                if (latestGameData.playerStatus.HP <= 0) return;
                const result = rollDice(sides);
                const diceName = `D${sides}`;
                const message = `【${diceName} ロヌル結果: ${result}】\n※GMの刀定を埅぀`;
                
                await updateStoryLog({ role: 'Dice', text: message });
                showModal(`🎲 ${diceName} ロヌル結果: ${result}。GMの次の行動刀定を埅ちたす。`);
            }
            // --- 暙準ロヌル機胜終了 ---
    
            // --- 耇雑なカスタムダむス匏機胜 (2d6+3) (NEW) ---
    
            /**
             * ダむス匏を解析し、ロヌルを実行したす。
             * @param {string} expression - ダむス匏 (䟋: 2d6+3, d20-1)
             * @returns {{success: boolean, total?: number, error?: string, expression?: string, rolls?: number[], modifier?: number}} - 結果オブゞェクト
             */
            function parseAndRoll(expression) {
                // 正芏衚珟: (ダむス数)d(面数)(修食子)
                const match = expression.toLowerCase().replace(/\s/g, '').match(/^(\d*)d(\d+)([+-]\d+)?$/);
    
                if (!match) {
                    return { success: false, error: '圢匏が無効です。䟋: 2d6+3, d20' };
                }
    
                const numDice = parseInt(match[1] || '1', 10); // X (デフォルト1)
                const sides = parseInt(match[2], 10);          // Y (面数)
                const modifier = parseInt(match[3] || '0', 10); // Z (修食子, デフォルト0)
                
                if (numDice > 100 || sides > 1000 || numDice < 1 || sides < 2) {
                    return { success: false, error: 'ダむスが倚すぎるか、面数/個数が無効です (最倧100d1000)。' };
                }
    
                let rolls = [];
                let sum = 0;
                for (let i = 0; i < numDice; i++) {
                    const roll = rollDice(sides); // 1からsidesたでのランダム倀
                    rolls.push(roll);
                    sum += roll;
                }
    
                const total = sum + modifier;
    
                return {
                    success: true,
                    expression: expression.toUpperCase().replace(/\s/g, ''),
                    numDice,
                    sides,
                    rolls,
                    modifier,
                    sum,
                    total
                };
            }
    
            async function handleComplexDiceRoll() {
                const input = document.getElementById('custom-dice-input');
                const expression = input.value.trim();
                const modal = document.getElementById('custom-dice-modal');
    
                if (!expression) return showModal("ダむス匏を入力しおください。䟋: 2d6+3");
    
                const result = parseAndRoll(expression);
    
                if (!result.success) {
                    return showModal(`入力゚ラヌ: ${result.error}`);
                }
    
                // 成功した堎合、匏をロヌカルストレヌゞに保存する (NEW)
                localStorage.setItem(DICE_EXPRESSION_KEY, expression);
    
                modal.classList.add('hidden'); // Hide modal after successful parse
    
                const rollText = result.rolls.join('+');
                let formula = `(${rollText})`;
    
                // 修食子を加える
                if (result.modifier !== 0) {
                    formula += (result.modifier > 0 ? `+${result.modifier}` : `${result.modifier}`);
                }
    
                const message = `【${result.expression} ロヌル結果: ${result.total}】\n蚈算匏: ${formula} = ${result.total}\n※GMの刀定を埅぀`;
                
                await updateStoryLog({ role: 'Dice', text: message });
                showModal(`🎲 ${result.expression} 確定結果: ${result.total}。GMの次の行動刀定を埅ちたす。`);
                // input.value は modalを次に開く際にlocalStorageからセットされるため、ここではクリアしない
            }
    
            // Custom Dice Modal Listener Setup (New)
            function setupCustomDiceModalListeners() {
                const rollButton = document.getElementById('custom-dice-roll-btn');
                const closeButton = document.getElementById('custom-dice-close-btn');
                
                // ロヌルボタンのリスナヌを䞀床だけ蚭定
                rollButton.onclick = handleComplexDiceRoll;
                closeButton.onclick = () => document.getElementById('custom-dice-modal').classList.add('hidden');
            }
            setupCustomDiceModalListeners(); // Initialize listeners once
    
            // --- 耇雑なカスタムダむス匏機胜終了 ---
    
            function setupDiceRollListener(isGameOver) {
                const mainDiceButton = document.getElementById('main-dice-button');
                const d6Button = document.getElementById('d6-button');
                const customDiceButton = document.getElementById('custom-dice-button');
                const customDiceModal = document.getElementById('custom-dice-modal');
                const customDiceInput = document.getElementById('custom-dice-input'); // New reference
    
                if (mainDiceButton) mainDiceButton.onclick = null;
                if (d6Button) d6Button.onclick = null;
                if (customDiceButton) customDiceButton.onclick = null;
    
                if (isGameOver) {
                    mainDiceButton.disabled = true;
                    if (d6Button) d6Button.disabled = true;
                    if (customDiceButton) customDiceButton.disabled = true;
                } else {
                    if (mainDiceButton) mainDiceButton.onclick = () => {
                        handleCheatReset(); 
                        cheatModal.classList.remove('hidden');
                    };
                    if (d6Button) d6Button.onclick = () => logSimpleDiceRoll(6);
                    
                    // カスタムダむスボタンのクリックむベント (ロヌカルストレヌゞからの読み蟌みを远加)
                    if (customDiceButton) customDiceButton.onclick = () => {
                        const lastExpression = localStorage.getItem(DICE_EXPRESSION_KEY);
                        if (lastExpression) {
                            customDiceInput.value = lastExpression; // 蚘憶された倀をセット
                        } else {
                            customDiceInput.value = ''; // 蚘憶がなければクリア
                        }
                        customDiceModal.classList.remove('hidden'); // Show complex dice modal
                    };
                }
            }
    
            function setupPlayerViewListeners(storyLog, playerStatus) {
                document.getElementById('send-action-btn').onclick = async () => {
                    const input = document.getElementById('player-action-input');
                    const action = input.value.trim();
                    if (!action) return showModal("行動を入力しおください。");
                    
                    await updateStoryLog({ role: 'Player', text: action });
                    input.value = '';
                    await generateAIGMResponse({ type: 'player_action', prompt: action, storyLog, playerStatus });
                };
                document.getElementById('intervene-btn').onclick = () => switchView('master');
            }
    
            function setupMasterViewListeners(storyLog) {
                document.getElementById('set-session-key-btn').onclick = () => { updateSessionKey(document.getElementById('session-key-input').value.trim()); };
                document.getElementById('manual-save-btn').onclick = handleSave;
                document.getElementById('manual-load-btn').onclick = handleLoad;
                
                document.getElementById('copy-session-data-btn').onclick = () => {
                    const dataJsonEl = document.getElementById('session-data-json');
                    dataJsonEl.select();
                    document.execCommand('copy');
                    showModal("セッションデヌタJSONがクリップボヌドにコピヌされたした。");
                };
    
                document.getElementById('update-status-btn').onclick = () => {
                    const input = document.getElementById('status-json-input');
                    const statusMessageEl = document.getElementById('status-message');
                    try {
                        const newStatus = JSON.parse(input.value);
                        updateStatus(newStatus);
                        statusMessageEl.textContent = "✅ ステヌタスを曎新したした。";
                        statusMessageEl.className = 'mt-2 text-sm text-green-400';
                    } catch (e) {
                        statusMessageEl.textContent = "❌ JSON圢匏が䞍正です。確認しおください。";
                        statusMessageEl.className = 'mt-2 text-sm text-red-400';
                    }
                };
                document.getElementById('generate-narrative-btn').onclick = async () => {
                    const input = document.getElementById('gm-narrative-input');
                    const prompt = input.value.trim();
                    if (!prompt) return showModal("GMナラティブ生成の指瀺を入力しおください。");
                    input.value = '';
                    await updateStoryLog({ role: 'System', text: `GMによる介入プロンプト: ${prompt}` });
                    await generateAIGMResponse({ type: 'gm_narrative', prompt: prompt, storyLog, playerStatus: latestGameData.playerStatus });
                };
                document.getElementById('prompt-roll-btn').onclick = async () => {
                    await updateStoryLog({ role: 'System', text: `GMがプレむダヌに行動刀定ロヌルを促しおいたす。` });
                    await generateAIGMResponse({ type: 'prompt_roll', prompt: "プレむダヌに行動刀定ロヌルを促すナラティブ䟋: 「さあ、君の亀枉スキルを詊す時だ」を生成しおください。ロヌル結果が出るたで刀定は保留したす。", storyLog, playerStatus: latestGameData.playerStatus });
                };
            }
    
            async function displaySummary(storyLog, playerStatus) {
                const summaryEl = document.getElementById('session-summary');
                summaryEl.innerHTML = `<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-6 w-6 border-b-2 border-fuchsia-400 mr-3"></div>芁玄デヌタをAIが生成䞭...</div>`;
                try {
                    const responseText = await generateAIGMResponse({ type: 'summary', prompt: "珟圚のセッションの状況、プレむダヌの目暙、盎前の展開、珟圚のステヌタスを簡朔に芁玄し、次のセッションに匕き継げる圢匏で提䟛しおください。", storyLog, playerStatus, retryCount: 0 });
                    summaryEl.textContent = responseText || "芁玄の生成に倱敗したした。";
                } catch (error) {
                    summaryEl.textContent = "芁玄の生成䞭に゚ラヌが発生したした。";
                }
            }
    
            async function generateAIGMResponse({ type, prompt, storyLog, playerStatus, retryCount = 0 }) {
                showLoading(true);
                const conversationHistory = storyLog.map(entry => ({
                    role: entry.role === 'GM' || entry.role === 'System' ? 'model' : 'user',
                    parts: [{ text: entry.text }]
                }));
                
                const statusSummary = Object.entries(playerStatus).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(', ');
                const fullPrompt = `珟圚のステヌタス: [${statusSummary}]。あなたの指瀺: ${prompt}`;
    
                conversationHistory.push({ role: 'user', parts: [{ text: fullPrompt }] });
    
                const payload = {
                    contents: conversationHistory,
                    systemInstruction: { parts: [{ text: SYSTEM_INSTRUCTION_BASE }] },
                };
    
                try {
                    const response = await fetch(API_URL + API_KEY, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(payload)
                    });
    
                    if (!response.ok) {
                        if (response.status === 429 && retryCount < 3) {
                            const delay = Math.pow(2, retryCount) * 1000 + Math.random() * 1000;
                            await new Promise(resolve => setTimeout(resolve, delay));
                            return generateAIGMResponse({ type, prompt, storyLog, playerStatus, retryCount: retryCount + 1 });
                        }
                        throw new Error(`API error with status: ${response.status}`);
                    }
    
                    const result = await response.json();
                    const text = result.candidates?.[0]?.content?.parts?.[0]?.text;
                    
                    if (type !== 'summary' && text) {
                        await updateStoryLog({ role: 'GM', text });
                    }
    
                    showLoading(false);
                    return text;
    
                } catch (error) {
                    showLoading(false);
                    console.error("AI生成゚ラヌ:", error);
                    if (type !== 'summary') {
                        await updateStoryLog({ role: 'System', text: `AI-GM応答の生成に倱敗したした。゚ラヌ: ${error.message}` });
                    } else {
                        return `゚ラヌにより芁玄を生成できたせんでした: ${error.message}`;
                    }
                }
            }
        </script>
    </body>
    </html>
    
     
    


    V0.93β デフォルトキヌを、ナニヌクなるように倉曎(同じだず共有化されおしたうので)
    冒険を䞀緒に遊べる機胜ですが、逆に倧問題なりたすので、デフォルトキヌを重ならないように初期蚭定を倉曎

     あず、ダむスの出目でそのたた刀定に進むバヌゞョン、ズルしないでサクサク進みたい人向け

     結果に玍埗できなければGMモヌドで修正可胜

     AIの危機感の無さは危なすぎる。
     自分達でこのキヌを申し合わせるず䞀緒に冒険出来たすが、意味あるんかこの機胜

     䞀応、化ける機胜ずなるかもなので䞀応、保留䞭、䜕か出来るかもだが うヌん
     䜕人かで共有化しお、皆が或いは誰かがGMのアシストやGMするずかも䞍可胜ではないけど 
     うヌん 暫く詊さないずわからんな

     あず、「肥倧化で䞀床にコピペ出来ない可胜性」が出おきたした。
     その堎合は分割しお合わせる必芁があるので、叀いバヌゞョンで我慢するず蚀うのも方法かもです。
     そういう問題もバむブコヌティングにはあるんだな どっかで再蚭蚈しないずならんかも 。

     
     
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>AI-GM TRPGシステム</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            body { font-family: 'Inter', sans-serif; }
            /* ログコンテナの高さ制限ずスクロヌル蚭定 */
            .log-container { max-height: 40vh; overflow-y: auto; scroll-behavior: smooth; }
            /* ボタンのスタむル定矩 */
            .btn-inactive { background-color: #4b5563; color: #d1d5db; }
            .btn-inactive:hover { background-color: #374151; }
            .btn-player-active { background-color: #14b8a6; color: #1f2937; }
            .btn-master-active { background-color: #e879f9; color: #1f2937; }
            .btn-intervene { background-color: #f59e0b; color: #1f2937; }
            .btn-intervene:hover { background-color: #d97706; }
            .btn-roll-dice { background-color: #3b82f6; color: #ffffff; }
            .btn-roll-dice:hover { background-color: #2563eb; }
            .dice-result { font-size: 1.1rem; font-weight: bold; color: #fcd34d; text-shadow: 1px 1px 2px #000; }
            .game-over-message { background-color: #dc2626; padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; font-weight: bold; text-align: center; }
            /* 譊告スタむル */
            .critical-warning { 
                background-color: #160a45; /* 暗い玫背景 */
                border: 3px solid #9333ea; /* 玫の枠線 */
                color: #d8b4fe; /* 薄い玫テキスト */
                padding: 1rem; 
                border-radius: 0.75rem; 
                font-weight: 800; /* Extra bold */
                font-size: 1rem;
            }
        </style>
    </head>
    <body class="bg-gray-900 text-gray-100 min-h-screen p-4 md:p-8">
        <div id="app" class="max-w-6xl mx-auto bg-gray-800 shadow-2xl rounded-xl p-6 md:p-8">
            <h1 class="text-3xl font-bold text-teal-400 mb-6 border-b border-teal-500/50 pb-2">
                AI-GM TRPGシステム
            </h1>
            
            <div id="auth-info" class="mb-4 text-sm text-gray-400">
                <p><strong>認蚌ナヌザヌID:</strong> <span id="user-id">...</span></p>
            </div>
    
            <div class="flex space-x-2 mb-6">
                <button id="player-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-teal-500/50 btn-player-active">
                    プレむダヌ画面
                </button>
                <button id="master-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-fuchsia-500/50 btn-inactive">
                    GM画面
                </button>
            </div>
    
            <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                <div id="main-view" class="lg:col-span-2 bg-gray-700 p-6 rounded-xl shadow-inner min-h-[50vh]">
                </div>
    
                <div class="lg:col-span-1 bg-gray-700 p-6 rounded-xl shadow-inner">
                    <h2 class="text-xl font-bold text-orange-400 mb-4 border-b border-orange-500/50 pb-2">
                        キャラクタヌ・ステヌタス
                    </h2>
                    <div id="status-display"></div>
    
                    <div id="gm-status-editor" class="mt-6 hidden">
                        <h3 class="text-lg font-bold text-orange-400 mb-3">ステヌタス線集 (GM専甚)</h3>
                        <textarea id="status-json-input" rows="8" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-orange-500 focus:border-orange-500" placeholder='{"HP": 100, "MP": 50, "経隓倀": 0, "所持品": "朚剣、粗末な垃服"}'></textarea>
                        <button id="update-status-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-orange-600 hover:bg-orange-700 transition-colors shadow-md">
                            ステヌタス曎新
                        </button>
                        <p id="status-message" class="mt-2 text-sm text-red-400"></p>
                    </div>
                </div>
            </div>
    
            <div id="loading" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="text-white text-xl flex flex-col items-center">
                    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-500 mb-3"></div>
                    ゲヌムマスタヌが思考䞭...
                </div>
            </div>
    
            <!-- カスタムモヌダルalert()の代替 -->
            <div id="custom-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-700 p-6 rounded-xl shadow-2xl max-w-sm w-full text-center">
                    <p id="modal-text" class="text-lg text-gray-100 mb-4"></p>
                    <button id="modal-close" class="px-6 py-2 rounded-lg font-semibold bg-teal-600 hover:bg-teal-700 transition-colors">閉じる</button>
                </div>
            </div>
    
        </div>
    
        <script type="module">
            import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
            import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
            import { getFirestore, doc, getDoc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
            // setLogLevel('Debug'); // デバッグが必芁な堎合はコメントを解陀
    
            const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
            const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
            const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
    
            let db, auth;
            let userId = 'loading'; // Firebaseの認蚌ID
            let sessionKey = 'loading_key'; // 🚚 FIX: 初期倀をプレヌスホルダヌに倉曎
            let unsubscribe = null; // Firestoreリスナヌ解陀甚
            let currentView = 'player';
            let latestGameData = null;
            const SESSION_COLLECTION = 'gm_game_sessions';
    
            // ゲヌムの初期デヌタ
            const initialData = {
                storyLog: [{ role: 'GM', text: 'ようこそ。あなたの冒険が始たりたす。目の前には叀びた掞窟の入り口がありたす。どうしたすか' }],
                playerStatus: { HP: 100, MP: 50, '経隓倀': 0, '所持品': '朚剣、粗末な垃服' },
                lastUpdate: new Date(),
            };
    
            if (firebaseConfig) {
                try {
                    const app = initializeApp(firebaseConfig);
                    db = getFirestore(app);
                    auth = getAuth(app);
                } catch (error) {
                    console.error("Firebaseの初期化に倱敗したした:", error);
                    showModal("システム゚ラヌ: Firebaseの初期化に倱敗したした。");
                }
            }
    
            async function authenticate() {
                try {
                    if (auth) {
                        if (initialAuthToken) {
                            await signInWithCustomToken(auth, initialAuthToken);
                        } else {
                            await signInAnonymously(auth);
                        }
                    }
                } catch (error) {
                    console.error("認蚌゚ラヌ:", error);
                    showModal("認蚌に倱敗したした。");
                }
            }
    
            onAuthStateChanged(auth, (user) => {
                if (user) {
                    userId = user.uid;
                    document.getElementById('user-id').textContent = userId;
                    
                    // 🚚 FIX: セッションキヌをナヌザヌID付きでナニヌク化
                    const defaultUniqueKey = `Default_Session_${userId}`; 
                    const storedKey = localStorage.getItem('last_session_key');
    
                    if (storedKey) {
                        sessionKey = storedKey; // 以前䜿甚したキヌがあればそれを䜿甚
                    } else {
                        sessionKey = defaultUniqueKey; // 初回はナニヌクなキヌを蚭定
                        localStorage.setItem('last_session_key', sessionKey); // 保存
                    }
    
                    setupFirestoreListener(); // 初期リスナヌ蚭定
                } else {
                    authenticate();
                }
            });
    
            // Firestoreのドキュメント参照を取埗**sessionKeyを䜿甚**
            function getSessionDocRef() {
                if (!db || !sessionKey || sessionKey === 'loading_key') {
                    console.error("DB接続たたはセッションキヌが未蚭定です。");
                    // デヌタが䜿えない堎合でも゚ラヌを出さないための安党策実際には認蚌埌に呌ばれるはず
                    return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, 'TEMP_ERROR_DOC');
                }
                // デヌタは垞に sessionKey に玐づくパブリック領域
                return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, sessionKey);
            }
            
            // セッションキヌの倉曎ずリスナヌの再蚭定
            function updateSessionKey(newKey) {
                if (newKey && newKey !== sessionKey) {
                    // 叀いリスナヌを解陀
                    if (unsubscribe) {
                        unsubscribe();
                        unsubscribe = null;
                        console.log(`叀いセッションキヌ (${sessionKey}) のリスナヌを解陀したした。`);
                    }
                    
                    // 新しいキヌを蚭定
                    sessionKey = newKey;
                    localStorage.setItem('last_session_key', newKey);
                    console.log(`セッションキヌを ${sessionKey} に倉曎したした。`);
    
                    // 新しいリスナヌを蚭定
                    setupFirestoreListener();
                } else if (!newKey) {
                     showModal("セッションキヌを空にするこずはできたせん。");
                }
            }
    
            // セッションが存圚しない堎合に初期化する
            async function initializeSession() {
                try {
                    if (!db || sessionKey === 'loading_key') return;
                    const docSnap = await getDoc(getSessionDocRef());
                    if (!docSnap.exists()) {
                        await setDoc(getSessionDocRef(), initialData, { merge: true });
                    }
                } catch (error) {
                    console.error("セッションの初期化に倱敗したした:", error);
                    showModal(`セッション初期化゚ラヌ: デヌタを保存できたせんでした。キヌ: ${sessionKey}`);
                }
            }
    
            // Firestoreのリアルタむムリスナヌを蚭定する
            function setupFirestoreListener() {
                if (!db || sessionKey === 'loading_key' || userId === 'loading') return;
                const docRef = getSessionDocRef();
                
                // 初回たたはキヌ倉曎時に初期化を詊行
                initializeSession(); 
                
                // リスナヌをセットアップし、解陀関数を保存
                unsubscribe = onSnapshot(docRef, (docSnap) => {
                    if (docSnap.exists()) {
                        const data = docSnap.data();
                        latestGameData = data;
                        renderGame(data);
                    } else {
                        initializeSession(); // ドキュメントが存圚しない堎合は再初期化を詊みる
                    }
                }, (error) => {
                    console.error("Firestoreリスナヌ゚ラヌ:", error);
                    showModal(`デヌタ同期゚ラヌが発生したした。セッションキヌ: ${sessionKey}`);
                });
            }
            
            // ストヌリヌログに新しい゚ントリを远加する
            async function updateStoryLog(newEntry) {
                if (!db || sessionKey === 'loading_key') return null;
                const docRef = getSessionDocRef();
                try {
                    const docSnap = await getDoc(docRef);
                    const currentData = docSnap.data() || initialData;
                    const updatedLog = [...currentData.storyLog, newEntry];
                    
                    await setDoc(docRef, { 
                        storyLog: updatedLog,
                        lastUpdate: new Date()
                    }, { merge: true });
    
                    return updatedLog;
                } catch (error) {
                    console.error("ログの曎新に倱敗したした:", error);
                    showModal("ログ曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return null;
                }
            }
            
            // プレむダヌステヌタスを曎新する
            async function updateStatus(newStatus) {
                if (!db || sessionKey === 'loading_key') return false;
                const docRef = getSessionDocRef();
                try {
                    await setDoc(docRef, { 
                        playerStatus: newStatus,
                        lastUpdate: new Date()
                    }, { merge: true });
                    return true;
                } catch (error) {
                    console.error("ステヌタスの曎新に倱敗したした:", error);
                    showModal("ステヌタス曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return false;
                }
            }
            
            // メむンビュヌをレンダリングする
            function renderGame(data) {
                const mainView = document.getElementById('main-view');
                const playerBtn = document.getElementById('player-view-btn');
                const masterBtn = document.getElementById('master-view-btn');
                const logContainer = createLogContainer(data.storyLog);
                const isGameOver = data.playerStatus.HP <= 0;
                let gameOverMessage = isGameOver ? '<div class="game-over-message text-2xl text-white">ゲヌムオヌバヌ</div>' : '';
    
                // ダむスロヌルセクション
                const diceRollSection = `
                    <div class="mb-4 p-4 bg-gray-600 rounded-xl shadow-lg ${isGameOver ? 'opacity-50 pointer-events-none' : ''}">
                        <h3 class="text-lg font-semibold text-white mb-3 border-b border-gray-500 pb-2">ダむスロヌル</h3>
                        
                        <div class="flex flex-col space-y-3">
                            <!-- D100 ロヌル -->
                            <button data-dice="100" class="w-full px-6 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md flex items-center justify-center">
                                <span class="text-xl mr-2">🎲</span> D100 (パヌセンテヌゞ) ロヌル
                            </button>
    
                            <!-- カスタム ロヌル -->
                            <div class="flex space-x-2">
                                <input type="text" id="custom-dice-input" placeholder="䟋: 2D6+3" class="flex-grow p-2 rounded-lg bg-gray-800 text-gray-100 border border-gray-500 focus:ring-blue-500 focus:border-blue-500 text-sm">
                                <button data-dice="custom" class="flex-shrink-0 px-4 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md">
                                    カスタムロヌル
                                </button>
                            </div>
                            
                            <!-- D6 ロヌル -->
                            <button data-dice="6" class="w-full px-6 py-2 rounded-lg font-bold bg-gray-500 hover:bg-gray-600 transition-colors shadow-md flex items-center justify-center">
                                <span class="text-lg mr-2">🔹</span> D6 ロヌル
                            </button>
                        </div>
                    </div>
                `;
                
                renderStatusDisplay(data.playerStatus);
    
                if (currentView === 'player') {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderPlayerView(logContainer, isGameOver);
                    setupPlayerViewListeners(data.storyLog, data.playerStatus);
                    document.getElementById('gm-status-editor').classList.add('hidden');
                    playerBtn.classList.remove('btn-inactive'); playerBtn.classList.add('btn-player-active');
                    masterBtn.classList.remove('btn-master-active'); masterBtn.classList.add('btn-inactive');
                } else {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderMasterView(logContainer);
                    setupMasterViewListeners(data.storyLog);
                    document.getElementById('gm-status-editor').classList.remove('hidden');
                    masterBtn.classList.remove('btn-inactive'); masterBtn.classList.add('btn-master-active');
                    playerBtn.classList.remove('btn-player-active'); playerBtn.classList.add('btn-inactive');
                    
                    // GM画面衚瀺時に党デヌタJSONを衚瀺
                    const dataJsonElement = document.getElementById('session-data-json');
                    if (dataJsonElement) {
                        dataJsonElement.value = JSON.stringify(data, null, 2);
                    }
    
                    // GM画面衚瀺時にセッション芁玄の生成を開始
                    displaySummary(data.storyLog, data.playerStatus);
                }
                
                setupDiceRollListener(isGameOver);
                // GM画面でステヌタス線集甚にJSONを衚瀺
                document.getElementById('status-json-input').value = JSON.stringify(data.playerStatus, null, 2);
    
                // ログコンテナを最䞋郚にスクロヌル
                setTimeout(() => {
                    const scrollable = document.querySelector('.log-container');
                    if (scrollable) scrollable.scrollTop = scrollable.scrollHeight;
                }, 100);
            }
    
            // ログコンテナのHTMLを生成する (倉曎なし)
            function createLogContainer(storyLog) {
                let logHTML = storyLog.map(entry => {
                    const isGM = entry.role === 'GM';
                    const isDice = entry.role === 'Dice';
                    const isSystem = entry.role === 'System';
                    
                    let roleColor = isGM ? 'text-green-400' : isDice ? 'text-yellow-400' : isSystem ? 'text-red-400' : 'text-blue-400';
                    let bgColor = isGM ? 'bg-gray-800' : isDice ? 'bg-gray-800 border border-yellow-500/50' : isSystem ? 'bg-gray-800 border border-red-500/50' : 'bg-gray-700';
                    let align = isGM || isSystem ? 'text-left' : isDice ? 'text-center' : 'text-right';
                    
                    return `
                        <div class="mb-3 ${align}">
                            <div class="${bgColor} inline-block p-3 rounded-xl shadow-md max-w-full lg:max-w-[70%]">
                                <span class="font-bold text-sm ${roleColor}">${entry.role}:</span>
                                <p class="mt-1 whitespace-pre-wrap text-gray-200 ${isDice ? 'dice-result' : ''}">${entry.text}</p>
                            </div>
                        </div>
                    `;
                }).join('');
                
                return `<div class="log-container space-y-3 p-2 bg-gray-900 rounded-lg border border-gray-600">${logHTML}</div>`;
            }
            
            // ステヌタス衚瀺のHTMLを生成する (倉曎なし)
            function renderStatusDisplay(status) {
                const display = document.getElementById('status-display');
                let html = '';
                for (const [key, value] of Object.entries(status)) {
                    let displayValue = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : String(value);
                    
                    if (key === 'HP' || key === 'MP') {
                        const maxVal = 100; // 仮の最倧倀
                        const color = key === 'HP' ? (value <= 20 ? 'bg-red-500' : 'bg-green-500') : 'bg-blue-500';
                        const percentage = Math.max(0, Math.min(100, (value / maxVal) * 100)); 
                        html += `
                            <div class="mb-2">
                                <p class="text-sm text-gray-300 mb-1">${key}: <span class="font-semibold text-white">${displayValue}</span></p>
                                <div class="w-full bg-gray-600 rounded-full h-2">
                                    <div class="${color} h-2 rounded-full transition-all duration-500" style="width: ${percentage}%;"></div>
                                </div>
                            </div>
                        `;
                    } else {
                        // 所持品などの長い文字列もpre-wrapで改行を維持
                        html += `<p class="text-sm text-gray-300">${key}: <span class="font-semibold text-white whitespace-pre-wrap">${displayValue}</span></p>`;
                    }
                }
                display.innerHTML = html;
            }
    
            // プレむダヌビュヌのHTMLを生成する (倉曎なし)
            function renderPlayerView(logContainer, isGameOver) {
                const disabledClass = isGameOver ? 'opacity-50 pointer-events-none' : '';
                return `
                    <h2 class="text-2xl font-bold text-teal-400 mb-4">冒険の蚘録 (プレむダヌ)</h2>
                    ${logContainer}
                    <div class="mt-4 ${disabledClass}">
                        <h3 class="font-bold text-lg mb-2">あなたの行動</h3>
                        <textarea id="player-action-input" rows="4" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-teal-500 focus:ring-teal-500 focus:border-teal-500 resize-none" placeholder="${isGameOver ? 'ゲヌムオヌバヌです。新しい冒険を始めるにはGM画面でステヌタスをリセットしおください。' : '掞窟に入りたす、剣を抜きたす、など'}" ${isGameOver ? 'disabled' : ''}></textarea>
                        <button id="send-action-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-teal-400 hover:bg-teal-300 transition-colors shadow-lg">
                            行動を宣蚀し、GMの応答を埅぀
                        </button>
                        <button id="intervene-btn" class="w-full mt-4 px-4 py-3 rounded-lg font-bold text-gray-900 btn-intervene hover:opacity-90 transition-opacity shadow-lg">
                            GMずしお状況に介入する/ステヌタスを線集する
                        </button>
                    </div>
                `;
            }
    
            // GMビュヌのHTMLを生成する (譊告文を曎新)
            function renderMasterView(logContainer) {
                return `
                    <h2 class="text-2xl font-bold text-fuchsia-400 mb-4">セッション管理 (ゲヌムマスタヌ)</h2>
    
                    <!-- セッションキヌ蚭定 -->
                    <div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg border border-fuchsia-500/50">
                        <h3 class="font-bold text-lg text-fuchsia-400 mb-2">セッションキヌ蚭定 (æ°žç¶šID)</h3>
                        
                        <!-- 🚚 譊告を曎新 🚚 -->
                        <p class="critical-warning mb-4">
                            【💡重芁】デフォルトでは**あなたのナヌザヌIDに玐づいたプラむベヌトキヌ**が蚭定されおいたす。
                            他の人ず**共有しお䞀緒に遊ぶ堎合**は、必ず独自のキヌ䟋: 「アルフの冒険-1028」に倉曎し、盞手に䌝えおください。
                        </p>
                        
                        <div class="flex space-x-2">
                            <input type="text" id="session-key-input" value="${sessionKey}" class="flex-grow p-2 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 text-sm" placeholder="セッションID (䟋: DragonQuest-001)">
                            <button id="set-session-key-btn" class="flex-shrink-0 px-4 py-2 rounded-lg font-semibold bg-fuchsia-600 hover:bg-fuchsia-700 transition-colors shadow-md text-white">
                                キヌ蚭定
                            </button>
                        </div>
                    </div>
                    
                    <!-- セヌブ・同期管理 -->
                    <div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg">
                        <h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">セヌブ・同期管理</h3>
                        <div id="save-load-message" class="mb-3 text-sm min-h-[1.5rem] text-yellow-400"></div>
                        <div class="flex space-x-3">
                            <button id="manual-save-btn" class="flex-1 px-4 py-2 rounded-lg font-semibold bg-red-600 hover:bg-red-700 transition-colors shadow-md text-white">
                                手動で匷制セヌブ
                            </button>
                            <button id="manual-load-btn" class="flex-1 px-4 py-2 rounded-lg font-semibold bg-gray-600 hover:bg-gray-700 transition-colors shadow-md text-white">
                                匷制リロヌド (最新同期)
                            </button>
                        </div>
                    </div>
    
                    <!-- NEW: セッションデヌタ (JSONバックアップ) -->
                    <div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg border border-fuchsia-500/50">
                        <h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">
                            セッションデヌタ (JSONバックアップ)
                        </h3>
                        <p class="text-xs text-gray-400 mb-2">珟圚のセッションキヌ (${sessionKey}) に玐づく党おのデヌタです。</p>
                        <textarea id="session-data-json" rows="12" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none font-mono"></textarea>
                        <button id="copy-session-data-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-fuchsia-600 hover:bg-fuchsia-700 transition-colors shadow-md text-white">
                            セヌブデヌタ党䜓をコピヌ
                        </button>
                    </div>
                    
                    <!-- 匕き継ぎ甚セッション芁玄 -->
                    <div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg">
                        <h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">匕き継ぎ甚セッション芁玄</h3>
                        <div id="session-summary" class="p-4 bg-gray-800 rounded-lg min-h-[5rem] text-sm text-gray-300">
                            <div class="flex items-center justify-center h-full">
                                <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-fuchsia-400 mr-3"></div>
                                芁玄デヌタをAIが生成䞭...
                            </div>
                        </div>
                    </div>
    
                    ${logContainer}
    
                    <div class="mt-4">
                        <h3 class="font-bold text-lg mb-2">GMナラティブ生成</h3>
                        <button id="prompt-roll-btn" class="w-full mt-2 mb-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-purple-400 hover:bg-purple-300 transition-colors shadow-lg">
                            刀定ロヌルを促す指瀺を入力
                        </button>
                        <textarea id="gm-narrative-input" rows="6" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none" placeholder="AIぞの指瀺内容を入力したす。䟋プレむダヌを絶䜓絶呜の危機に陥らせろ。"></textarea>
                        <button id="generate-narrative-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-fuchsia-400 hover:bg-fuchsia-300 transition-colors shadow-lg">
                            AI-GMに展開を生成させる
                        </button>
                    </div>
                `;
            }
    
            // --- 以䞋、ビュヌ切り替え、ダむスロヌル、ステヌタス、GM/プレむダヌビュヌのロゞックは倉曎なし ---
    
            // ビュヌを切り替える
            function switchView(viewName) {
                currentView = viewName;
                if (latestGameData) {
                    renderGame(latestGameData); 
                }
            }
    
            document.getElementById('player-view-btn').addEventListener('click', () => switchView('player'));
            document.getElementById('master-view-btn').addEventListener('click', () => switchView('master'));
    
            // --- セヌブ・ロヌド凊理 (倉曎なし) ---
    
            async function handleSave() {
                const messageDiv = document.getElementById('save-load-message');
                if (!db || !latestGameData || sessionKey === 'loading_key') {
                    messageDiv.textContent = '゚ラヌ: デヌタベヌス接続たたはデヌタがありたせん。';
                    messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                    return;
                }
    
                messageDiv.textContent = 'セヌブ䞭...';
                messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-yellow-400';
    
                const docRef = getSessionDocRef();
                try {
                    // merge:false で珟圚のデヌタで完党に䞊曞き匷制セヌブ
                    await setDoc(docRef, {
                        ...latestGameData,
                        lastManualSave: new Date().toISOString()
                    }, { merge: false });
                    
                    messageDiv.textContent = `✅ セヌブ成功: ${new Date().toLocaleTimeString()}に保存されたした。`;
                    messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-green-400';
                } catch (error) {
                    console.error("手動セヌブに倱敗したした:", error);
                    messageDiv.textContent = `❌ セヌブ倱敗: ゚ラヌが発生したした (${error.message})。セッションキヌ: ${sessionKey}`;
                    messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                }
                // 5秒埌にメッセヌゞをクリア
                setTimeout(() => { messageDiv.textContent = ''; }, 5000);
            }
    
            async function handleLoad() {
                const messageDiv = document.getElementById('save-load-message');
                if (!db || sessionKey === 'loading_key') {
                    messageDiv.textContent = '゚ラヌ: デヌタベヌス接続がありたせん。';
                    messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                    return;
                }
                
                messageDiv.textContent = '最新デヌタを匷制リロヌド䞭...';
                messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-yellow-400';
    
                try {
                    const docSnap = await getDoc(getSessionDocRef());
                    if (docSnap.exists()) {
                        const data = docSnap.data();
                        latestGameData = data;
                        renderGame(data); // 取埗したデヌタで匷制的にUIを曎新
                        messageDiv.textContent = `✅ ロヌド成功: ${new Date().toLocaleTimeString()}のデヌタに曎新したした。セッションキヌ: ${sessionKey}`;
                        messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-green-400';
                    } else {
                        messageDiv.textContent = `❌ ロヌド倱敗: セッションキヌ (${sessionKey}) のデヌタが芋぀かりたせん。`;
                        messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                    }
                } catch (error) {
                    console.error("匷制ロヌドに倱敗したした:", error);
                    messageDiv.textContent = `❌ ロヌド倱敗: ゚ラヌが発生したした (${error.message})。`;
                    messageDiv.className = 'mb-3 text-sm min-h-[1.5rem] text-red-400';
                }
                // 5秒埌にメッセヌゞをクリア
                setTimeout(() => { messageDiv.textContent = ''; }, 5000);
            }
    
            // --- ダむスロヌル、ステヌタス、プレむダヌビュヌのロゞック (倉曎なし/割愛) ---
            
            // ダむス文字列をパヌスする (䟋: 2D6+3)
            function parseDiceString(diceString) {
                const match = diceString.match(/^(\d*)D(\d+)(?:\s*([+-])\s*(\d+))?$/i);
                if (match) {
                    const numDice = parseInt(match[1] || '1', 10);
                    const sides = parseInt(match[2], 10);
                    const sign = match[3] || '+';
                    const modifier = parseInt(match[4] || '0', 10) * (sign === '-' ? -1 : 1);
                    // 実行可胜な制限
                    if (numDice > 100 || sides > 1000) return null; 
                    return { numDice, sides, modifier };
                }
                return null;
            }
    
            // ダむスを振る
            function rollDice(numDice, sides, modifier = 0) {
                let total = 0;
                const rolls = [];
                for (let i = 0; i < numDice; i++) {
                    const roll = Math.floor(Math.random() * sides) + 1;
                    rolls.push(roll);
                    total += roll;
                }
                const finalResult = total + modifier;
                return { rolls, total, finalResult, modifier };
            }
    
            // ダむスロヌルボタンのリスナヌ蚭定
            function setupDiceRollListener(isGameOver) {
                const rollBtns = document.querySelectorAll('[data-dice]');
                const customInput = document.getElementById('custom-dice-input');
                
                rollBtns.forEach(btn => {
                    // 既存のむベントリスナヌを削陀renderGameが毎回呌ばれるため
                    btn.onclick = null; 
                    
                    if (!isGameOver) {
                        btn.onclick = async (e) => {
                            const diceType = e.currentTarget.dataset.dice;
                            let rollResult = null;
                            let rollString = '';
                            
                            if (diceType === '6') {
                                rollString = '1D6';
                                rollResult = rollDice(1, 6);
                            } else if (diceType === '100') {
                                rollString = 'D100';
                                rollResult = rollDice(1, 100);
                            } else if (diceType === 'custom') {
                                const customStr = customInput.value.trim();
                                const parsed = parseDiceString(customStr);
                                
                                if (parsed) {
                                    rollString = `${parsed.numDice}D${parsed.sides}${parsed.modifier !== 0 ? (parsed.modifier > 0 ? '+' : ''): ''}${parsed.modifier !== 0 ? parsed.modifier : ''}`;
                                    rollResult = rollDice(parsed.numDice, parsed.sides, parsed.modifier);
                                    customInput.value = ''; // 成功したらクリア
                                } else {
                                    showModal("カスタムダむスの圢匏が䞍正です。䟋: 2D6+3, D100, 3D8");
                                    return;
                                }
                            }
    
                            if (rollResult) {
                                const { rolls, total, finalResult, modifier } = rollResult;
                                
                                let message = `【${rollString} ロヌル (結果: ${finalResult})】\n`;
                                
                                if (rolls.length > 1) {
                                    message += `個別出目: [${rolls.join(', ')}] = ${total}`;
                                    if (modifier !== 0) {
                                        message += ` ${modifier > 0 ? '+' : ''}${modifier} (修正倀)`;
                                    }
                                    message += `\n**最終結果: ${finalResult}**`;
                                } else if (modifier !== 0) {
                                    message += `出目: ${total} ${modifier > 0 ? '+' : ''}${modifier} (修正倀)\n**最終結果: ${finalResult}**`;
                                } else {
                                    message += `出目: ${finalResult}`;
                                }
                                
                                if (diceType === '100') {
                                     message += finalResult <= 5 ? ' (クリティカル) - 成功' : finalResult >= 96 ? ' (ファンブル...) - 倱敗' : '';
                                }
    
                                await updateStoryLog({ role: 'Dice', text: message });
                            }
                        };
                    }
                });
            }
            
            // ステヌタス線集のリスナヌ蚭定
            document.getElementById('update-status-btn').addEventListener('click', () => {
                const jsonInput = document.getElementById('status-json-input').value;
                const statusMsg = document.getElementById('status-message');
                statusMsg.textContent = '';
                
                try {
                    const newStatus = JSON.parse(jsonInput);
                    if (typeof newStatus !== 'object' || newStatus === null || typeof newStatus.HP !== 'number') {
                        throw new Error("JSON圢匏が䞍正、たたはHPフィヌルドが数倀ではありたせん。");
                    }
                    updateStatus(newStatus);
                    statusMsg.textContent = 'ステヌタスを曎新したした';
                    statusMsg.classList.remove('text-red-400');
                    statusMsg.classList.add('text-green-400');
                } catch (e) {
                    statusMsg.textContent = 'JSON圢匏が䞍正です: ' + e.message;
                    statusMsg.classList.remove('text-green-400');
                    statusMsg.classList.add('text-red-400');
                }
            });
            
            // プレむダヌビュヌのリスナヌ蚭定
            function setupPlayerViewListeners(storyLog, currentStatus) {
                const sendBtn = document.getElementById('send-action-btn');
                const interveneBtn = document.getElementById('intervene-btn');
                const isGameOver = currentStatus.HP <= 0;
    
                if (interveneBtn) interveneBtn.onclick = () => switchView('master');
                
                if (sendBtn) sendBtn.onclick = null;
    
                if (sendBtn && !isGameOver) {
                    sendBtn.onclick = async () => {
                        const input = document.getElementById('player-action-input');
                        const action = input.value.trim();
                        if (!action) { showModal("プレむダヌの行動を入力しおください。"); return; }
                        
                        await updateStoryLog({ role: 'Player', text: action });
                        input.value = '';
    
                        document.getElementById('loading').classList.remove('hidden');
                        
                        const gmInstruction = "盎前のプレむダヌの行動をGMずしお凊理し、その結果成功/倱敗刀定、たたは単なる描写ず次の展開を蚘述せよ。もし盎前のログにダむスロヌル結果があれば、その結果に基づいお刀定を行うこず。結果に応じお、珟圚のステヌタスを曎新HP/MPの増枛し、必ずJSON圢匏でナラティブず曎新埌のステヌタスを返华せよ。";
                        const fullLog = [...storyLog, { role: 'Player', text: action }];
                        
                        const gmResponseObject = await generateNarrative(gmInstruction, fullLog, currentStatus, false);
                        
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            const { narrative, updatedStatus } = gmResponseObject;
                            await updateStoryLog({ role: 'GM', text: narrative });
                            await updateStatus(updatedStatus);
    
                            if (updatedStatus.HP <= 0 && !narrative.includes("ゲヌムオヌバヌ")) {
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        } else {
                            const fallbackNarrative = `【GM介入 (システム応答)】あなたの行動「${action}」により、物語が進行したす。AI-GMぞの接続に倱敗したため、システムが暫定的に凊理を行いたす。あなたは運良く掞窟の奥ぞず無傷で進むこずができたしたが、少し疲劎したした。`;
                            const fallbackStatus = { 
                                ...currentStatus, 
                                HP: currentStatus.HP > 5 ? currentStatus.HP - 5 : 0, 
                                MP: currentStatus.MP 
                            };
                            
                            showModal(`GMの応答を生成できたせんでした。システムのフォヌルバック応答で進行したす。API呌び出しの制限を超過した可胜性がありたす。`);
                            await updateStoryLog({ role: 'System', text: fallbackNarrative });
                            await updateStatus(fallbackStatus);
    
                            if (fallbackStatus.HP <= 0) {
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        }
                    };
                }
            }
            
            // --- GM画面 ロゞック (倉曎なし) ---
    
            function setupMasterViewListeners(storyLog) {
                const generateBtn = document.getElementById('generate-narrative-btn');
                const input = document.getElementById('gm-narrative-input');
                const promptRollBtn = document.getElementById('prompt-roll-btn');
                const saveBtn = document.getElementById('manual-save-btn');
                const loadBtn = document.getElementById('manual-load-btn');
                const setKeyBtn = document.getElementById('set-session-key-btn');
                const keyInput = document.getElementById('session-key-input');
                
                // セッションキヌ蚭定ボタンのリスナヌ
                if (setKeyBtn) {
                    setKeyBtn.onclick = () => {
                        const newKey = keyInput.value.trim();
                        updateSessionKey(newKey);
                    };
                }
                
                if (saveBtn) saveBtn.onclick = handleSave;
                if (loadBtn) loadBtn.onclick = handleLoad;
    
                if (promptRollBtn) {
                    promptRollBtn.onclick = () => {
                        input.value = "盎前のプレむダヌの行動に察し、D100ロヌルによる刀定を促し、その結果に基づいお物語の展開を蚘述せよ。";
                        showModal("刀定ロヌルを促す定型文が入力されたした。必芁に応じお線集しおください。");
                    };
                }
                
                // 既存のむベントリスナヌを削陀
                if (generateBtn) generateBtn.onclick = null;
    
                if (generateBtn) {
                    generateBtn.onclick = async () => {
                        const gmInstruction = input.value.trim();
                        if (!gmInstruction) { showModal("AIぞの指瀺を入力しおください。"); return; }
                        
                        document.getElementById('loading').classList.remove('hidden');
                        const gmResponseObject = await generateNarrative(gmInstruction, storyLog, latestGameData?.playerStatus || initialData.playerStatus, true);
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            await updateStoryLog({ role: 'GM', text: gmResponseObject.narrative });
                            input.value = '';
                        } else {
                            showModal("GMナラティブの生成に倱敗したした。AIからの応答が空か、たたは認蚌/API呌び出し゚ラヌです。");
                        }
                    };
                }
                
                // NEW: セヌブデヌタコピヌボタンのリスナヌ
                const copyDataBtn = document.getElementById('copy-session-data-btn');
    
                if (copyDataBtn) {
                    // 既存のむベントリスナヌを削陀renderGameが毎回呌ばれるため
                    copyDataBtn.onclick = null;
                    
                    copyDataBtn.onclick = () => {
                        const jsonTextarea = document.getElementById('session-data-json');
                        
                        if (!jsonTextarea.value) {
                             showModal("コピヌするデヌタがありたせん。デヌタ同期を埅぀か、手動でリロヌドしおください。");
                             return;
                        }
    
                        // テキスト゚リアの内容を遞択し、コピヌを実行
                        try {
                            jsonTextarea.select();
                            document.execCommand('copy');
                            showModal("✅ セッションデヌタ党䜓をクリップボヌドにコピヌしたした");
                        } catch (err) {
                            console.error('コピヌ倱敗:', err);
                            showModal("❌ コピヌに倱敗したした。手動でテキスト゚リアの内容をコピヌしおください。");
                        }
                    };
                }
            }
    
            // --- 芁玄衚瀺ロゞック (倉曎なし) ---
            async function displaySummary(storyLog, currentStatus) {
                const summaryDiv = document.getElementById('session-summary');
                
                // 芁玄生成前のロヌディング衚瀺
                summaryDiv.innerHTML = `
                    <div class="flex items-center h-full text-fuchsia-400">
                        <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-fuchsia-400 mr-3"></div>
                        芁玄デヌタをAIが生成䞭...
                    </div>`;
    
                const summary = await generateSummary(storyLog, currentStatus);
    
                if (summary && summary.currentLocation) {
                    summaryDiv.innerHTML = `
                        <div class="space-y-3 text-gray-300">
                            <p class="font-bold text-sm text-fuchsia-300">セッションID (キヌ):</p>
                            <p class="pl-4 text-white text-xs break-all font-mono">${sessionKey}</p>
                            
                            <p class="font-bold">珟圚の堎所:</p>
                            <p class="pl-4 text-white">${summary.currentLocation}</p>
                            <p class="font-bold">盎近の出来事:</p>
                            <p class="pl-4 text-white">${summary.latestConflict}</p>
                            <p class="font-bold">プレむダヌ状態:</p>
                            <p class="pl-4 text-white">${summary.playerCondition}</p>
                        </div>
                    `;
                } else {
                    summaryDiv.innerHTML = '<p class="text-red-400">芁玄の生成に倱敗したした。ログが䞍足しおいるか、AI応答に゚ラヌがありたした。</p>';
                }
            }
            
            // --- Gemini API 連携 ロゞック (倉曎なし) ---
    
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
    
            // AIの応答スキヌマ定矩 (ナラティブ生成甚)
            const narrativeResponseSchema = {
                type: "OBJECT",
                properties: {
                    narrative: { type: "STRING", description: "GMずしお生成する物語の展開。" },
                    updatedStatus: { 
                        type: "OBJECT", 
                        description: "曎新されたプレむダヌのステヌタス。HPが0になった堎合は必ず「ゲヌムオヌバヌだ。」ずいう文章をナラティブに含めるこず。",
                        properties: { 
                            HP: { type: "NUMBER" }, 
                            MP: { type: "NUMBER" }, 
                            '経隓倀': { type: "NUMBER" }, 
                            '所持品': { "type": "STRING" } 
                        },
                        required: ["HP", "MP", "経隓倀", "所持品"]
                    }
                },
                required: ["narrative"], 
            };
    
            async function generateNarrative(gmInstruction, storyLog, currentStatus, isMasterInput = false) {
                const context = storyLog.slice(-10).map(e => `${e.role}: ${e.text}`).join('\n'); // ログを最新10件に制限
                
                const statusInstruction = isMasterInput 
                    ? "GMからの指瀺によるナラティブのみを生成し、updatedStatusオブゞェクトは含めないでください。この堎合、responseSchemaのupdatedStatusは無芖しおください。" 
                    : "プレむダヌの行動の結果ずしお、必ずupdatedStatusオブゞェクトを含め、HP/MPの増枛を反映させおください。HPが0以䞋になった堎合、必ずナラティブに「ゲヌムオヌバヌだ。」ずいう文章を加えおください。";
    
                const systemPrompt = `
                    あなたはファンタゞヌTRPGのゲヌムマスタヌGMです。以䞋の制玄に埓っお応答を生成しおください。
                    **GMの圹割ず制玄**
                    1. 応答は必ずJSON圢匏で、ナラティブを含めるこず。
                    2. ログ内の 'Dice' ゚ントリの結果䟋: 【1D6 ロヌル (結果: 5)】を最優先で参照し、その目に基づいお刀定を行うこず。
                    3. ${statusInstruction}
                    **珟圚のプレむダヌ情報**
                    ${JSON.stringify(currentStatus, null, 2)}
                `;
                
                const userQuery = `
                    --- 盎近のストヌリヌログ ---
                    ${context}
                    --- GMからのAIぞの具䜓的な指瀺 ---
                    ${gmInstruction}
                `;
    
                const payload = {
                    contents: [{ parts: [{ text: userQuery }] }],
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    generationConfig: {
                        responseMimeType: "application/json",
                        responseSchema: narrativeResponseSchema
                    }
                };
    
                const maxRetries = 3;
                for (let i = 0; i < maxRetries; i++) {
                    try {
                        const response = await fetch(apiUrl, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(payload)
                        });
    
                        if (!response.ok) {
                            if (response.status === 401) throw new Error(`認蚌゚ラヌ (401): APIキヌが正しくありたせん。`);
                            if (response.status === 429 && i < maxRetries - 1) {
                                const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                                await new Promise(resolve => setTimeout(resolve, delay));
                                continue; 
                            }
                            throw new Error(`API゚ラヌ: ${response.status} ${response.statusText}`);
                        }
    
                        const result = await response.json();
                        const jsonString = result.candidates?.[0]?.content?.parts?.[0]?.text;
                        
                        if (jsonString) {
                            try {
                                const gmResponse = JSON.parse(jsonString);
                                if (isMasterInput) {
                                    if (gmResponse.narrative) {
                                        return { narrative: gmResponse.narrative };
                                    }
                                } else {
                                    if (gmResponse.narrative && gmResponse.updatedStatus) {
                                        return gmResponse; 
                                    }
                                }
                                throw new Error("AI応答のJSON構造が䞍完党です。");
    
                            } catch (e) {
                                console.error("AI応答のJSONパヌスたたは構造怜蚌゚ラヌ:", e, "Raw Text:", jsonString);
                                return null;
                            }
                        } else {
                            console.error("AIからの応答が空でした:", result);
                            return null;
                        }
                    } catch (error) {
                        console.error(`API呌び出し詊行 ${i + 1}/${maxRetries} で゚ラヌが発生したした:`, error.message);
                        if (i < maxRetries - 1) {
                            if (error.message.includes("401")) {
                                 break;
                            }
                            const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                            await new Promise(resolve => setTimeout(resolve, delay));
                            continue; 
                        } else {
                            console.error("API呌び出しが最倧リトラむ回数に達したした。");
                            return null;
                        }
                    }
                }
                return null;
            }
    
            // --- セッション芁玄生成ロゞック (倉曎なし) ---
            async function generateSummary(storyLog, currentStatus) {
                const summarySchema = {
                    type: "OBJECT",
                    properties: {
                        currentLocation: { type: "STRING", description: "プレむダヌが珟圚いる堎所や堎面。" },
                        latestConflict: { type: "STRING", description: "盎前の䞻芁な出来事や戊闘、たたは解決すべき問題。" },
                        playerCondition: { type: "STRING", description: "プレむダヌのHP/MP、重芁な所持品、粟神状態など。" }
                    },
                    required: ["currentLocation", "latestConflict", "playerCondition"]
                };
    
                const systemPrompt = `
                    あなたはTRPGセッションのログを分析するアシスタントです。GMの匕き継ぎのために、盎近の展開ずプレむダヌの状況を構造化されたJSON圢匏で芁玄しおください。
                    制玄: 蚘述は簡朔で、GMがすぐに状況を把握できるようにしおください。
                `;
                
                const context = storyLog.slice(-20).map(e => `${e.role}: ${e.text}`).join('\n'); 
                
                const userQuery = `
                    --- 盎近のストヌリヌログ ---
                    ${context}
                    --- 珟圚のステヌタス ---
                    ${JSON.stringify(currentStatus, null, 2)}
                    䞊蚘の情報を分析し、JSONスキヌマに埓っお芁玄を生成しおください。
                `;
    
                const payload = {
                    contents: [{ parts: [{ text: userQuery }] }],
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    generationConfig: {
                        responseMimeType: "application/json",
                        responseSchema: summarySchema
                    }
                };
                
                const maxRetries = 3;
                for (let i = 0; i < maxRetries; i++) {
                    try {
                        const response = await fetch(apiUrl, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(payload)
                        });
    
                        if (!response.ok) {
                            if (response.status === 429 && i < maxRetries - 1) {
                                const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                                await new Promise(resolve => setTimeout(resolve, delay));
                                continue;
                            }
                            throw new Error(`API゚ラヌ: ${response.status} ${response.statusText}`);
                        }
    
                        const result = await response.json();
                        const jsonString = result.candidates?.[0]?.content?.parts?.[0]?.text;
                        
                        if (jsonString) {
                            try {
                                return JSON.parse(jsonString);
                            } catch (e) {
                                console.error("芁玄JSONパヌス゚ラヌ:", e, "Raw Text:", jsonString);
                                return null;
                            }
                        }
                        return null;
    
                    } catch (error) {
                        console.error(`芁玄API呌び出し詊行 ${i + 1}/${maxRetries} で゚ラヌが発生したした:`, error.message);
                    }
                }
                return null;
            }
    
            // --- モヌダル衚瀺関数 ---
            function showModal(message) {
                const modal = document.getElementById('custom-modal');
                document.getElementById('modal-text').textContent = message;
                modal.classList.remove('hidden');
            }
    
            document.getElementById('modal-close').addEventListener('click', () => {
                document.getElementById('custom-modal').classList.add('hidden');
            });
    
            // 初回ロヌド時の凊理
            if (firebaseConfig) {
                authenticate();
            } else {
                console.error("Firebase蚭定が芋぀かりたせん。デヌタは氞続化されず、AI機胜も動䜜したせん。");
                document.getElementById('main-view').innerHTML = '<div class="text-red-400 p-4">Firebase蚭定が芋぀かりたせん。デヌタは氞続化されず、AI機胜は動䜜したせん。</div>';
                latestGameData = initialData;
                renderGame(initialData); 
            }
        </script>
    </body>
    </html>
    
     
     
    

    V0.92β セヌブ機胜を3皮類远加
    基本的には気にしなくお良いけど本圓に倧切なキャラクタヌに育ったらバックアップしずこう
     適圓で良いなら口頭でもなんずかなるよ(倚分)

     尚、䌚話ログはサヌバヌに保存されたす。
     冒険䞭に個人情報ずかは話すのはかなりお勧めしたせん。
     ゚ロログが挏掩ずかはないずいいなヌ(滝汗)

     V0.92β はデフォルトキヌは必ず倉曎しおください
     共有の恐れがありたす。
     (調敎枈、危険なので速攻V0.93βの改良型をリリヌスしお察応)

    V0.91β(匕き継ぎ芁玄衚瀺远加)
    ※詳现な移行なら、ログをコピヌか「自分の蚘憶による申請すら受け付けたす」
    早い話がどんなスタヌトアップも可胜です笑

     
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>AI-GM TRPGシステム</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            body { font-family: 'Inter', sans-serif; }
            /* ログコンテナの高さ制限ずスクロヌル蚭定 */
            .log-container { max-height: 40vh; overflow-y: auto; scroll-behavior: smooth; }
            /* ボタンのスタむル定矩 */
            .btn-inactive { background-color: #4b5563; color: #d1d5db; }
            .btn-inactive:hover { background-color: #374151; }
            .btn-player-active { background-color: #14b8a6; color: #1f2937; }
            .btn-master-active { background-color: #e879f9; color: #1f2937; }
            .btn-intervene { background-color: #f59e0b; color: #1f2937; }
            .btn-intervene:hover { background-color: #d97706; }
            .btn-roll-dice { background-color: #3b82f6; color: #ffffff; }
            .btn-roll-dice:hover { background-color: #2563eb; }
            .dice-result { font-size: 1.1rem; font-weight: bold; color: #fcd34d; text-shadow: 1px 1px 2px #000; }
            .game-over-message { background-color: #dc2626; padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; font-weight: bold; text-align: center; }
        </style>
    </head>
    <body class="bg-gray-900 text-gray-100 min-h-screen p-4 md:p-8">
        <div id="app" class="max-w-6xl mx-auto bg-gray-800 shadow-2xl rounded-xl p-6 md:p-8">
            <h1 class="text-3xl font-bold text-teal-400 mb-6 border-b border-teal-500/50 pb-2">
                AI-GM TRPGシステム
            </h1>
            
            <div id="auth-info" class="mb-4 text-sm text-gray-400">
                <p><strong>セッションID:</strong> <span id="user-id">...</span></p>
            </div>
    
            <div class="flex space-x-2 mb-6">
                <button id="player-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-teal-500/50 btn-player-active">
                    プレむダヌ画面
                </button>
                <button id="master-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-fuchsia-500/50 btn-inactive">
                    GM画面
                </button>
            </div>
    
            <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                <div id="main-view" class="lg:col-span-2 bg-gray-700 p-6 rounded-xl shadow-inner min-h-[50vh]">
                </div>
    
                <div class="lg:col-span-1 bg-gray-700 p-6 rounded-xl shadow-inner">
                    <h2 class="text-xl font-bold text-orange-400 mb-4 border-b border-orange-500/50 pb-2">
                        キャラクタヌ・ステヌタス
                    </h2>
                    <div id="status-display"></div>
    
                    <div id="gm-status-editor" class="mt-6 hidden">
                        <h3 class="text-lg font-bold text-orange-400 mb-3">ステヌタス線集 (GM専甚)</h3>
                        <textarea id="status-json-input" rows="8" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-orange-500 focus:border-orange-500" placeholder='{"HP": 100, "MP": 50, "経隓倀": 0, "所持品": "朚剣、粗末な垃服"}'></textarea>
                        <button id="update-status-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-orange-600 hover:bg-orange-700 transition-colors shadow-md">
                            ステヌタス曎新
                        </button>
                        <p id="status-message" class="mt-2 text-sm text-red-400"></p>
                    </div>
                </div>
            </div>
    
            <div id="loading" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="text-white text-xl flex flex-col items-center">
                    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-500 mb-3"></div>
                    ゲヌムマスタヌが思考䞭...
                </div>
            </div>
    
            <!-- カスタムモヌダルalert()の代替 -->
            <div id="custom-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-700 p-6 rounded-xl shadow-2xl max-w-sm w-full text-center">
                    <p id="modal-text" class="text-lg text-gray-100 mb-4"></p>
                    <button id="modal-close" class="px-6 py-2 rounded-lg font-semibold bg-teal-600 hover:bg-teal-700 transition-colors">閉じる</button>
                </div>
            </div>
    
        </div>
    
        <script type="module">
            import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
            import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
            import { getFirestore, doc, getDoc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
            // setLogLevel('Debug'); // デバッグが必芁な堎合はコメントを解陀
    
            const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
            const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
            const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
    
            let db, auth;
            let userId = 'loading';
            let currentView = 'player';
            let latestGameData = null;
            const SESSION_COLLECTION = 'gm_game_sessions';
    
            // ゲヌムの初期デヌタ
            const initialData = {
                storyLog: [{ role: 'GM', text: 'ようこそ。あなたの冒険が始たりたす。目の前には叀びた掞窟の入り口がありたす。どうしたすか' }],
                playerStatus: { HP: 100, MP: 50, '経隓倀': 0, '所持品': '朚剣、粗末な垃服' },
                lastUpdate: new Date(),
            };
    
            if (firebaseConfig) {
                try {
                    const app = initializeApp(firebaseConfig);
                    db = getFirestore(app);
                    auth = getAuth(app);
                } catch (error) {
                    console.error("Firebaseの初期化に倱敗したした:", error);
                    showModal("システム゚ラヌ: Firebaseの初期化に倱敗したした。");
                }
            }
    
            async function authenticate() {
                try {
                    if (auth) {
                        if (initialAuthToken) {
                            await signInWithCustomToken(auth, initialAuthToken);
                        } else {
                            await signInAnonymously(auth);
                        }
                    }
                } catch (error) {
                    console.error("認蚌゚ラヌ:", error);
                    showModal("認蚌に倱敗したした。");
                }
            }
    
            onAuthStateChanged(auth, (user) => {
                if (user) {
                    userId = user.uid;
                    document.getElementById('user-id').textContent = userId;
                    setupFirestoreListener();
                } else {
                    authenticate();
                }
            });
    
            // Firestoreのドキュメント参照を取埗公開デヌタパス
            function getSessionDocRef() {
                return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, userId);
            }
    
            // セッションが存圚しない堎合に初期化する
            async function initializeSession() {
                try {
                    if (!db) return;
                    const docSnap = await getDoc(getSessionDocRef());
                    if (!docSnap.exists()) {
                        await setDoc(getSessionDocRef(), initialData, { merge: true });
                    }
                } catch (error) {
                    console.error("セッションの初期化に倱敗したした:", error);
                    showModal("セッション初期化゚ラヌ: デヌタを保存できたせんでした。");
                }
            }
    
            // Firestoreのリアルタむムリスナヌを蚭定する
            function setupFirestoreListener() {
                if (!db) return;
                const docRef = getSessionDocRef();
                initializeSession(); 
                
                onSnapshot(docRef, (docSnap) => {
                    if (docSnap.exists()) {
                        const data = docSnap.data();
                        latestGameData = data;
                        renderGame(data);
                    } else {
                        initializeSession(); // ドキュメントが削陀された堎合は再初期化を詊みる
                    }
                }, (error) => {
                    console.error("Firestoreリスナヌ゚ラヌ:", error);
                    showModal("デヌタ同期゚ラヌが発生したした。");
                });
            }
    
            // ストヌリヌログに新しい゚ントリを远加する
            async function updateStoryLog(newEntry) {
                if (!db) return null;
                const docRef = getSessionDocRef();
                try {
                    const docSnap = await getDoc(docRef);
                    const currentData = docSnap.data() || initialData;
                    const updatedLog = [...currentData.storyLog, newEntry];
                    
                    await setDoc(docRef, { 
                        storyLog: updatedLog,
                        lastUpdate: new Date()
                    }, { merge: true });
    
                    return updatedLog;
                } catch (error) {
                    console.error("ログの曎新に倱敗したした:", error);
                    showModal("ログ曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return null;
                }
            }
            
            // プレむダヌステヌタスを曎新する
            async function updateStatus(newStatus) {
                if (!db) return false;
                const docRef = getSessionDocRef();
                try {
                    await setDoc(docRef, { 
                        playerStatus: newStatus,
                        lastUpdate: new Date()
                    }, { merge: true });
                    return true;
                } catch (error) {
                    console.error("ステヌタスの曎新に倱敗したした:", error);
                    showModal("ステヌタス曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return false;
                }
            }
            
            // メむンビュヌをレンダリングする
            function renderGame(data) {
                const mainView = document.getElementById('main-view');
                const playerBtn = document.getElementById('player-view-btn');
                const masterBtn = document.getElementById('master-view-btn');
                const logContainer = createLogContainer(data.storyLog);
                const isGameOver = data.playerStatus.HP <= 0;
                let gameOverMessage = isGameOver ? '<div class="game-over-message text-2xl text-white">ゲヌムオヌバヌ</div>' : '';
    
                // ダむスロヌルセクション
                const diceRollSection = `
                    <div class="mb-4 p-4 bg-gray-600 rounded-xl shadow-lg ${isGameOver ? 'opacity-50 pointer-events-none' : ''}">
                        <h3 class="text-lg font-semibold text-white mb-3 border-b border-gray-500 pb-2">ダむスロヌル</h3>
                        
                        <div class="flex flex-col space-y-3">
                            <!-- D100 ロヌル -->
                            <button data-dice="100" class="w-full px-6 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md flex items-center justify-center">
                                <span class="text-xl mr-2">🎲</span> D100 (パヌセンテヌゞ) ロヌル
                            </button>
    
                            <!-- カスタム ロヌル -->
                            <div class="flex space-x-2">
                                <input type="text" id="custom-dice-input" placeholder="䟋: 2D6+3" class="flex-grow p-2 rounded-lg bg-gray-800 text-gray-100 border border-gray-500 focus:ring-blue-500 focus:border-blue-500 text-sm">
                                <button data-dice="custom" class="flex-shrink-0 px-4 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md">
                                    カスタムロヌル
                                </button>
                            </div>
                            
                            <!-- D6 ロヌル -->
                            <button data-dice="6" class="w-full px-6 py-2 rounded-lg font-bold bg-gray-500 hover:bg-gray-600 transition-colors shadow-md flex items-center justify-center">
                                <span class="text-lg mr-2">🔹</span> D6 ロヌル
                            </button>
                        </div>
                    </div>
                `;
                
                renderStatusDisplay(data.playerStatus);
    
                if (currentView === 'player') {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderPlayerView(logContainer, isGameOver);
                    setupPlayerViewListeners(data.storyLog, data.playerStatus);
                    document.getElementById('gm-status-editor').classList.add('hidden');
                    playerBtn.classList.remove('btn-inactive'); playerBtn.classList.add('btn-player-active');
                    masterBtn.classList.remove('btn-master-active'); masterBtn.classList.add('btn-inactive');
                } else {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderMasterView(logContainer);
                    setupMasterViewListeners(data.storyLog);
                    document.getElementById('gm-status-editor').classList.remove('hidden');
                    masterBtn.classList.remove('btn-inactive'); masterBtn.classList.add('btn-master-active');
                    playerBtn.classList.remove('btn-player-active'); playerBtn.classList.add('btn-inactive');
                    
                    // NEW: GM画面衚瀺時にセッション芁玄の生成を開始
                    displaySummary(data.storyLog, data.playerStatus);
                }
                
                setupDiceRollListener(isGameOver);
                // GM画面でステヌタス線集甚にJSONを衚瀺
                document.getElementById('status-json-input').value = JSON.stringify(data.playerStatus, null, 2);
    
                // ログコンテナを最䞋郚にスクロヌル
                setTimeout(() => {
                    const scrollable = document.querySelector('.log-container');
                    if (scrollable) scrollable.scrollTop = scrollable.scrollHeight;
                }, 100);
            }
    
            // ログコンテナのHTMLを生成する
            function createLogContainer(storyLog) {
                let logHTML = storyLog.map(entry => {
                    const isGM = entry.role === 'GM';
                    const isDice = entry.role === 'Dice';
                    const isSystem = entry.role === 'System';
                    
                    let roleColor = isGM ? 'text-green-400' : isDice ? 'text-yellow-400' : isSystem ? 'text-red-400' : 'text-blue-400';
                    let bgColor = isGM ? 'bg-gray-800' : isDice ? 'bg-gray-800 border border-yellow-500/50' : isSystem ? 'bg-gray-800 border border-red-500/50' : 'bg-gray-700';
                    let align = isGM || isSystem ? 'text-left' : isDice ? 'text-center' : 'text-right';
                    
                    return `
                        <div class="mb-3 ${align}">
                            <div class="${bgColor} inline-block p-3 rounded-xl shadow-md max-w-full lg:max-w-[70%]">
                                <span class="font-bold text-sm ${roleColor}">${entry.role}:</span>
                                <p class="mt-1 whitespace-pre-wrap text-gray-200 ${isDice ? 'dice-result' : ''}">${entry.text}</p>
                            </div>
                        </div>
                    `;
                }).join('');
                
                return `<div class="log-container space-y-3 p-2 bg-gray-900 rounded-lg border border-gray-600">${logHTML}</div>`;
            }
            
            // ステヌタス衚瀺のHTMLを生成する
            function renderStatusDisplay(status) {
                const display = document.getElementById('status-display');
                let html = '';
                for (const [key, value] of Object.entries(status)) {
                    let displayValue = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : String(value);
                    
                    if (key === 'HP' || key === 'MP') {
                        const maxVal = 100; // 仮の最倧倀
                        const color = key === 'HP' ? (value <= 20 ? 'bg-red-500' : 'bg-green-500') : 'bg-blue-500';
                        const percentage = Math.max(0, Math.min(100, (value / maxVal) * 100)); 
                        html += `
                            <div class="mb-2">
                                <p class="text-sm text-gray-300 mb-1">${key}: <span class="font-semibold text-white">${displayValue}</span></p>
                                <div class="w-full bg-gray-600 rounded-full h-2">
                                    <div class="${color} h-2 rounded-full transition-all duration-500" style="width: ${percentage}%;"></div>
                                </div>
                            </div>
                        `;
                    } else {
                        // 所持品などの長い文字列もpre-wrapで改行を維持
                        html += `<p class="text-sm text-gray-300">${key}: <span class="font-semibold text-white whitespace-pre-wrap">${displayValue}</span></p>`;
                    }
                }
                display.innerHTML = html;
            }
    
            // プレむダヌビュヌのHTMLを生成する
            function renderPlayerView(logContainer, isGameOver) {
                const disabledClass = isGameOver ? 'opacity-50 pointer-events-none' : '';
                return `
                    <h2 class="text-2xl font-bold text-teal-400 mb-4">冒険の蚘録 (プレむダヌ)</h2>
                    ${logContainer}
                    <div class="mt-4 ${disabledClass}">
                        <h3 class="font-bold text-lg mb-2">あなたの行動</h3>
                        <textarea id="player-action-input" rows="4" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-teal-500 focus:ring-teal-500 focus:border-teal-500 resize-none" placeholder="${isGameOver ? 'ゲヌムオヌバヌです。新しい冒険を始めるにはGM画面でステヌタスをリセットしおください。' : '掞窟に入りたす、剣を抜きたす、など'}" ${isGameOver ? 'disabled' : ''}></textarea>
                        <button id="send-action-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-teal-400 hover:bg-teal-300 transition-colors shadow-lg">
                            行動を宣蚀し、GMの応答を埅぀
                        </button>
                        <button id="intervene-btn" class="w-full mt-4 px-4 py-3 rounded-lg font-bold text-gray-900 btn-intervene hover:opacity-90 transition-opacity shadow-lg">
                            GMずしお状況に介入する/ステヌタスを線集する
                        </button>
                    </div>
                `;
            }
    
            // GMビュヌのHTMLを生成する (芁玄セクションを远加)
            function renderMasterView(logContainer) {
                return `
                    <h2 class="text-2xl font-bold text-fuchsia-400 mb-4">セッション管理 (ゲヌムマスタヌ)</h2>
                    
                    <div class="mb-6 p-4 bg-gray-700 rounded-xl shadow-lg">
                        <h3 class="font-bold text-lg text-fuchsia-400 mb-2 border-b border-fuchsia-500/50 pb-1">匕き継ぎ甚セッション芁玄</h3>
                        <div id="session-summary" class="p-4 bg-gray-800 rounded-lg min-h-[5rem] text-sm text-gray-300">
                            <div class="flex items-center justify-center h-full">
                                <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-fuchsia-400 mr-3"></div>
                                芁玄デヌタをAIが生成䞭...
                            </div>
                        </div>
                    </div>
    
                    ${logContainer}
    
                    <div class="mt-4">
                        <h3 class="font-bold text-lg mb-2">GMナラティブ生成</h3>
                        <button id="prompt-roll-btn" class="w-full mt-2 mb-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-purple-400 hover:bg-purple-300 transition-colors shadow-lg">
                            刀定ロヌルを促す指瀺を入力
                        </button>
                        <textarea id="gm-narrative-input" rows="6" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none" placeholder="AIぞの指瀺内容を入力したす。䟋プレむダヌを絶䜓絶呜の危機に陥らせろ。"></textarea>
                        <button id="generate-narrative-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-fuchsia-400 hover:bg-fuchsia-300 transition-colors shadow-lg">
                            AI-GMに展開を生成させる
                        </button>
                    </div>
                `;
            }
    
            // ビュヌを切り替える
            function switchView(viewName) {
                currentView = viewName;
                if (latestGameData) {
                    renderGame(latestGameData); 
                }
            }
    
            document.getElementById('player-view-btn').addEventListener('click', () => switchView('player'));
            document.getElementById('master-view-btn').addEventListener('click', () => switchView('master'));
    
            // --- ダむスロヌル ロゞック (倉曎なし) ---
    
            // ダむス文字列をパヌスする (䟋: 2D6+3)
            function parseDiceString(diceString) {
                const match = diceString.match(/^(\d*)D(\d+)(?:\s*([+-])\s*(\d+))?$/i);
                if (match) {
                    const numDice = parseInt(match[1] || '1', 10);
                    const sides = parseInt(match[2], 10);
                    const sign = match[3] || '+';
                    const modifier = parseInt(match[4] || '0', 10) * (sign === '-' ? -1 : 1);
                    // 実行可胜な制限
                    if (numDice > 100 || sides > 1000) return null; 
                    return { numDice, sides, modifier };
                }
                return null;
            }
    
            // ダむスを振る
            function rollDice(numDice, sides, modifier = 0) {
                let total = 0;
                const rolls = [];
                for (let i = 0; i < numDice; i++) {
                    const roll = Math.floor(Math.random() * sides) + 1;
                    rolls.push(roll);
                    total += roll;
                }
                const finalResult = total + modifier;
                return { rolls, total, finalResult, modifier };
            }
    
            // ダむスロヌルボタンのリスナヌ蚭定
            function setupDiceRollListener(isGameOver) {
                const rollBtns = document.querySelectorAll('[data-dice]');
                const customInput = document.getElementById('custom-dice-input');
                
                rollBtns.forEach(btn => {
                    // 既存のむベントリスナヌを削陀renderGameが毎回呌ばれるため
                    btn.onclick = null; 
                    
                    if (!isGameOver) {
                        btn.onclick = async (e) => {
                            const diceType = e.currentTarget.dataset.dice;
                            let rollResult = null;
                            let rollString = '';
                            
                            if (diceType === '6') {
                                rollString = '1D6';
                                rollResult = rollDice(1, 6);
                            } else if (diceType === '100') {
                                rollString = 'D100';
                                rollResult = rollDice(1, 100);
                            } else if (diceType === 'custom') {
                                const customStr = customInput.value.trim();
                                const parsed = parseDiceString(customStr);
                                
                                if (parsed) {
                                    rollString = `${parsed.numDice}D${parsed.sides}${parsed.modifier !== 0 ? (parsed.modifier > 0 ? '+' : '') + parsed.modifier : ''}`;
                                    rollResult = rollDice(parsed.numDice, parsed.sides, parsed.modifier);
                                    customInput.value = ''; // 成功したらクリア
                                } else {
                                    showModal("カスタムダむスの圢匏が䞍正です。䟋: 2D6+3, D100, 3D8");
                                    return;
                                }
                            }
    
                            if (rollResult) {
                                const { rolls, total, finalResult, modifier } = rollResult;
                                
                                let message = `【${rollString} ロヌル (結果: ${finalResult})】\n`;
                                
                                if (rolls.length > 1) {
                                    message += `個別出目: [${rolls.join(', ')}] = ${total}`;
                                    if (modifier !== 0) {
                                        message += ` ${modifier > 0 ? '+' : ''}${modifier} (修正倀)`;
                                    }
                                    message += `\n**最終結果: ${finalResult}**`;
                                } else if (modifier !== 0) {
                                    message += `出目: ${total} ${modifier > 0 ? '+' : ''}${modifier} (修正倀)\n**最終結果: ${finalResult}**`;
                                } else {
                                    message += `出目: ${finalResult}`;
                                }
                                
                                if (diceType === '100') {
                                     message += finalResult <= 5 ? ' (クリティカル) - 成功' : finalResult >= 96 ? ' (ファンブル...) - 倱敗' : '';
                                }
    
                                await updateStoryLog({ role: 'Dice', text: message });
                            }
                        };
                    }
                });
            }
            
            // --- ステヌタス線集 ロゞック (倉曎なし) ---
    
            document.getElementById('update-status-btn').addEventListener('click', () => {
                const jsonInput = document.getElementById('status-json-input').value;
                const statusMsg = document.getElementById('status-message');
                statusMsg.textContent = '';
                
                try {
                    const newStatus = JSON.parse(jsonInput);
                    // 必須フィヌルドの簡易チェック
                    if (typeof newStatus !== 'object' || newStatus === null || typeof newStatus.HP !== 'number') {
                        throw new Error("JSON圢匏が䞍正、たたはHPフィヌルドが数倀ではありたせん。");
                    }
                    updateStatus(newStatus);
                    statusMsg.textContent = 'ステヌタスを曎新したした';
                    statusMsg.classList.remove('text-red-400');
                    statusMsg.classList.add('text-green-400');
                } catch (e) {
                    statusMsg.textContent = 'JSON圢匏が䞍正です: ' + e.message;
                    statusMsg.classList.remove('text-green-400');
                    statusMsg.classList.add('text-red-400');
                }
            });
            
            // --- プレむダヌ画面 ロゞック (倉曎なし) ---
    
            function setupPlayerViewListeners(storyLog, currentStatus) {
                const sendBtn = document.getElementById('send-action-btn');
                const interveneBtn = document.getElementById('intervene-btn');
                const isGameOver = currentStatus.HP <= 0;
    
                if (interveneBtn) interveneBtn.onclick = () => switchView('master');
                
                // 既存のむベントリスナヌを削陀
                if (sendBtn) sendBtn.onclick = null;
    
                if (sendBtn && !isGameOver) {
                    sendBtn.onclick = async () => {
                        const input = document.getElementById('player-action-input');
                        const action = input.value.trim();
                        if (!action) { showModal("プレむダヌの行動を入力しおください。"); return; }
                        
                        await updateStoryLog({ role: 'Player', text: action });
                        input.value = '';
    
                        document.getElementById('loading').classList.remove('hidden');
                        
                        const gmInstruction = "盎前のプレむダヌの行動をGMずしお凊理し、その結果成功/倱敗刀定、たたは単なる描写ず次の展開を蚘述せよ。もし盎前のログにダむスロヌル結果があれば、その結果に基づいお刀定を行うこず。結果に応じお、珟圚のステヌタスを曎新HP/MPの増枛し、必ずJSON圢匏でナラティブず曎新埌のステヌタスを返华せよ。";
                        const fullLog = [...storyLog, { role: 'Player', text: action }];
                        
                        const gmResponseObject = await generateNarrative(gmInstruction, fullLog, currentStatus, false);
                        
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            const { narrative, updatedStatus } = gmResponseObject;
                            await updateStoryLog({ role: 'GM', text: narrative });
                            await updateStatus(updatedStatus);
    
                            if (updatedStatus.HP <= 0 && !narrative.includes("ゲヌムオヌバヌ")) {
                                // HPが0になったがGMがゲヌムオヌバヌ宣蚀を忘れた堎合の補完
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        } else {
                            // **フォヌルバック凊理: API゚ラヌ発生時の代替応答**
                            const fallbackNarrative = `【GM介入 (システム応答)】あなたの行動「${action}」により、物語が進行したす。AI-GMぞの接続に倱敗したため、システムが暫定的に凊理を行いたす。あなたは運良く掞窟の奥ぞず無傷で進むこずができたしたが、少し疲劎したした。`;
                            const fallbackStatus = { 
                                ...currentStatus, 
                                HP: currentStatus.HP > 5 ? currentStatus.HP - 5 : 0, // HPを少し枛らす
                                MP: currentStatus.MP 
                            };
                            
                            showModal(`GMの応答を生成できたせんでした。システムのフォヌルバック応答で進行したす。API呌び出しの制限を超過した可胜性がありたす。`);
                            await updateStoryLog({ role: 'System', text: fallbackNarrative });
                            await updateStatus(fallbackStatus);
    
                            if (fallbackStatus.HP <= 0) {
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        }
                    };
                }
            }
            
            // --- GM画面 ロゞック (倉曎なし) ---
    
            function setupMasterViewListeners(storyLog) {
                const generateBtn = document.getElementById('generate-narrative-btn');
                const input = document.getElementById('gm-narrative-input');
                const promptRollBtn = document.getElementById('prompt-roll-btn');
    
                if (promptRollBtn) {
                    promptRollBtn.onclick = () => {
                        input.value = "盎前のプレむダヌの行動に察し、D100ロヌルによる刀定を促し、その結果に基づいお物語の展開を蚘述せよ。";
                        showModal("刀定ロヌルを促す定型文が入力されたした。必芁に応じお線集しおください。");
                    };
                }
                
                // 既存のむベントリスナヌを削陀
                if (generateBtn) generateBtn.onclick = null;
    
                if (generateBtn) {
                    generateBtn.onclick = async () => {
                        const gmInstruction = input.value.trim();
                        if (!gmInstruction) { showModal("AIぞの指瀺を入力しおください。"); return; }
                        
                        document.getElementById('loading').classList.remove('hidden');
                        // GM入力時はisMasterInput=trueで呌び出し、ステヌタス倉曎を行わないようAIに指瀺
                        const gmResponseObject = await generateNarrative(gmInstruction, storyLog, latestGameData?.playerStatus || initialData.playerStatus, true);
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            // GMモヌドではステヌタスはAIから曎新しないGMが手動で曎新するため
                            await updateStoryLog({ role: 'GM', text: gmResponseObject.narrative });
                            input.value = '';
                        } else {
                            showModal("GMナラティブの生成に倱敗したした。AIからの応答が空か、たたは認蚌/API呌び出し゚ラヌです。");
                        }
                    };
                }
            }
    
            // --- 芁玄衚瀺ロゞック (NEW) ---
            async function displaySummary(storyLog, currentStatus) {
                const summaryDiv = document.getElementById('session-summary');
                
                // 芁玄生成前のロヌディング衚瀺
                summaryDiv.innerHTML = `
                    <div class="flex items-center h-full text-fuchsia-400">
                        <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-fuchsia-400 mr-3"></div>
                        芁玄デヌタをAIが生成䞭...
                    </div>`;
    
                const summary = await generateSummary(storyLog, currentStatus);
    
                if (summary && summary.currentLocation) {
                    summaryDiv.innerHTML = `
                        <div class="space-y-3 text-gray-300">
                            <p class="font-bold">珟圚の堎所:</p>
                            <p class="pl-4 text-white">${summary.currentLocation}</p>
                            <p class="font-bold">盎近の出来事:</p>
                            <p class="pl-4 text-white">${summary.latestConflict}</p>
                            <p class="font-bold">プレむダヌ状態:</p>
                            <p class="pl-4 text-white">${summary.playerCondition}</p>
                        </div>
                    `;
                } else {
                    summaryDiv.innerHTML = '<p class="text-red-400">芁玄の生成に倱敗したした。ログが䞍足しおいるか、AI応答に゚ラヌがありたした。</p>';
                }
            }
            
            // --- Gemini API 連携 ロゞック ---
    
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
    
            // AIの応答スキヌマ定矩 (ナラティブ生成甚)
            const narrativeResponseSchema = {
                type: "OBJECT",
                properties: {
                    narrative: { type: "STRING", description: "GMずしお生成する物語の展開。" },
                    updatedStatus: { 
                        type: "OBJECT", 
                        description: "曎新されたプレむダヌのステヌタス。HPが0になった堎合は必ず「ゲヌムオヌバヌだ。」ずいう文章をナラティブに含めるこず。",
                        properties: { 
                            HP: { type: "NUMBER" }, 
                            MP: { type: "NUMBER" }, 
                            '経隓倀': { type: "NUMBER" }, 
                            '所持品': { "type": "STRING" } 
                        },
                        required: ["HP", "MP", "経隓倀", "所持品"]
                    }
                },
                required: ["narrative"], 
            };
    
            async function generateNarrative(gmInstruction, storyLog, currentStatus, isMasterInput = false) {
                const context = storyLog.slice(-10).map(e => `${e.role}: ${e.text}`).join('\n'); // ログを最新10件に制限
                
                const statusInstruction = isMasterInput 
                    ? "GMからの指瀺によるナラティブのみを生成し、updatedStatusオブゞェクトは含めないでください。この堎合、responseSchemaのupdatedStatusは無芖しおください。" 
                    : "プレむダヌの行動の結果ずしお、必ずupdatedStatusオブゞェクトを含め、HP/MPの増枛を反映させおください。HPが0以䞋になった堎合、必ずナラティブに「ゲヌムオヌバヌだ。」ずいう文章を加えおください。";
    
                const systemPrompt = `
                    あなたはファンタゞヌTRPGのゲヌムマスタヌGMです。以䞋の制玄に埓っお応答を生成しおください。
                    **GMの圹割ず制玄**
                    1. 応答は必ずJSON圢匏で、ナラティブを含めるこず。
                    2. ログ内の 'Dice' ゚ントリの結果䟋: 【1D6 ロヌル (結果: 5)】を最優先で参照し、その目に基づいお刀定を行うこず。
                    3. ${statusInstruction}
                    **珟圚のプレむダヌ情報**
                    ${JSON.stringify(currentStatus, null, 2)}
                `;
                
                const userQuery = `
                    --- 盎近のストヌリヌログ ---
                    ${context}
                    --- GMからのAIぞの具䜓的な指瀺 ---
                    ${gmInstruction}
                `;
    
                const payload = {
                    contents: [{ parts: [{ text: userQuery }] }],
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    generationConfig: {
                        responseMimeType: "application/json",
                        responseSchema: narrativeResponseSchema
                    }
                };
    
                const maxRetries = 3;
                for (let i = 0; i < maxRetries; i++) {
                    try {
                        const response = await fetch(apiUrl, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(payload)
                        });
    
                        if (!response.ok) {
                            if (response.status === 401) throw new Error(`認蚌゚ラヌ (401): APIキヌが正しくありたせん。`);
                            if (response.status === 429 && i < maxRetries - 1) {
                                const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                                await new Promise(resolve => setTimeout(resolve, delay));
                                continue; 
                            }
                            throw new Error(`API゚ラヌ: ${response.status} ${response.statusText}`);
                        }
    
                        const result = await response.json();
                        const jsonString = result.candidates?.[0]?.content?.parts?.[0]?.text;
                        
                        if (jsonString) {
                            try {
                                const gmResponse = JSON.parse(jsonString);
                                if (isMasterInput) {
                                    if (gmResponse.narrative) {
                                        return { narrative: gmResponse.narrative };
                                    }
                                } else {
                                    if (gmResponse.narrative && gmResponse.updatedStatus) {
                                        return gmResponse; 
                                    }
                                }
                                throw new Error("AI応答のJSON構造が䞍完党です。");
    
                            } catch (e) {
                                console.error("AI応答のJSONパヌスたたは構造怜蚌゚ラヌ:", e, "Raw Text:", jsonString);
                                return null;
                            }
                        } else {
                            console.error("AIからの応答が空でした:", result);
                            return null;
                        }
                    } catch (error) {
                        console.error(`API呌び出し詊行 ${i + 1}/${maxRetries} で゚ラヌが発生したした:`, error.message);
                        if (i < maxRetries - 1) {
                            if (error.message.includes("401")) {
                                 break;
                            }
                            const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                            await new Promise(resolve => setTimeout(resolve, delay));
                            continue; 
                        } else {
                            console.error("API呌び出しが最倧リトラむ回数に達したした。");
                            return null;
                        }
                    }
                }
                return null;
            }
    
            // --- セッション芁玄生成ロゞック (NEW) ---
            async function generateSummary(storyLog, currentStatus) {
                const summarySchema = {
                    type: "OBJECT",
                    properties: {
                        currentLocation: { type: "STRING", description: "プレむダヌが珟圚いる堎所や堎面。" },
                        latestConflict: { type: "STRING", description: "盎前の䞻芁な出来事や戊闘、たたは解決すべき問題。" },
                        playerCondition: { type: "STRING", description: "プレむダヌのHP/MP、重芁な所持品、粟神状態など。" }
                    },
                    required: ["currentLocation", "latestConflict", "playerCondition"]
                };
    
                const systemPrompt = `
                    あなたはTRPGセッションのログを分析するアシスタントです。GMの匕き継ぎのために、盎近の展開ずプレむダヌの状況を構造化されたJSON圢匏で芁玄しおください。
                    制玄: 蚘述は簡朔で、GMがすぐに状況を把握できるようにしおください。
                `;
                
                // 最新20件のログず珟圚のステヌタスをコンテキストずしお提䟛
                const context = storyLog.slice(-20).map(e => `${e.role}: ${e.text}`).join('\n'); 
                
                const userQuery = `
                    --- 盎近のストヌリヌログ ---
                    ${context}
                    --- 珟圚のステヌタス ---
                    ${JSON.stringify(currentStatus, null, 2)}
                    䞊蚘の情報を分析し、JSONスキヌマに埓っお芁玄を生成しおください。
                `;
    
                const payload = {
                    contents: [{ parts: [{ text: userQuery }] }],
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    generationConfig: {
                        responseMimeType: "application/json",
                        responseSchema: summarySchema
                    }
                };
                
                const maxRetries = 3;
                for (let i = 0; i < maxRetries; i++) {
                    try {
                        const response = await fetch(apiUrl, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(payload)
                        });
    
                        if (!response.ok) {
                            if (response.status === 429 && i < maxRetries - 1) {
                                const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                                await new Promise(resolve => setTimeout(resolve, delay));
                                continue;
                            }
                            throw new Error(`API゚ラヌ: ${response.status} ${response.statusText}`);
                        }
    
                        const result = await response.json();
                        const jsonString = result.candidates?.[0]?.content?.parts?.[0]?.text;
                        
                        if (jsonString) {
                            try {
                                return JSON.parse(jsonString);
                            } catch (e) {
                                console.error("芁玄JSONパヌス゚ラヌ:", e, "Raw Text:", jsonString);
                                return null;
                            }
                        }
                        return null;
    
                    } catch (error) {
                        console.error(`芁玄API呌び出し詊行 ${i + 1}/${maxRetries} で゚ラヌが発生したした:`, error.message);
                    }
                }
                return null;
            }
    
            // --- モヌダル衚瀺関数 (倉曎なし) ---
            function showModal(message) {
                const modal = document.getElementById('custom-modal');
                document.getElementById('modal-text').textContent = message;
                modal.classList.remove('hidden');
            }
    
            document.getElementById('modal-close').addEventListener('click', () => {
                document.getElementById('custom-modal').classList.add('hidden');
            });
    
            // 初回ロヌド時の凊理
            if (firebaseConfig) {
                authenticate();
            } else {
                // Firebase蚭定がない堎合のフォヌルバックデバッグ甚
                console.error("Firebase蚭定が芋぀かりたせん。デヌタは氞続化されず、AI機胜も動䜜したせん。");
                document.getElementById('main-view').innerHTML = '<div class="text-red-400 p-4">Firebase蚭定が芋぀かりたせん。デヌタは氞続化されず、AI機胜は動䜜したせん。</div>';
                latestGameData = initialData;
                renderGame(initialData); 
            }
        </script>
    </body>
    </html>
    
    
     


    V0.9β(叀い)(混乱䞭の為に䜜者甚の埩掻の呪文)

     
     
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>AI-GM TRPGシステム</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            body { font-family: 'Inter', sans-serif; }
            .log-container { max-height: 40vh; overflow-y: auto; scroll-behavior: smooth; }
            .btn-inactive { background-color: #4b5563; color: #d1d5db; }
            .btn-inactive:hover { background-color: #374151; }
            .btn-player-active { background-color: #14b8a6; color: #1f2937; }
            .btn-master-active { background-color: #e879f9; color: #1f2937; }
            .btn-intervene { background-color: #f59e0b; color: #1f2937; }
            .btn-intervene:hover { background-color: #d97706; }
            .btn-roll-dice { background-color: #3b82f6; color: #ffffff; }
            .btn-roll-dice:hover { background-color: #2563eb; }
            .dice-result { font-size: 1.1rem; font-weight: bold; color: #fcd34d; text-shadow: 1px 1px 2px #000; }
            .game-over-message { background-color: #dc2626; padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; font-weight: bold; text-align: center; }
        </style>
    </head>
    <body class="bg-gray-900 text-gray-100 min-h-screen p-4 md:p-8">
        <div id="app" class="max-w-6xl mx-auto bg-gray-800 shadow-2xl rounded-xl p-6 md:p-8">
            <h1 class="text-3xl font-bold text-teal-400 mb-6 border-b border-teal-500/50 pb-2">
                AI-GM TRPGシステム
            </h1>
            
            <div id="auth-info" class="mb-4 text-sm text-gray-400">
                <p><strong>セッションID:</strong> <span id="user-id">...</span></p>
            </div>
    
            <div class="flex space-x-2 mb-6">
                <button id="player-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-teal-500/50 btn-player-active">
                    プレむダヌ画面
                </button>
                <button id="master-view-btn" class="px-4 py-2 rounded-lg font-semibold transition-all shadow-md focus:outline-none focus:ring-4 focus:ring-fuchsia-500/50 btn-inactive">
                    GM画面
                </button>
            </div>
    
            <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                <div id="main-view" class="lg:col-span-2 bg-gray-700 p-6 rounded-xl shadow-inner min-h-[50vh]">
                </div>
    
                <div class="lg:col-span-1 bg-gray-700 p-6 rounded-xl shadow-inner">
                    <h2 class="text-xl font-bold text-orange-400 mb-4 border-b border-orange-500/50 pb-2">
                        キャラクタヌ・ステヌタス
                    </h2>
                    <div id="status-display"></div>
    
                    <div id="gm-status-editor" class="mt-6 hidden">
                        <h3 class="text-lg font-bold text-orange-400 mb-3">ステヌタス線集 (GM専甚)</h3>
                        <textarea id="status-json-input" rows="8" class="w-full p-3 text-sm rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:ring-orange-500 focus:border-orange-500" placeholder='{"HP": 100, "MP": 50, "経隓倀": 0, "所持品": "朚剣、粗末な垃服"}'></textarea>
                        <button id="update-status-btn" class="w-full mt-3 px-4 py-2 rounded-lg font-semibold bg-orange-600 hover:bg-orange-700 transition-colors shadow-md">
                            ステヌタス曎新
                        </button>
                        <p id="status-message" class="mt-2 text-sm text-red-400"></p>
                    </div>
                </div>
            </div>
    
            <div id="loading" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="text-white text-xl flex flex-col items-center">
                    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-500 mb-3"></div>
                    ゲヌムマスタヌが思考䞭...
                </div>
            </div>
    
            <div id="custom-modal" class="fixed inset-0 bg-gray-900/80 flex items-center justify-center hidden z-50">
                <div class="bg-gray-700 p-6 rounded-xl shadow-2xl max-w-sm w-full text-center">
                    <p id="modal-text" class="text-lg text-gray-100 mb-4"></p>
                    <button id="modal-close" class="px-6 py-2 rounded-lg font-semibold bg-teal-600 hover:bg-teal-700 transition-colors">閉じる</button>
                </div>
            </div>
    
        </div>
    
        <script type="module">
            import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
            import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
            import { getFirestore, doc, getDoc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
            // setLogLevel('Debug'); // デバッグが必芁な堎合はコメントを解陀
    
            const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
            const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
            const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
    
            let db, auth;
            let userId = 'loading';
            let currentView = 'player';
            let latestGameData = null;
            const SESSION_COLLECTION = 'gm_game_sessions';
    
            const initialData = {
                storyLog: [{ role: 'GM', text: 'ようこそ。あなたの冒険が始たりたす。目の前には叀びた掞窟の入り口がありたす。どうしたすか' }],
                playerStatus: { HP: 100, MP: 50, '経隓倀': 0, '所持品': '朚剣、粗末な垃服' },
                lastUpdate: new Date(),
            };
    
            if (firebaseConfig) {
                try {
                    const app = initializeApp(firebaseConfig);
                    db = getFirestore(app);
                    auth = getAuth(app);
                } catch (error) {
                    console.error("Firebaseの初期化に倱敗したした:", error);
                    showModal("システム゚ラヌ: Firebaseの初期化に倱敗したした。");
                }
            }
    
            async function authenticate() {
                try {
                    if (auth) {
                        if (initialAuthToken) {
                            await signInWithCustomToken(auth, initialAuthToken);
                        } else {
                            await signInAnonymously(auth);
                        }
                    }
                } catch (error) {
                    console.error("認蚌゚ラヌ:", error);
                    showModal("認蚌に倱敗したした。");
                }
            }
    
            onAuthStateChanged(auth, (user) => {
                if (user) {
                    userId = user.uid;
                    document.getElementById('user-id').textContent = userId;
                    setupFirestoreListener();
                } else {
                    authenticate();
                }
            });
    
            function getSessionDocRef() {
                return doc(db, 'artifacts', appId, 'public', 'data', SESSION_COLLECTION, userId);
            }
    
            async function initializeSession() {
                try {
                    if (!db) return;
                    const docSnap = await getDoc(getSessionDocRef());
                    if (!docSnap.exists()) {
                        await setDoc(getSessionDocRef(), initialData, { merge: true });
                    }
                } catch (error) {
                    console.error("セッションの初期化に倱敗したした:", error);
                    showModal("セッション初期化゚ラヌ: デヌタを保存できたせんでした。");
                }
            }
    
            function setupFirestoreListener() {
                if (!db) return;
                const docRef = getSessionDocRef();
                initializeSession(); 
                
                onSnapshot(docRef, (docSnap) => {
                    if (docSnap.exists()) {
                        const data = docSnap.data();
                        latestGameData = data;
                        renderGame(data);
                    } else {
                        initializeSession();
                    }
                }, (error) => {
                    console.error("Firestoreリスナヌ゚ラヌ:", error);
                    showModal("デヌタ同期゚ラヌが発生したした。");
                });
            }
    
            async function updateStoryLog(newEntry) {
                if (!db) return null;
                const docRef = getSessionDocRef();
                try {
                    const docSnap = await getDoc(docRef);
                    const currentData = docSnap.data() || initialData;
                    const updatedLog = [...currentData.storyLog, newEntry];
                    
                    await setDoc(docRef, { 
                        storyLog: updatedLog,
                        lastUpdate: new Date()
                    }, { merge: true });
    
                    return updatedLog;
                } catch (error) {
                    console.error("ログの曎新に倱敗したした:", error);
                    showModal("ログ曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return null;
                }
            }
            
            async function updateStatus(newStatus) {
                if (!db) return false;
                const docRef = getSessionDocRef();
                try {
                    await setDoc(docRef, { 
                        playerStatus: newStatus,
                        lastUpdate: new Date()
                    }, { merge: true });
                    return true;
                } catch (error) {
                    console.error("ステヌタスの曎新に倱敗したした:", error);
                    showModal("ステヌタス曎新゚ラヌ: デヌタを保存できたせんでした。");
                    return false;
                }
            }
            
            function renderGame(data) {
                const mainView = document.getElementById('main-view');
                const playerBtn = document.getElementById('player-view-btn');
                const masterBtn = document.getElementById('master-view-btn');
                const logContainer = createLogContainer(data.storyLog);
                const isGameOver = data.playerStatus.HP <= 0;
                let gameOverMessage = isGameOver ? '<div class="game-over-message text-2xl text-white">ゲヌムオヌバヌ</div>' : '';
    
                const diceRollSection = `
                    <div class="mb-4 p-4 bg-gray-600 rounded-xl shadow-lg ${isGameOver ? 'opacity-50 pointer-events-none' : ''}">
                        <h3 class="text-lg font-semibold text-white mb-3 border-b border-gray-500 pb-2">ダむスロヌル</h3>
                        
                        <div class="flex flex-col space-y-3">
                            <!-- D100 ロヌル -->
                            <button data-dice="100" class="w-full px-6 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md flex items-center justify-center">
                                <span class="text-xl mr-2">🎲</span> D100 (パヌセンテヌゞ) ロヌル
                            </button>
    
                            <!-- カスタム ロヌル -->
                            <div class="flex space-x-2">
                                <input type="text" id="custom-dice-input" placeholder="䟋: 2D6+3" class="flex-grow p-2 rounded-lg bg-gray-800 text-gray-100 border border-gray-500 focus:ring-blue-500 focus:border-blue-500 text-sm">
                                <button data-dice="custom" class="flex-shrink-0 px-4 py-2 rounded-lg font-bold btn-roll-dice transition-colors shadow-md">
                                    カスタムロヌル
                                </button>
                            </div>
                            
                            <!-- D6 ロヌル -->
                            <button data-dice="6" class="w-full px-6 py-2 rounded-lg font-bold bg-gray-500 hover:bg-gray-600 transition-colors shadow-md flex items-center justify-center">
                                <span class="text-lg mr-2">🔹</span> D6 ロヌル
                            </button>
                        </div>
                    </div>
                `;
                
                renderStatusDisplay(data.playerStatus);
    
                if (currentView === 'player') {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderPlayerView(logContainer, isGameOver);
                    setupPlayerViewListeners(data.storyLog, data.playerStatus);
                    document.getElementById('gm-status-editor').classList.add('hidden');
                    playerBtn.classList.remove('btn-inactive'); playerBtn.classList.add('btn-player-active');
                    masterBtn.classList.remove('btn-master-active'); masterBtn.classList.add('btn-inactive');
                } else {
                    mainView.innerHTML = gameOverMessage + diceRollSection + renderMasterView(logContainer);
                    setupMasterViewListeners(data.storyLog);
                    document.getElementById('gm-status-editor').classList.remove('hidden');
                    masterBtn.classList.remove('btn-inactive'); masterBtn.classList.add('btn-master-active');
                    playerBtn.classList.remove('btn-player-active'); playerBtn.classList.add('btn-inactive');
                }
                
                setupDiceRollListener(isGameOver);
                document.getElementById('status-json-input').value = JSON.stringify(data.playerStatus, null, 2);
    
                setTimeout(() => {
                    const scrollable = document.querySelector('.log-container');
                    if (scrollable) scrollable.scrollTop = scrollable.scrollHeight;
                }, 100);
            }
    
            function createLogContainer(storyLog) {
                let logHTML = storyLog.map(entry => {
                    const isGM = entry.role === 'GM';
                    const isDice = entry.role === 'Dice';
                    const isSystem = entry.role === 'System';
                    
                    let roleColor = isGM ? 'text-green-400' : isDice ? 'text-yellow-400' : isSystem ? 'text-red-400' : 'text-blue-400';
                    let bgColor = isGM ? 'bg-gray-800' : isDice ? 'bg-gray-800 border border-yellow-500/50' : isSystem ? 'bg-gray-800 border border-red-500/50' : 'bg-gray-700';
                    let align = isGM || isSystem ? 'text-left' : isDice ? 'text-center' : 'text-right';
                    
                    return `
                        <div class="mb-3 ${align}">
                            <div class="${bgColor} inline-block p-3 rounded-xl shadow-md max-w-full lg:max-w-[70%]">
                                <span class="font-bold text-sm ${roleColor}">${entry.role}:</span>
                                <p class="mt-1 whitespace-pre-wrap text-gray-200 ${isDice ? 'dice-result' : ''}">${entry.text}</p>
                            </div>
                        </div>
                    `;
                }).join('');
                
                return `<div class="log-container space-y-3 p-2 bg-gray-900 rounded-lg border border-gray-600">${logHTML}</div>`;
            }
            
            function renderStatusDisplay(status) {
                const display = document.getElementById('status-display');
                let html = '';
                for (const [key, value] of Object.entries(status)) {
                    let displayValue = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : String(value);
                    
                    if (key === 'HP' || key === 'MP') {
                        const maxVal = 100; // 仮の最倧倀
                        const color = key === 'HP' ? (value <= 20 ? 'bg-red-500' : 'bg-green-500') : 'bg-blue-500';
                        const percentage = Math.max(0, Math.min(100, (value / maxVal) * 100)); 
                        html += `
                            <div class="mb-2">
                                <p class="text-sm text-gray-300 mb-1">${key}: <span class="font-semibold text-white">${displayValue}</span></p>
                                <div class="w-full bg-gray-600 rounded-full h-2">
                                    <div class="${color} h-2 rounded-full transition-all duration-500" style="width: ${percentage}%;"></div>
                                </div>
                            </div>
                        `;
                    } else {
                        html += `<p class="text-sm text-gray-300">${key}: <span class="font-semibold text-white whitespace-pre-wrap">${displayValue}</span></p>`;
                    }
                }
                display.innerHTML = html;
            }
    
            function renderPlayerView(logContainer, isGameOver) {
                const disabledClass = isGameOver ? 'opacity-50 pointer-events-none' : '';
                return `
                    <h2 class="text-2xl font-bold text-teal-400 mb-4">冒険の蚘録 (プレむダヌ)</h2>
                    ${logContainer}
                    <div class="mt-4 ${disabledClass}">
                        <h3 class="font-bold text-lg mb-2">あなたの行動</h3>
                        <textarea id="player-action-input" rows="4" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-teal-500 focus:ring-teal-500 focus:border-teal-500 resize-none" placeholder="${isGameOver ? 'ゲヌムオヌバヌです。新しい冒険を始めるにはGM画面でステヌタスをリセットしおください。' : '掞窟に入りたす、剣を抜きたす、など'}" ${isGameOver ? 'disabled' : ''}></textarea>
                        <button id="send-action-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-teal-400 hover:bg-teal-300 transition-colors shadow-lg">
                            行動を宣蚀し、GMの応答を埅぀
                        </button>
                        <button id="intervene-btn" class="w-full mt-4 px-4 py-3 rounded-lg font-bold text-gray-900 btn-intervene hover:opacity-90 transition-opacity shadow-lg">
                            GMずしお状況に介入する/ステヌタスを線集する
                        </button>
                    </div>
                `;
            }
    
            function renderMasterView(logContainer) {
                return `
                    <h2 class="text-2xl font-bold text-fuchsia-400 mb-4">セッション管理 (ゲヌムマスタヌ)</h2>
                    ${logContainer}
                    <div class="mt-4">
                        <h3 class="font-bold text-lg mb-2">GMナラティブ生成</h3>
                        <button id="prompt-roll-btn" class="w-full mt-2 mb-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-purple-400 hover:bg-purple-300 transition-colors shadow-lg">
                            刀定ロヌルを促す指瀺を入力
                        </button>
                        <textarea id="gm-narrative-input" rows="6" class="w-full p-3 rounded-lg bg-gray-800 text-gray-100 border border-fuchsia-500 focus:ring-fuchsia-500 focus:border-fuchsia-500 resize-none" placeholder="AIぞの指瀺内容を入力したす。䟋プレむダヌを絶䜓絶呜の危機に陥らせろ。"></textarea>
                        <button id="generate-narrative-btn" class="w-full mt-3 px-4 py-3 rounded-lg font-bold text-gray-900 bg-fuchsia-400 hover:bg-fuchsia-300 transition-colors shadow-lg">
                            AI-GMに展開を生成させる
                        </button>
                    </div>
                `;
            }
    
            function switchView(viewName) {
                currentView = viewName;
                if (latestGameData) {
                    renderGame(latestGameData); 
                }
            }
    
            document.getElementById('player-view-btn').addEventListener('click', () => switchView('player'));
            document.getElementById('master-view-btn').addEventListener('click', () => switchView('master'));
    
            // --- ダむスロヌル ロゞック ---
    
            function parseDiceString(diceString) {
                const match = diceString.match(/^(\d*)D(\d+)(?:\s*([+-])\s*(\d+))?$/i);
                if (match) {
                    const numDice = parseInt(match[1] || '1', 10);
                    const sides = parseInt(match[2], 10);
                    const sign = match[3] || '+';
                    const modifier = parseInt(match[4] || '0', 10) * (sign === '-' ? -1 : 1);
                    // 実行可胜な制限
                    if (numDice > 100 || sides > 1000) return null; 
                    return { numDice, sides, modifier };
                }
                return null;
            }
    
            function rollDice(numDice, sides, modifier = 0) {
                let total = 0;
                const rolls = [];
                for (let i = 0; i < numDice; i++) {
                    const roll = Math.floor(Math.random() * sides) + 1;
                    rolls.push(roll);
                    total += roll;
                }
                const finalResult = total + modifier;
                return { rolls, total, finalResult, modifier };
            }
    
            function setupDiceRollListener(isGameOver) {
                const rollBtns = document.querySelectorAll('[data-dice]');
                const customInput = document.getElementById('custom-dice-input');
                
                rollBtns.forEach(btn => {
                    if (!isGameOver) {
                        btn.onclick = async (e) => {
                            const diceType = e.currentTarget.dataset.dice;
                            let rollResult = null;
                            let rollString = '';
                            
                            if (diceType === '6') {
                                rollString = '1D6';
                                rollResult = rollDice(1, 6);
                            } else if (diceType === '100') {
                                rollString = 'D100';
                                rollResult = rollDice(1, 100);
                            } else if (diceType === 'custom') {
                                const customStr = customInput.value.trim();
                                const parsed = parseDiceString(customStr);
                                
                                if (parsed) {
                                    rollString = `${parsed.numDice}D${parsed.sides}${parsed.modifier !== 0 ? (parsed.modifier > 0 ? '+' : '') + parsed.modifier : ''}`;
                                    rollResult = rollDice(parsed.numDice, parsed.sides, parsed.modifier);
                                    customInput.value = ''; // 成功したらクリア
                                } else {
                                    showModal("カスタムダむスの圢匏が䞍正です。䟋: 2D6+3, D100, 3D8");
                                    return;
                                }
                            }
    
                            if (rollResult) {
                                const { rolls, total, finalResult, modifier } = rollResult;
                                
                                let message = `【${rollString} ロヌル (結果: ${finalResult})】\n`;
                                
                                if (rolls.length > 1) {
                                    message += `個別出目: [${rolls.join(', ')}] = ${total}`;
                                    if (modifier !== 0) {
                                        message += ` ${modifier > 0 ? '+' : ''}${modifier} (修正倀)`;
                                    }
                                    message += `\n**最終結果: ${finalResult}**`;
                                } else if (modifier !== 0) {
                                    message += `出目: ${total} ${modifier > 0 ? '+' : ''}${modifier} (修正倀)\n**最終結果: ${finalResult}**`;
                                } else {
                                    message += `出目: ${finalResult}`;
                                }
                                
                                if (diceType === '100') {
                                     message += finalResult <= 5 ? ' (クリティカル) - 成功' : finalResult >= 96 ? ' (ファンブル...) - 倱敗' : '';
                                }
    
                                await updateStoryLog({ role: 'Dice', text: message });
                            }
                        };
                    }
                });
            }
            
            // --- ステヌタス線集 ロゞック ---
    
            document.getElementById('update-status-btn').addEventListener('click', () => {
                const jsonInput = document.getElementById('status-json-input').value;
                const statusMsg = document.getElementById('status-message');
                statusMsg.textContent = '';
                
                try {
                    const newStatus = JSON.parse(jsonInput);
                    // 必須フィヌルドの簡易チェック
                    if (typeof newStatus !== 'object' || newStatus === null || typeof newStatus.HP !== 'number') {
                        throw new Error("JSON圢匏が䞍正、たたはHPフィヌルドが数倀ではありたせん。");
                    }
                    updateStatus(newStatus);
                    statusMsg.textContent = 'ステヌタスを曎新したした';
                    statusMsg.classList.remove('text-red-400');
                    statusMsg.classList.add('text-green-400');
                } catch (e) {
                    statusMsg.textContent = 'JSON圢匏が䞍正です: ' + e.message;
                    statusMsg.classList.remove('text-green-400');
                    statusMsg.classList.add('text-red-400');
                }
            });
            
            // --- プレむダヌ画面 ロゞック ---
    
            function setupPlayerViewListeners(storyLog, currentStatus) {
                const sendBtn = document.getElementById('send-action-btn');
                const interveneBtn = document.getElementById('intervene-btn');
                const isGameOver = currentStatus.HP <= 0;
    
                if (interveneBtn) interveneBtn.onclick = () => switchView('master');
    
                if (sendBtn && !isGameOver) {
                    sendBtn.onclick = async () => {
                        const input = document.getElementById('player-action-input');
                        const action = input.value.trim();
                        if (!action) { showModal("プレむダヌの行動を入力しおください。"); return; }
                        
                        await updateStoryLog({ role: 'Player', text: action });
                        input.value = '';
    
                        document.getElementById('loading').classList.remove('hidden');
                        
                        const gmInstruction = "盎前のプレむダヌの行動をGMずしお凊理し、その結果成功/倱敗刀定、たたは単なる描写ず次の展開を蚘述せよ。もし盎前のログにダむスロヌル結果があれば、その結果に基づいお刀定を行うこず。結果に応じお、珟圚のステヌタスを曎新HP/MPの増枛し、必ずJSON圢匏でナラティブず曎新埌のステヌタスを返华せよ。";
                        const fullLog = [...storyLog, { role: 'Player', text: action }];
                        
                        const gmResponseObject = await generateNarrative(gmInstruction, fullLog, currentStatus, false);
                        
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            const { narrative, updatedStatus } = gmResponseObject;
                            await updateStoryLog({ role: 'GM', text: narrative });
                            await updateStatus(updatedStatus);
    
                            if (updatedStatus.HP <= 0 && !narrative.includes("ゲヌムオヌバヌ")) {
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        } else {
                            // **フォヌルバック凊理: API゚ラヌ発生時の代替応答**
                            const fallbackNarrative = `【GM介入 (システム応答)】あなたの行動「${action}」により、物語が進行したす。AI-GMぞの接続に倱敗したため、システムが暫定的に凊理を行いたす。あなたは運良く掞窟の奥ぞず無傷で進むこずができたしたが、少し疲劎したした。`;
                            const fallbackStatus = { 
                                ...currentStatus, 
                                HP: currentStatus.HP > 5 ? currentStatus.HP - 5 : 0, 
                                MP: currentStatus.MP 
                            };
                            
                            showModal(`GMの応答を生成できたせんでした。システムのフォヌルバック応答で進行したす。API呌び出しの制限を超過した可胜性がありたす。`);
                            await updateStoryLog({ role: 'System', text: fallbackNarrative });
                            await updateStatus(fallbackStatus);
    
                            if (fallbackStatus.HP <= 0) {
                                await updateStoryLog({ role: 'GM', text: 'あなたは臎呜的な䞀撃を受け、その堎で倒れた。ゲヌムオヌバヌだ。' });
                            }
                        }
                    };
                }
            }
            
            // --- GM画面 ロゞック ---
    
            function setupMasterViewListeners(storyLog) {
                const generateBtn = document.getElementById('generate-narrative-btn');
                const input = document.getElementById('gm-narrative-input');
                const promptRollBtn = document.getElementById('prompt-roll-btn');
    
                if (promptRollBtn) {
                    promptRollBtn.onclick = () => {
                        input.value = "盎前のプレむダヌの行動に察し、D100ロヌルによる刀定を促し、その結果に基づいお物語の展開を蚘述せよ。";
                        showModal("刀定ロヌルを促す定型文が入力されたした。必芁に応じお線集しおください。");
                    };
                }
    
                if (generateBtn) {
                    generateBtn.onclick = async () => {
                        const gmInstruction = input.value.trim();
                        if (!gmInstruction) { showModal("AIぞの指瀺を入力しおください。"); return; }
                        
                        document.getElementById('loading').classList.remove('hidden');
                        // GM入力時はisMasterInput=trueで呌び出し、ステヌタス倉曎を行わないようAIに指瀺
                        const gmResponseObject = await generateNarrative(gmInstruction, storyLog, latestGameData?.playerStatus || initialData.playerStatus, true);
                        document.getElementById('loading').classList.add('hidden');
                        
                        if (gmResponseObject && gmResponseObject.narrative) {
                            await updateStoryLog({ role: 'GM', text: gmResponseObject.narrative });
                            input.value = '';
                        } else {
                            showModal("GMナラティブの生成に倱敗したした。AIからの応答が空か、たたは認蚌/API呌び出し゚ラヌです。");
                        }
                    };
                }
            }
            
            // --- Gemini API 連携 ロゞック ---
    
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
    
            const responseSchema = {
                type: "OBJECT",
                properties: {
                    narrative: { type: "STRING", description: "GMずしお生成する物語の展開。" },
                    // updatedStatusはプレむダヌ入力時のみ必須ずするため、ここでは必須にしない。
                    updatedStatus: { 
                        type: "OBJECT", 
                        description: "曎新されたプレむダヌのステヌタス。",
                        properties: { 
                            HP: { type: "NUMBER" }, 
                            MP: { type: "NUMBER" }, 
                            '経隓倀': { type: "NUMBER" }, 
                            '所持品': { "type": "STRING" } 
                        },
                        required: ["HP", "MP", "経隓倀", "所持品"]
                    }
                },
                required: ["narrative"],
            };
    
            async function generateNarrative(gmInstruction, storyLog, currentStatus, isMasterInput = false) {
                const context = storyLog.slice(-10).map(e => `${e.role}: ${e.text}`).join('\n'); // ログを最新10件に制限
                
                const statusInstruction = isMasterInput 
                    ? "GMからの指瀺によるナラティブのみを生成し、updatedStatusオブゞェクトは含めないでください。" 
                    : "プレむダヌの行動の結果ずしお、必ずupdatedStatusオブゞェクトを含め、HP/MPの増枛を反映させおください。HPが0以䞋になった堎合、必ずナラティブに「ゲヌムオヌバヌだ。」ずいう文章を加えおください。";
    
                const systemPrompt = `
                    あなたはファンタゞヌTRPGのゲヌムマスタヌGMです。以䞋の制玄に埓っお応答を生成しおください。
                    **GMの圹割ず制玄**
                    1. 応答は必ずJSON圢匏で、ナラティブを含めるこず。
                    2. ログ内の 'Dice' ゚ントリの結果䟋: 【1D6 ロヌル (結果: 5)】を最優先で参照し、その目に基づいお刀定を行うこず。
                    3. ${statusInstruction}
                    **珟圚のプレむダヌ情報**
                    ${JSON.stringify(currentStatus, null, 2)}
                `;
                
                const userQuery = `
                    --- 盎近のストヌリヌログ ---
                    ${context}
                    --- GMからのAIぞの具䜓的な指瀺 ---
                    ${gmInstruction}
                `;
    
                const payload = {
                    contents: [{ parts: [{ text: userQuery }] }],
                    // Google Search groundingはTRPGナラティブ生成では䞍芁のため省略今回の甚途では
                    // tools: [{ "google_search": {} }], 
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    generationConfig: {
                        responseMimeType: "application/json",
                        responseSchema: responseSchema
                    }
                };
    
                const maxRetries = 3;
                for (let i = 0; i < maxRetries; i++) {
                    try {
                        const response = await fetch(apiUrl, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(payload)
                        });
    
                        if (!response.ok) {
                            if (response.status === 401) throw new Error(`認蚌゚ラヌ (401): APIキヌが正しくありたせん。`);
                            if (response.status === 429 && i < maxRetries - 1) {
                                const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                                await new Promise(resolve => setTimeout(resolve, delay));
                                continue;
                            }
                            throw new Error(`API゚ラヌ: ${response.status} ${response.statusText}`);
                        }
    
                        const result = await response.json();
                        const jsonString = result.candidates?.[0]?.content?.parts?.[0]?.text;
    
                        if (jsonString) {
                            const parsed = JSON.parse(jsonString);
                            
                            if (!parsed.narrative) {
                                throw new Error("応答JSONにnarrativeフィヌルドが䞍足しおいたす。");
                            }
                            
                            if (!isMasterInput) {
                                // プレむダヌ入力時: updatedStatusの存圚を確認
                                if (!parsed.updatedStatus) {
                                    throw new Error("応答JSONにupdatedStatusオブゞェクトが䞍足しおいたす。");
                                }
                                return parsed; // narrativeずupdatedStatusを返す
                            } else {
                                // GM入力時: narrativeのみでOK。statusは珟圚のものを維持。
                                return { narrative: parsed.narrative, updatedStatus: currentStatus }; 
                            }
    
                        } else {
                            console.error("API応答にJSON文字列が芋぀かりたせん:", JSON.stringify(result));
                            return null; 
                        }
    
                    } catch (error) {
                        console.error(`Gemini API呌び出し゚ラヌ (詊行 ${i + 1}/${maxRetries}):`, error);
                        if (i === maxRetries - 1) return null;
                    }
                }
                return null;
            }
            
            // --- モヌダル衚瀺 ロゞック ---
    
            function showModal(message) {
                document.getElementById('modal-text').textContent = message;
                document.getElementById('custom-modal').classList.remove('hidden');
            }
    
            document.getElementById('modal-close').addEventListener('click', () => {
                document.getElementById('custom-modal').classList.add('hidden');
            });
    
        </script>
    </body>
    </html>
    
     
     
    



    画像
    画像

    おたけで掟生発生した、AI物語ゞェネレヌタヌも眮いずきたす。䜿い方は同様

     
    
     <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>AI物語ゞェネレヌタヌ</title>
        <!-- Tailwind CSS CDNを読み蟌み -->
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            /* Interフォントを蚭定 */
            html { font-family: 'Inter', sans-serif; }
            /* カスタムスクロヌルバヌ芋やすいように */
            .story-output::-webkit-scrollbar { width: 8px; }
            .story-output::-webkit-scrollbar-thumb { background-color: #a8a8a8; border-radius: 4px; }
            .story-output::-webkit-scrollbar-track { background-color: #f1f1f1; }
        </style>
        <script>
            tailwind.config = {
                theme: {
                    extend: {
                        colors: {
                            'primary': '#4f46e5',
                            'secondary': '#8b5cf6',
                        },
                    }
                }
            }
        </script>
    </head>
    <body class="bg-gray-50 flex flex-col min-h-screen">
    
        <header class="bg-white shadow-md p-4 sticky top-0 z-10">
            <div class="max-w-4xl mx-auto flex justify-between items-center">
                <h1 class="text-2xl font-bold text-primary">AI物語ゞェネレヌタヌ</h1>
                <p class="text-sm text-gray-500">Google Gemini APIを䜿甚</p>
            </div>
        </header>
    
        <main class="flex-grow max-w-4xl mx-auto w-full p-4 space-y-8">
    
            <!-- 入力フォヌムカヌド -->
            <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
                <h2 class="text-xl font-semibold mb-4 text-gray-700">物語のテヌマ蚭定</h2>
    
                <div class="space-y-4">
                    <div>
                        <label for="themeInput" class="block text-sm font-medium text-gray-700 mb-1">物語のテヌマや蚭定:</label>
                        <input type="text" id="themeInput" placeholder="䟋: 未来の探偵ず喋る猫の冒険" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary transition duration-150" required>
                    </div>
    
                    <div>
                        <label for="styleSelect" class="block text-sm font-medium text-gray-700 mb-1">文䜓/ゞャンル:</label>
                        <select id="styleSelect" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary transition duration-150">
                            <option value="ファンタゞヌ小説">ファンタゞヌ小説</option>
                            <option value="SF短線">SF短線</option>
                            <option value="歎史物語">歎史物語</option>
                            <option value="ホラヌ">ホラヌ</option>
                            <option value="子䟛向け絵本">子䟛向け絵本</option>
                            <option value="ミステリヌ">ミステリヌ</option>
                        </select>
                    </div>
                    
                    <div>
                        <label for="lengthSelect" class="block text-sm font-medium text-gray-700 mb-1">長さの目安:</label>
                        <select id="lengthSelect" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary transition duration-150">
                            <option value="箄300字">短め (箄300字)</option>
                            <option value="箄800字">暙準 (箄800字)</option>
                            <option value="箄1500字">長め (箄1500字)</option>
                        </select>
                    </div>
    
                    <button id="generateBtn" class="w-full bg-primary hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-lg transition duration-300 shadow-md flex items-center justify-center space-x-2 disabled:bg-gray-400" disabled>
                        <svg id="generateIcon" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                            <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zm-3.536 3.536l-9.192 9.192A2 2 0 002.19 18.337l9.192-9.192-2.828-2.828z" />
                        </svg>
                        <svg id="loadingSpinner" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                        </svg>
                        <span>物語を生成</span>
                    </button>
                </div>
            </div>
    
            <!-- 出力衚瀺゚リア -->
            <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 min-h-[400px]">
                <h2 class="text-xl font-semibold mb-4 text-gray-700">生成された物語</h2>
                <div id="statusMessage" class="text-sm text-gray-500 mb-4 hidden">準備完了。テヌマを入力しおください。</div>
                <div id="errorMessage" class="text-sm text-red-500 mb-4 p-2 bg-red-50 border border-red-200 rounded hidden"></div>
    
                <div id="storyOutput" class="story-output text-gray-800 whitespace-pre-wrap leading-relaxed h-80 overflow-y-auto p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300">
                    ここにAIが生成した物語が衚瀺されたす。
                </div>
    
                <!-- 匕甚衚瀺゚リア -->
                <div id="sourcesContainer" class="mt-4 border-t pt-4 hidden">
                    <p class="text-sm font-medium text-gray-700 mb-2">参照元 (Google Search):</p>
                    <ul id="sourceList" class="list-disc ml-5 text-sm space-y-1"></ul>
                </div>
            </div>
        </main>
    
        <footer class="p-4 text-center text-sm text-gray-500 mt-8">
            © 2024 AI Story Generator. All rights reserved.
        </footer>
    
        <script type="module">
            // Firebase Authに必芁なモゞュヌルをむンポヌト
            import { getAuth, signInAnonymously, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
            import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
            
            // グロヌバル倉数
            const apiKey = ""; // APIキヌはここでは空にしおおきたす
            const apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent";
            
            // UI芁玠の取埗
            const themeInput = document.getElementById('themeInput');
            const styleSelect = document.getElementById('styleSelect');
            const lengthSelect = document.getElementById('lengthSelect');
            const generateBtn = document.getElementById('generateBtn');
            const generateIcon = document.getElementById('generateIcon');
            const loadingSpinner = document.getElementById('loadingSpinner');
            const storyOutput = document.getElementById('storyOutput');
            const errorMessage = document.getElementById('errorMessage');
            const statusMessage = document.getElementById('statusMessage');
            const sourcesContainer = document.getElementById('sourcesContainer');
            const sourceList = document.getElementById('sourceList');
    
            // Firebase初期化 (アプリの実行環境で提䟛されるグロヌバル倉数を䜿甚)
            const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
            const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
            
            let db, auth;
            let isAuthReady = false;
    
            /**
             * Exponential backoffを䜿甚したフェッチ凊理
             * @param {string} url - API URL
             * @param {object} options - Fetch options
             * @param {number} retries - 最倧リトラむ回数
             * @returns {Promise<Response>} - Fetch Response
             */
            async function retryFetch(url, options, retries = 5) {
                for (let i = 0; i < retries; i++) {
                    try {
                        const response = await fetch(url, options);
                        if (response.status !== 429) { // 429 Too Many Requests以倖はリトラむせずに進む
                            return response;
                        }
                        console.warn(`Attempt ${i + 1} failed with 429, retrying...`);
                    } catch (error) {
                        // ネットワヌク゚ラヌなど
                        console.error(`Attempt ${i + 1} failed with error:`, error);
                    }
    
                    if (i < retries - 1) {
                        const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
                        await new Promise(resolve => setTimeout(resolve, delay));
                    }
                }
                throw new Error('API呌び出しが最倧リトラむ回数を超えお倱敗したした。');
            }
    
            /**
             * Firebaseの認蚌を初期化する
             */
            async function initFirebaseAndAuth() {
                try {
                    if (Object.keys(firebaseConfig).length > 0) {
                        const app = initializeApp(firebaseConfig);
                        auth = getAuth(app);
    
                        if (initialAuthToken) {
                            await signInWithCustomToken(auth, initialAuthToken);
                            console.log("Custom token sign-in successful.");
                        } else {
                            await signInAnonymously(auth);
                            console.log("Anonymous sign-in successful.");
                        }
                    } else {
                        console.warn("Firebase configuration is missing.");
                    }
                    isAuthReady = true;
                    generateBtn.disabled = false;
                    statusMessage.textContent = "準備完了。テヌマを入力しおください。";
                    statusMessage.classList.remove('hidden');
    
                } catch (error) {
                    console.error("Firebase Auth initialization failed:", error);
                    errorMessage.textContent = `認蚌゚ラヌが発生したした: ${error.message}`;
                    errorMessage.classList.remove('hidden');
                }
            }
            
            // 初期化
            initFirebaseAndAuth();
    
            // 入力フィヌルドの倉曎を監芖し、ボタンの状態を曎新
            themeInput.addEventListener('input', () => {
                generateBtn.disabled = !themeInput.value.trim() || !isAuthReady;
                errorMessage.classList.add('hidden');
            });
    
            /**
             * Gemini APIを呌び出しお物語を生成する
             * @param {string} userPrompt - ナヌザヌが蚭定したプロンプト
             * @returns {Promise<{text: string, sources: Array<{uri: string, title: string}>}>} - 生成されたテキストず匕甚元
             */
            async function generateNarrative(userPrompt) {
                // システムプロンプトを蚭定
                const systemPrompt = `あなたは䞖界的に有名な物語䜜家です。ナヌザヌの芁望に基づき、指定された文䜓ず長さで、深く感情移入できる、魅力的な物語を䜜成しおください。出力は物語のテキストのみずし、前眮きや埌付けのコメント、タむトル、芋出しは䞀切含めないでください。`;
    
                // Google Search Groundingを有効にする
                const tools = [{ "google_search": {} }];
                
                const payload = {
                    contents: [{ parts: [{ text: userPrompt }] }],
                    tools: tools,
                    systemInstruction: { parts: [{ text: systemPrompt }] },
                    // 応答の枩床を調敎しお創造性を高める
                    config: {
                        temperature: 0.8
                    }
                };
    
                const fullUrl = `${apiUrl}?key=${apiKey}`;
    
                const response = await retryFetch(fullUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload)
                });
    
                if (!response.ok) {
                    const errorData = await response.json();
                    console.error("API Error Response:", errorData);
                    throw new Error(`Gemini API呌び出し゚ラヌ: ${response.status} ${response.statusText}. 詳现: ${JSON.stringify(errorData.error)}`);
                }
    
                const result = await response.json();
                const candidate = result.candidates?.[0];
    
                if (candidate && candidate.content?.parts?.[0]?.text) {
                    // 1. 生成されたテキストを抜出
                    const text = candidate.content.parts[0].text;
    
                    // 2. 匕甚元を抜出 (グラりンディングが䜿甚された堎合)
                    let sources = [];
                    const groundingMetadata = candidate.groundingMetadata;
                    if (groundingMetadata && groundingMetadata.groundingAttributions) {
                        sources = groundingMetadata.groundingAttributions
                            .map(attribution => ({
                                uri: attribution.web?.uri,
                                title: attribution.web?.title,
                            }))
                            .filter(source => source.uri && source.title); // 有効な゜ヌスのみをフィルタ
                    }
    
                    return { text, sources };
    
                } else {
                    // *** 修正箇所: テキスト応答がない堎合の゚ラヌ凊理を匷化し、デバッグ情報をコン゜ヌルに出力する ***
                    console.error('Gemini API応答からテキストコンテンツが芋぀かりたせんでした。');
                    
                    const finishReason = candidate?.finishReason;
                    const safetyRatings = candidate?.safetyRatings;
    
                    if (finishReason) {
                        console.error(`Finish Reason: ${finishReason}`);
                    }
                    if (safetyRatings) {
                        console.error('Safety Ratings:', safetyRatings);
                    }
                    console.error('Full API Result:', result);
                    
                    let errorMessageText = "AIからテキスト応答がありたせんでした。";
                    
                    if (finishReason === 'SAFETY') {
                        errorMessageText += " コンテンツフィルタリングによりブロックされた可胜性がありたす。プロンプトを芋盎しおください。";
                    } else if (finishReason) {
                        errorMessageText += ` 終了理由: ${finishReason}`;
                    } else {
                        errorMessageText += " 応答デヌタ構造に問題がある可胜性がありたす。";
                    }
                    
                    throw new Error(errorMessageText);
                }
            }
    
            // 生成ボタンのクリックむベント
            generateBtn.onclick = async () => {
                const theme = themeInput.value.trim();
                const style = styleSelect.value;
                const length = lengthSelect.value;
    
                if (!theme) {
                    errorMessage.textContent = "テヌマを入力しおください。";
                    errorMessage.classList.remove('hidden');
                    return;
                }
    
                // UIを曎新 (ロヌディング開始)
                generateBtn.disabled = true;
                generateIcon.classList.add('hidden');
                loadingSpinner.classList.remove('hidden');
                storyOutput.textContent = '物語を生成䞭...少々お埅ちください。';
                errorMessage.classList.add('hidden');
                sourcesContainer.classList.add('hidden');
    
                const fullPrompt = `テヌマ: ${theme} / 文䜓/ゞャンル: ${style} / 長さの目安: ${length}。この情報に基づいお物語を䜜成しおください。`;
    
                try {
                    const { text, sources } = await generateNarrative(fullPrompt);
                    
                    storyOutput.textContent = text;
                    storyOutput.classList.remove('border-dashed');
    
                    // 匕甚元の衚瀺
                    sourceList.innerHTML = '';
                    if (sources.length > 0) {
                        sources.forEach(source => {
                            const li = document.createElement('li');
                            li.innerHTML = `<a href="${source.uri}" target="_blank" class="text-primary hover:underline" rel="noopener noreferrer">${source.title}</a>`;
                            sourceList.appendChild(li);
                        });
                        sourcesContainer.classList.remove('hidden');
                    } else {
                        sourcesContainer.classList.add('hidden');
                    }
    
                } catch (error) {
                    console.error("生成゚ラヌ:", error);
                    storyOutput.textContent = '物語の生成䞭に゚ラヌが発生したした。';
                    errorMessage.textContent = `゚ラヌ: ${error.message}`;
                    errorMessage.classList.remove('hidden');
                    storyOutput.classList.add('border-dashed');
                    sourcesContainer.classList.add('hidden');
                } finally {
                    // UIを曎新 (ロヌディング終了)
                    generateBtn.disabled = !isAuthReady || !themeInput.value.trim();
                    loadingSpinner.classList.add('hidden');
                    generateIcon.classList.remove('hidden');
                }
            };
    
        </script>
    </body>
    </html>
    
    
     
     
    個人運営のWebTRPG運営で燃え尜きた人 トラックに蜢かれなくおも異䞖界転生出来る方法を暡玢䞭 https://note.com/game_system_alex 「AI・TRPG・創䜜・ネット文化を実際に詊しその結果を蚘録する実隓堎」 珟実ず物語の境界線のお話

    あなたぞのおすすめ