# ノベルゲーム台本システム仕様書

## 1. 目的

本仕様書は、チャット風 UI を前提としたノベルゲーム向け台本システムのデータ構造と実行ルールを定義する。

本システムは以下を目的とする。

* ゲーム進行上の再生タイミングごとに台本を管理できること
* 条件に応じて複数候補の中から再生する台本を選択できること
* 台本データを人間が読み書きしやすいこと
* トリガーやアクションを構造化データとして扱い、検証しやすいこと
* 背景・立ち絵・キャラ移動などの複雑な演出を前提にせず、低コストで運用できること

データ記述形式は YAML を基本とする。実行時に内部で JSON 相当へ変換してよい。

---

## 2. 設計方針

### 2.1 UI 前提

本システムはチャット風 UI を前提とする。

* キャラクターの移動、向き、立ち位置制御は行わない
* 背景は必要に応じて台本途中で切り替え可能とする
* 演出はメッセージ単位で進行する
* セリフ以外に、心の声、地の文、システム通知、時間経過、日付変更などを扱う

### 2.2 データ構成の基本方針

* **event** はゲーム進行上の再生タイミングを表す単位とする
* **skit** は event 内で条件に応じて選択される、実際の会話・演出の単位とする
* **trigger** は skit を候補に含める条件とする
* **action** は skit 再生後にゲームステートへ反映する処理とする
* **script** は skit の上演内容を表す
* **step** は script 内の 1 要素を表す

---

## 3. 用語定義

### 3.1 event

ゲーム進行上の再生タイミングを表す単位。

例:

* オープニング
* 回想部分
* 入塾テスト部分
* 授業前イベント
* 授業後イベント
* 模試前イベント
* 入試後イベント

1 event は 1 ファイルで管理することを基本とする。

### 3.2 skit

ある event 内で条件に応じて選ばれる 1 本分の会話・演出単位。

例:

* 先生に褒められる授業後会話
* 成績が悪く励まされる授業後会話
* 汎用の雑談会話

### 3.3 trigger

skit を再生候補に含めるための条件。

* ゲームステートを参照できる
* 構造化データで記述する
* 文字列式は標準仕様では採用しない

### 3.4 action

skit の再生後にゲームステートへ反映する処理。

例:

* フラグを立てる
* 好感度を増減する
* 既読状態を保存する
* 次の event を解放する

### 3.5 script

skit の中身であり、実際に表示・実行される手順列。

### 3.6 step

script を構成する 1 つの要素。

例:

* 発言
* 心の声
* 地の文
* 背景変更
* 時間経過表示
* 日付変更表示
* システム通知

---

## 4. ファイル単位

### 4.1 基本方針

* 1 event = 1 ファイル
* 1 event の中に複数の skit を持てる

### 4.2 推奨ディレクトリ構成例

```text
scenario/
  opening/
    opening.yaml
    flashback.yaml
    entrance_test.yaml
  lesson/
    lesson_before_01.yaml
    lesson_after_01.yaml
  mock_exam/
    mock_exam_before_spring.yaml
    mock_exam_after_spring.yaml
  exam/
    exam_before.yaml
    exam_after.yaml
  ending/
    ending.yaml
```

---

## 5. event データモデル

```yaml
event_id: lesson_after_01
version: 1
display_name: 授業後イベント01
category: lesson_after
selection_rule:
  strategy: highest_priority_random_tiebreak
skit_defaults:
  once_per_playthrough: true
skits: []
```

### 5.1 event フィールド

* `event_id`: event の一意識別子
* `version`: データバージョン
* `display_name`: 管理用表示名
* `category`: event の分類
* `selection_rule`: skit 選択方式
* `skit_defaults`: skit のデフォルト値
* `skits`: skit 一覧

---

## 6. skit データモデル

```yaml
skit_id: praise_teacher
priority: 100
once_per_playthrough: true
trigger:
  all: []
actions: []
script:
  steps: []
```

### 6.1 skit フィールド

* `skit_id`: skit の一意識別子
* `priority`: 優先度。数値が大きいほど優先される
* `once_per_playthrough`: 1 プレイスルー中に 1 回だけ再生可能かどうか
* `trigger`: skit の出現条件
* `actions`: skit 再生完了後に実行する処理群
* `script`: 表示・演出内容

### 6.2 once_per_playthrough のデフォルト

省略時は `true` を推奨する。

