
系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code / Claude Cowork
今日進度:劇情決策樹定案,data/story/graph.json格式拍板,劇本初稿完成
昨天把戰鬥系統的資料格式定下來了,今天輪到劇情。「有分支劇情」四個字很容易寫,但真正做過的人都知道,分支會指數爆炸:五個二選一的抉擇就是 32 條路徑,每條都要寫對白、都要測試、都要確認不會卡死。單人 30 天,這是不可能的任務。
今天的目標是設計一個分支有限、結局有意義、資料驅動的敘事架構,並且跟昨天一樣先定資料格式——Go 的劇情狀態機(Day 10)與 Vue 的對話框(Day 18)都要讀同一份圖。
我不想做「好結局/壞結局」這種二元道德題,那太容易讓玩家「猜作者想要什麼」而不是「想自己要什麼」。《薪火要塞》的核心矛盾是:面對灰潮,你選擇守、燒,還是聽? 三位角色各代表一種答案,而且三種答案都有道理、都有代價:
| route | 命運之路 | 代表人物 | 核心信念 | ending | 結局 |
|---|---|---|---|---|---|
vigil |
守望之路 | 參謀艾瑟琳 | 守住每一個人,初火自會延續 | dawn |
黎明結局 |
blaze |
燃盡之路 | 老將軍卡爾登 | 焦土,讓灰潮無處可食 | ash |
灰燼結局 |
truce |
餘燼之路 | 信使米菈 | 灰潮不是來殺人的,他們在找什麼 | kindling |
薪火相傳結局 |
分支控制的關鍵設計是:選擇不切換路徑,而是累積傾向。 每個選項對 vigil/blaze/truce 三個 flag 加分,直到終章前才依最高分決定路線。這樣五個抉擇不會產生 32 條路徑,而是 3 條路徑 × 不同的累積過程——對白可以依 flag 微調,但骨幹只有三條,測試也只要測三條。
平手時的優先序定為 vigil > truce > blaze。理由是「守望」是沒有明確立場時最合理的預設,而「燃盡」是三條裡代價最重的,不該在玩家沒有明確選擇時發生。這條規則看起來是小事,但 Day 10 寫測試時它會是一個獨立的測試案例。
我請 Claude Code 用 Mermaid 把結構畫出來,貼進 docs/story/tree.md。它會依據 graph.json 自動產生,之後圖改了重跑一次即可:

為什麼是五個抉擇而不是更多?因為每個抉擇都要對應一場戰鬥的「前情」,六關只有五個關卡間隙;而且累積制之下,五個抉擇已經足以讓三個 flag 拉開差距(最極端是 5:0:0,最曖昧是 2:2:1),再多只會稀釋每個選擇的重量。節點總數恰好 60 個:dialogue 39、choice 5、battle 8(n_b1–n_b5 加終章三變體 n_b6_vigil/n_b6_blaze/n_b6_truce)、route 5、ending 3。route 型別除了終章判定 n_final_gate,還有四個 n_tint2–n_tint5:這幾個不是新節點型別,而是借用同一個 route 機制依 flag 微調語氣的對白分支——判定邏輯跟 n_final_gate 一模一樣,只是目的地是對白節點而不是戰鬥。
五個抉擇的主題是刻意排過的:糧倉(資源)、難民(人)、斷橋(退路)、修道院祕密(真相)、米菈的請求(信任)。從具體到抽象,玩家越到後面越清楚自己在選什麼。
第六關「薪火王城」依路線有三個變體:開場對白不同,boss 燼王瓦洛斯的屬性也會修正。這裡有個容易寫錯、值得今天講清楚的地方:boss 的差異必須是絕對值,寫在關卡檔的 variants 裡(blaze → ashlord.armor 0.5、truce → ashlord.hp 2000),而不是我原先想的「血量減 20%」這種倍率——倍率意味著 Go 模擬器與前端 TypeScript 各自拿浮點數再乘一次,兩邊四捨五入方式只要有一點差異,boss 血量就會算不一樣。至於「燃盡路線玩家起手薪火較多」,那不是關卡屬性,而是選項一路給 ember 的自然結果:純燃盡路線累積 3 點、純餘燼路線 2 點、純守望路線 1 點——薪火多寡完全來自選項本身的效果,不需要也不該在關卡檔裡另外設定。這是昨天資源三角之外,劇情影響戰鬥的第二個耦合點。
劇情圖是一張有向圖,節點型別只有四種:dialogue(對白)、choice(抉擇)、battle(戰鬥)、ending(結局),外加一個特殊的 route(路線判定)。型別越少,狀態機越簡單。data/story/graph.json 節錄:
{
"start": "n_prologue",
"nodes": {
"n_prologue": { "type": "dialogue", "speaker": "艾瑟琳",
"text": "守望者,灰潮已越過北嶺。邊境只剩你了。", "next": "n_c1" },
"n_c1": { "type": "choice", "speaker": "卡爾登", "text": "糧倉留不住了。你打算怎麼做?",
"choices": [
{ "text": "死守糧倉,一個人也不能少。", "effects": { "vigil": 1 }, "next": "n_b1" },
{ "text": "燒了它,別讓灰潮吃飽。", "effects": { "blaze": 1, "ember": 1 }, "next": "n_b1" },
{ "text": "留下糧食,看看灰潮到底要什麼。", "effects": { "truce": 1 }, "next": "n_b1" } ] },
"n_b1": { "type": "battle", "level": "ashfield", "onWin": "n_after1", "onLose": "n_b1" },
"n_after1": { "type": "dialogue", "speaker": "米菈",
"text": "……他們不是來殺人的。他們在找什麼。", "next": "n_c2" },
"n_final_gate": { "type": "route",
"routes": { "vigil": "n_b6_vigil", "blaze": "n_b6_blaze", "truce": "n_b6_truce" } },
"n_ending_dawn": { "type": "ending", "ending": "dawn" },
"n_ending_ash": { "type": "ending", "ending": "ash" },
"n_ending_kindling": { "type": "ending", "ending": "kindling" }
}
}
幾條規則寫進 docs/story/README.md,之後 Go 狀態機與前端都要遵守:
effects 的 key 只能是 vigil、blaze、truce、ember,狀態機遇到其他 key 要報錯而不是忽略。battle 節點的 onLose 可以指回自己(重打),前端要處理這個 loop,不能無限跳頁。choice 可帶 "requires": { "ember": 2 },薪火不足時選項顯示但灰化——這正是 Day 2 對話框 artboard 畫出的那個灰色選項。顯示但不可選,是為了讓玩家知道「原來還有這條路」。route 節點依 flag 最高者跳轉,平手依 vigil > truce > blaze。五種節點型別各自怎麼離開,畫成圖比條列清楚,這張圖 Day 10 寫狀態機時會直接拿來對照:

