‹ マニュアルへ戻る

ソースコードコピー専用ページ

こちらのページからコードをコピーしてツールをご利用ください。

【セットアップ手順】
  1. パソコン上に新規フォルダ(例:my-novel-game)を作成します。
  2. 下記の各タブから「コードをコピー」ボタンを押し、太字で表示されているファイル名で保存してください。
  3. 保存したファイルを同じフォルダ内に配置すれば、ツールのセットアップ完了です!
  4. 「index.html」と「scenario.txt」は必須ですが、「editor.html」と「debug.html」は必要に応じてご利用ください。

⚠それぞれのツールはファイル名で連携しているため、別の名前で保存するときちんと動作しなくなります。
⚠サンプルゲーム内で使用している画像や音楽のデータは同梱されていません。

index.html ゲームのシステム本体(プレイヤーがブラウザで遊ぶ画面)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>ノベルゲーム制作ツール</title>
  <style>
    :root {
      /* ベース背景色 */
      --bg-outer: #050a14;                /* 画面外側の背景色 */
      --bg-game: #0d1b2a;                 /* ゲーム画面・モーダル背景色 */
      --border-game: #1b263b;             /* ゲーム画面枠線 */

      /* メインテーマカラー(ボタン・アクセント) */
      --color-primary: #ffd166;         /* 主ボタン背景・強調表示 */
      --color-primary-hover: #ffeea9;  /* 主ボタンホバー時 */
      --color-primary-text: #1d3557;   /* 主ボタン・好感度枠のテキスト色 */

      /* サブボタン・システムUI */
      --color-secondary: #ffffff;        /* システムボタン背景 */
      --color-secondary-border: #1d3557; /* システムボタン枠線 */
      --color-secondary-text: #1d3557;   /* システムボタンテキスト色 */
      --color-active: #e63946;           /* AUTO/SKIP有効時ハイライト色 */

      /* メッセージウィンドウ */
      --msg-box-bg: rgba(255, 255, 255, 0.92); /* メッセージ枠背景 */
      --msg-box-border: #1d3557;        /* メッセージ枠線 */
      --msg-speaker-text: #1d3557;      /* 話者名テキスト色 */
      --msg-body-text: #2b2d42;          /* 本文テキスト色 */

      /* セーブ・ロード・バックログ(モーダル画面) */
      --modal-bg: rgba(13, 27, 42, 0.95); /* モーダル全体の背景 */
      --modal-card-bg: #ffffff;          /* セーブスロット背景色 */
      --modal-card-border: #457b9d;      /* セーブスロット枠線 */
      --modal-btn-bg: #457b9d;            /* 閉じるボタン背景色 */
      --modal-btn-hover: #1d3557;        /* 閉じるボタンホバー時 */
      --text-light: #ffffff;              /* モーダル用基本文字色 */
    }

    html, body {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      background-color: var(--bg-outer);
      overflow: hidden;
      font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", MeiRyo, sans-serif;
    }

    #game-wrapper {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 1;
    }

    #game-container {
      width: 800px;
      height: 450px;
      position: absolute;
      background-color: var(--bg-game);
      background-size: cover;
      background-position: center;
      color: #333;
      overflow: hidden;
      border: 4px solid var(--border-game);
      border-radius: 12px;
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
      transform-origin: center center;
      box-sizing: border-box;
    }

    #fade-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: var(--bg-game);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.5s ease;
      z-index: 50;
    }

    #fade-overlay.is-active {
      opacity: 1;
      pointer-events: auto;
    }

    /* タイトル画面レイヤー */
    #title-screen {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-image: url('images/bg01.webp'); /* ←ここを変更してください */
      background-size: cover;
      background-position: center;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      gap: 15px;
      z-index: 100;
    }

    #title-screen::before {
      content: "";
      position: absolute;
      top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(13, 27, 42, 0.55);
      z-index: -1;
    }

    #title-screen h1 {
      font-size: 38px;
      margin-bottom: 20px;
      color: var(--text-light);
      letter-spacing: 2px;
      text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
    }

    .menu-btn {
      padding: 12px 28px;
      font-size: 16px;
      font-weight: bold;
      background-color: var(--color-primary);
      color: var(--color-primary-text);
      border: 3px solid var(--text-light);
      border-radius: 25px;
      cursor: pointer;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
      transition: all 0.2s ease;
      width: 200px;
    }

    .menu-btn:hover {
      background-color: var(--color-primary-hover);
      transform: translateY(-3px);
      box-shadow: 0 6px 14px rgba(0, 0, 0, 0.3);
    }

    #system-menu {
      position: absolute;
      top: 12px;
      right: 12px;
      display: none;
      gap: 6px;
      z-index: 10;
    }

    .sys-btn {
      padding: 6px 12px;
      font-size: 12px;
      font-weight: bold;
      background: var(--color-secondary);
      color: var(--color-secondary-text);
      border: 2px solid var(--color-secondary-border);
      border-radius: 15px;
      cursor: pointer;
      box-shadow: 0 2px 6px rgba(0,0,0,0.15);
      transition: all 0.2s ease;
    }

    .sys-btn:hover {
      background: var(--color-primary);
      transform: translateY(-2px);
    }

    .sys-btn.is-active {
      background: var(--color-active);
      color: var(--text-light);
      border-color: var(--text-light);
      box-shadow: 0 0 8px rgba(230, 57, 70, 0.8);
    }

    #favor-display {
      position: absolute;
      top: 12px;
      left: 12px;
      padding: 8px 14px;
      font-size: 12px;
      font-weight: bold;
      background: var(--color-primary);
      color: var(--color-primary-text);
      border: 2px solid var(--text-light);
      border-radius: 12px;
      display: none;
      flex-direction: column;
      gap: 3px;
      z-index: 10;
      box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
      text-align: left;
      min-width: 90px;
    }

    .favor-item {
      display: flex;
      justify-content: space-between;
      gap: 12px;
    }

    #log-modal {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: var(--modal-bg);
      display: none;
      flex-direction: column;
      align-items: center;
      padding: 20px;
      box-sizing: border-box;
      z-index: 200;
    }

    #log-modal h2 {
      margin: 0 0 15px 0;
      color: var(--color-primary);
      font-size: 22px;
    }

    #log-container {
      width: 90%;
      height: 300px;
      overflow-y: auto;
      background: rgba(255, 255, 255, 0.05);
      border: 2px solid var(--modal-card-border);
      border-radius: 8px;
      padding: 15px;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .log-item {
      display: flex;
      flex-direction: column;
      border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
      padding-bottom: 8px;
      text-align: left;
    }

    .log-speaker {
      font-weight: bold;
      color: var(--color-primary);
      font-size: 13px;
      margin-bottom: 2px;
    }

    .log-text {
      color: var(--text-light);
      font-size: 14px;
      line-height: 1.4;
    }

    #staff-credits {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: var(--bg-game);
      display: none;
      z-index: 80;
    }

    #credits-container {
      display: flex;
      width: 100%;
      height: 100%;
      position: relative;
    }

    #credits-image-area {
      width: 50%;
      height: 100%;
      background-size: cover;
      background-position: center;
      transition: width 0.5s ease;
    }

    #credits-text-area {
      width: 50%;
      height: 100%;
      background-color: var(--modal-bg);
      color: var(--text-light);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      padding: 30px;
      box-sizing: border-box;
      text-align: center;
    }

    .credits-role {
      font-size: 14px;
      color: var(--color-primary);
      font-weight: bold;
      margin-bottom: 8px;
      letter-spacing: 1px;
    }

    .credits-name {
      font-size: 20px;
      font-weight: bold;
      margin-bottom: 24px;
      line-height: 1.4;
    }

    #thanks-text {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      font-size: 42px;
      font-weight: bold;
      color: var(--text-light);
      text-shadow: 0 4px 12px rgba(0, 0, 0, 0.8), 0 0 8px rgba(255, 209, 102, 0.8);
      display: none;
      z-index: 90;
      white-space: nowrap;
      pointer-events: none;
    }

    #save-modal {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: var(--modal-bg);
      display: none;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      z-index: 200;
    }

    #save-modal h2 {
      margin-bottom: 20px;
      color: var(--color-primary);
      font-size: 24px;
    }

    .slot-list {
      display: flex;
      flex-direction: column;
      gap: 12px;
      width: 80%;
      max-width: 500px;
    }

    .slot-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background: var(--modal-card-bg);
      color: var(--color-secondary-text);
      border: 2px solid var(--modal-card-border);
      padding: 12px 18px;
      border-radius: 12px;
      cursor: pointer;
      box-shadow: 0 3px 6px rgba(0,0,0,0.15);
      transition: all 0.2s ease;
    }

    .slot-item:hover {
      background: #e0f4ff;
      border-color: var(--color-primary);
      transform: translateY(-2px);
    }

    .slot-info {
      display: flex;
      flex-direction: column;
      gap: 4px;
      text-align: left;
    }

    .slot-title {
      font-weight: bold;
      color: var(--color-secondary-text);
    }

    .slot-preview {
      font-size: 12px;
      color: var(--modal-card-border);
    }

    .slot-date {
      font-size: 10px;
      color: #6c757d;
    }

    .modal-close-btn {
      margin-top: 20px;
      padding: 8px 24px;
      background: var(--modal-btn-bg);
      color: var(--text-light);
      font-weight: bold;
      border: none;
      border-radius: 18px;
      cursor: pointer;
      transition: background 0.2s;
    }

    .modal-close-btn:hover {
      background: var(--modal-btn-hover);
    }

    /* 立ち絵レイヤー(3人対応) */
    #character-layer {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 1;
    }

    .character-img {
      position: absolute;
      bottom: 0;
      max-height: 380px;
      opacity: 0;
      transform: translateX(-50%);
      transition: left 0.35s ease, opacity 0.3s ease;
      display: none;
    }

    .character-img.pos-left {
      left: 20%;
    }

    .character-img.pos-center {
      left: 50%;
    }

    .character-img.pos-right {
      left: 80%;
    }

    .character-img.is-visible {
      opacity: 1;
      display: block;
    }

    #message-box {
      position: absolute;
      bottom: 12px;
      left: 12px;
      right: 12px;
      height: 105px;
      background: var(--msg-box-bg);
      border: 3px solid var(--msg-box-border);
      border-radius: 12px;
      padding: 12px 18px;
      cursor: pointer;
      box-sizing: border-box;
      z-index: 2;
      display: none;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
    }

    #speaker-name {
      font-weight: bold;
      color: var(--msg-speaker-text);
      margin-bottom: 6px;
      font-size: 15px;
    }

    #message-text {
      color: var(--msg-body-text);
      line-height: 1.5;
      font-size: 14px;
    }

    #choice-container {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      display: flex;
      flex-direction: column;
      gap: 12px;
      z-index: 10;
    }

    .choice-btn {
      padding: 12px 24px;
      font-size: 14px;
      font-weight: bold;
      background: var(--color-primary);
      color: var(--color-primary-text);
      border: 2px solid var(--text-light);
      border-radius: 20px;
      cursor: pointer;
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
      transition: all 0.2s ease;
    }

    .choice-btn:hover {
      background: var(--color-primary-hover);
      color: var(--color-primary-text);
      transform: scale(1.05);
    }
  </style>