理由:

* 同一 skit の多重再生を避けやすい
* ノベルゲームの会話イベント運用に向いている
* 例外的に繰り返したい skit のみ `false` を指定すればよい

---

## 7. skit 選択ルール

標準の選択ルールは以下とする。

1. event 内の全 skit を取得する
2. `trigger` が真の skit のみを候補とする
3. `once_per_playthrough = true` かつ既に再生済みの skit を除外する
4. 候補の中から最大 `priority` を持つ skit 群を抽出する
5. 同一 priority の skit が複数ある場合はランダムに 1 件選択する
6. 選択された skit の `script` を実行する
7. script 完了後、`actions` を順番に実行する
8. `once_per_playthrough = true` の場合、当該 skit を再生済みとして記録する

### 7.1 候補が 0 件の場合

候補が 0 件の場合は以下のいずれかを採用する。

* 低優先度の汎用 skit を必ず 1 件以上用意する
* エンジン側で「何も起こらない」扱いにする
* event ごとに fallback skit を用意する

標準運用としては、汎用 skit を置くことを推奨する。

---

## 8. ゲームステート参照モデル

trigger および action はゲームステートを参照・更新できる。

想定する参照対象の例:

* flags
* stats
* affection
* progress
* inventory
* seen_skit
* current_date
* current_season
* lesson_count_in_season
* mock_exam_count

例:

```yaml
flags:
  met_teacher_a: true
  intro_completed: true
stats:
  math: 72
  english: 65
affection:
  teacher_a: 12
progress:
  season_index: 2
  lesson_count_in_season: 3
calendar:
  date: 2026-04-12
```

---

## 9. trigger 仕様

### 9.1 基本方針

trigger は構造化データで記述する。

* 文字列式は標準では採用しない
* ネスト可能とする
* AND / OR / NOT を表現可能とする
* 比較演算、フラグ参照、集合判定などを持てるようにする

### 9.2 trigger の基本形

```yaml
trigger:
  all:
    - flag: flags.met_teacher_a
    - gte:
        affection.teacher_a: 10
    - not:
        flag: flags.seen_teacher_praise
```

### 9.3 論理演算

* `all`: 配下の条件がすべて真
* `any`: 配下の条件のいずれかが真
* `not`: 配下の条件が偽

例:

```yaml
trigger:
  any:
    - flag: flags.route_teacher
    - flag: flags.route_friend
```

### 9.4 比較演算

以下の比較演算を持つ。

* `eq`
* `ne`
* `gt`
* `gte`
* `lt`
* `lte`

例:

```yaml
trigger:
  all:
    - gte:
        stats.math: 70
    - lt:
        affection.teacher_a: 20
```

### 9.5 真偽値判定

* `flag`: 指定パスの値が truthy であることを要求する
* `flag_false`: 指定パスの値が falsy であることを要求する

例:

```yaml
trigger:
  all:
    - flag: flags.intro_completed
    - flag_false: flags.exam_finished
```

### 9.6 集合判定

必要に応じて以下をサポートしてよい。

* `in`
* `not_in`
* `contains`
* `not_contains`

例:

```yaml
trigger:
  all:
    - in:
        progress.current_phase: [spring, summer, autumn]
```

### 9.7 trigger で参照可能な値のパス

ゲームステート参照はドット区切りパスで表現する。

例:

* `flags.met_teacher_a`
* `affection.teacher_a`
* `progress.season_index`
* `calendar.date`

---

## 10. action 仕様

### 10.1 基本方針

action は構造化データで記述し、skit 再生完了後に順番に実行する。

### 10.2 action の基本形

```yaml
actions:
  - set:
      flags.seen_teacher_praise: true
  - add:
      affection.teacher_a: 1
```

### 10.3 標準 action 一覧

#### set

指定値を代入する。

```yaml
- set:
    flags.intro_completed: true
```

#### add

数値を加算する。負数で減算可。

```yaml
- add:
    affection.teacher_a: 1
```

#### inc

数値を 1 増やす。

```yaml
- inc: progress.lesson_count_in_season
```

#### dec

数値を 1 減らす。

```yaml
- dec: stamina.current
```

#### append

配列へ要素を追加する。

```yaml
- append:
    seen_events: lesson_after_01
```

#### remove

配列から要素を除去する。

```yaml
- remove:
    available_events: mock_exam_retry
```

