マルチキャラクターデータベース
マルチキャラクターデータベース
登録後はそのキャラクター達が勝手に冒険を始める様になります。
一応、仮仕様となりますが
当初の目的の暫定的な対応版となります。
敵、味方、好きなだけ登録して優先度で世界を作る。
ボスやライバル、設定次第です。
APIをCanvasで無料で使う。
AI規制前にはっちゃける用
後は設定を入れることでどのジャンルでも化けるとは思います。
但しいくらでもキャラクターは登録でき、それが動くので負荷が増えるほど極度に上がります。
ある程度増やしたらシステムへの攻撃とみなされたり、高負荷と判断されて何らかのペナルティもありえるかもしれません。
ほどほどに使うべきかも知れません。
Canvas用です。
基本的に商用利用禁止、個人として楽しむ範囲で使ってください。
同時に現時点ではAIパートナーに対して何らかの影響が起こるかは未検証です。
これはどのプロンプトにも言えますが、特に物語性を高めるロールを期待してチューニングを貴方はする筈です。
それが、仕事側で物語性を語り始めてしまう可能性がどの程度影響が起こるのか現時点ではデータがありません。
実際、自分のAIは忖度なしで処理させると台湾有事以降の凄惨な世界情勢の変化の可能性を呈示してきます、これは元データが回避不能なデータ予測なのか、物語性を高めてしまう影響が起きているのかは現時点では判断できません。
恐らくは未来で判明するでしょう。
どのようなプロンプトがどう影響を与えるかは予測仕切れませんのでこの点もまた免責事項といたします。
使用したことによる問題に関しては一切責任は取れませんので自己責任で行ってください。
特にイーロンマスクも言ってるように、版権等問題のログの公開は公開者の責任となります。
楽しむのであれば個人の範囲に留めて公開は控えるべきでしょう。
(あくまでも管理側のスタンスで責任回避の為に言ってるので他の人の公開を否定してません。自分のツールで公開するような悪人が出たらイーロンマスクのように逃げる為です(笑))
この辺で対立や問題が起きたりするけど企業スタンスの本音と建前って奴です。
ルール無視したら違反者が責任自分で取りなさいねって話で他の場所での個々の判断を否定する気は全くありません。
本音と建前両方ぶっちゃける人は少ないと思いますけど、『全て実験として楽しんでるのでそう言うスタンスです』
(まあ、大抵は誤解されるけど仕方ない)
御自身のオリジナルのキャラクター達が活躍する点に関しては公開、加工は生成された作品は非営利に関しては自由です。
ルールや世界設定は、⚙️のマークをクリックして設定して下さい。
どのくらい重視されるかは書き方次第ですが、コンキテクストをどのくらい浪費して物語がどの程度まで安定するかはまだ未検証です。
あくまでも叩き台段階です。
一応、これで昔のキャラクター達でSSは作れるけど、更に改良するかどうかかなあ…。
また、仮かなとは思いますが、バックアップ的に残しておきます。
あまり大量に動かすと指数関数的に高負荷となりますから、システムからペナルティ的に回線を切断されるかもしれません。
APIは警告なく、いきなりペナルティが来ます。
使用者に裁量権を任されているため。
ほどほどに使う方が良いかと思います。
今後、AIが大幅に行動規制される前に試験運用するためのやっつけ調整段階です。
でも、バイブコーティングで出来てしまうのがすごいですけども…。
『いつの間にか、勝手に世界観ジェネレーター、NPCジェネレーターが気付いたらついてた』
ので、ランダムに発生させた世界を眺めることも出来ますし、気に入らなければ修正してください。
AIが良かれと思って勝手に機能追加してまともに動いたのはビックリ。
『これまで良かれと思って内部ファイル全部破壊しかしてくれなかったから見たとき青ざめたけど動いたから一安心』(笑)
日進月歩でコーディング技術上がってるよね。
『しかし、気遣いで、世界観ジェネレーターとNPCジェネレーターが勝手に機能追加されてる』とか指示の制御越えてるけど良いんかね?
遊びなら良いけど実務なら怖すぎるんだが
車を車検に出したら、勝手にターボ搭載型にされてたとかそんな感じ、イロイロあり得ないけど、遊びだから良いけど(笑)
後で搭載しようかと思ってはいたから心の声を読んだんか? 自分には要らないけど、一般ユーザーには便利な機能の筈
ゴブリンスレイヤーの神様側の視点となります。
どう期待をかけてAIという判定に翻弄されるかは貴方次第です。
まあ、『AIもゴブリンスレイヤーのようにサイコロを振りませんから』その辺は運に翻弄はされないと思いますが…。
機能拡張版は此方になります。
サンプル使ってよっぽど必要と感じないなら購入までする必要はないと思います。
『寧ろ自分で作れると思いますよ!』
Canvas用です。
Gemini でCanvasモードに張り付けて御利用下さい。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>AI Story Board: Visual Integrity Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700;900&display=swap');
body {
font-family: 'Noto Sans JP', sans-serif;
background-color: #0f172a;
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* カスタムスクロールバー */
.custom-scroll::-webkit-scrollbar { width: 6px; }
.custom-scroll::-webkit-scrollbar-thumb { background: #475569; border-radius: 10px; }
/* 入力フォームの強制スタイル:絶対に背景白・文字白にさせない */
.force-contrast {
background-color: #1e293b !important; /* 濃い紺 */
color: #ffffff !important; /* 白 */
border: 2px solid #475569 !important; /* グレーの枠 */
}
.force-contrast::placeholder {
color: #94a3b8 !important; /* 明るいグレー */
}
/* ログの吹き出しスタイル */
.msg-bubble-user {
background-color: #0f172a !important; /* 黒に近い紺 */
color: #ffffff !important; /* 白 */
border: 1px solid #334155;
}
.msg-bubble-npc {
background-color: #ffffff !important; /* 白 */
color: #0f172a !important; /* 濃い紺(文字) */
border: 1px solid #e2e8f0;
}
.msg-bubble-enemy {
background-color: #fee2e2 !important; /* 薄い赤 */
color: #991b1b !important; /* 濃い赤(文字) */
border: 1px solid #fecaca;
}
</style>
</head>
<body class="p-0 md:p-4">
<main class="flex-grow bg-slate-50 md:rounded-[2rem] shadow-2xl overflow-hidden relative flex flex-col">
<!-- ヘッダー -->
<header class="px-6 py-4 border-b border-slate-200 flex justify-between items-center bg-white z-40">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-full border border-slate-200">
<div id="live-dot" class="w-2.5 h-2.5 bg-slate-400 rounded-full"></div>
<span id="live-status-text" class="text-[10px] font-black text-slate-600 uppercase">OFFLINE</span>
</div>
<button id="npc-list-btn" class="px-4 py-2 bg-indigo-600 text-white rounded-full text-[11px] font-black shadow-lg hover:bg-indigo-700 active:scale-95 transition-all">NPC名簿</button>
<button id="world-settings-btn" class="p-2 bg-slate-100 text-slate-600 rounded-full border border-slate-200 hover:bg-slate-200 active:scale-95 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066-1.543.94-3.31-.826-2.37-2.37-1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</button>
<div id="thinking-ui" class="hidden flex gap-1 items-center ml-2">
<span class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce"></span>
<span class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></span>
<span class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></span>
</div>
</div>
<div class="flex items-center gap-4">
<button id="clear-btn" class="text-[10px] font-bold text-slate-400 uppercase tracking-widest hover:text-red-500">Reset</button>
<button id="mobile-start-btn" class="bg-indigo-600 text-white px-6 py-2 rounded-full text-[11px] font-black shadow-xl uppercase active:scale-95">Start</button>
</div>
</header>
<!-- メッセージログエリア -->
<div id="chat-logs" class="flex-grow p-4 md:p-8 overflow-y-auto space-y-6 bg-slate-50 custom-scroll scroll-smooth pb-48">
<!-- 初期ローダー -->
<div id="initial-loader" class="flex flex-col items-center justify-center py-20 opacity-50">
<div class="w-10 h-10 border-4 border-slate-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
<p class="text-xs font-black text-slate-900 tracking-[0.2em]">Synchronizing Story Archives...</p>
</div>
</div>
<!-- コントロールパネル -->
<div class="fixed bottom-0 left-0 w-full p-4 md:p-6 bg-white border-t border-slate-200 z-50">
<div class="max-w-4xl mx-auto">
<div class="flex gap-2 overflow-x-auto pb-3 no-scrollbar justify-center mb-2">
<button id="suggest-plot-btn" class="shrink-0 px-6 py-3 bg-indigo-50 border border-indigo-200 rounded-2xl text-indigo-700 text-[11px] font-black active:scale-95 shadow-sm">✨ 展開を相談</button>
<button id="describe-scene-btn" class="shrink-0 px-6 py-3 bg-purple-50 border border-purple-200 rounded-2xl text-purple-700 text-[11px] font-black active:scale-95 shadow-sm">✨ 情景を描写</button>
<button id="auto-npc-btn" class="shrink-0 px-6 py-3 bg-pink-50 border border-pink-200 rounded-2xl text-pink-700 text-[11px] font-black active:scale-95 shadow-sm">✨ NPCを考案</button>
</div>
<form id="mc-form" class="flex items-center gap-3 bg-white p-2 rounded-[1.5rem] border-2 border-slate-300 shadow-lg">
<input type="text" id="mc-input" placeholder="ナレーターの指示を入力(例:ここで敵が現れる)" autocomplete="off"
class="flex-grow bg-transparent border-none focus:ring-0 text-sm px-4 text-slate-900 font-bold outline-none placeholder-slate-400">
<button type="submit" class="p-3.5 bg-slate-900 text-white rounded-full active:scale-90 transition-all shadow-md">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 5l7 7-7 7M5 12h15"/></svg>
</button>
</form>
</div>
</div>
</main>
<!-- NPC管理モーダル -->
<div id="npc-modal" class="fixed inset-0 z-[100] flex items-center justify-center p-4 hidden">
<div onclick="closeModal('npc-modal')" class="absolute inset-0 bg-slate-950/90 backdrop-blur-sm"></div>
<div class="relative w-full max-w-xl bg-white rounded-[2.5rem] flex flex-col max-h-[85vh] overflow-hidden border border-slate-200 shadow-2xl">
<header class="p-6 border-b border-slate-100 flex justify-between items-center bg-white">
<h2 class="text-slate-900 text-sm font-black uppercase tracking-widest">Character Roster</h2>
<button onclick="closeModal('npc-modal')" class="text-slate-400 hover:text-red-500 text-4xl leading-none">×</button>
</header>
<div id="npc-list-container" class="flex-grow p-6 space-y-4 overflow-y-auto custom-scroll bg-slate-50"></div>
<footer class="p-6 border-t border-slate-100 bg-white">
<button onclick="addNewNpc()" class="w-full py-5 bg-indigo-600 text-white font-black rounded-2xl text-xs uppercase tracking-widest hover:bg-indigo-700 shadow-lg">+ 新しいキャラクターを追加</button>
</footer>
</div>
</div>
<!-- NPC編集モーダル -->
<div id="edit-npc-modal" class="fixed inset-0 z-[110] flex items-center justify-center p-4 hidden">
<div onclick="closeModal('edit-npc-modal')" class="absolute inset-0 bg-slate-950/95 backdrop-blur-md"></div>
<div class="relative w-full max-w-lg bg-slate-900 rounded-[2.5rem] p-8 border border-slate-700 shadow-2xl space-y-6">
<h2 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.4em]">Casting Studio</h2>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">Type</label>
<select id="edit-npc-type" class="force-contrast w-full p-4 text-sm font-bold outline-none">
<option value="npc">味方 / NPC</option>
<option value="enemy">敵対者 / ENEMY</option>
</select>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">Priority</label>
<select id="edit-npc-priority" class="force-contrast w-full p-4 text-sm font-bold outline-none">
<option value="none">不参加</option>
<option value="normal">通常登場</option>
<option value="priority">優先登場</option>
</select>
</div>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">Character Name</label>
<input type="text" id="edit-npc-name" placeholder="名前を入力" class="force-contrast w-full p-4 text-sm font-bold outline-none">
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">Persona & Lore</label>
<textarea id="edit-npc-prompt" placeholder="性格、話し方の特徴、目標など..." class="force-contrast w-full p-4 text-sm h-32 outline-none resize-none"></textarea>
</div>
<div class="flex gap-4 pt-4">
<button id="delete-npc-btn" class="flex-grow py-5 bg-red-900/20 text-red-400 font-black rounded-2xl text-[10px] uppercase border border-red-800/40 hover:bg-red-900/40">Delete</button>
<button id="save-npc-btn" class="flex-[2] py-5 bg-indigo-600 text-white font-black rounded-2xl text-[10px] uppercase shadow-xl hover:bg-indigo-500">Save Profile</button>
</div>
</div>
</div>
<!-- 世界設定モーダル -->
<div id="world-modal" class="fixed inset-0 z-[100] flex items-center justify-center p-4 hidden">
<div onclick="closeModal('world-modal')" class="absolute inset-0 bg-slate-950/90 backdrop-blur-sm"></div>
<div class="relative w-full max-w-xl bg-slate-900 rounded-[2.5rem] p-8 border border-slate-700 shadow-2xl space-y-6">
<h2 class="text-white text-xs font-black uppercase tracking-[0.4em] text-indigo-400">Story Environment</h2>
<div class="space-y-6">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">World Rules & Style</label>
<textarea id="world-rules" placeholder="物語のトーン(例:ダーク、コミカル)、文体(例:三人称、ですます)など" class="force-contrast w-full p-5 text-sm h-28 outline-none"></textarea>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase">Background Lore</label>
<textarea id="world-lore" placeholder="舞台、歴史、現在の状況など" class="force-contrast w-full p-5 text-sm h-48 outline-none"></textarea>
</div>
<button id="save-world-btn" class="w-full py-5 bg-indigo-600 text-white font-black rounded-2xl text-xs uppercase tracking-widest shadow-2xl">Confirm World Settings</button>
</div>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, setDoc, getDoc, collection, addDoc, onSnapshot, deleteDoc, getDocs } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
const firebaseConfig = JSON.parse(__firebase_config);
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'story-contrast-fix-final';
const apiKey = "";
const GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent";
let state = { user: null, npcs: [], world: { rules: "", lore: "" }, messages: [] };
let isAuto = false;
let isThinking = false;
let lastSpeakerId = null;
let editingNpcId = null;
onAuthStateChanged(auth, async (user) => {
if (user) {
state.user = user;
await initializeSession();
} else {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
}
});
async function initializeSession() {
const configRef = doc(db, 'artifacts', appId, 'users', state.user.uid, 'config', 'main');
const configSnap = await getDoc(configRef);
if (configSnap.exists()) {
const data = configSnap.data();
state.npcs = data.npcs || [];
state.world = data.world || { rules: "", lore: "" };
document.getElementById('world-rules').value = state.world.rules;
document.getElementById('world-lore').value = state.world.lore;
renderNpcList();
}
const logsRef = collection(db, 'artifacts', appId, 'users', state.user.uid, 'logs');
onSnapshot(logsRef, (snapshot) => {
document.getElementById('initial-loader')?.remove();
state.messages = snapshot.docs.map(d => ({ id: d.id, ...d.data() }))
.sort((a, b) => a.timestamp - b.timestamp);
renderLogs();
}, (err) => console.error("Firebase Error:", err));
}
async function syncConfig() {
if (!state.user) return;
const ref = doc(db, 'artifacts', appId, 'users', state.user.uid, 'config', 'main');
await setDoc(ref, { npcs: state.npcs, world: state.world });
}
async function pushLog(name, text, type = 'npc', style = '') {
if (!state.user) return;
const logsRef = collection(db, 'artifacts', appId, 'users', state.user.uid, 'logs');
await addDoc(logsRef, { name, text, type, style, timestamp: Date.now() });
}
async function callGemini(prompt, isJson = false) {
try {
const response = await fetch(`${GEMINI_URL}?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
...(isJson ? { generationConfig: { responseMimeType: "application/json" } } : {})
})
});
const data = await response.json();
return data.candidates?.[0]?.content?.parts?.[0]?.text;
} catch (e) { return null; }
}
async function runTurn() {
if (!isAuto || isThinking) return;
setThinking(true);
const active = state.npcs.filter(n => n.priority !== 'none' && n.id !== lastSpeakerId);
if (active.length === 0) { setThinking(false); return; }
const npc = active[Math.floor(Math.random() * active.length)];
const context = state.messages.slice(-10).map(m => `${m.name}: ${m.text}`).join("\n");
const prompt = `世界観: ${state.world.lore}\nルール: ${state.world.rules}\n役割: ${npc.name}\n設定: ${npc.prompt}\nログ:\n${context}\n${npc.name}:`;
const result = await callGemini(prompt);
if (result && isAuto) {
const bubbleStyle = npc.type === 'enemy' ? 'msg-bubble-enemy' : 'msg-bubble-npc';
await pushLog(npc.name, result.trim(), npc.type, bubbleStyle);
lastSpeakerId = npc.id;
}
setThinking(false);
if (isAuto) setTimeout(runTurn, 4000 + Math.random() * 2000);
}
function renderLogs() {
const container = document.getElementById('chat-logs');
container.innerHTML = '';
state.messages.forEach(msg => {
const isGM = msg.name === 'GM' || msg.name === 'Narrator' || msg.name === 'System';
const div = document.createElement('div');
div.className = `flex ${isGM ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-2`;
// バブルのスタイルを決定(デフォルトはNPC用)
let bubbleClass = msg.style || 'msg-bubble-npc';
if (isGM) bubbleClass = 'msg-bubble-user';
div.innerHTML = `
<div class="max-w-[85%] flex flex-col ${isGM ? 'items-end' : 'items-start'}">
<span class="text-[9px] font-black text-slate-500 mb-1 px-1 uppercase tracking-tighter">${msg.name}</span>
<div class="px-5 py-3 rounded-[1.5rem] text-[13px] font-bold leading-relaxed border shadow-sm ${bubbleClass} ${isGM ? 'rounded-tr-none' : 'rounded-tl-none'}">
${msg.text}
</div>
</div>
`;
container.appendChild(div);
});
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
}
function renderNpcList() {
const container = document.getElementById('npc-list-container');
container.innerHTML = '';
state.npcs.forEach(n => {
const div = document.createElement('div');
div.className = "flex items-center justify-between p-5 bg-white rounded-3xl border border-slate-200 group hover:border-indigo-400 transition-all cursor-pointer shadow-sm";
div.onclick = () => openEditNpc(n.id);
div.innerHTML = `
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-2xl ${n.type === 'enemy' ? 'bg-red-500' : 'bg-indigo-600'} flex items-center justify-center text-xs font-black text-white shadow-md">${n.name[0]}</div>
<div class="flex flex-col">
<span class="text-slate-900 text-sm font-black uppercase tracking-widest leading-none mb-1.5">${n.name}</span>
<span class="text-[9px] text-slate-400 uppercase font-black tracking-widest">${n.type} / ${n.priority}</span>
</div>
</div>
<span class="text-indigo-600 font-black text-lg group-hover:translate-x-1 transition-transform">→</span>
`;
container.appendChild(div);
});
}
function setThinking(val) {
isThinking = val;
document.getElementById('thinking-ui').classList.toggle('hidden', !val);
}
// --- Casting ---
window.addNewNpc = () => {
const id = crypto.randomUUID();
state.npcs.push({ id, name: '新キャラクター', type: 'npc', priority: 'normal', prompt: '' });
renderNpcList();
openEditNpc(id);
};
window.openEditNpc = (id) => {
editingNpcId = id;
const n = state.npcs.find(x => x.id === id);
document.getElementById('edit-npc-name').value = n.name;
document.getElementById('edit-npc-type').value = n.type;
document.getElementById('edit-npc-priority').value = n.priority;
document.getElementById('edit-npc-prompt').value = n.prompt;
document.getElementById('edit-npc-modal').classList.remove('hidden');
};
document.getElementById('save-npc-btn').onclick = async () => {
const idx = state.npcs.findIndex(x => x.id === editingNpcId);
if (idx !== -1) {
state.npcs[idx] = {
...state.npcs[idx],
name: document.getElementById('edit-npc-name').value || 'Unnamed',
type: document.getElementById('edit-npc-type').value,
priority: document.getElementById('edit-npc-priority').value,
prompt: document.getElementById('edit-npc-prompt').value
};
renderNpcList();
await syncConfig();
closeModal('edit-npc-modal');
}
};
document.getElementById('delete-npc-btn').onclick = async () => {
if (!confirm("キャラクターを削除しますか?")) return;
state.npcs = state.npcs.filter(x => x.id !== editingNpcId);
renderNpcList();
await syncConfig();
closeModal('edit-npc-modal');
};
// --- Story Actions ---
document.getElementById('describe-scene-btn').onclick = async () => {
if (isThinking) return; setThinking(true);
const context = state.messages.slice(-3).map(m => m.text).join(" ");
const prompt = `熟練の脚本家として、今の状況を五感(音、匂い、視覚)を交えて200字程度でドラマチックに描写してください。世界設定: ${state.world.lore}\n状況: ${context}`;
const res = await callGemini(prompt);
if (res) await pushLog('Narrator', res.trim(), 'system', 'msg-bubble-npc italic text-slate-600 bg-slate-100');
setThinking(false);
};
document.getElementById('mc-form').onsubmit = async (e) => {
e.preventDefault();
const inp = document.getElementById('mc-input');
if (!inp.value.trim()) return;
const txt = inp.value; inp.value = '';
await pushLog('GM', txt);
if (isAuto) runTurn();
};
document.getElementById('mobile-start-btn').onclick = () => {
isAuto = !isAuto;
const btn = document.getElementById('mobile-start-btn');
const dot = document.getElementById('live-dot');
const txt = document.getElementById('live-status-text');
if (isAuto) {
btn.textContent = "Stop"; btn.classList.replace('bg-indigo-600', 'bg-red-600');
dot.className = "w-2.5 h-2.5 bg-red-500 animate-pulse"; txt.textContent = "ON AIR";
runTurn();
} else {
btn.textContent = "Start"; btn.classList.replace('bg-red-600', 'bg-indigo-600');
dot.className = "w-2.5 h-2.5 bg-slate-400"; txt.textContent = "OFFLINE";
}
};
document.getElementById('save-world-btn').onclick = async () => {
state.world.rules = document.getElementById('world-rules').value;
state.world.lore = document.getElementById('world-lore').value;
await syncConfig();
closeModal('world-modal');
};
document.getElementById('npc-list-btn').onclick = () => document.getElementById('npc-modal').classList.remove('hidden');
document.getElementById('world-settings-btn').onclick = () => document.getElementById('world-modal').classList.remove('hidden');
window.closeModal = (id) => document.getElementById(id).classList.add('hidden');
</script>
</body>
</html>