</head>
<body>

<div id="game-wrapper">
  <div id="game-container">
    <div id="fade-overlay"></div>

    <!-- タイトル画面 -->
    <div id="title-screen">
      <h1>ノベルゲームタイトル</h1>
      <button id="start-btn" class="menu-btn">NEW GAME</button>
      <button id="title-load-btn" class="menu-btn">LOAD GAME</button>
    </div>

    <div id="staff-credits">
      <div id="credits-container">
        <div id="credits-image-area"></div>
        <div id="credits-text-area"></div>
      </div>
      <div id="thanks-text">Thanks for playing!</div>
    </div>

    <!-- 好感度・ステータス表示 -->
    <div id="favor-display"></div>

    <div id="system-menu">
      <button id="auto-btn" class="sys-btn">AUTO</button>
      <button id="skip-btn" class="sys-btn">SKIP</button>
      <button id="log-btn" class="sys-btn">LOG</button>
      <button id="save-btn" class="sys-btn">SAVE</button>
      <button id="load-btn" class="sys-btn">LOAD</button>
      <button id="to-title-btn" class="sys-btn">TITLE</button>
    </div>

    <div id="log-modal">
      <h2>BACKLOG</h2>
      <div id="log-container"></div>
      <button id="log-close-btn" class="modal-close-btn">閉じる</button>
    </div>

    <div id="save-modal">
      <h2 id="modal-title">SAVE DATA</h2>
      <div class="slot-list">
        <div class="slot-item" data-slot="1">
          <div class="slot-info">
            <div class="slot-title">SLOT 1</div>
            <div class="slot-preview" id="slot-preview-1">-- NO DATA --</div>
            <div class="slot-date" id="slot-date-1"></div>
          </div>
        </div>
        <div class="slot-item" data-slot="2">
          <div class="slot-info">
            <div class="slot-title">SLOT 2</div>
            <div class="slot-preview" id="slot-preview-2">-- NO DATA --</div>
            <div class="slot-date" id="slot-date-2"></div>
          </div>
        </div>
        <div class="slot-item" data-slot="3">
          <div class="slot-info">
            <div class="slot-title">SLOT 3</div>
            <div class="slot-preview" id="slot-preview-3">-- NO DATA --</div>
            <div class="slot-date" id="slot-date-3"></div>
          </div>
        </div>
      </div>
      <button id="modal-close-btn" class="modal-close-btn">閉じる</button>
    </div>

    <!-- 立ち絵レイヤー(3人対応) -->
    <div id="character-layer">
      <img id="chara-left" class="character-img pos-left" src="" alt="立ち絵左">
      <img id="chara-center" class="character-img pos-center" src="" alt="立ち絵中央">
      <img id="chara-right" class="character-img pos-right" src="" alt="立ち絵右">
    </div>
    
    <div id="choice-container"></div>

    <div id="message-box">
      <div id="speaker-name"></div>
      <div id="message-text"></div>
    </div>
  </div>
</div>