#### set_seen

既読状態を記録する。

```yaml
- set_seen: skits.lesson_after_01.praise_teacher
```

#### unlock_event

特定 event を解放する。

```yaml
- unlock_event: exam_before
```

#### lock_event

特定 event をロックする。

```yaml
- lock_event: remedial_lesson
```

### 10.4 action 実行順

* 記述順に実行する
* 前の action の結果は後続 action から参照できるものとする

---

## 11. character / speaker / expression 仕様

### 11.1 基本方針

* `character` はキャラクター定義データ
* `speaker` は script 内で話者を参照するための ID
* 初期実装では `speaker = character_id` として扱う
* 将来必要になれば `speaker` と `character` を分離できる設計にする

### 11.2 character 定義の最小構成

```yaml
characters:
  protagonist:
    default_display_name: 主人公
    chat_side: right
    expressions:
      neutral: /assets/avatars/protagonist/neutral.png
      troubled: /assets/avatars/protagonist/troubled.png
      happy: /assets/avatars/protagonist/happy.png

  teacher_a:
    default_display_name: ？？？
    chat_side: left
    expressions:
      neutral: /assets/avatars/teacher_a/neutral.png
      happy: /assets/avatars/teacher_a/happy.png
      troubled: /assets/avatars/teacher_a/troubled.png
```

最小フィールド:

* `default_display_name`: 初期表示名
* `chat_side`: チャット UI 上の基本表示位置 (`left` または `right`)
* `expressions`: 表情識別子とアバター画像の対応表

### 11.3 expression の位置づけ

本システムでは `pose` は採用しない。
アバター差し替え用の `expression` を採用する。

理由:

* 立ち絵レイアウトや身体向きの管理を避けるため
* チャット UI で表情差し替えのみを行いたいため
* 実装コストを抑えつつ感情表現を可能にするため

### 11.4 step における expression

`expression` は `say` と `think` で使用できる。

```yaml
- type: say
  speaker: teacher_a
  expression: happy
  text: 今日の小テスト、よくできてたね。

- type: think
  speaker: protagonist
  expression: troubled
  text: 本当にこのままで大丈夫だろうか。
```

補足:

* `say` は通常の発話用
* `think` は心の声用
* 主人公の思考にも表情アバターを付けてよい
* `narrate` と `system` は話者なしとし、`speaker` や `expression` を持たない

### 11.5 表示名解決ルール

1. step に `display_name` が明示されていればそれを使う
2. 明示がなければ StateStore の現在表示名マッピングを使う
3. それもなければ character 定義の `default_display_name` を使う

### 11.6 表情解決ルール

1. step に `expression` が明示されていればそれを使う
2. 明示がなければ `neutral` を使う
3. `neutral` が未定義なら character のデフォルトアバターへフォールバックする

### 11.7 話者名表示の途中変更

キャラクターの表示名は `speaker_intro` によって script 途中で変更できる。

これにより、同一キャラクターを以下のように表示できる。

* 未紹介時: `？？？`
* 仮表示時: `女の先生`
* 紹介後: `高橋先生`

---

## 12. script 仕様

### 11.1 基本方針

script は `steps` の配列で表現する。

各 step は 1 つの表示または制御命令を表す。

```yaml
script:
  steps: []
```

### 11.2 標準 step 一覧

#### say

通常の発話を表す。

```yaml
- type: say
  speaker: teacher_a
  expression: happy
  text: 今日の小テスト、よくできてたね。
```

フィールド:

* `type`: `say`
* `speaker`: 話者 ID
* `text`: 表示テキスト
* `expression`: 任意。話者アバターの表情識別子
* `display_name`: 任意。話者表示名の上書き

#### think

心の声を表す。

```yaml
- type: think
  speaker: protagonist
  expression: troubled
  text: 先生に褒められた。少し嬉しい。
```

フィールド:

* `type`: `think`
* `text`: 表示テキスト
* `speaker`: 任意。ただし表情付きアバターを表示したい場合は指定する
* `expression`: 任意。話者アバターの表情識別子

補足:

* 主人公の内面描写でも `speaker` を指定してよい
* `think` でも `expression` を指定可能
* `say` と異なる見た目の吹き出しや装飾にしてよい

#### narrate

地の文や状況説明を表す。

```yaml
- type: narrate
  text: 教室の空気が少し和らいだ。
```

