主題
廢土傳承 — 玩家進度存檔機制
概述
存檔系統負責保存和還原玩家的完整遊戲進度。由於遊戲涉及多代家族、地圖探索、族誌累積等長期狀態,存檔資料量會隨遊戲進行逐漸增長。
技術方案
| 階段 | 儲存方式 | 容量上限 | 說明 |
|---|---|---|---|
| 開發階段 | localStorage | ~5 MB | 簡單、方便除錯,夠用於原型測試 |
| 正式版 | IndexedDB | ~數百 MB | 支援大容量、非同步讀寫、結構化查詢 |
兩種方式共用相同的資料結構,正式版只需替換儲存層,不動遊戲邏輯。
存檔觸發時機
自動存檔
| 時機 | 說明 |
|---|---|
| 每回合結束 | 玩家行動階段結束、所有結算完成後自動存檔 |
| 重大事件後 | 角色死亡、家族階段升級、主線推進等不可逆事件後 |
| 離開遊戲前 | 偵測到頁面關閉(beforeunload)時嘗試快速存檔 |
手動存檔
| 操作 | 說明 |
|---|---|
| 存檔按鈕 | UI 上提供手動存檔按鈕,隨時可用 |
| 存檔欄位 | 提供 3 個手動存檔欄位,玩家可自行管理 |
存檔欄位設計
| 欄位 | 說明 |
|---|---|
auto | 自動存檔,每回合覆寫,僅保留最新狀態 |
save_1 | 手動存檔欄位 1 |
save_2 | 手動存檔欄位 2 |
save_3 | 手動存檔欄位 3 |
存檔資料結構
頂層結構
typescript
interface SaveData {
// === 存檔元資料 ===
meta: SaveMeta;
// === 時間狀態 ===
time: TimeState;
// === 家族狀態 ===
family: FamilyState;
// === 資源狀態 ===
resources: ResourceState;
// === 地圖狀態 ===
map: MapState;
// === 庄園狀態 ===
estate: EstateState;
// === 族誌 ===
chronicle: ChronicleState;
// === 事件狀態 ===
events: EventState;
// === 主線進度 ===
mainQuest: MainQuestState;
// === 源血基因碼庫 ===
geneCodes: GeneCodeState;
}各區塊詳細定義
SaveMeta — 存檔元資料
typescript
interface SaveMeta {
version: string; // 存檔格式版本號,如 "1.0.0"
slotId: string; // 存檔欄位 ID:"auto" | "save_1" | "save_2" | "save_3"
createdAt: string; // 首次建立時間(ISO 8601)
updatedAt: string; // 最後更新時間(ISO 8601)
playTime: number; // 累計遊玩時間(秒)
familyName: string; // 家族名稱(顯示在存檔列表)
generation: number; // 當前世代數(顯示在存檔列表)
summary: string; // 自動生成的摘要,如「第3代・定居階段・12人」
}TimeState — 時間狀態
typescript
interface TimeState {
year: number; // 當前年份
season: "spring" | "summer" | "autumn" | "winter";
totalTurns: number; // 已過的總回合數
}FamilyState — 家族狀態
typescript
interface FamilyState {
name: string; // 家族姓氏
stage: "wandering" | "settled" | "estate"; // 發展階段
leaderId: string | null; // 當前族長 ID
members: FamilyMember[]; // 所有家族成員(含已死亡)
}
interface FamilyMember {
id: string; // 成員唯一 ID
name: string; // 名字
gender: "male" | "female";
birthYear: number; // 出生年
deathYear: number | null; // 死亡年(null = 存活)
parentIds: [string, string] | null; // 父母 ID
// 屬性
attributes: {
constitution: number; // 體質
intelligence: number; // 智力
perception: number; // 感知
strength: number; // 力量
};
// 健康
health: {
current: number; // 當前健康值
max: number; // 最大健康值
naturalLifespan: number; // 自然壽命(年)
lifespanBonus: number; // 壽命加成(年)
radiation: number; // 輻射累積值
};
// 狀態
status: "dispatched" | "nurturing" | "training" | "resting" | "pregnant" | "idle" | "dead";
statusTurnsLeft: number; // 當前狀態剩餘回合數
tags: string[]; // 角色標籤(wounded, cursed 等)
// 基因
genes: Gene[];
// 技能
skills: {
survival: number; // 生存
gathering: number; // 採集
combat: number; // 戰鬥
psychic: number; // 靈能
genetics: number; // 基因
leadership: number; // 家族經營(族長專屬)
};
// 派遣資訊(僅派遣中時有值)
dispatch: DispatchInfo | null;
// 遺言(死亡時填入)
lastWords: string | null;
}
interface Gene {
id: string; // 基因 ID
name: string; // 基因名稱
type: "advantage" | "defect" | "neutral";
effects: GeneEffect[]; // 基因效果
inheritRate: number; // 遺傳概率 0–1
}
interface DispatchInfo {
destination: string; // 目的地編碼,如 "R02_L03"
activity: "explore" | "scavenge" | "hunt" | "patrol" | "trade";
departureTurn: number; // 出發回合
returnTurn: number; // 預計返回回合
journalEvents: string[]; // 途中事件 ID 記錄
}ResourceState — 資源狀態
typescript
interface ResourceState {
food: number; // 糧水
material: number; // 材料
knowledge: number; // 知識
reputation: number; // 聲望
}MapState — 地圖狀態
typescript
interface MapState {
locations: LocationStatus[]; // 所有地點的狀態
}
interface LocationStatus {
id: string; // 地點編碼,如 "R01_L01"
status: "unknown" | "rumored" | "discovered" | "explored" | "cleared";
visitCount: number; // 造訪次數
lastVisitTurn: number | null; // 最後造訪回合
notes: string[]; // 探索中發現的備註(自動記錄)
}EstateState — 庄園狀態
typescript
interface EstateState {
location: string | null; // 庄園所在地點編碼(流浪階段為 null)
facilities: Facility[]; // 已建設施
}
interface Facility {
id: string; // 設施 ID
type: string; // 設施類型
level: number; // 等級
buildTurn: number; // 建造完成的回合
}ChronicleState — 族誌
typescript
interface ChronicleState {
entries: ChronicleEntry[]; // 所有族誌條目
}
interface ChronicleEntry {
id: string; // 條目唯一 ID
turn: number; // 發生回合
year: number; // 發生年份
season: string; // 發生季節
memberId: string; // 相關成員 ID
type: "birth" | "marriage" | "child" | "exploration" | "skill_learned"
| "injury" | "disease" | "radiation" | "area_cleared" | "death" | "last_words";
content: string; // 記錄文字
mechanicalEffect: string | null; // 跨代機械效果描述(供系統讀取)
}EventState — 事件狀態
typescript
interface EventState {
triggeredEvents: TriggeredEvent[]; // 已觸發的事件記錄
cooldowns: EventCooldown[]; // 事件冷卻計時
activeChains: ActiveChain[]; // 進行中的事件鏈
}
interface TriggeredEvent {
eventId: string; // 事件編碼
turn: number; // 觸發回合
memberId: string | null; // 相關成員(探索事件)
choiceId: string | null; // 玩家選擇(家園事件)
checkResult: number | null; // D20 檢定結果
success: boolean | null; // 是否成功
}
interface EventCooldown {
eventId: string;
availableTurn: number; // 可再次觸發的回合
}
interface ActiveChain {
chainRoot: string; // 事件鏈起始 ID
currentStep: string; // 當前步驟 ID
data: Record<string, any>; // 事件鏈中暫存的資料
}MainQuestState — 主線進度
typescript
interface MainQuestState {
stage: number; // 主線階段編號
cluesFound: string[]; // 已收集的線索 ID
flags: Record<string, boolean>; // 主線旗標(用於條件判斷)
}GeneCodeState — 源血基因碼
typescript
interface GeneCodeState {
owned: GeneCode[]; // 擁有的基因碼
used: string[]; // 已使用的基因碼 ID
}
interface GeneCode {
id: string; // 基因碼唯一 ID
code: string; // 編碼後的短碼(用於跨玩家交換)
sourceGene: Gene; // 來源基因
quality: "low" | "medium" | "high" | "perfect";
sourceMemberId: string; // 犧牲者 ID
createdTurn: number; // 萃取回合
}儲存與讀取流程
存檔流程
1. 收集所有系統的當前狀態
2. 組裝成 SaveData 物件
3. 更新 meta(updatedAt、playTime、summary)
4. JSON.stringify 序列化
5. 寫入 localStorage(開發)或 IndexedDB(正式)讀檔流程
1. 從 localStorage / IndexedDB 讀取 JSON 字串
2. JSON.parse 反序列化
3. 檢查 meta.version 進行版本遷移(若需要)
4. 將各區塊資料注入對應的 Zustand store
5. 恢復遊戲畫面localStorage 儲存格式
key: "wasteland_save_auto" → value: JSON 字串
key: "wasteland_save_1" → value: JSON 字串
key: "wasteland_save_2" → value: JSON 字串
key: "wasteland_save_3" → value: JSON 字串IndexedDB 儲存格式(正式版)
Database: "WastelandLegacy"
└─ ObjectStore: "saves"
├─ key: "auto" → value: SaveData 物件
├─ key: "save_1" → value: SaveData 物件
├─ key: "save_2" → value: SaveData 物件
└─ key: "save_3" → value: SaveData 物件版本遷移策略
遊戲更新可能改變存檔結構。透過版本號管理遷移:
遷移原則
meta.version記錄存檔格式版本- 讀檔時比對當前遊戲版本與存檔版本
- 若版本不同,依序執行遷移函式
- 遷移只做「加欄位、設預設值」,不刪除舊資料
遷移範例
typescript
const migrations: Record<string, (data: any) => any> = {
"1.0.0_to_1.1.0": (data) => {
// 新增庄園系統,舊存檔沒有 estate 欄位
if (!data.estate) {
data.estate = { location: null, facilities: [] };
}
data.meta.version = "1.1.0";
return data;
},
"1.1.0_to_1.2.0": (data) => {
// 新增源血基因碼系統
if (!data.geneCodes) {
data.geneCodes = { owned: [], used: [] };
}
data.meta.version = "1.2.0";
return data;
}
};存檔大小估算
| 遊戲階段 | 家族成員數 | 族誌條目數 | 預估存檔大小 |
|---|---|---|---|
| 第 1 代(開始) | 2–3 人 | ~10 條 | ~5 KB |
| 第 3 代 | 8–12 人 | ~80 條 | ~30 KB |
| 第 10 代 | 20–30 人(含已故) | ~500 條 | ~150 KB |
| 第 20 代(長期) | 50–80 人(含已故) | ~2000 條 | ~500 KB |
localStorage 5 MB 上限,4 個欄位約可支撐到第 20+ 代,足夠開發階段使用。 IndexedDB 無實質上限,正式版完全不用擔心容量。
存檔列表 UI 顯示
存檔選擇畫面顯示的資訊:
┌─────────────────────────────────────┐
│ 📁 自動存檔 │
│ 陳氏家族 ・ 第 5 代 ・ 定居階段 │
│ 第 12 年 冬季 ・ 15 人 │
│ 2026/02/21 14:30 │
├─────────────────────────────────────┤
│ 📁 存檔欄位 1 │
│ 陳氏家族 ・ 第 3 代 ・ 流浪階段 │
│ 第 6 年 秋季 ・ 8 人 │
│ 2026/02/20 22:15 │
├─────────────────────────────────────┤
│ 📁 存檔欄位 2 │
│ (空) │
├─────────────────────────────────────┤
│ 📁 存檔欄位 3 │
│ (空) │
└─────────────────────────────────────┘存檔編碼系統
概述
存檔編碼(Save Code)將完整遊戲進度壓縮為一段可複製的文字字串。這段編碼:
- 開發用途:開發者複製編碼,隨時貼上還原到特定遊戲進度,方便測試和除錯
- 玩家用途:跨裝置轉移進度,不需要帳號系統
- VIP 銜接:後續 VIP 付費功能可基於編碼做進階操作(雲端備份、分享存檔、排行榜)
編碼格式
WL1:xxxxxxxxxxxxxxxxxxxxxxxx...| 部分 | 說明 |
|---|---|
WL | 前綴,代表 Wasteland Legacy |
1 | 編碼版本號(未來格式變更時遞增) |
: | 分隔符 |
xxxx... | SaveData 壓縮後的 base64url 字串 |
編碼流程
SaveData 物件
→ JSON.stringify()
→ 壓縮(lz-string compressToEncodedURIComponent)
→ 加上版本前綴 "WL1:"
→ 輸出存檔編碼字串解碼流程
存檔編碼字串
→ 檢查前綴 "WL1:"
→ 取出 base64url 部分
→ lz-string decompressFromEncodedURIComponent
→ JSON.parse()
→ 版本遷移檢查(meta.version)
→ 注入 Zustand store壓縮效果預估
使用 lz-string 壓縮 JSON 字串,預估壓縮率 60–80%:
| 遊戲階段 | 原始 JSON | 壓縮後 base64url | 字元數 |
|---|---|---|---|
| 第 1 代(開始) | ~5 KB | ~1.5 KB | ~2,000 字元 |
| 第 3 代 | ~30 KB | ~8 KB | ~11,000 字元 |
| 第 10 代 | ~150 KB | ~40 KB | ~55,000 字元 |
第 1–3 代的編碼長度適合直接複製貼上。10 代以上會很長,但功能上仍可用。
UI 顯示
位置:主遊戲畫面 StatusBar 右上角
平時狀態:
[WL1:a3f8…]- 顯示前綴 + 前 4 碼 + 省略號
- 字體較小(text-xs),磷光綠低透明度(opacity-50)
- 點擊展開存檔編碼面板
展開面板(覆蓋在 StatusBar 下方):
┌─────────────────────────────────┐
│ 存檔編碼 [×] │
├─────────────────────────────────┤
│ WL1:eJzNV01v2zgQvQv4P0iB... │
│ ________________________________│
│ [複製編碼] │
├─────────────────────────────────┤
│ 匯入編碼: │
│ [________________________] [載入] │
│ │
│ ⚠ 載入編碼會覆蓋目前的遊戲進度 │
└─────────────────────────────────┘| 元素 | 說明 |
|---|---|
| 編碼顯示區 | 全選可複製的 <textarea readonly>,自動換行 |
| [複製編碼] | 點擊複製到剪貼簿,按鈕文字變「已複製 ✓」1s 後恢復 |
| 匯入編碼 | 文字輸入框 + [載入] 按鈕 |
| ⚠ 警告 | 提醒載入會覆蓋目前進度 |
| [×] | 關閉面板 |
錯誤處理:
| 狀況 | 顯示 |
|---|---|
| 格式無效(無 WL1: 前綴) | 「編碼格式不正確」 |
| 解壓失敗 | 「編碼已損壞,無法讀取」 |
| 版本不相容 | 「此編碼來自不同版本的遊戲」 |
| 載入成功 | 面板關閉,StatusBar 顯示「[已載入]」磷光綠 1s 後淡出 |
技術依賴
新增 npm 套件:
bash
npm install lz-string
npm install -D @types/lz-stringlz-string 特點:
- 純 JavaScript,無外部依賴
- 壓縮率高,適合 JSON 文字
compressToEncodedURIComponent輸出 URL 安全字元,適合複製貼上- gzip 約 3 KB,不影響載入速度
VIP 銜接預留
Phase 1 僅實作本機編碼(encode/decode),以下功能為後續 VIP 預留接口:
| 功能 | Phase 1 | VIP 版本 |
|---|---|---|
| 產生編碼 | 本機壓縮 | 同左 |
| 複製/貼上匯入 | 本機操作 | 同左 |
| 雲端備份 | — | 編碼上傳到伺服器,綁定帳號 |
| 分享存檔 | — | 產生短網址,他人可一鍵載入 |
| 排行榜 | — | 從編碼中擷取統計數據 |
| 存檔加密 | — | 防止竄改,驗證完整性 |
匯出與分享
存檔匯出
存檔匯出功能由「存檔編碼系統」取代。玩家透過複製存檔編碼即可備份或跨裝置轉移。
源血基因碼分享
源血基因碼獨立於存檔系統,使用短碼格式方便分享:
基因碼格式:WL-XXXX-XXXX-XXXX(12 位英數字)- 玩家可複製短碼,透過任何通訊管道分享
- 接收方在遊戲中輸入短碼即可導入
- 短碼包含加密的基因資訊,不需要連線或伺服器
開發順序
| 階段 | 實作內容 |
|---|---|
| MVP Phase 1 | localStorage 存讀、自動存檔、基礎 SaveData(time + family + resources + events)、存檔編碼(encode/decode + UI) |
| MVP Phase 2 | 手動存檔欄位、存檔列表 UI、版本遷移框架 |
| MVP Phase 3 | 補充 map + estate + chronicle 區塊 |
| 正式版 | 切換到 IndexedDB、源血基因碼短碼系統 |
| VIP 版本 | 存檔編碼雲端備份、分享短網址、排行榜 |