
系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code / Claude Cowork
今日進度:docs/gdd.md定稿,三個遊戲資料檔格式拍板
昨天的四張 artboard 讓畫面存在了,但畫面裡的數字——塔多少錢、敵人多少血、一波幾隻——還全是空的。今天要把 Day 1 的原則一「資料驅動」落實:先定資料格式,再寫程式。
理由很實際:程式會改、會重構,但資料格式一旦定下,前端引擎、後端模擬器、平衡調校都得靠它,它是三方的共同契約。契約先定好,之後三方可以各自開工而不互相等待——這也是我打算在 Day 12 做 worktree 平行開發實驗的前提。今天是 Claude Code 第一次正式出場。
我把 docs/pitch.md 與 docs/design/README.md 放進 repo 後開啟 claude:
Prompt(給 Claude Code)
「讀docs/pitch.md與docs/design/。產出docs/gdd.md,涵蓋:波次結構、四種塔(每種三級)、敵人類型、資源三角、傷害公式。所有數值先給合理初值並標註『待平衡』。塔名與敵人名必須原創、符合薪火/灰潮世界觀。」
第一版產出的四塔是「弓箭塔/魔法塔/砲塔/冰塔」——功能正確但命名毫無靈魂,敵人也是「哥布林/獸人」這種通用奇幻詞。我補了一句「塔名要像王國軍事設施,敵人名要像灰燼生物」,第二版就有了:
| id | 塔 | 傷害類型 | 特性 |
|---|---|---|---|
bolt |
弩塔 | physical | 單體、射速快、便宜 |
arcane |
秘法塔 | magic | 單體、無視護甲、對高魔抗無力 |
ember |
火砲 | physical | 範圍濺射,攻速慢,打不到飛行 |
frost |
霜語塔 | magic | 命中減速 40%,持續 2 秒 |
每種塔三級的設計也有講究:升級不只是數字變大,而是「特性變強」——霜語塔的減速從 40% 到 60%、火砲的濺射半徑從 40 到 56。三級是一個折衷:兩級的成長感不夠,四級以上的數值表會膨脹到單人 30 天調不完。升級費用刻意設成遞增但不翻倍(弩塔 70 → 110 → 160),讓「升一座舊塔」與「蓋一座新塔」在多數時候是真正的兩難,而不是明顯的最佳解。
五種敵人則是灰燼步兵、疾風掠奪者、石殼巨獸、霧翼,以及終章 boss 燼王瓦洛斯。四塔與五敵互相牽制:石殼巨獸(護甲 0.5)讓弩塔只剩一半傷害,逼你買秘法塔;霧翼會飛,火砲打不到;疾風掠奪者速度 95,不放霜語塔根本攔不住。沒有一種塔能解決所有敵人,玩家每一關都得依波次組成重新思考配置——這是塔防好玩的根本,也是 Day 23 平衡調校要守住的底線。
一般塔防只有金幣與生命:金幣建塔、漏敵扣命。我加了第三個資源「薪火」(ember):它只從劇情選擇獲得,戰鬥中花 1 點發動「初火之息」讓全場敵人減速 3 秒。
這個設計是為了 Day 1 原則二「劇情與戰鬥耦合」。沒有它,劇情選擇只影響對白與結局,戰鬥完全一樣,玩家很快就會覺得劇情是「跳過也沒差」的過場。有了薪火,選擇焦土戰術的玩家會多拿薪火、戰鬥更輕鬆,但劇情會往灰燼結局走——機制本身在講故事。
三個資源各自的來源與去處,畫成一張圖就是資源三角:

傷害公式定得很簡單,簡單才好平衡、好解釋、好在 Day 11 用模擬器驗證:
dmg × (1 − armor)
dmg × (1 − magicResist)
cost;賣塔退回累計花費的 60%。護甲與魔抗都是 0 到 1 的比例而不是整數減傷,理由是比例制不會出現「傷害低於護甲就打零」的邊界情況,前端引擎與後端模擬器只要各實作一行乘法,兩邊不可能算出不同結果。完整結算順序如下:

三個檔案都放在 project/data/——monorepo 根目錄下、前端引擎、後端模擬器、平衡試算表共讀的唯一位置。有一件事今天就該定下來但先不動手做:資料檔要有版本號,不然平衡調整幾輪之後根本分不清哪份資料對應哪次調校。這件事先記著,等真的要調數值時再補(Day 23)。
data/towers.json(四塔皆同格式,以下列兩塔):
{
"bolt": { "id": "bolt", "name": "弩塔", "damageType": "physical", "hitsFlying": true,
"levels": [
{ "cost": 70, "damage": [8, 12], "range": 120, "cooldown": 0.8 },
{ "cost": 110, "damage": [14, 20], "range": 130, "cooldown": 0.7 },
{ "cost": 160, "damage": [24, 34], "range": 140, "cooldown": 0.6 } ] },
"frost": { "id": "frost", "name": "霜語塔", "damageType": "magic", "hitsFlying": true,
"levels": [
{ "cost": 90, "damage": [4, 6], "range": 110, "cooldown": 1.0, "slow": 0.4, "slowDuration": 2.0 },
{ "cost": 140, "damage": [6, 9], "range": 120, "cooldown": 0.9, "slow": 0.5, "slowDuration": 2.5 },
{ "cost": 200, "damage": [9, 13], "range": 130, "cooldown": 0.8, "slow": 0.6, "slowDuration": 3.0 } ] }
}
ember 多一個 splash(40/48/56)欄位;arcane 三級費用 100/160/240、傷害 18–26 / 34–46 / 60–80。damage 是 [min, max] 區間,命中時隨機取值,讓戰鬥不那麼機械。這張表背後還有一組沒寫進 JSON、但會變成 loader 驗證規則的約束:levels 長度固定 3、cost 嚴格遞增、cooldown 單調不遞增、range 單調不遞減——升級不能讓塔變貴又變弱。今天先記下來,Day 8 寫 Go 的資料載入器時會直接拿來當驗證邏輯。
data/enemies.json(完整):
{
"ashwalker": { "id": "ashwalker", "name": "灰燼步兵", "hp": 60, "speed": 50, "armor": 0, "magicResist": 0, "bounty": 6, "lives": 1, "flying": false },
"windreaver": { "id": "windreaver", "name": "疾風掠奪者", "hp": 40, "speed": 95, "armor": 0, "magicResist": 0.2, "bounty": 8, "lives": 1, "flying": false },
"stoneshell": { "id": "stoneshell", "name": "石殼巨獸", "hp": 320, "speed": 30, "armor": 0.5, "magicResist": 0, "bounty": 25, "lives": 3, "flying": false },
"mistwing": { "id": "mistwing", "name": "霧翼", "hp": 55, "speed": 70, "armor": 0, "magicResist": 0.3, "bounty": 10, "lives": 1, "flying": true },
"ashlord": { "id": "ashlord", "name": "燼王瓦洛斯", "hp": 2500, "speed": 25, "armor": 0.4, "magicResist": 0.4, "bounty": 200, "lives": 10, "flying": false, "boss": true }
}
speed 單位是邏輯像素/秒,以 Day 2 定的 960×540 畫布為準;lives 是漏掉一隻扣的生命數,石殼巨獸漏一隻扣 3,boss 漏了直接扣 10。
data/levels/ashfield.json(第一關「灰原邊境」,節錄前兩波):
{
"id": "ashfield", "name": "灰原邊境", "lives": 20, "startGold": 220,
"paths": [ [[0,300],[200,300],[200,150],[500,150],[500,450],[800,450],[960,450]] ],
"slots": [ [150,220], [300,80], [420,240], [620,380] ],
"waves": [
{ "spawns": [ { "enemy": "ashwalker", "count": 8, "interval": 1.2 } ] },
{ "spawns": [ { "enemy": "ashwalker", "count": 10, "interval": 1.0 },
{ "enemy": "windreaver", "count": 4, "interval": 0.8, "delay": 6 } ] }
]
}
paths 是二維陣列,一關可以有多條路徑(第二關霧河渡口就會用到),spawn 可指定 "path": 0;delay 是該群組相對波次開始的秒數,讓同一波裡疾風掠奪者晚 6 秒才出現,製造節奏變化。第一關共 5 波,完整檔案在 repo。六關的 id 依序是 ashfield、mistford、brokenbridge、abbey、blackrock、emberkeep,全部共用這個格式——加一關只是加一個 JSON 檔,不用動程式。
數值「合不合理」光看 JSON 看不出來,220 金幣起手能買三座弩塔還是一座火砲加一座霜語塔?第一波 8 隻步兵給 48 金幣,夠不夠在第二波前升級?我請 Cowork 讀 data/ 後產出 docs/balance-v0.xlsx,欄位設計是今天最後的討論:
| 欄位 | 公式 | 用途 |
|---|---|---|
| DPS | avg(damage) / cooldown |
塔的原始輸出 |
| 金幣效率 | DPS / cost |
每塊錢買到多少輸出 |
| 有效 DPS(vs 敵人) | DPS × (1 − 對應抗性) |
四塔 × 五敵矩陣 |
| 波次總血量 | Σ count × hp |
每波壓力 |
| 波次賞金 | Σ count × bounty |
每波經濟回饋 |
起手 220 金幣是刻意算過的:剛好買三座一級弩塔(210)或一座火砲加一座霜語塔(215),但買不起兩座秘法塔(200,但第一波沒有需要秘法塔的敵人,買了等於浪費)。玩家第一次配置就必須做選擇,而不是「錢夠多、全部蓋」。
初版數字顯示弩塔一級金幣效率 0.18、秘法塔 0.15、火砲 0.09(未計濺射)。火砲看起來很差,但濺射對成群步兵的實際輸出可能是三倍——這種「靜態表看不出來」的東西,正是 Day 11 要用 Go 模擬器實測的理由。這份試算表只是起點,Day 23 會用模擬數據正式調校。
今天的產出是三個 JSON 與一份試算表,沒有一行程式。但這三個檔案接下來會被 Go 後端讀(Day 8)、被模擬器跑(Day 11)、被 Vue 引擎渲染(Day 15)——先定契約,三方才能平行前進。踩雷:Claude 第一版命名太「通用」,補一句世界觀描述就解決;教訓是命名要求要寫進 Prompt,不能指望它猜。給讀者的建議:如果你也想做資料驅動的遊戲,把資料格式當 API 來設計——想清楚誰會讀它、怎麼擴充、哪些欄位是可選的,然後才動手寫程式。
docs/gdd.md(波次、四塔、五敵、資源三角、傷害公式)data/towers.json、data/enemies.json、data/levels/ashfield.json
docs/balance-v0.xlsx(DPS/金幣效率/有效 DPS 矩陣)Day 04:三條命運之路——多重劇情分支的敘事架構設計(決策樹 × 資料驅動劇情)。