#### system

システム通知や UI 補助メッセージを表す。

```yaml
- type: system
  text: 好感度が 1 上がった
```

#### background

背景変更を表す。

```yaml
- type: background
  background_id: classroom_evening
```

フィールド:

* `type`: `background`
* `background_id`: 背景識別子
* `transition`: 任意。切替演出種別

#### time_pass

時間経過を表す。

```yaml
- type: time_pass
  label: 数時間後
```

フィールド:

* `type`: `time_pass`
* `label`: 表示文言

#### date_change

日付変更または日付表示更新を表す。

```yaml
- type: date_change
  date: 2026-04-13
  label: 翌日
```

フィールド:

* `type`: `date_change`
* `date`: 任意。内部日付を併記したい場合に使用
* `label`: 任意。表示文言

#### speaker_intro

未紹介キャラクターの話者名表示を制御する。

```yaml
- type: speaker_intro
  speaker: teacher_a
  display_name: 女教師
```

または正式公開時:

```yaml
- type: speaker_intro
  speaker: teacher_a
  display_name: 高橋先生
```

フィールド:

* `type`: `speaker_intro`
* `speaker`: 対象話者 ID
* `display_name`: この時点以降の表示名

用途:

* 初登場時は「？？？」や「女の先生」として表示する
* 紹介後に正式名へ更新する

#### transition

画面遷移演出を表す。

```yaml
- type: transition
  transition_id: battle_like
  duration_ms: 500
```

フィールド:

* `type`: `transition`
* `transition_id`: 遷移演出識別子
* `duration_ms`: 任意。演出時間

用途:

* 別画面への切り替え前に演出を挟む
* フェード、フラッシュ、バトル突入風演出などを表現する

補足:

* `transition` は演出のみを担当する
* 実際の画面切り替えや機能起動は `minigame` など別 step が担当する

#### minigame

別画面のミニゲームを起動する step を表す。

```yaml
- type: minigame
  minigame_id: mock_exam_quiz
  params:
    subject: math
    difficulty: normal
    question_count: 10
  result_key: minigame.mock_exam_result
```

フィールド:

* `type`: `minigame`
* `minigame_id`: 起動対象ミニゲーム識別子
* `params`: 任意。起動パラメータ
* `result_key`: 任意。ミニゲーム結果の保存先 state パス

用途:

* 授業クイズ、模試クイズ、入試クイズなどの別画面ミニゲームを起動する
* 終了後の結果を game state へ書き戻す

補足:

* `minigame` は script の進行を一時停止し、結果受領後に再開する前提とする
* ミニゲーム結果は標準では `result_key` に保存する
* 後続の trigger や action はその結果を参照してよい

#### choice

将来拡張用。初期実装では省略可能。

---

## 13. 話者名表示制御仕様

### 12.1 基本方針

キャラクターは内部的に `speaker` ID を持つ。
表示名は別管理とし、script 内で変更可能とする。

これにより、同一キャラクターを以下のように表示できる。

* 未紹介時: `？？？`
* 仮表示時: `女の先生`
* 紹介後: `高橋先生`

### 12.2 表示名解決ルール

1. step に `display_name` が明示されていればそれを使う
2. 明示がなければ現在の話者表示名マッピングを使う
3. マッピングが存在しなければ speaker ID からデフォルト解決する

---

## 14. 時間経過・日付変更仕様

### 13.1 time_pass

時間経過の演出用 step とする。

例:

* 数分後
* 放課後
* しばらくして
* 模試当日

これは表示上の区切りを主目的とする。

### 13.2 date_change

日付変更を表示する step とする。

例:

* 翌日
* 4 月 13 日
* 夏休み初日

必要に応じて、action 側で内部カレンダー更新を行う。

推奨運用:

* 表示は `date_change`
* 状態更新は `actions` または専用 action

例:

```yaml
- type: date_change
  label: 翌日

actions:
  - set:
      calendar.date: 2026-04-13
```

---

## 15. 検証ルール

ロード時に最低限以下を検証することを推奨する。

* `event_id` が存在すること
* `skit_id` が event 内で一意であること
* `priority` が整数であること
* `trigger` が許可された構造のみで構成されること
* `actions` が許可された action のみで構成されること
* `script.steps` が配列であること
* 各 `step.type` が定義済みであること
* `speaker_intro` の `speaker` が有効であること
* `background_id` が有効な背景定義を参照していること
* `say` / `think` の `speaker` が有効な character_id であること
* `expression` が指定されている場合、その character に定義済みであること

