從一個模糊的靈感,走到選項系統、存讀檔、UI 分層全部到位,Day14 終於進到一個比較像作品的節點:第一個可以玩的 Demo。
這裡的「可以玩」不是指單一場景能跑,也不是指某個元件 demo 成功,而是玩家可以從標題畫面開始,讀序章、進第一章自由行動、打完第二章三關,最後看到 Demo 結局或 Bad End。那一刻會很明顯地感覺到,前面做的所有系統不再只是散裝零件。
照 PRD 定下的第一版目標,當時的 Demo 範圍刻意收得很小:
首版 Demo 時,Ink 的核心結構是把章節和結局拆開管理。
ink
INCLUDE chapters/chapter_00.ink // 序章〈夢醒九重宮〉
INCLUDE chapters/chapter_01.ink // 第一章〈藏鋒〉
INCLUDE chapters/chapter_02.ink // 第二章〈使臣叩關〉
INCLUDE chapters/endings_demo.ink // Demo 結局 fallback
現在的結局判定分成兩層。前端的 EndingResolver.ts 是權威判定,story.ts 裡的 _resolveEnding() 會讀取 Ink/Pinia 狀態,算出 ending id 後寫回 resolved_ending_id。chapters/endings_demo.ink 則保留本地 fallback:如果前端沒有先寫入結果,Ink 會用 envoy_wins 和 disguise 做最基本的分流。
ink
=== demo_ending_resolution ===
# ending:resolve
{ resolved_ending_id == "":
- envoy_wins >= 2 && disguise < 60: ~ resolved_ending_id = "DEMO_END_A"
- envoy_wins >= 1 && disguise >= 60: ~ resolved_ending_id = "DEMO_END_B"
- else: ~ resolved_ending_id = "DEMO_END_C"
}
- resolved_ending_id == "DEMO_END_A": -> demo_end_a
- resolved_ending_id == "DEMO_END_B": -> demo_end_b
- resolved_ending_id == "DEMO_END_C": -> demo_end_c
- else: -> demo_bad_end // DEMO_BAD_END
EndingResolver.ts 裡的判定,它會先檢查 male_lead_dead,再檢查「沒有證據又攻擊侍衛、帝心過低」這種直接 Bad End 的狀況;接著才判斷 DEMO_END_A、DEMO_END_B,最後落到 DEMO_END_C。
到這一步,前面幾週講的每個系統第一次真的接在一起:
StoryRuntime 執行 Ink 劇本StoryCommandRouter 分派畫面指令SaveManager 負責存讀檔ChoiceMenu 和三個小遊戲元件承接第二章玩法EndingResolver 判斷 Demo 結局這些東西各自測通,不代表作品能玩。真正的驗收是從標題畫面一路走到第二章結尾,中間存一次檔、讀一次檔、打開證據、觸發小遊戲,再看結局判定有沒有照玩家選擇反映出來。Demo 的價值就在這裡:它逼我驗證整條技術管線,而不是只驗證單一模組。
這也是為什麼企劃階段要先做序章+兩章的 Demo,而不是直接鋪開八章。先驗證整條管線走得通,後面再擴章節,心裡會踏實很多。
這個階段最有價值的不是「內容量」,而是暴露整合問題。記錄幾個實際踩到的:
角色 id 顯示問題:真正從頭玩到結局之前,DialogueBox 的名牌有一段時間會顯示英文 id(例如 noble_son)而不是「權貴公子」。根因是 findCharacter() 找不到角色時,fallback 邏輯是「直接顯示原始 id 字串」。這個 fallback 在單元測試裡看不出問題,要實際跑完 CH01 醫館這段劇情才會觸發。後來把缺漏角色補進 characters-scenes.json,現在 noble_son 會正確顯示成「權貴公子」。
Ink 變數與 Pinia 同步的時序問題:_syncVariables() 原本主要跟著 advance() 走,但選項和小遊戲會走 store.choose()。這條路徑如果沒有同步,結局判定就可能讀到上一拍的舊值。現在 choose() 也會在選完後呼叫 _syncVariables(),避免選項改了 Ink 變數,Pinia 還停在上一個狀態。
小遊戲結束沒有交回控制權:文辯、算籌、武鬥的 store.minigame 在某些分支路徑下沒有正確清空,玩家點到選項後畫面像是卡住。現在 choose() 會在處理選項後把 minigame 清成 null,讓控制權回到一般對話流程。這個 bug 只在完整跑過某一段特定分支路線時才出現,局部測試很難看出來。
三個 bug 的共同點:各自獨立測試時「看起來都對」,串成完整路徑之後才暴露。Demo 的意義就是把這些「各自都對、串起來才錯」的問題提前抓出來。
✅ 完成第一個可遊玩的 MVP
Day14 的成果不是「所有功能都完成」,而是證明最小閉環成立:劇本能跑、畫面能更新、玩家能選、狀態能存、結局能判斷。
下週開始要往技術深化篇走。PixiJS 的圖層管理策略、Pinia 跟 Ink 狀態的同步機制,這些都是讓 Demo 從「能跑」變成「跑得穩」的關鍵。Demo 跑起來只是第一步,真正麻煩的事情通常都藏在第二輪、第三輪遊玩裡。

