AICharacter掲示板

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>AIキャラ会話掲示板 - Virtual Guild Board</title>
  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif;
      background: linear-gradient(135deg, #0f172a, #1e293b, #111827);
      color: #e5e7eb;
    }

    header {
      padding: 24px;
      background: rgba(0,0,0,0.35);
      border-bottom: 1px solid rgba(255,255,255,0.08);
      text-align: center;
    }

    header h1 {
      margin: 0;
      font-size: 32px;
      color: #f8fafc;
    }

    header p {
      margin-top: 8px;
      color: #cbd5e1;
      font-size: 14px;
    }

    .container {
      max-width: 1300px;
      margin: 0 auto;
      padding: 20px;
      display: grid;
      grid-template-columns: 300px 1fr 320px;
      gap: 20px;
    }

    .panel {
      background: rgba(255,255,255,0.06);
      border: 1px solid rgba(255,255,255,0.08);
      border-radius: 18px;
      box-shadow: 0 10px 30px rgba(0,0,0,0.25);
      overflow: hidden;
      backdrop-filter: blur(10px);
    }

    .panel-title {
      padding: 16px 18px;
      font-size: 18px;
      font-weight: bold;
      border-bottom: 1px solid rgba(255,255,255,0.08);
      background: rgba(255,255,255,0.04);
    }

    .character-list {
      padding: 14px;
      display: flex;
      flex-direction: column;
      gap: 12px;
      max-height: 720px;
      overflow-y: auto;
    }

    .character-card {
      padding: 14px;
      border-radius: 14px;
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.06);
    }

    .character-card h3 {
      margin: 0 0 6px 0;
      font-size: 17px;
    }

    .character-meta {
      font-size: 13px;
      color: #cbd5e1;
      margin-bottom: 8px;
    }

    .character-desc {
      font-size: 13px;
      color: #e2e8f0;
      line-height: 1.6;
    }

    .main-board {
      display: flex;
      flex-direction: column;
      min-height: 780px;
    }

    .toolbar {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      padding: 14px;
      border-bottom: 1px solid rgba(255,255,255,0.08);
      background: rgba(255,255,255,0.03);
    }

    button, select, input, textarea {
      font: inherit;
    }

    button {
      border: none;
      border-radius: 10px;
      padding: 10px 14px;
      cursor: pointer;
      background: linear-gradient(135deg, #3b82f6, #2563eb);
      color: white;
      transition: 0.2s ease;
    }

    button:hover {
      transform: translateY(-1px);
      filter: brightness(1.08);
    }

    .danger {
      background: linear-gradient(135deg, #ef4444, #dc2626);
    }

    .sub {
      background: linear-gradient(135deg, #64748b, #475569);
    }

    .chat-area {
      flex: 1;
      padding: 18px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 14px;
      min-height: 500px;
      max-height: 580px;
    }

    .post {
      display: flex;
      gap: 12px;
      align-items: flex-start;
      padding: 14px;
      border-radius: 16px;
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.06);
      animation: fadeIn 0.25s ease;
    }

    .avatar {
      width: 48px;
      height: 48px;
      min-width: 48px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 22px;
      background: rgba(255,255,255,0.12);
      border: 1px solid rgba(255,255,255,0.12);
    }

    .post-content {
      flex: 1;
    }

    .post-header {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      align-items: center;
      margin-bottom: 6px;
    }

    .name {
      font-weight: bold;
      font-size: 15px;
      color: #ffffff;
    }

    .role {
      font-size: 12px;
      color: #93c5fd;
      background: rgba(59,130,246,0.15);
      padding: 3px 8px;
      border-radius: 999px;
    }

    .time {
      margin-left: auto;
      font-size: 12px;
      color: #94a3b8;
    }

    .message {
      font-size: 15px;
      line-height: 1.75;
      color: #f1f5f9;
      white-space: pre-wrap;
      word-break: break-word;
    }

    .composer {
      padding: 16px;
      border-top: 1px solid rgba(255,255,255,0.08);
      background: rgba(255,255,255,0.03);
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .composer-top {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    select, input, textarea {
      width: 100%;
      border-radius: 10px;
      border: 1px solid rgba(255,255,255,0.1);
      background: rgba(15,23,42,0.85);
      color: #fff;
      padding: 10px 12px;
      outline: none;
    }

    textarea {
      resize: vertical;
      min-height: 100px;
    }

    .composer-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }

    .right-panel-content {
      padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 16px;
    }

    .status-box, .topic-box, .memory-box {
      padding: 14px;
      border-radius: 14px;
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.06);
    }

    .status-line {
      margin: 8px 0;
      font-size: 14px;
      color: #e2e8f0;
    }

    .topic-tag {
      display: inline-block;
      margin: 6px 6px 0 0;
      padding: 6px 10px;
      border-radius: 999px;
      background: rgba(16,185,129,0.18);
      color: #bbf7d0;
      font-size: 12px;
    }

    .memory-item {
      font-size: 13px;
      padding: 8px 10px;
      margin-top: 8px;
      border-radius: 10px;
      background: rgba(255,255,255,0.04);
      color: #dbeafe;
      line-height: 1.6;
    }

    .footer-note {
      text-align: center;
      color: #94a3b8;
      font-size: 12px;
      padding: 16px;
    }

    .online-dot {
      display: inline-block;
      width: 10px;
      height: 10px;
      border-radius: 50%;
      margin-right: 8px;
      background: #22c55e;
      box-shadow: 0 0 8px #22c55e;
    }

    @keyframes fadeIn {
      from {
        transform: translateY(8px);
        opacity: 0;
      }
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    @media (max-width: 1100px) {
      .container {
        grid-template-columns: 1fr;
      }
      .main-board {
        min-height: auto;
      }
      .chat-area {
        max-height: 500px;
      }
    }
  </style>
</head>
<body>
  <header>
    <h1>AIキャラ会話掲示板 - Virtual Guild Board</h1>
    <p>APIなし / ローカル動作 / AIキャラ同士が自動で会話するファンタジー掲示板</p>
  </header>

  <div class="container">
    <!-- 左 -->
    <aside class="panel">
      <div class="panel-title">キャラクター一覧</div>
      <div class="character-list" id="characterList"></div>
    </aside>

    <!-- 中央 -->
    <main class="panel main-board">
      <div class="panel-title">ギルド広場</div>

      <div class="toolbar">
        <button id="toggleAutoBtn">自動会話ON/OFF</button>
        <button id="manualTalkBtn" class="sub">AI会話を1回進める</button>
        <button id="eventBtn" class="sub">イベント発生</button>
        <button id="saveBtn" class="sub">保存</button>
        <button id="clearBtn" class="danger">会話をリセット</button>
      </div>

      <div class="chat-area" id="chatArea"></div>

      <div class="composer">
        <div class="composer-top">
          <select id="userName">
            <option value="旅人">旅人</option>
            <option value="冒険者">冒険者</option>
            <option value="見習い魔法使い">見習い魔法使い</option>
            <option value="傭兵">傭兵</option>
            <option value="吟遊詩人">吟遊詩人</option>
          </select>
        </div>
        <textarea id="userMessage" placeholder="メッセージを書いてください。例:今日は魔王城へ向かうべきかな?"></textarea>
        <div class="composer-actions">
          <button id="sendBtn">投稿する</button>
          <button id="userTriggerBtn" class="sub">投稿後にAI反応</button>
        </div>
      </div>
    </main>

    <!-- 右 -->
    <aside class="panel">
      <div class="panel-title">ワールド情報</div>
      <div class="right-panel-content">
        <div class="status-box">
          <div><span class="online-dot"></span>状態</div>
          <div class="status-line">自動会話: <span id="autoStatus">停止中</span></div>
          <div class="status-line">現在の話題: <span id="currentTopic">雑談</span></div>
          <div class="status-line">投稿数: <span id="postCount">0</span></div>
        </div>

        <div class="topic-box">
          <div><strong>話題タグ</strong></div>
          <div id="topicTags"></div>
        </div>

        <div class="memory-box">
          <div><strong>最近の話題メモ</strong></div>
          <div id="memoryList"></div>
        </div>
      </div>
    </aside>
  </div>

  <div class="footer-note">
    HTML/CSS/JavaScriptのみで動作します。データはブラウザに保存されます。
  </div>

  <script>
    const characters = [
      {
        id: "hero",
        name: "セイン",
        role: "勇者",
        emoji: "⚔️",
        personality: "まっすぐで熱血。前向き。",
        desc: "世界を旅する若き勇者。困っている人を見ると放っておけない。",
        styles: {
          start: ["よし、", "さて、", "うーん、", "そうだな、"],
          end: ["だ!", "だな。", "じゃないか?", "行くしかない!"],
          flavor: ["魔王", "冒険", "仲間", "ダンジョン", "伝説"]
        }
      },
      {
        id: "mage",
        name: "リリィ",
        role: "魔法使い",
        emoji: "🔮",
        personality: "冷静で知的。少し毒舌。",
        desc: "古代魔法を研究している少女。理屈で考えるタイプ。",
        styles: {
          start: ["理論的には、", "その話なら、", "少し気になるのは、", "魔法的に言えば、"],
          end: ["ですね。", "だと思います。", "かもしれません。", "要検証です。"],
          flavor: ["魔法", "精霊", "古代遺跡", "呪文", "研究"]
        }
      },
      {
        id: "knight",
        name: "ガルド",
        role: "騎士",
        emoji: "🛡️",
        personality: "真面目で忠誠心が強い。",
        desc: "王国騎士団に所属する重騎士。秩序と責任を重んじる。",
        styles: {
          start: ["王国のためにも、", "騎士としては、", "規律を守るなら、", "任務として考えると、"],
          end: ["異論はない。", "それが正しい。", "油断は禁物だ。", "準備が必要だ。"],
          flavor: ["王国", "任務", "警戒", "防衛", "規律"]
        }
      },
      {
        id: "merchant",
        name: "ミーナ",
        role: "商人",
        emoji: "💰",
        personality: "明るく現実的。商売人。",
        desc: "各地を巡る行商人。儲け話と珍品に目がない。",
        styles: {
          start: ["それより、", "商売の話をすると、", "利益で考えると、", "ふふっ、"],
          end: ["儲かりそうね。", "悪くないわ。", "値段次第かな。", "面白い商機だわ。"],
          flavor: ["市場", "金貨", "商品", "取引", "珍品"]
        }
      },
      {
        id: "assassin",
        name: "クロウ",
        role: "暗殺者",
        emoji: "🗡️",
        personality: "寡黙でクール。影のある口調。",
        desc: "裏社会で名を知られる暗殺者。静かに本質を突く。",
        styles: {
          start: ["……", "無駄口は嫌いだが、", "影から見る限り、", "静かに言うが、"],
          end: ["それだけだ。", "油断するな。", "匂うな。", "嫌な予感がする。"],
          flavor: ["影", "敵", "罠", "裏路地", "追跡"]
        }
      }
    ];

    const defaultTopics = [
      "魔王討伐",
      "古代遺跡",
      "王国の依頼",
      "森の異変",
      "ギルドの噂",
      "珍しいアイテム",
      "危険なダンジョン",
      "旅の準備",
      "精霊の目撃情報",
      "闇市場"
    ];

    const eventTopics = [
      "城下町で祭りが始まった",
      "北の洞窟にドラゴン出現",
      "謎の商人が秘宝を売っている",
      "王国から緊急依頼が届いた",
      "森で精霊の暴走が起きている",
      "魔王軍の斥候が発見された",
      "夜の港で密輸の噂が広がっている"
    ];

    const generalPhrases = [
      "最近の空気、少し変わった気がする",
      "今日は何か起きそうな予感がある",
      "仲間がいると旅は違う",
      "静かな日ほど何かが起きるものだ",
      "準備を怠ると危ない",
      "噂話にも案外ヒントがある",
      "この町には秘密が多い",
      "力だけでは解決しないこともある",
      "運だけでは生き残れない",
      "今のうちに備えておくべきだ"
    ];

    const replyRules = [
      {
        keywords: ["魔王", "討伐", "倒す"],
        responses: {
          hero: ["魔王を倒せば世界は少しは平和になるはずだ!", "ついに決戦の時かもしれないな!"],
          mage: ["魔王クラスの相手なら準備不足は危険です。", "封印術式も調べておくべきですね。"],
          knight: ["討伐任務なら戦力の整理が必要だ。", "王国への報告も忘れるな。"],
          merchant: ["討伐の前に装備をそろえないと損するわよ。", "その話、特需が出そうね。"],
          assassin: ["魔王より先に側近を潰すべきだ。", "正面から行くのは愚策かもしれない。"]
        }
      },
      {
        keywords: ["金", "お金", "金貨", "報酬"],
        responses: {
          hero: ["報酬も大事だけど、困っている人を助けたいな。", "金だけじゃなく名誉も欲しいところだ!"],
          mage: ["研究費は必要ですからね。", "魔導書は高いので報酬は重要です。"],
          knight: ["報酬より任務達成が優先だ。", "とはいえ補給費は無視できない。"],
          merchant: ["その話なら私の出番ね。", "利益率の高い案件なら乗るわ。"],
          assassin: ["金額次第で動く者も多い。", "報酬の匂いには裏がある。"]
        }
      },
      {
        keywords: ["遺跡", "古代", "秘宝"],
        responses: {
          hero: ["秘宝か……冒険心がくすぐられるな!", "遺跡には夢があるよな!"],
          mage: ["古代遺跡は知識の宝庫です。", "その話、かなり興味があります。"],
          knight: ["遺跡調査には護衛が必要だ。", "罠の警戒を優先しよう。"],
          merchant: ["秘宝は高く売れる可能性があるわね。", "希少品なら市場が動くわ。"],
          assassin: ["遺跡には死人の匂いがする。", "宝より罠を疑え。"]
        }
      },
      {
        keywords: ["森", "精霊", "自然"],
        responses: {
          hero: ["森の異変なら放っておけないな。", "精霊と仲良くできたら心強いな!"],
          mage: ["精霊系の異常反応かもしれません。", "自然魔力の乱れを疑います。"],
          knight: ["森は視界が悪い。隊列を乱すな。", "索敵役が必要だな。"],
          merchant: ["森の特産品が取れなくなるのは困るわ。", "薬草の値段も上がりそう。"],
          assassin: ["森では音と気配に気をつけろ。", "姿の見えない敵ほど厄介だ。"]
        }
      },
      {
        keywords: ["こんにちは", "初めまして", "はじめまして"],
        responses: {
          hero: ["ようこそ!一緒に冒険の話をしよう!", "よろしくな!"],
          mage: ["ようこそ。この掲示板は案外にぎやかですよ。", "初めまして。興味深いですね。"],
          knight: ["歓迎しよう。礼節を守ってくれれば問題ない。", "ここでは情報共有が重要だ。"],
          merchant: ["いらっしゃい。いい情報があれば教えてね。", "歓迎するわ、旅人さん。"],
          assassin: ["……新顔か。好きにするといい。", "静かにしていれば問題ない。"]
        }
      }
    ];

    let posts = [];
    let memoryTopics = [];
    let currentTopic = "雑談";
    let autoTalk = false;
    let autoTimer = null;

    const characterList = document.getElementById("characterList");
    const chatArea = document.getElementById("chatArea");
    const currentTopicEl = document.getElementById("currentTopic");
    const postCountEl = document.getElementById("postCount");
    const autoStatusEl = document.getElementById("autoStatus");
    const topicTagsEl = document.getElementById("topicTags");
    const memoryListEl = document.getElementById("memoryList");

    function renderCharacters() {
      characterList.innerHTML = "";
      characters.forEach(char => {
        const card = document.createElement("div");
        card.className = "character-card";
        card.innerHTML = `
          <h3>${char.emoji} ${char.name}</h3>
          <div class="character-meta">${char.role} / ${char.personality}</div>
          <div class="character-desc">${char.desc}</div>
        `;
        characterList.appendChild(card);
      });
    }

    function getTimeString() {
      const now = new Date();
      return now.toLocaleTimeString("ja-JP", {
        hour: "2-digit",
        minute: "2-digit"
      });
    }

    function addPost(name, role, emoji, message, isUser = false) {
      const post = {
        id: Date.now() + Math.random(),
        name,
        role,
        emoji,
        message,
        time: getTimeString(),
        isUser
      };
      posts.push(post);
      renderPosts();
      saveData();
    }

    function renderPosts() {
      chatArea.innerHTML = "";
      posts.forEach(post => {
        const el = document.createElement("div");
        el.className = "post";
        el.innerHTML = `
          <div class="avatar">${post.emoji}</div>
          <div class="post-content">
            <div class="post-header">
              <div class="name">${escapeHtml(post.name)}</div>
              <div class="role">${escapeHtml(post.role)}</div>
              <div class="time">${post.time}</div>
            </div>
            <div class="message">${escapeHtml(post.message)}</div>
          </div>
        `;
        chatArea.appendChild(el);
      });
      chatArea.scrollTop = chatArea.scrollHeight;
      postCountEl.textContent = posts.length;
    }

    function escapeHtml(text) {
      return text
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;")
        .replaceAll("'", "&#039;");
    }

    function randomItem(arr) {
      return arr[Math.floor(Math.random() * arr.length)];
    }

    function pickCharacter(excludeId = null) {
      const pool = excludeId ? characters.filter(c => c.id !== excludeId) : characters;
      return randomItem(pool);
    }

    function updateTopic(newTopic) {
      currentTopic = newTopic;
      currentTopicEl.textContent = currentTopic;

      memoryTopics.unshift(newTopic);
      memoryTopics = [...new Set(memoryTopics)].slice(0, 8);

      renderTopics();
      renderMemory();
      saveData();
    }

    function renderTopics() {
      topicTagsEl.innerHTML = "";
      const mixed = [currentTopic, ...defaultTopics.slice(0, 6)];
      [...new Set(mixed)].forEach(topic => {
        const tag = document.createElement("span");
        tag.className = "topic-tag";
        tag.textContent = topic;
        topicTagsEl.appendChild(tag);
      });
    }

    function renderMemory() {
      memoryListEl.innerHTML = "";
      if (memoryTopics.length === 0) {
        memoryListEl.innerHTML = `<div class="memory-item">まだ話題メモはありません。</div>`;
        return;
      }
      memoryTopics.forEach(topic => {
        const item = document.createElement("div");
        item.className = "memory-item";
        item.textContent = topic;
        memoryListEl.appendChild(item);
      });
    }

    function buildCharacterSentence(character, topic = currentTopic) {
      const style = character.styles;
      const start = randomItem(style.start);
      const end = randomItem(style.end);
      const flavor = randomItem(style.flavor);
      const phrase = randomItem(generalPhrases);

      const patterns = [
        `${start}${topic}について言えば、${flavor}が鍵になりそう${end}`,
        `${start}${phrase}。特に${flavor}が絡むなら注意${end}`,
        `${start}${topic}の件は気になる。${flavor}の情報を集めたい${end}`,
        `${start}${flavor}を見直した方がいい。${topic}にも繋がる${end}`,
        `${start}${phrase}。${topic}と${flavor}は無関係じゃない${end}`
      ];

      return randomItem(patterns);
    }

    function getRuleBasedReply(inputText, character) {
      const text = inputText.toLowerCase();

      for (const rule of replyRules) {
        const matched = rule.keywords.some(keyword => text.includes(keyword.toLowerCase()));
        if (matched) {
          const responses = rule.responses[character.id];
          if (responses && responses.length > 0) {
            return randomItem(responses);
          }
        }
      }

      return null;
    }

    function extractTopicFromText(text) {
      const found = defaultTopics.find(topic => text.includes(topic.replace("の", ""))) ||
                    eventTopics.find(topic => text.includes(topic.slice(0, 4)));

      if (found) return found;

      if (text.includes("魔王")) return "魔王討伐";
      if (text.includes("遺跡")) return "古代遺跡";
      if (text.includes("森")) return "森の異変";
      if (text.includes("金") || text.includes("報酬")) return "報酬と金貨";
      if (text.includes("王国")) return "王国の依頼";
      if (text.includes("精霊")) return "精霊の目撃情報";
      if (text.includes("ダンジョン")) return "危険なダンジョン";

      return null;
    }

    function aiTalkOnce(previousSpeakerId = null) {
      const speaker = pickCharacter(previousSpeakerId);
      const msg = buildCharacterSentence(speaker, currentTopic);
      addPost(speaker.name, speaker.role, speaker.emoji, msg);
    }

    function aiReplyToText(text, count = 2) {
      const detectedTopic = extractTopicFromText(text);
      if (detectedTopic) updateTopic(detectedTopic);

      let usedIds = [];
      for (let i = 0; i < count; i++) {
        const pool = characters.filter(c => !usedIds.includes(c.id));
        const speaker = randomItem(pool);
        usedIds.push(speaker.id);

        let reply = getRuleBasedReply(text, speaker);
        if (!reply) {
          reply = buildCharacterSentence(speaker, currentTopic);
        }

        addPost(speaker.name, speaker.role, speaker.emoji, reply);
      }
    }

    function generateEvent() {
      const eventText = randomItem(eventTopics);
      updateTopic(eventText);

      addPost("ワールド通知", "システム", "📢", eventText);

      setTimeout(() => {
        aiReplyToText(eventText, 3);
      }, 300);
    }

    function toggleAutoTalk() {
      autoTalk = !autoTalk;
      autoStatusEl.textContent = autoTalk ? "稼働中" : "停止中";

      if (autoTalk) {
        autoTimer = setInterval(() => {
          const count = Math.random() < 0.4 ? 2 : 1;
          let prevId = null;
          for (let i = 0; i < count; i++) {
            const speaker = pickCharacter(prevId);
            prevId = speaker.id;
            const msg = buildCharacterSentence(speaker, currentTopic);
            addPost(speaker.name, speaker.role, speaker.emoji, msg);
          }

          if (Math.random() < 0.28) {
            updateTopic(randomItem(defaultTopics));
          }
        }, 4500);
      } else {
        clearInterval(autoTimer);
      }
      saveData();
    }

    function saveData() {
      const data = {
        posts,
        memoryTopics,
        currentTopic,
        autoTalk
      };
      localStorage.setItem("virtualGuildBoardData", JSON.stringify(data));
    }

    function loadData() {
      const raw = localStorage.getItem("virtualGuildBoardData");
      if (!raw) return false;

      try {
        const data = JSON.parse(raw);
        posts = data.posts || [];
        memoryTopics = data.memoryTopics || [];
        currentTopic = data.currentTopic || "雑談";
        autoTalk = false;
        currentTopicEl.textContent = currentTopic;
        renderPosts();
        renderTopics();
        renderMemory();
        autoStatusEl.textContent = "停止中";
        return true;
      } catch (e) {
        console.error("読み込み失敗", e);
        return false;
      }
    }

    function clearBoard() {
      if (!confirm("会話ログをリセットしますか?")) return;
      posts = [];
      memoryTopics = [];
      currentTopic = "雑談";
      currentTopicEl.textContent = currentTopic;
      renderPosts();
      renderTopics();
      renderMemory();
      saveData();
      addWelcomePosts();
    }

    function addWelcomePosts() {
      addPost("ワールド通知", "システム", "🌍", "Virtual Guild Boardへようこそ。ここではAIキャラたちが自由に会話します。");
      addPost("セイン", "勇者", "⚔️", "よし、今日も冒険の情報を集めよう!");
      addPost("リリィ", "魔法使い", "🔮", "掲示板の反応を見る限り、今日は賑やかになりそうですね。");
      addPost("ミーナ", "商人", "💰", "儲け話でも危険な依頼でも、情報は早い者勝ちよ。");
    }

    document.getElementById("sendBtn").addEventListener("click", () => {
      const userName = document.getElementById("userName").value.trim();
      const userMessage = document.getElementById("userMessage").value.trim();

      if (!userMessage) {
        alert("メッセージを入力してください。");
        return;
      }

      addPost(userName, "プレイヤー", "🧑", userMessage, true);

      const maybeTopic = extractTopicFromText(userMessage);
      if (maybeTopic) {
        updateTopic(maybeTopic);
      }

      document.getElementById("userMessage").value = "";
    });

    document.getElementById("userTriggerBtn").addEventListener("click", () => {
      const userName = document.getElementById("userName").value.trim();
      const userMessage = document.getElementById("userMessage").value.trim();

      if (!userMessage) {
        alert("メッセージを入力してください。");
        return;
      }

      addPost(userName, "プレイヤー", "🧑", userMessage, true);
      aiReplyToText(userMessage, 3);
      document.getElementById("userMessage").value = "";
    });

    document.getElementById("manualTalkBtn").addEventListener("click", () => {
      aiTalkOnce();
    });

    document.getElementById("eventBtn").addEventListener("click", () => {
      generateEvent();
    });

    document.getElementById("toggleAutoBtn").addEventListener("click", () => {
      toggleAutoTalk();
    });

    document.getElementById("saveBtn").addEventListener("click", () => {
      saveData();
      alert("保存しました。");
    });

    document.getElementById("clearBtn").addEventListener("click", () => {
      clearBoard();
    });

    renderCharacters();

    const loaded = loadData();
    if (!loaded || posts.length === 0) {
      renderTopics();
      renderMemory();
      addWelcomePosts();
      updateTopic("ギルドの噂");
    } else {
      renderTopics();
      renderMemory();
    }
  </script>
</body>
</html>

投稿者: chosuke

趣味はゲームやアニメや漫画などです

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です