---

## 16. 完全例

```yaml
event_id: lesson_after_01
version: 1
display_name: 授業後イベント01
category: lesson_after
selection_rule:
  strategy: highest_priority_random_tiebreak
skit_defaults:
  once_per_playthrough: true

skits:
  - skit_id: teacher_praise_first
    priority: 100
    trigger:
      all:
        - flag: flags.met_teacher_a
        - gte:
            stats.math: 70
        - not:
            flag: flags.seen_teacher_praise_first
    actions:
      - set:
          flags.seen_teacher_praise_first: true
      - add:
          affection.teacher_a: 1
    script:
      steps:
        - type: background
          background_id: classroom_evening

        - type: speaker_intro
          speaker: teacher_a
          display_name: 女の先生

        - type: say
          speaker: teacher_a
          expression: happy
          text: 今日の小テスト、よくできてたね。

        - type: think
          speaker: protagonist
          expression: troubled
          text: 褒められた。少し嬉しいけど、次も取れるかな。

        - type: narrate
          text: 先生は答案を揃えながら、小さくうなずいた。

        - type: time_pass
          label: 少しして

        - type: say
          speaker: protagonist
          expression: neutral
          text: ありがとうございました。

        - type: system
          text: 好感度が 1 上がった

  - skit_id: generic_after_lesson
    priority: 0
    once_per_playthrough: false
    trigger:
      all: []
    actions: []
    script:
      steps:
        - type: background
          background_id: school_hallway_evening
        - type: narrate
          text: 今日の授業も終わり、生徒たちはそれぞれ帰路についた。
```

---

## 17. ミニゲーム・トランジッション仕様

### 16.1 基本方針

本システムは会話主体の台本進行を基本とするが、必要に応じて別画面ミニゲームを script 内から起動できるものとする。

また、別画面への切り替え時に演出用 transition を挟めるものとする。

### 16.2 transition と minigame の役割分離

* `transition`: 見た目上の画面遷移演出のみを担当する
* `minigame`: 実際の別画面ミニゲーム起動と結果受領を担当する

この分離により、演出と機能を独立して扱えるようにする。

### 16.3 推奨利用例

```yaml
script:
  steps:
    - type: narrate
      text: 模試の時間がやってきた。

    - type: transition
      transition_id: battle_like
      duration_ms: 500

    - type: minigame
      minigame_id: mock_exam_quiz
      params:
        subject: math
        difficulty: normal
      result_key: minigame.mock_exam_result
```

### 16.4 ミニゲーム結果の扱い

ミニゲーム結果は標準では `result_key` に保存する。

結果例:

```yaml
minigame:
  mock_exam_result:
    score: 82
    cleared: true
    rank: a
```

後続の trigger では以下のように参照できる。

```yaml
trigger:
  all:
    - gte:
        minigame.mock_exam_result.score: 80
```

### 16.5 トランジッション識別子

標準仕様では個別演出の中身までは固定せず、`transition_id` の識別子のみを持つ。

例:

* `fade`
* `flash`
* `zoom`
* `battle_like`

実際の描画内容は UI 実装側が解釈して決定する。

---

## 18. 今後の拡張候補

現時点では標準に含めないが、将来的に追加可能な要素を以下に示す。

* choice による分岐選択
* script 途中での state 更新 step
* 音声、効果音、BGM 命令
* メッセージ表示速度指定
* 自動再生待機時間
* 条件付き step 実行
* 外部 script 参照
* ローカライズ対応

---

## 19. コアエンジン責務分割

本システムは、UI と独立したコアエンジンとして実装することを前提とする。

責務は以下のモジュールに分割する。

### 19.1 StateStore

ゲームステートの保持と更新を担当する。

責務:

* 現在のゲームステートを保持する
* ドット区切りパスによる参照を提供する
* ドット区切りパスによる更新を提供する
* 再生済み skit 情報を保持する
* 話者表示名マッピングを保持する

例:

* `get(path)`
* `set(path, value)`
* `add(path, delta)`
* `hasSeenSkit(skit_id)`
* `markSeenSkit(skit_id)`
* `getSpeakerDisplayName(speaker)`
* `setSpeakerDisplayName(speaker, display_name)`