你會發現第二個選項同時給 blaze 與 ember:燃盡之路的玩家在戰鬥中會更強,但劇情代價是走向灰燼結局。機制本身就在說故事,這是我最想做到的事。
對白量不小:5 個抉擇 × 3 選項、每關前後各一段、三個結局變體,粗估 60 個節點。我把 graph.json 的骨架丟給 Cowork:
Prompt(給 Claude Cowork)
「讀data/story/graph.json與docs/pitch.md,產出docs/story/script.md:先列角色設定表(姓名、立場、說話風格、禁忌),再依節點 id 逐一寫對白。艾瑟琳冷靜精確、卡爾登短句粗獷、米菈遲疑但誠實。每段對白不超過 40 字,因為對話框只有 30% 高度。」
角色設定表裡的「禁忌」欄很有用:卡爾登絕不說「也許」,米菈絕不下命令。這些限制讓三個人的聲音在 60 段對白裡保持可辨識。
劇本寫完後,最怕的是前後矛盾——例如米菈在第 2 關才登場,卻在序章被提到名字;或某個 next 指向不存在的節點,玩到一半卡死。我請 Claude Code 做檢查:
Prompt(給 Claude Code)
「讀docs/story/script.md與graph.json,列出:①角色首次登場前就被提及的地方;②任何從start無法到達的節點;③沒有出口的非 ending 節點;④三條路線各自的最短選項序列。」
它抓到兩個問題:n_after3 提到「修道院的祕密」但那要到第 4 關才揭露;n_c4 第三個選項的 next 打錯字指向不存在的節點。第 ② ③ 項在 Day 25 會變成正式的 lint 工具 cmd/storylint,放進 CI 裡每次改劇情都自動跑。第 ④ 項的輸出直接變成 Day 25 三結局測試的輸入資料。
今天最重要的決定是「累積傾向而非切換路徑」——它把分支從指數壓回線性,讓 30 天內做完三個結局變得可能。資料格式沿用 Day 3 的哲學:先定契約,Go 狀態機與 Vue 對話框才能各自開工。踩雷:Cowork 第一版劇本每段對白都超過 60 字,我忘了告訴它介面限制;補上「40 字、面板 30% 高」後才對。教訓跟昨天一樣:介面限制也是 Prompt 的一部分。 給想做分支劇情的讀者一個建議:先決定「分支怎麼收斂」再決定「分支怎麼展開」,收斂機制定了,展開多少都不怕。明天暫時離開遊戲內容,回答一個工程問題:這些東西到底要用什麼做?
docs/story/tree.md(Mermaid 決策樹)data/story/graph.json(骨架,60 節點)與 docs/story/README.md(規則)docs/story/script.md(角色設定表 + 全部對白初稿)Day 05:Vue 3 + Go——技術選型的取捨,為什麼前後端分離是這場戰役的兵法。