<script>
  function resizeGame() {
    const container = document.getElementById("game-container");
    const baseWidth = 800;
    const baseHeight = 450;
    const windowWidth = window.innerWidth;
    const windowHeight = window.innerHeight;
    const scaleX = windowWidth / baseWidth;
    const scaleY = windowHeight / baseHeight;
    const scale = Math.min(scaleX, scaleY) * 0.96;
    container.style.transform = `scale(${scale})`;
  }

  window.addEventListener("resize", resizeGame);
  window.addEventListener("orientationchange", resizeGame);

  // 共通定数設定
  const TITLE_BGM_URL = "sound/sound01.mp3";
  const ENDING_BGM_URL = "sound/sound01.mp3";
  const SOUND_EFFECTS = {
    click: "sound/click.mp3",
    type: "sound/type.mp3",
    select: "sound/select.mp3"
  };

  // 画面表示用ラベルのマッピング設定
  const FAVOR_NAME_MAP = {
    charaA: "ほのか",
    charaB: "裕也",
    charaC: "三月"
  };

  // スタッフロールデータ
  const creditsData = [
    {
      img: "images/bg01.webp",
      roles: [
        { role: "制作・シナリオ", name: "okojo" },
      ]
    },
    {
      img: "images/bg02.webp",
      roles: [
        { role: "イラスト", name: "ゴリラの素材屋さん" },
      ]
    },
    {
      img: "images/bg03.webp",
      roles: [
        { role: "音楽・BGM", name: "魔王魂" },
      ]
    },
    {
      img: "images/bg04.webp",
      roles: [
        { role: "効果音(SE)", name: "魔王魂" },
      ]
    },
    {
      img: "images/bg05.webp",
      roles: [
        { role: "スペシャルサンクス", name: "Gemini" },
      ]
    },
    {
      img: "images/bg01.webp",
      isFinal: true
    }
  ];

  let scenario = [];
  let labelMap = {};
  let favorability = createInitialFavorability();
  let currentSceneId = 0;
  let typeTimer = null;
  let autoTimer = null;
  let creditsTimer = null;
  let isTyping = false;
  let isFading = false;
  let fullText = "";
  const typingSpeed = 50;
  const fadeDuration = 500;

  let isAutoMode = false;
  let isSkipMode = false;
  const autoDelay = 1500;
  const skipDelay = 100;

  let readSceneIds = new Set();
  let backlog = [];
  let currentCreditIndex = 0;
  let isCreditsActive = false;
  let isFinalCreditScreen = false;
  let currentModalMode = "save";

  // 現在のキャラ配置管理用
  let activeCharacters = { left: "", center: "", right: "" };

  const bgmAudio = new Audio();
  bgmAudio.loop = true;
  bgmAudio.volume = 0.3;
  let currentBgmSrc = "";

  function parseScenarioText(text) {
    const lines = text.split(/\r?\n/).filter(line => line.trim() !== "");
    const parsedScenario = [];
    labelMap = {};
    let pendingCommands = [];
    let currentScene = { commands: [] };
    let sceneIdCounter = 0;

    lines.forEach(line => {
      line = line.trim();

      if (line.startsWith("#") || line.startsWith("//")) return;

      if (line.startsWith("@")) {
        const firstColonIdx = line.indexOf(":");
        let command = "", param = "";

        if (firstColonIdx !== -1) {
          command = line.slice(1, firstColonIdx).trim();
          param = line.slice(firstColonIdx + 1).trim();
        } else {
          command = line.slice(1).trim();
        }

        const cmdObj = { command, param };

        if (command === "label") {
          labelMap[param] = sceneIdCounter;
          cmdObj.label = param;
        } else if (command === "branch") {
          cmdObj.isBranch = true;
          const parts = param.split("|");
          parts.forEach(p => {
            if (p.startsWith("type:")) cmdObj.branchType = p.replace("type:", "").trim();
            if (p.startsWith("chara:")) cmdObj.branchChara = p.replace("chara:", "").trim();
            if (p.startsWith("border:")) cmdObj.branchBorder = parseInt(p.replace("border:", "").trim());
            if (p.startsWith("target:")) cmdObj.branchTarget = p.replace("target:", "").trim();
            if (p.startsWith("else:")) cmdObj.elseTarget = p.replace("else:", "").trim();
            if (p.startsWith("cond:")) cmdObj.branchCond = p.replace("cond:", "").trim();
            if (p.startsWith("happy:")) cmdObj.happyTarget = p.replace("happy:", "").trim();
            if (p.startsWith("normal:")) cmdObj.normalTarget = p.replace("normal:", "").trim();
          });
        } else if (command === "choice") {
          const choiceItems = param.split("/");
          cmdObj.choices = choiceItems.map(item => {
            const parts = item.split("|").map(s => s.trim());
            const text = parts[0];
            let nextTarget = null, favorTarget = "charaA", favorChange = 0;

            parts.slice(1).forEach(p => {
              if (p.startsWith("next:")) nextTarget = p.replace("next:", "").trim();
              if (p.startsWith("favor:")) {
                const val = p.replace("favor:", "").trim();
                if (val.includes(":")) {
                  const [target, amount] = val.split(":");
                  favorTarget = target.trim();
                  favorChange = parseInt(amount);
                } else {
                  favorChange = parseInt(val);
                }
              }
            });
            return { text, nextTarget, favorTarget, favorChange };
          });
        }

        pendingCommands.push(cmdObj);
        return;
      }

      const speakerMatch = line.match(/^([^:@<>\n\s]+):(.*)$/);
      if (speakerMatch) {
        currentScene.speaker = speakerMatch[1].trim();
        currentScene.text = speakerMatch[2].trim();
      } else {
        currentScene.speaker = "";
        currentScene.text = line;
      }

      currentScene.commands = [...pendingCommands];
      pendingCommands = [];

      currentScene.id = sceneIdCounter;
      currentScene.next = sceneIdCounter + 1;
      parsedScenario.push(currentScene);
      sceneIdCounter++;
      currentScene = { commands: [] };
    });

    parsedScenario.forEach(scene => {
      scene.commands.forEach(cmd => {
        if (cmd.choices) {
          cmd.choices.forEach(choice => {
            if (choice.nextTarget) {
              choice.next = (labelMap[choice.nextTarget] !== undefined) ? labelMap[choice.nextTarget] : parseInt(choice.nextTarget);
            }
          });
        }
      });
    });

    return parsedScenario;
  }

  async function loadScenarioFromTxt(filePath) {
    try {
      const response = await fetch(filePath);
      const text = await response.text();
      return parseScenarioText(text);
    } catch (error) {
      console.error("scenario.txt の読み込みエラー:", error);
      return [];
    }
  }

  function createInitialFavorability() {
    return { charaA: 0, charaB: 0, charaC: 0 };
  }

  const fadeOverlayEl = document.getElementById("fade-overlay");
  const titleScreenEl = document.getElementById("title-screen");
  const systemMenuEl = document.getElementById("system-menu");
  const favorDisplayEl = document.getElementById("favor-display");
  const startBtnEl = document.getElementById("start-btn");
  const titleLoadBtnEl = document.getElementById("title-load-btn");
  const saveBtnEl = document.getElementById("save-btn");
  const loadBtnEl = document.getElementById("load-btn");
  const toTitleBtnEl = document.getElementById("to-title-btn");
  const saveModalEl = document.getElementById("save-modal");
  const modalTitleEl = document.getElementById("modal-title");
  const modalCloseBtnEl = document.getElementById("modal-close-btn");
  const gameContainerEl = document.getElementById("game-container");
  
  const charaImgEls = {
    left: document.getElementById("chara-left"),
    center: document.getElementById("chara-center"),
    right: document.getElementById("chara-right")
  };

  const speakerNameEl = document.getElementById("speaker-name");
  const messageTextEl = document.getElementById("message-text");
  const messageBoxEl = document.getElementById("message-box");
  const choiceContainerEl = document.getElementById("choice-container");

  const autoBtnEl = document.getElementById("auto-btn");
  const skipBtnEl = document.getElementById("skip-btn");

  const logBtnEl = document.getElementById("log-btn");
  const logModalEl = document.getElementById("log-modal");
  const logContainerEl = document.getElementById("log-container");
  const logCloseBtnEl = document.getElementById("log-close-btn");

  const staffCreditsEl = document.getElementById("staff-credits");
  const creditsImgAreaEl = document.getElementById("credits-image-area");
  const creditsTextAreaEl = document.getElementById("credits-text-area");
  const thanksTextEl = document.getElementById("thanks-text");

  function playSe(src, volume = 0.4) {
    if (!src) return;
    const se = new Audio(src);
    se.volume = volume;
    se.play().catch(err => console.log("SE再生エラー:", err));
  }

  function playBgm(src) {
    if (!src) {
      bgmAudio.pause();
      bgmAudio.currentTime = 0;
      currentBgmSrc = "";
      return;
    }

    if (currentBgmSrc === src) return;

    bgmAudio.pause();
    bgmAudio.src = src;
    bgmAudio.currentTime = 0;
    
    bgmAudio.play().then(() => {
      currentBgmSrc = src;
    }).catch(error => {
      console.log("BGM再生エラー:", error);
    });
  }

  function loadReadScenes() {
    const data = localStorage.getItem("novel_game_read_scenes");
    if (data) {
      try {
        readSceneIds = new Set(JSON.parse(data));
      } catch (e) {
        console.error("既読データの読み込みエラー", e);
      }
    }
  }

  function saveReadScenes() {
    localStorage.setItem("novel_game_read_scenes", JSON.stringify(Array.from(readSceneIds)));
  }

  function markSceneAsRead(sceneId) {
    if (!readSceneIds.has(sceneId)) {
      readSceneIds.add(sceneId);
      saveReadScenes();
    }
  }

  function isSceneRead(sceneId) {
    return readSceneIds.has(sceneId);
  }

  function updateFavorDisplay() {
    if (typeof favorability === "object" && favorability !== null) {
      let html = "";
      for (const [key, val] of Object.entries(favorability)) {
        const labelName = FAVOR_NAME_MAP[key] || key;
        html += `<div class="favor-item"><span>${labelName}:</span><span>${val}</span></div>`;
      }
      favorDisplayEl.innerHTML = html;
    } else {
      favorDisplayEl.textContent = favorability;
    }
  }

  function updateAutoSkipUI() {
    autoBtnEl.classList.toggle("is-active", isAutoMode);
    skipBtnEl.classList.toggle("is-active", isSkipMode);
  }

  function stopAutoSkip() {
    isAutoMode = false;
    isSkipMode = false;
    clearTimeout(autoTimer);
    updateAutoSkipUI();
  }

  function scheduleNextStep() {
    clearTimeout(autoTimer);
    if (choiceContainerEl.children.length > 0) return;

    if (isSkipMode) {
      autoTimer = setTimeout(() => {
        advanceStory();
      }, skipDelay);
    } else if (isAutoMode) {
      autoTimer = setTimeout(() => {
        advanceStory();
      }, autoDelay);
    }
  }

  function parseColorTags(str) {
    if (!str) return "";
    return str.replace(/<color:(.*?)>(.*?)<\/color>/g, (match, color, p2) => {
      return `<span style="color:${color.trim()};">${p2}</span>`;
    });
  }

  function typeWriter(speaker, text) {
    clearInterval(typeTimer);
    clearTimeout(autoTimer);
    fullText = text;
    messageTextEl.innerHTML = "";
    isTyping = true;

    const lastLog = backlog[backlog.length - 1];
    if (!lastLog || lastLog.speaker !== speaker || lastLog.text !== text) {
      backlog.push({ speaker, text });
    }

    const parsedHtml = parseColorTags(text);

    if (isSkipMode) {
      messageTextEl.innerHTML = parsedHtml;
      isTyping = false;
      scheduleNextStep();
      return;
    }

    let currentHtml = "";
    let i = 0;

    typeTimer = setInterval(() => {
      if (i < parsedHtml.length) {
        if (parsedHtml[i] === '<') {
          const closingIdx = parsedHtml.indexOf('>', i);
          if (closingIdx !== -1) {
            currentHtml += parsedHtml.slice(i, closingIdx + 1);
            i = closingIdx + 1;
          } else {
            currentHtml += parsedHtml[i];
            i++;
          }
        } else {
          currentHtml += parsedHtml[i];
          if (parsedHtml[i].trim() !== "") {
            playSe(SOUND_EFFECTS.type, 0.2);
          }
          i++;
        }
        messageTextEl.innerHTML = currentHtml;
      } else {
        clearInterval(typeTimer);
        messageTextEl.innerHTML = parsedHtml;
        isTyping = false;
        scheduleNextStep();
      }
    }, typingSpeed);
  }

  function openLogModal() {
    logContainerEl.innerHTML = "";
    
    if (backlog.length === 0) {
      logContainerEl.innerHTML = '<div style="color:#aaa; padding:20px;">履歴がありません。</div>';
    } else {
      backlog.forEach(item => {
        const itemEl = document.createElement("div");
        itemEl.className = "log-item";
        
        const speakerHtml = item.speaker ? `<div class="log-speaker">${item.speaker}</div>` : "";
        const parsedLogText = parseColorTags(item.text);
        itemEl.innerHTML = `${speakerHtml}<div class="log-text">${parsedLogText}</div>`;
        
        logContainerEl.appendChild(itemEl);
      });
    }

    logModalEl.style.display = "flex";
    logContainerEl.scrollTop = logContainerEl.scrollHeight;
  }

  function closeLogModal() {
    logModalEl.style.display = "none";
  }

  function showGameUI() {
    titleScreenEl.style.display = "none";
    staffCreditsEl.style.display = "none";
    messageBoxEl.style.display = "block";
    systemMenuEl.style.display = "flex";
    favorDisplayEl.style.display = "flex";
    isCreditsActive = false;
    isFinalCreditScreen = false;
  }

  function clearAllCharacters() {
    activeCharacters = { left: "", center: "", right: "" };
    ["left", "center", "right"].forEach(pos => {
      const img = charaImgEls[pos];
      img.classList.remove("is-visible");
      img.src = "";
    });
  }

  function showTitleUI() {
    stopAutoSkip();
    clearTimeout(creditsTimer);
    titleScreenEl.style.display = "flex";
    staffCreditsEl.style.display = "none";
    messageBoxEl.style.display = "none";
    systemMenuEl.style.display = "none";
    favorDisplayEl.style.display = "none";
    clearAllCharacters();
    choiceContainerEl.innerHTML = "";
    isCreditsActive = false;
    isFinalCreditScreen = false;

    if (TITLE_BGM_URL) {
      playBgm(TITLE_BGM_URL);
    }
  }

  function renderCreditStep(index) {
    clearTimeout(creditsTimer);

    const data = creditsData[index];
    if (!data) return;

    if (data.isFinal) {
      creditsImgAreaEl.style.width = "100%";
      creditsImgAreaEl.style.backgroundImage = `url('${data.img}')`;
      creditsTextAreaEl.style.display = "none";
      thanksTextEl.style.display = "block";
      isFinalCreditScreen = true;
    } else {
      creditsImgAreaEl.style.width = "50%";
      creditsImgAreaEl.style.backgroundImage = `url('${data.img}')`;
      creditsTextAreaEl.style.display = "flex";
      thanksTextEl.style.display = "none";

      let html = "";
      data.roles.forEach(item => {
        html += `<div class="credits-role">${item.role}</div>`;
        html += `<div class="credits-name">${item.name}</div>`;
      });
      creditsTextAreaEl.innerHTML = html;
      isFinalCreditScreen = false;

      creditsTimer = setTimeout(() => {
        nextCreditStep();
      }, 3000);
    }
  }

  function nextCreditStep() {
    if (isFading) return;

    if (isFinalCreditScreen) {
      playSe(SOUND_EFFECTS.click);
      fadeOverlayEl.classList.add("is-active");
      setTimeout(() => {
        showTitleUI();
        fadeOverlayEl.classList.remove("is-active");
      }, fadeDuration);
      return;
    }

    currentCreditIndex++;
    if (currentCreditIndex >= creditsData.length) return;

    isFading = true;
    fadeOverlayEl.classList.add("is-active");

    setTimeout(() => {
      renderCreditStep(currentCreditIndex);
      fadeOverlayEl.classList.remove("is-active");
      setTimeout(() => {
        isFading = false;
      }, fadeDuration);
    }, fadeDuration);
  }

  function startCredits() {
    stopAutoSkip();
    isCreditsActive = true;
    currentCreditIndex = 0;
    messageBoxEl.style.display = "none";
    systemMenuEl.style.display = "none";
    favorDisplayEl.style.display = "none";
    clearAllCharacters();

    if (ENDING_BGM_URL) {
      playBgm(ENDING_BGM_URL);
    }

    fadeOverlayEl.classList.add("is-active");
    setTimeout(() => {
      staffCreditsEl.style.display = "block";
      renderCreditStep(0);
      fadeOverlayEl.classList.remove("is-active");
    }, fadeDuration);
  }

  function updateCharacterDisplay(param) {
    if (param === undefined) return;

    if (!param) {
      clearAllCharacters();
      return;
    }

    const parts = param.split("|").map(s => s.trim());
    let imgSrc = parts[0];
    let pos = "center";
    let clearTarget = null;

    parts.slice(1).forEach(p => {
      if (p.startsWith("pos:")) pos = p.replace("pos:", "").trim();
      if (p.startsWith("clear:")) clearTarget = p.replace("clear:", "").trim();
    });

    if (clearTarget) {
      if (charaImgEls[clearTarget]) {
        activeCharacters[clearTarget] = "";
        charaImgEls[clearTarget].classList.remove("is-visible");
        charaImgEls[clearTarget].src = "";
      }
      return;
    }

    if (!["left", "center", "right"].includes(pos)) pos = "center";

    ["left", "center", "right"].forEach(p => {
      if (p !== pos && activeCharacters[p] === imgSrc) {
        activeCharacters[p] = "";
        charaImgEls[p].classList.remove("is-visible");
        charaImgEls[p].src = "";
      }
    });

    activeCharacters[pos] = imgSrc;
    const targetImg = charaImgEls[pos];
    targetImg.src = imgSrc;
    targetImg.classList.add("is-visible");
  }

  function updateSceneElements(scene) {
    currentSceneId = scene.id;
    speakerNameEl.textContent = scene.speaker;
    choiceContainerEl.innerHTML = "";

    if (scene.commands && scene.commands.length > 0) {
      scene.commands.forEach(cmd => {
        const command = cmd.command;
        const param = cmd.param;

        if (command === "bg") {
          if (param) {
            gameContainerEl.style.backgroundImage = `url('${param}')`;
          } else {
            gameContainerEl.style.backgroundImage = "none";
          }
        }
        else if (command === "character") {
          updateCharacterDisplay(param);
        }
        else if (command === "bgm") {
          playBgm(param);
        }
        else if (command === "se") {
          playSe(param, 0.5);
        }
        else if (cmd.choices) {
          stopAutoSkip();
          cmd.choices.forEach(choice => {
            const button = document.createElement("button");
            button.className = "choice-btn";
            button.textContent = choice.text;
            button.onclick = (e) => {
              e.stopPropagation();
              playSe(SOUND_EFFECTS.select);

              if (choice.favorChange !== undefined) {
                const target = choice.favorTarget || "charaA";
                if (typeof favorability !== "object" || favorability === null) {
                  favorability = createInitialFavorability();
                }
                if (favorability[target] === undefined) {
                  favorability[target] = 0;
                }
                favorability[target] += choice.favorChange;
                updateFavorDisplay();
              }

              renderScene(choice.next);
            };
            choiceContainerEl.appendChild(button);
          });
        }
      });
    }

    typeWriter(scene.speaker, scene.text);
  }

  function renderScene(sceneId) {
    clearTimeout(autoTimer);
    if (isFading) return;

    const scene = scenario.find(s => s.id === sceneId);
    if (!scene) return;

    if (isSkipMode && !isSceneRead(scene.id)) {
      stopAutoSkip();
    }

    markSceneAsRead(scene.id);

    const hasFade = scene.commands && scene.commands.some(c => c.command === "fade" && c.param === "true");
    const useFade = hasFade && !isSkipMode;

    if (useFade) {
      isFading = true;
      fadeOverlayEl.classList.add("is-active");

      setTimeout(() => {
        updateSceneElements(scene);
        fadeOverlayEl.classList.remove("is-active");

        setTimeout(() => {
          isFading = false;
        }, fadeDuration);
      }, fadeDuration);
    } else {
      updateSceneElements(scene);
    }
  }

  function getSlotKey(slotNum) {
    return `novel_game_slot_${slotNum}`;
  }

  function updateModalSlots() {
    for (let i = 1; i <= 3; i++) {
      const dataStr = localStorage.getItem(getSlotKey(i));
      const previewEl = document.getElementById(`slot-preview-${i}`);
      const dateEl = document.getElementById(`slot-date-${i}`);

      if (dataStr) {
        const data = JSON.parse(dataStr);
        const cleanText = data.text.replace(/<color:(.*?)>(.*?)<\/color>/g, "$2");
        const shortText = cleanText.length > 20 ? cleanText.substring(0, 20) + "..." : cleanText;
        
        let favorText = "";
        if (typeof data.favorability === "object" && data.favorability !== null) {
          favorText = Object.entries(data.favorability)
            .map(([k, v]) => `${FAVOR_NAME_MAP[k] || k}:${v}`)
            .join(' ');
        } else {
          favorText = data.favorability ?? 0;
        }

        <!-- セーブ・ロード画面に文字を表示する -->
        previewEl.textContent = `${data.speaker}: ${shortText} (${favorText})`;
        dateEl.textContent = data.savedAt;
      } else {
        previewEl.textContent = "-- NO DATA --";
        dateEl.textContent = "";
      }
    }
  }

  function openModal(mode) {
    stopAutoSkip();
    currentModalMode = mode;
    modalTitleEl.textContent = mode === "save" ? "SAVE DATA" : "LOAD DATA";
    updateModalSlots();
    saveModalEl.style.display = "flex";
  }

  function closeModal() {
    saveModalEl.style.display = "none";
  }

  function saveToSlot(slotNum) {
    const scene = scenario.find(s => s.id === currentSceneId);
    const saveDataObj = {
      sceneId: currentSceneId,
      speaker: scene ? scene.speaker : "",
      text: scene ? scene.text : "",
      favorability: favorability,
      activeCharacters: activeCharacters,
      backlog: backlog,
      savedAt: new Date().toLocaleString()
    };

    localStorage.setItem(getSlotKey(slotNum), JSON.stringify(saveDataObj));
    updateModalSlots();
    playSe(SOUND_EFFECTS.click);
    alert(`スロット ${slotNum} にセーブしました。`);
  }

  function loadFromSlot(slotNum) {
    const dataStr = localStorage.getItem(getSlotKey(slotNum));
    if (!dataStr) {
      alert("このスロットにはデータがありません。");
      return;
    }

    const data = JSON.parse(dataStr);
    
    if (typeof data.favorability === "number") {
      favorability = createInitialFavorability();
      favorability.charaA = data.favorability;
    } else {
      favorability = data.favorability || createInitialFavorability();
    }

    backlog = data.backlog || [];
    updateFavorDisplay();

    clearAllCharacters();
    if (data.activeCharacters) {
      activeCharacters = { ...data.activeCharacters };
      ["left", "center", "right"].forEach(pos => {
        if (activeCharacters[pos]) {
          charaImgEls[pos].src = activeCharacters[pos];
          charaImgEls[pos].classList.add("is-visible");
        }
      });
    }

    playSe(SOUND_EFFECTS.click);
    closeModal();
    showGameUI();
    renderScene(data.sceneId);
  }

  function checkConditionString(condStr) {
    if (!condStr) return false;
    const conditions = condStr.split(",").map(c => c.trim());
    
    return conditions.every(cond => {
      const match = cond.match(/^([a-zA-Z0-9_]+)\s*(>=|<=|>|<|==|!=)\s*(-?\d+)$/);
      if (!match) return false;
      
      const [, varName, op, valStr] = match;
      const targetVal = Number(valStr);
      const currentVal = (favorability && favorability[varName] !== undefined) ? favorability[varName] : 0;

      switch (op) {
        case ">=": return currentVal >= targetVal;
        case "<=": return currentVal <= targetVal;
        case ">":  return currentVal > targetVal;
        case "<":  return currentVal < targetVal;
        case "==": return currentVal === targetVal;
        case "!=": return currentVal !== targetVal;
        default:   return false;
      }
    });
  }

  function advanceStory() {
    clearTimeout(autoTimer);
    const currentScene = scenario.find(s => s.id === currentSceneId);
    if (!currentScene) return;

    if (currentScene.commands) {
      for (const cmd of currentScene.commands) {
        if (cmd.command === "startCredits" && cmd.param === "true") {
          playSe(SOUND_EFFECTS.click);
          startCredits();
          return;
        }

        if (cmd.command === "jump" && cmd.param) {
          if (labelMap[cmd.param] !== undefined) {
            renderScene(labelMap[cmd.param]);
            return;
          }
        }

        if (cmd.isBranch) {
          playSe(SOUND_EFFECTS.click);

          const branchType = cmd.branchType || "single";
          const a = (typeof favorability === "object" && favorability !== null) ? (favorability.charaA || 0) : 0;
          const b = (typeof favorability === "object" && favorability !== null) ? (favorability.charaB || 0) : 0;
          const c = (typeof favorability === "object" && favorability !== null) ? (favorability.charaC || 0) : 0;

          if (branchType === "single") {
            const targetChara = cmd.branchChara || "charaA";
            const border = (cmd.branchBorder !== undefined) ? cmd.branchBorder : 10;
            const currentVal = (typeof favorability === "object" && favorability !== null) ? (favorability[targetChara] || 0) : 0;

            if (currentVal >= border) {
              const targetLabel = cmd.branchTarget || cmd.happyTarget;
              if (targetLabel && labelMap[targetLabel] !== undefined) {
                renderScene(labelMap[targetLabel]);
                return;
              }
            }
            
            const elseLabel = cmd.elseTarget || cmd.normalTarget;
            if (elseLabel && labelMap[elseLabel] !== undefined) {
              renderScene(labelMap[elseLabel]);
            } else {
              renderScene(currentScene.next !== undefined ? currentScene.next : 0);
            }
            return;
          }
          else if (branchType === "max") {
            const maxFavor = Math.max(a, b, c);

            if (maxFavor <= 0) {
              const elseLabel = cmd.elseTarget || cmd.normalTarget;
              renderScene(elseLabel && labelMap[elseLabel] !== undefined ? labelMap[elseLabel] : currentScene.next);
              return;
            }

            if (maxFavor === a && labelMap["A_route"] !== undefined) {
              renderScene(labelMap["A_route"]);
            } else if (maxFavor === b && labelMap["B_route"] !== undefined) {
              renderScene(labelMap["B_route"]);
            } else if (maxFavor === c && labelMap["C_route"] !== undefined) {
              renderScene(labelMap["C_route"]);
            } else {
              const elseLabel = cmd.elseTarget || cmd.normalTarget;
              renderScene(elseLabel && labelMap[elseLabel] !== undefined ? labelMap[elseLabel] : currentScene.next);
            }
            return;
          }
          else if (branchType === "multi") {
            const isPassed = checkConditionString(cmd.branchCond);

            if (isPassed && cmd.branchTarget && labelMap[cmd.branchTarget] !== undefined) {
              renderScene(labelMap[cmd.branchTarget]);
            } else {
              const elseLabel = cmd.elseTarget || cmd.normalTarget;
              renderScene(elseLabel && labelMap[elseLabel] !== undefined ? labelMap[elseLabel] : currentScene.next);
            }
            return;
          }
        }
      }
    }

    if (currentScene.next !== undefined) {
      playSe(SOUND_EFFECTS.click);
      renderScene(currentScene.next);
    }
  }

  autoBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    isSkipMode = false;
    isAutoMode = !isAutoMode;
    updateAutoSkipUI();

    if (isAutoMode && !isTyping) {
      scheduleNextStep();
    }
  });

  skipBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    isAutoMode = false;
    isSkipMode = !isSkipMode;
    updateAutoSkipUI();

    if (isSkipMode) {
      if (isTyping) {
        clearInterval(typeTimer);
        messageTextEl.innerHTML = parseColorTags(fullText);
        isTyping = false;
      }
      scheduleNextStep();
    }
  });

  logBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    stopAutoSkip();
    openLogModal();
  });

  logCloseBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    closeLogModal();
  });

  gameContainerEl.addEventListener("wheel", (e) => {
    if (messageBoxEl.style.display === "block" && logModalEl.style.display !== "flex") {
      if (e.deltaY < 0) {
        stopAutoSkip();
        openLogModal();
      }
    }
  });

  let touchStartY = 0;
  gameContainerEl.addEventListener("touchstart", (e) => {
    touchStartY = e.touches[0].clientY;
  }, { passive: true });

  gameContainerEl.addEventListener("touchend", (e) => {
    const touchEndY = e.changedTouches[0].clientY;
    if (touchStartY - touchEndY > 50 && messageBoxEl.style.display === "block" && logModalEl.style.display !== "flex") {
      stopAutoSkip();
      openLogModal();
    }
  }, { passive: true });

  staffCreditsEl.addEventListener("click", () => {
    if (isFinalCreditScreen) {
      nextCreditStep();
    }
  });

  document.addEventListener("keydown", (e) => {
    if (e.key === "a" || e.key === "A") {
      autoBtnEl.click();
    } else if (e.key === "s" || e.key === "S") {
      skipBtnEl.click();
    } else if (e.key === "Enter") {
      if (isCreditsActive) {
        if (isFinalCreditScreen) {
          nextCreditStep();
        }
      } else if (messageBoxEl.style.display === "block" && logModalEl.style.display !== "flex") {
        messageBoxEl.click();
      }
    }
  });

  document.querySelectorAll(".slot-item").forEach(item => {
    item.addEventListener("click", (e) => {
      e.stopPropagation();
      const slotNum = item.getAttribute("data-slot");
      if (currentModalMode === "save") {
        saveToSlot(slotNum);
      } else {
        loadFromSlot(slotNum);
      }
    });
  });

  startBtnEl.addEventListener("click", () => {
    favorability = createInitialFavorability();
    backlog = [];
    stopAutoSkip();
    updateFavorDisplay();
    clearAllCharacters();
    playSe(SOUND_EFFECTS.click);
    showGameUI();
    renderScene(0);
  });

  titleLoadBtnEl.addEventListener("click", () => {
    playSe(SOUND_EFFECTS.click);
    openModal("load");
  });

  saveBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    openModal("save");
  });

  loadBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    openModal("load");
  });

  modalCloseBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    closeModal();
  });

  toTitleBtnEl.addEventListener("click", (e) => {
    e.stopPropagation();
    playSe(SOUND_EFFECTS.click);
    if (confirm("タイトル画面に戻りますか?")) {
      playBgm("");
      showTitleUI();
    }
  });

  messageBoxEl.addEventListener("click", () => {
    if (isFading) return;
    if (choiceContainerEl.children.length > 0) return;

    if (isAutoMode || isSkipMode) {
      stopAutoSkip();
    }

    if (isTyping) {
      playSe(SOUND_EFFECTS.click);
      clearInterval(typeTimer);
      messageTextEl.innerHTML = parseColorTags(fullText);
      isTyping = false;
      return;
    }

    advanceStory();
  });

  async function initApp() {
    resizeGame();
    loadReadScenes();
    scenario = await loadScenarioFromTxt("scenario.txt");
    updateModalSlots();
    
    showTitleUI();

    const enableAudioOnFirstTouch = () => {
      if (titleScreenEl.style.display !== "none" && TITLE_BGM_URL) {
        playBgm(TITLE_BGM_URL);
      }
      document.removeEventListener("click", enableAudioOnFirstTouch);
      document.removeEventListener("touchstart", enableAudioOnFirstTouch);
    };

    document.addEventListener("click", enableAudioOnFirstTouch);
    document.addEventListener("touchstart", enableAudioOnFirstTouch);
  }

  initApp();
  
  const debugChannel = new BroadcastChannel('novel_game_debug');

  function sendDebugStatus() {
    debugChannel.postMessage({
      type: 'STATE_UPDATE',
      favorability: favorability,
      favorNameMap: FAVOR_NAME_MAP,
      currentSceneId: currentSceneId,
      currentSpeaker: speakerNameEl.textContent,
      currentText: fullText
    });
  }

  debugChannel.onmessage = (event) => {
    const data = event.data;
    
    if (data.type === 'SET_FAVORABILITY') {
      if (data.target && typeof data.target === "string") {
        if (typeof favorability !== "object" || favorability === null) {
          favorability = createInitialFavorability();
        }
        favorability[data.target] = Number(data.value);
      } else if (typeof data.value === "object" && data.value !== null) {
        favorability = { ...data.value };
      }
      updateFavorDisplay();
      sendDebugStatus();
    } 
    else if (data.type === 'JUMP_SCENE') {
      const targetId = Number(data.value);
      showGameUI();
      renderScene(targetId);
      sendDebugStatus();
    }
    else if (data.type === 'REQUEST_INITIAL_STATE') {
      sendDebugStatus();
    }
  };

  const originalRenderScene = renderScene;
  renderScene = function(sceneId) {
    originalRenderScene(sceneId);
    sendDebugStatus();
  };

  const originalUpdateFavorDisplay = updateFavorDisplay;
  updateFavorDisplay = function() {
    originalUpdateFavorDisplay();
    sendDebugStatus();
  };

  window.updateScenarioFromText = function(txtContent) {
    scenario = parseScenarioText(txtContent);
    favorability = createInitialFavorability();
    backlog = [];
    stopAutoSkip();
    clearAllCharacters();
    showGameUI();
    renderScene(0);
    sendDebugStatus();
  };