### 19.2 TriggerEvaluator

構造化 trigger を評価し、真偽値を返す。

責務:

* `all`, `any`, `not` の評価
* 比較演算の評価
* フラグ判定の評価
* ゲームステート参照

入力:

* trigger データ
* StateStore または state snapshot

出力:

* boolean

### 19.3 SkitSelector

1 event 内の skit 候補から再生対象を 1 件選ぶ。

責務:

* trigger 一致 skit の抽出
* 再生済み skit の除外
* 最大 priority の絞り込み
* 同一 priority のランダム選択

入力:

* event データ
* StateStore
* RNG

出力:

* 選択された skit、または null

### 19.4 ActionExecutor

action 群を順番に実行し、StateStore を更新する。

責務:

* `set`, `add`, `inc`, `dec` の実行
* 配列更新 action の実行
* 既読記録 action の実行
* event 解放・ロック action の実行

入力:

* actions 配列
* StateStore

出力:

* 更新後 state
* 必要に応じて action 実行ログ

### 19.5 ScriptRunner

script.steps を順番に処理し、UI に渡す表示イベント列または制御イベント列へ変換する。

責務:

* `say`, `think`, `narrate`, `system` の出力
* `background`, `time_pass`, `date_change`, `speaker_intro` の処理
* `transition`, `minigame` の制御イベント生成
* UI 非依存の表示イベント生成

入力:

* script
* StateStore

出力:

* presentation events 配列
* requiresExternalFlow フラグ

### 19.6 EventRunner

指定 event の再生全体を統括する高水準モジュール。

責務:

* event の取得
* skit の選択
* script の実行
* actions の実行
* 再生済み記録の更新
* 実行結果の返却

入力:

* event_id
* StateStore

出力:

* 実行結果オブジェクト

### 19.7 ScenarioRepository

event データの読み込みと参照を担当する。

責務:

* YAML ファイルの読み込み
* event_id による event 取得
* バリデーション済み event の返却

### 19.8 Validator

シナリオデータの構造検証を担当する。

責務:

* event 構造検証
* skit 構造検証
* trigger 構造検証
* action 構造検証
* script step 構造検証

### 19.9 RNG

ランダム選択を抽象化する。

責務:

* seed 指定可能な乱数生成
* テスト時の再現性確保

---

## 20. コアエンジン API 案

以下は実装言語非依存の概念 API である。

### 20.1 StateStore API

```text
get(path: string): any
set(path: string, value: any): void
add(path: string, delta: number): void
inc(path: string): void
dec(path: string): void
append(path: string, value: any): void
remove(path: string, value: any): void
hasSeenSkit(skitId: string): boolean
markSeenSkit(skitId: string): void
getSpeakerDisplayName(speakerId: string): string | null
setSpeakerDisplayName(speakerId: string, displayName: string): void
snapshot(): GameState
```

### 20.2 TriggerEvaluator API

```text
evaluate(trigger: Trigger, state: StateStore): boolean
```

### 20.3 SkitSelector API

```text
select(event: EventData, state: StateStore, rng: RNG): SkitData | null
```

### 20.4 ActionExecutor API

```text
execute(actions: Action[], state: StateStore): ActionExecutionResult
```

`ActionExecutionResult` には以下を含めてよい。

* 実行成功可否
* 実行ログ
* 更新対象一覧

### 20.5 ScriptRunner API

```text
run(script: Script, state: StateStore): PresentationEvent[]
```

### 20.6 EventRunner API

```text
runEvent(eventId: string, state: StateStore): EventRunResult
```

`EventRunResult` は最低限以下を含む。

```text
{
  eventId: string,
  selectedSkitId: string | null,
  presentationEvents: PresentationEvent[],
  actionResult: ActionExecutionResult | null,
  stateSnapshot: GameState
}
```

### 20.7 ScenarioRepository API

```text
getEvent(eventId: string): EventData
hasEvent(eventId: string): boolean
listEvents(): EventSummary[]
```

### 20.8 Validator API

```text
validateEvent(event: unknown): ValidationResult
validateAll(events: unknown[]): ValidationResult[]
```

---

## 21. PresentationEvent 仕様

ScriptRunner は UI へ直接描画せず、presentation events を返す。

これにより UI を差し替え可能にする。

例:

```text
{ type: "say", speaker: "teacher_a", displayName: "女の先生", expression: "happy", avatarUrl: "/assets/avatars/teacher_a/happy.png", chatSide: "left", text: "今日の小テスト、よくできてたね。" }
{ type: "think", speaker: "protagonist", displayName: "主人公", expression: "troubled", avatarUrl: "/assets/avatars/protagonist/troubled.png", chatSide: "right", text: "褒められた。少し嬉しい。" }
{ type: "background", backgroundId: "classroom_evening" }
{ type: "time_pass", label: "少しして" }
{ type: "date_change", label: "翌日", date: "2026-04-13" }
{ type: "system", text: "好感度が 1 上がった" }
```

`say` / `think` イベントには以下のフィールドを含む:

* `speaker`: 話者 ID
* `displayName`: 表示名
* `expression`: 表情識別子
* `avatarUrl`: 解決済みアバター画像 URL
* `chatSide`: チャット UI 上の表示位置 (`left` または `right`)
* `text`: 表示テキスト

UI の責務はこの配列を受けて描画することに限定する。

---

## 22. 実行フロー

`runEvent(eventId)` の標準フローは以下とする。

1. ScenarioRepository から event を取得する
2. Validator 済みであることを前提に event を受け取る
3. SkitSelector が再生対象 skit を 1 件選ぶ
4. skit が存在しない場合は空結果を返す
5. ScriptRunner が script を presentation events / control events に変換する
6. `minigame` など外部フローが必要な場合は、その完了を待って state を更新する
7. ActionExecutor が actions を順番に実行する
8. `once_per_playthrough = true` の場合は再生済み記録を行う
9. EventRunResult を返す

---

## 23. 実装順序

推奨実装順は以下とする。

1. GameState 型定義
2. StateStore 実装
3. TriggerEvaluator 実装
4. SkitSelector 実装
5. ActionExecutor 実装
6. ScriptRunner 実装
7. ScenarioRepository 実装
8. Validator 実装
9. EventRunner 実装
10. ユニットテスト整備
11. デバッグ用仮 UI 実装
12. 本番 UI 実装

---

## 24. テスト方針

### 24.1 TriggerEvaluator テスト

* `all` が正しく評価される
* `any` が正しく評価される
* `not` が正しく評価される
* 比較演算が正しく評価される
* 存在しないパスの扱いが定義どおりである

### 24.2 SkitSelector テスト

* trigger 一致 skit のみが候補になる
* 再生済み skit が除外される
* 最大 priority のみが残る
* 同一 priority 時に RNG に従って選択される

### 24.3 ActionExecutor テスト

* `set`, `add`, `inc`, `dec` が正しく動作する
* 複数 action が順に実行される
* ネストパス更新が正しく行われる

### 24.4 ScriptRunner テスト

* steps が順番どおり処理される
* `speaker_intro` が表示名へ反映される
* `background`, `time_pass`, `date_change` が正しく出力される
* `transition`, `minigame` が正しく制御イベントとして出力される

### 24.5 EventRunner 統合テスト

* event 全体が正しい順序で実行される
* skit 選択、script 実行、actions 実行が統合して成立する
* ミニゲーム結果の反映後に actions が正しく実行される
* 再生済み記録が次回実行に影響する

---

## 25. デバッグ UI 方針

本番 UI の前に、簡易なデバッグ UI を用意することを推奨する。

最低限の機能:

* event_id を指定して再生できる
* presentation events を 1 step ずつ確認できる
* 現在の StateStore を表示できる
* 選択された skit_id を確認できる
* RNG seed を指定できる

これにより、UI 実装前にコアエンジンの検証を十分に行える。

---

## 26. 現時点の結論

本システムでは、以下を標準構成とする。

* 1 event = 1 ファイル
* event 内に複数 skit を持つ
* skit は trigger / priority / actions / script を持つ
* trigger と action は構造化データで記述する
* script は step 配列で表現する
* step の標準型として `say`, `think`, `narrate`, `system`, `background`, `time_pass`, `date_change`, `speaker_intro`, `transition`, `minigame` を持つ
* 同一 priority の skit はランダム選択する
* skit は原則 1 プレイスルー 1 回とする
* コアエンジンは UI 非依存で実装し、presentation events を返す
* UI はコアエンジンの実行結果を描画する責務に限定する

この仕様により、実装コストを抑えながら、チャット風 UI に適した会話主導のノベルゲーム進行を実現する。
