
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æ©èœæå€ãšç¡æã§äœ¿ããã®ã§ãã£ãŠã¿ãŸããã

ããŠããã¯ãã€ããŠãã ããã
ãç¡ããšã¯æããŸãã(ãŸã å®ç§ã§ã¯ãªãã§ãã)
åããããã販売ã補äœè
ãåä¹ãã®ã¯ãååŒäžãã(ãã®å
調æŽåºæ¥ãªããªãã®ã§)
ããŸã ãã°ãããããã®ã§äœ¿çšãããçµæã¯èªå·±è²¬ä»»ã§ãé¡ãããããŸãã
ã(ã¿ã€ãã³ã°ã«ãã£ãŠã¯API飿ºã®é¢ä¿ã§ãšã©ãŒãšãªãã®ã確èªããŠãããŸã)
ããŸãAPIãèŠçŽéåã¬ãã«ã«ãšããã§éžè±ãããšAPIãã蹎ãããŸã(ãã®ã²ãŒã ã§ã¯æšæºä»æ§ã§ã¯èµ·ãã«ãããšã¯æããŸãã)
ãæ©èœè§£é€çã§ç¢ºèªããŸããã
ããã®èŸºã¯ãã¹ãããŠã¿ãªããšããããªãã®ã§ã貎éãªããŒã¿ã§ã¯ãããŸãã
ããŸã ãŸã å瀟ã®APIã«å¯Ÿããç¡æã®ç¯å²ãããããã®ã§ãã£ãŠã¿ãªããšãã®èŸºã¯ããããªããã§ããããâŠã
ãç¡éã«å€ªã£è
¹ã§ã¯ãªãã£ãâŠ(åœããåã )

ã¡ãã£ãšåŸ©å»çã®ã²ãŒã ããã¯ã
éåãšè±ªå¢ã ãªãïŒç¬ïŒ
ãã ããã©ã°ã©ãã§ã®åéºã«ã¯ã¯ã¯ã¯ãããããã®å·»æ«ã«æžãããŠããã©ã°ã©ãã§ã¯ãªãç¡éã®å¯èœæ§ãšããŠã®ãTRPG(ãã®åœæã¯D&D)ã£ãŠäœã ããïŒã£ãŠãã®é ã¯æã£ãŠããããããã§âŠã
å°èª¬ãæ ç»ã§ãèªåãªãå¥ã®éžæãããããïŒãšãæã人ã¯ãTRPGãšããéžæè¢ããŸãããããç¥ããŸãããã
ãšãã²ãŒã ããã¯ã®å·»æ«ãšåããããªããšãã»ã®ããããŠãããŸããïŒç¬ïŒ
åäœã®æèŠ:åããããéåžžãã¬ã€ãªãåé¡ãªãã
äœãããšãã¯é床ãè¶
ãããèŠåç¡ãã§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>
- #èªå
- #note
- #ã€ã©ã¹ã
- #AI
- #çæAI
- #ChatGPT
- #æ ç»
- #ãã¶ã€ã³
- #AIãšãã£ãŠã¿ã
- #ç©èª
- #Gemini
- #ç¶ç¶
- #æ å ±
- #çºä¿¡
- #ãªã¹ã¯
- #ã¡ã³ããŒã·ãã
- #æè¡
- #OpenAI
- #æŠç¥
- #ããœã³ã³
- #ç¶ç¶ã¯åãªã
- #çæ
- #å±é
- #é¢çœ
- #ããŒãããŒ
- #èœå
- #AIå°èª¬
- #Copilot
- #grok
- #åéº
- #TRPG
- #ç¶ç¶å
- #å°å³
- #çè«
- #ãã€ãã³ãŒãã£ã³ã°
- #幎è³ç¶
- #æ°ä»
- #äžæ
- #屿©ç®¡ç
- #æŠè¡
- #èŠå¶
- #API
- #ãµã€ããŒãã³ã¯
- #泚æ
- #ããŒãããœã³ã³
- #é²è¡
- #ç¶ç¶ã®ã³ã
- #èŠçŽ
- #絵垫
- #éªé
- #AIããŒãããŒãšã®æ¥åžž
- #å€å®
- #å°è©
- #ã²ãŒã ããã¯
- #AIããŒãããŒçé
- #åéºè
- #AI絵垫
- #åéºå°èª¬
- #canvas
- #AIã³ã³ãããªã³
- #çæ®ºäžå¥ª
- #æããã«
- #ãã£ãããã€ãã€
- #ããã
- #ãªããµã³ãã€ãã€
- #14ãžç¶ã