</script>

</body>
</html>
editor.html シナリオエディタ(制作・編集用)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>scenario.txt プレビューエディタ - ノベルゲーム制作ツール</title>
  
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/dracula.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>

  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: "Helvetica Neue", Arial, sans-serif;
      background-color: #181825;
      color: #cdd6f4;
      display: flex;
      flex-direction: column;
      height: 100vh;
      overflow: hidden;
    }

    header {
      background-color: #11111b;
      padding: 10px 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      border-bottom: 2px solid #313244;
      height: 50px;
    }

    h1 { font-size: 16px; color: #f9e2af; font-weight: bold; }

    .btn-group { display: flex; gap: 10px; align-items: center; }

    button, label.file-label {
      padding: 6px 14px;
      font-size: 12px;
      font-weight: bold;
      border-radius: 6px;
      border: none;
      cursor: pointer;
      transition: all 0.2s;
    }

    .btn-primary { background-color: #89b4fa; color: #11111b; }
    .btn-primary:hover { background-color: #b4befe; }

    .btn-secondary { background-color: #45475a; color: #cdd6f4; }
    .btn-secondary:hover { background-color: #585b70; }
    
    .btn-preview { background-color: #a6e3a1; color: #11111b; }
    .btn-preview:hover { background-color: #b3f0af; }

    input[type="file"] { display: none; }

    main {
      position: absolute;
      top: 50px;
      bottom: 0;
      left: 0;
      right: 0;
      display: flex;
      width: 100%;
    }

    .editor-pane {
      width: 50%;
      height: 100%;
      border-right: 2px solid #313244;
      display: flex;
      flex-direction: column;
    }

    .snippet-bar {
      background-color: #181825;
      border-bottom: 1px solid #313244;
      padding: 6px 10px;
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-items: center;
    }

    .snippet-group {
      display: flex;
      gap: 4px;
      align-items: center;
      border-right: 1px solid #313244;
      padding-right: 8px;
      margin-right: 4px;
    }

    .snippet-group:last-child {
      border-right: none;
    }

    .snippet-btn {
      padding: 3px 8px;
      font-size: 11px;
      font-weight: bold;
      border-radius: 4px;
      border: 1px solid #45475a;
      background-color: #313244;
      color: #cdd6f4;
      cursor: pointer;
      transition: all 0.15s;
    }

    .snippet-btn:hover {
      background-color: #585b70;
      border-color: #89b4fa;
      color: #ffffff;
    }

    .snippet-btn-cmd { color: #f38ba8; }
    .snippet-btn-val { color: #89dceb; }
    
    .snippet-btn-red   { color: #f38ba8; border-color: #f38ba8; }
    .snippet-btn-blue  { color: #89b4fa; border-color: #89b4fa; }
    .snippet-btn-green { color: #a6e3a1; border-color: #a6e3a1; }

    .CodeMirror {
      flex: 1;
      height: auto;
      font-family: "Consolas", "Courier New", monospace;
      font-size: 14px;
      line-height: 1.6;
    }

    .preview-pane {
      width: 50%;
      height: 100%;
      background-color: #050a14;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      position: relative;
    }

    .preview-header {
      position: absolute;
      top: 10px;
      left: 15px;
      font-size: 12px;
      color: #a6adc8;
      background: rgba(0, 0, 0, 0.6);
      padding: 4px 10px;
      border-radius: 4px;
      pointer-events: none;
      z-index: 10;
    }

    iframe {
      width: 100%;
      height: 100%;
      border: none;
    }

    .cm-scenario-command { color: #f38ba8 !important; font-weight: bold; }
    .cm-scenario-speaker { color: #f9e2af !important; font-weight: bold; }
    .cm-scenario-label   { color: #a6e3a1 !important; font-weight: bold; }
    .cm-scenario-comment { color: #6c7086 !important; font-style: italic; }
    .cm-scenario-param   { color: #89dceb !important; }
    .cm-scenario-tag     { color: #f5c2e7 !important; font-weight: bold; }
  </style>
</head>

<body>

<header>
  <h1>scenario.txt プレビューエディタ</h1>
  <div class="btn-group">
    <button id="preview-btn" class="btn-preview">▶ プレビュー再生</button>
    <label class="file-label btn-secondary">
      ファイルを開く
      <input type="file" id="file-input" accept=".txt">
    </label>
    <button id="download-btn" class="btn-primary">scenario.txt を保存</button>
  </div>
</header>

<main>
  <div class="editor-pane">
    <div class="snippet-bar">
      <div class="snippet-group">
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@bg: images/">@bg</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@bgm: sound/">@bgm</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@se: sound/">@se</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@fade: true">@fade</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@label: ">@label</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@jump: ">@jump</button>
      </div>
      <div class="snippet-group">
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: images/|pos:left">立ち絵:左</button>
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: images/|pos:center">立ち絵:中</button>
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: images/|pos:right">立ち絵:右</button>
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: |clear:left">消去:左</button>
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: |clear:right">消去:右</button>
        <button class="snippet-btn snippet-btn-val" data-snippet="@character: ">全消去</button>
      </div>
      <div class="snippet-group">
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@choice: 選択肢1|next:label1|favor:charaA:10 / 選択肢2|next:label2|favor:charaB:10">@choice</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@branch: type:single|chara:charaA|border:10|target:happy_end|else:normal_end">@branch(個別)</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@branch: type:max|else:normal_end">@branch(最多)</button>
        <button class="snippet-btn snippet-btn-cmd" data-snippet="@branch: type:multi|cond:charaA>=10,charaB>=10|target:true_end|else:normal_end">@branch(複合)</button>
      </div>
      <div class="snippet-group">
        <button class="snippet-btn snippet-btn-red" data-tag-color="red">文字:赤</button>
        <button class="snippet-btn snippet-btn-blue" data-tag-color="blue">文字:青</button>
        <button class="snippet-btn snippet-btn-green" data-tag-color="green">文字:緑</button>
      </div>
    </div>

    <textarea id="editor"></textarea>
  </div>

  <div class="preview-pane">
    <div class="preview-header">PREVIEW</div>
    <iframe id="preview-frame" src="index.html"></iframe>
  </div>
</main>

<script>
  // 複数行コマンド対応の新しい色分けルール
  CodeMirror.defineMode("novel-scenario", function() {
    return {
      token: function(stream) {
        if (stream.sol() && (stream.match("#") || stream.match("//"))) {
          stream.skipToEnd();
          return "scenario-comment";
        }

        if (stream.sol() && stream.match("@")) {
          if (stream.match("label") || stream.match("jump")) {
            stream.skipToEnd();
            return "scenario-label";
          }
          stream.eatWhile(/[a-zA-Z]/);
          return "scenario-command";
        }

        if (stream.sol() && stream.match(/^[^:@<>\n\s]+(?=:)/)) {
          return "scenario-speaker";
        }

        if (stream.match(/<\/color>/i) || stream.match(/<color:[^>]+>/i)) {
          return "scenario-tag";
        }

        if (stream.eat("|") || stream.eat("/")) {
          return "scenario-param";
        }

        stream.next();
        return null;
      }
    };
  });

  const initialText = `@bg: images/bg03.webp
@bgm: sound/sound02.mp3

@character: images/honoka.webp|pos:center
ほのか:このエディタを使うとツール内の独自コードを記述しやすくなるよ!

ほのか:例えば、上側のツールバーにあるボタンをクリックすると、よく使うコマンドをぱっとエディタに入力できるんだ。

@character: images/ryo.webp|pos:left
亮:<color:green>「立ち絵」</color>のコマンドを挿入すると、こんなふうにキャラクターが表示できるぞ。

@character: images/azusa.webp|pos:right
梓:テキストの色を変えたいときは、文字を選択した状態で<color:red>「文字:赤」</color>とかを選ぶと<color:red>こんな感じで</color>文字の色を変えられちゃうよん!

ほのか:このツールでは、<color:green>「左」「中」「右」</color>に表示できるキャラは1人だけなんだ。だからこうやって「中」にいるキャラを「右」に移動させると……

@character: images/honoka.webp|pos:right
ほのか:右のキャラが消えちゃうんだ。

@character: images/azusa.webp|pos:right
梓:で、また右にキャラを表示させると、いま右にいたキャラが消えるってわけ。

@character: images/honoka.webp|pos:center
ほのか:同じキャラを2か所に表示することもできないから気を付けてね!

亮:そうそう、このチュートリアルでは<color:blue>好感度(変数)</color>を使用していないから、左上の好感度ウインドウがちょっと邪魔だよな?

亮:そういうときは<color:green>「index.html」</color>内のコードを少し変更することで非表示にできるぞ。

梓:「UIをもっとシンプルなカラーリングにしたい!」ってときも、<color:green>「index.html」</color>内のコードをいじればいいよん!

ほのか:もっと詳しく知りたい人はWebマニュアルを読んでみてね!`;

  const editor = CodeMirror.fromTextArea(document.getElementById("editor"), {
    mode: "novel-scenario",
    theme: "dracula",
    lineNumbers: true,
    lineWrapping: true
  });

  editor.setValue(initialText);

  function insertSnippet(text) {
    const doc = editor.getDoc();
    const cursor = doc.getCursor();
    
    doc.replaceRange(text, cursor);
    editor.focus();
  }

  function insertColorTag(color) {
    const doc = editor.getDoc();
    const selection = doc.getSelection();
    
    if (selection) {
      const wrappedText = `<color:${color}>${selection}</color>`;
      doc.replaceSelection(wrappedText);
    } else {
      const template = `<color:${color}>テキスト</color>`;
      insertSnippet(template);
    }
    editor.focus();
  }

  document.querySelectorAll('.snippet-btn').forEach(button => {
    button.addEventListener('click', function() {
      const snippet = this.getAttribute('data-snippet');
      const tagColor = this.getAttribute('data-tag-color');
      
      if (tagColor) {
        insertColorTag(tagColor);
      } else if (snippet) {
        insertSnippet(snippet);
      }
    });
  });

  const previewFrame = document.getElementById("preview-frame");
  const previewBtn = document.getElementById("preview-btn");

  function updatePreview() {
    const scenarioText = editor.getValue();
    
    if (previewFrame.contentWindow && typeof previewFrame.contentWindow.updateScenarioFromText === 'function') {
      previewFrame.contentWindow.updateScenarioFromText(scenarioText);
    }
  }

  previewBtn.addEventListener("click", updatePreview);

  document.getElementById("file-input").addEventListener("change", function(e) {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = function(e) {
      editor.setValue(e.target.result);
    };
    reader.readAsText(file);
  });

  document.getElementById("download-btn").addEventListener("click", function() {
    const text = editor.getValue();
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement("a");
    a.href = url;
    a.download = "scenario.txt";
    a.click();
    
    URL.revokeObjectURL(url);
  });
</script>

</body>
</html>
debug.html 変数管理ツール(デバッグ用)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>変数管理・デバッグツール - ノベルゲーム制作ツール</title>
  <style>
    body {
      font-family: "Helvetica Neue", Arial, sans-serif;
      background: #111827;
      color: #f3f4f6;
      margin: 0;
      padding: 20px;
    }
    h1 {
      font-size: 18px;
      border-bottom: 2px solid #374151;
      padding-bottom: 8px;
      color: #ffd166;
      margin-top: 0;
    }
    .card {
      background: #1f2937;
      padding: 15px;
      border-radius: 8px;
      margin-bottom: 15px;
      border: 1px solid #374151;
    }
    label {
      display: block;
      font-size: 13px;
      color: #9ca3af;
      margin-bottom: 6px;
    }
    .chara-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: #111827;
      padding: 8px 12px;
      border-radius: 6px;
      margin-bottom: 8px;
      border: 1px solid #374151;
    }
    .chara-info {
      display: flex;
      flex-direction: column;
    }
    .chara-name {
      font-weight: bold;
      color: #ffffff;
      font-size: 14px;
    }
    .chara-key {
      font-size: 11px;
      color: #6b7280;
    }
    .input-group {
      display: flex;
      gap: 6px;
    }
    input[type="number"] {
      width: 70px;
      padding: 6px;
      border-radius: 4px;
      border: 1px solid #4b5563;
      background: #374151;
      color: #fff;
      text-align: right;
      font-weight: bold;
    }
    button {
      padding: 6px 12px;
      border: none;
      border-radius: 4px;
      background: #2563eb;
      color: #fff;
      font-weight: bold;
      cursor: pointer;
      font-size: 12px;
      transition: background 0.2s;
    }
    button:hover {
      background: #1d4ed8;
    }
    .btn-apply-all {
      width: 100%;
      margin-top: 6px;
      background: #059669;
      padding: 8px;
    }
    .btn-apply-all:hover {
      background: #047857;
    }
    .info-val {
      font-size: 20px;
      font-weight: bold;
      color: #60a5fa;
    }
    .scene-preview {
      font-size: 13px;
      color: #d1d5db;
      background: #111827;
      padding: 10px;
      border-radius: 4px;
      margin-top: 5px;
      line-height: 1.4;
    }
  </style>
</head>
<body>

  <h1>ノベルゲーム 変数管理ツール</h1>

  <!-- キャラクター別 好感度・ステータス管理 -->
  <div class="card">
    <label>好感度・ステータスパラメータ (favorability)</label>
    
    <div id="favor-container">
      <!-- JSで動的に入力欄を生成 -->
    </div>

    <button class="btn-apply-all" onclick="setAllFavorability()">全パラメータを一括変更</button>
  </div>

  <!-- シーン・進行管理 -->
  <div class="card">
    <label>現在のシーンID (currentSceneId)</label>
    <div class="info-val" id="scene-id-disp">-</div>
    
    <label style="margin-top: 15px;">指定シーンへジャンプ (テスト用)</label>
    <div class="input-group">
      <input type="number" id="jump-scene-input" value="0" placeholder="ID">
      <button onclick="jumpToScene()">ジャンプ</button>
    </div>

    <label style="margin-top: 12px;">現在のテキスト</label>
    <div class="scene-preview">
      <b id="speaker-disp" style="color: #ffd166;"></b><br>
      <span id="text-disp">---</span>
    </div>
  </div>

  <script>
    const channel = new BroadcastChannel('novel_game_debug');

    // 初期表示名マッピング(本体から受信するまでの仮のデータ)
    let FAVOR_NAME_MAP = {
      charaA: "ほのか",
      charaB: "裕也",
      charaC: "三月"
    };

    // 初期状態の取得要求
    channel.postMessage({ type: 'REQUEST_INITIAL_STATE' });

    // ゲームからの通知を受信してUIを更新
    channel.onmessage = (event) => {
      const data = event.data;
      if (data.type === 'STATE_UPDATE') {
        // 本体から名称マッピングが送られてきた場合は更新
        if (data.favorNameMap) {
          FAVOR_NAME_MAP = data.favorNameMap;
        }

        renderFavorInputs(data.favorability);
        document.getElementById('scene-id-disp').textContent = data.currentSceneId;
        document.getElementById('speaker-disp').textContent = data.currentSpeaker ? `【${data.currentSpeaker}】` : '(話者なし)';
        
        // <color:...> タグを取り除いた純粋なテキストをプレビュー表示
        const cleanText = (data.currentText || '').replace(/<color:(.*?)>(.*?)<\/color>/g, '$2');
        document.getElementById('text-disp').textContent = cleanText;
      }
    };

    // キャラごとの入力UIを動的に生成・更新
    function renderFavorInputs(favorability) {
      const container = document.getElementById('favor-container');
      
      if (typeof favorability !== "object" || favorability === null) {
        favorability = { charaA: Number(favorability) || 0, charaB: 0, charaC: 0 };
      }

      for (const [key, val] of Object.entries(favorability)) {
        let row = document.getElementById(`favor-row-${key}`);
        const displayName = FAVOR_NAME_MAP[key] || key;

        if (!row) {
          row = document.createElement('div');
          row.id = `favor-row-${key}`;
          row.className = 'chara-row';
          row.innerHTML = `
            <div class="chara-info">
              <span class="chara-name" id="favor-label-${key}">${displayName}</span>
              <span class="chara-key">${key}</span>
            </div>
            <div class="input-group">
              <input type="number" id="favor-input-${key}" value="${val}">
              <button onclick="setSingleFavorability('${key}')">反映</button>
            </div>
          `;
          container.appendChild(row);
        } else {
          // すでに要素が存在する場合でも表示名を最新に上書きする
          const labelEl = document.getElementById(`favor-label-${key}`);
          if (labelEl) {
            labelEl.textContent = displayName;
          }

          const input = document.getElementById(`favor-input-${key}`);
          if (document.activeElement !== input) {
            input.value = val;
          }
        }
      }
    }

    // (以下、setSingleFavorability や jumpToScene などは既存のまま変更なし)
    // 特定パラメータのみ変更
    function setSingleFavorability(targetKey) {
      const val = document.getElementById(`favor-input-${targetKey}`).value;
      channel.postMessage({
        type: 'SET_FAVORABILITY',
        target: targetKey,
        value: Number(val)
      });
    }

    // 全パラメータを一括変更
    function setAllFavorability() {
      const updatedFavor = {};
      const inputs = document.querySelectorAll('[id^="favor-input-"]');
      
      inputs.forEach(input => {
        const key = input.id.replace('favor-input-', '');
        updatedFavor[key] = Number(input.value);
      });

      channel.postMessage({
        type: 'SET_FAVORABILITY',
        value: updatedFavor
      });
    }

    // 指定シーンへ直接ジャンプ
    function jumpToScene() {
      const val = document.getElementById('jump-scene-input').value;
      channel.postMessage({ type: 'JUMP_SCENE', value: val });
    }
  </script>

</body>
</html>
scenario.txt サンプルシナリオデータ(テキストファイル)
@bg: images/bg01.webp
@bgm: sound/sound01.mp3
@character: images/honoka.webp|pos:center
ほのか:こんにちは!<color:green>『ノベルゲーム制作ツール』</color>へようこそ!
#実行したいコマンドを「@」で入力したあとセリフを入力してね!

ほのか:このツールでは、プログラミングができなくてもノベルゲームが作れちゃうんだよ。

ほのか:これから私と一緒に、このツールで「どんなことができるか」を体験してみよう!

@choice: ぜひ教えて!|next:explain1|favor:charaA:10 / 簡単にできるの?|next:explain2|favor:charaA:0
ほのか:心の準備はいいかな?
#「@choice」で選択肢を表示してから「選択肢に表示される文字|ジャンプ先のラベル|変数の増減値」を指定してるんだよ。

@label: explain1
#「@label」は「ジャンプ先の目印」だよ。
@se: sound/ok.mp3
<color:blue>ほのかの好感度が10あがった!</color>

@label: explain2
#「簡単にできるの?」を選択した場合は、「@label: explain1」は無視してここにジャンプするよ!
ほのか:まずは <color:green>【画面演出と立ち絵の位置移動】</color> から紹介するね!

@character: images/honoka.webp|pos:left
ほのか:こんなふうに『<color:green>|pos:left</color>』と書くと立ち絵を左側に表示できるよ。

@character: images/honoka.webp|pos:right
ほのか:そして『<color:green>|pos:right</color>』と書けば右側にも移動できちゃうんだ!

@character: images/honoka.webp|pos:center
@bg: images/bg02.webp
@bgm: sound/sound02.mp3
ほのか:背景(<color:green>@bg</color>)やBGM(<color:green>@bgm</color>)を変えるだけで、一気に場面転換の雰囲気を変えられるよ。

ほのか:次は<color:green>【快適なシステム機能】</color>の紹介だよ!画面右上のボタンを見てみてね。

ほのか:<color:green>『AUTO』</color>で文章を自動進行、<color:green>『SKIP』</color>で一度読んだ文章(既読)だけを高速スキップできるんだ。

ほのか:<color:green>『LOG』</color>を押すか上ホイール・上スワイプをすると、いつでもバックログ(既読履歴)を確認できるよ。

ほのか:もちろん<color:green>『SAVE/LOAD』</color>で最大3つのスロットにゲームデータを保存・読み込みもできるから安心だね!

ほのか:それじゃあ次は<color:green>【選択肢とルート分岐】</color>を試してみようか!

ほのか:あっ、ちょうどいいところに!裕也くーん!三月ちゃーん!

@character: images/yuya.webp|pos:left
裕也:ん?なんだよ、なんか用か?

@character: images/mitsuki.webp|pos:right
三月:なに?そんなにヒマじゃないんだけど。

@character: images/honoka.webp|pos:center
ほのか:じゃあ、さっそく選択肢を出してみるね。

@choice: ほのかと話す|next:talk_honoka|favor:charaA:10 / 裕也と話す|next:talk_yuya|favor:charaB:10 / 三月と話す|next:talk_mitsuki|favor:charaC:10
ほのか:誰と一緒にチュートリアルの後半を進めたい?

@label: talk_honoka
@se: sound/ok.mp3
ほのか:私を選んでくれたんだね!ありがとう!

@jump: branch_check
ほのか:選択肢の『<color:green>favor:charaA:10</color>』パラメータで、左上の画面に表示されている好感度が加算されたよ!
#「このセリフのあとこのラベルにジャンプさせたい」というときは、セリフの上側に「@jump」を入力してね!

@label: talk_yuya
@se: sound/ok.mp3
裕也:お?オレを選ぶなんてお前、お目が高いな!

@jump: branch_check
裕也:選択肢の『<color:green>favor:charaB:10</color>』でオレの好感度が上がったぞ!画面左上をチェックしてみてくれ!

@label: talk_mitsuki
@se: sound/ok.mp3
三月:……私?ふうん。変わってるね。

@jump: branch_check
三月:『<color:green>favor:charaC:10</color>』のコマンドで、私の数値が10加算されたよ。

@label: branch_check
@character: images/honoka.webp|pos:center
ほのか:ここからは『<color:green>@branch</color>』コマンドを使った、自動ルート分岐のテストだよ!

@branch: type:max
ほのか:今回は一番好感度が高いキャラクターの個別ルート(<color:green>type:max</color>)へ自動でジャンプしてみるね!
#セリフの上に「@branch: type:max」を記述しておくことで、このセリフのあとに「好感度によるルート分岐」が発生するよ!

@label: A_route
@fade: true
#セリフの上側に「@fade: true」を入れておけば、そのシーンに移動するときフェードインの演出が入るんだ。
@bg: images/bg03.webp
@character: |clear:left
@character: |clear:right
@character: images/honoka.webp|pos:center
#「裕也」と「三月」の立ち絵を消して「ほのか」だけを残しているよ!
ほのか:ここは【ほのかルート】だよ!私の好感度が一番高かったからここに来たんだね!

@jump: ending_tutorial
ほのか:『<color:green>type:max</color>』を使うと、一番値が高いキャラのルート(A_route, B_route, C_route)へ自動分岐できるんだよ。

@label: B_route
@fade: true
@bg: images/bg04.webp
@character: |clear:right
@character: images/yuya.webp|pos:center
#「三月」の立ち絵を消して「裕也」の立ち絵を中央に移動させているよ。
#それぞれの位置に配置できるキャラは一人だけだから、中央にいた「ほのか」は自動的に非表示になるんだ。
裕也:ここは【裕也ルート】だ!オレの数値を一番高くしてくれたんだな、サンキュな!

@jump: ending_tutorial
裕也:ルート分岐を使えばこんなふうに「一番仲良くなったキャラの個別エンディングに行く」みたいな演出が作れるぞ!

@label: C_route
@fade: true
@bg: images/bg05.webp
@character: |clear:left
@character: images/mitsuki.webp|pos:center
三月:ここは【三月ルート】……私の数値が最高だった証拠。

@jump: ending_tutorial
三月:複雑なフラグ管理をしなくても、好感度の比較だけで分岐できるのは……とても合理的。

@label: ending_tutorial
@character: images/honoka.webp|pos:center
ほのか:チュートリアルはこれでバッチリ!

@character: images/yuya.webp|pos:left
裕也:あとはシナリオテキストを書き換えるだけで、お前だけのオリジナルノベルゲームが作れるぜ!

@character: images/mitsuki.webp|pos:right
三月:最後に『<color:green>@startCredits:true</color>』を呼び出すと、スタッフロールが再生されるよ。

@startCredits: true
ほのか:それじゃあ、素敵なゲームを作ってね!バイバイー!
#「@startCredits」を入力するのもセリフの上側だから気を付けてね!
✓ クリップボードにコピーしました!