Skip to content

廢土傳承 — 玩家進度存檔機制 ​

概述 ​

存檔系統負責保存和還原玩家的完整遊戲進度。由於遊戲涉及多代家族、地圖探索、族誌累積等長期狀態,存檔資料量會隨遊戲進行逐漸增長。

技術方案 ​

階段儲存方式容量上限說明
開發階段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 物件

版本遷移策略 ​

遊戲更新可能改變存檔結構。透過版本號管理遷移:

遷移原則 ​

  1. meta.version 記錄存檔格式版本
  2. 讀檔時比對當前遊戲版本與存檔版本
  3. 若版本不同,依序執行遷移函式
  4. 遷移只做「加欄位、設預設值」,不刪除舊資料

遷移範例 ​

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)將完整遊戲進度壓縮為一段可複製的文字字串。這段編碼:

  1. 開發用途:開發者複製編碼,隨時貼上還原到特定遊戲進度,方便測試和除錯
  2. 玩家用途:跨裝置轉移進度,不需要帳號系統
  3. 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-string

lz-string 特點:

  • 純 JavaScript,無外部依賴
  • 壓縮率高,適合 JSON 文字
  • compressToEncodedURIComponent 輸出 URL 安全字元,適合複製貼上
  • gzip 約 3 KB,不影響載入速度

VIP 銜接預留 ​

Phase 1 僅實作本機編碼(encode/decode),以下功能為後續 VIP 預留接口:

功能Phase 1VIP 版本
產生編碼本機壓縮同左
複製/貼上匯入本機操作同左
雲端備份—編碼上傳到伺服器,綁定帳號
分享存檔—產生短網址,他人可一鍵載入
排行榜—從編碼中擷取統計數據
存檔加密—防止竄改,驗證完整性

匯出與分享 ​

存檔匯出 ​

存檔匯出功能由「存檔編碼系統」取代。玩家透過複製存檔編碼即可備份或跨裝置轉移。

源血基因碼分享 ​

源血基因碼獨立於存檔系統,使用短碼格式方便分享:

基因碼格式:WL-XXXX-XXXX-XXXX(12 位英數字)
  • 玩家可複製短碼,透過任何通訊管道分享
  • 接收方在遊戲中輸入短碼即可導入
  • 短碼包含加密的基因資訊,不需要連線或伺服器

開發順序 ​

階段實作內容
MVP Phase 1localStorage 存讀、自動存檔、基礎 SaveData(time + family + resources + events)、存檔編碼(encode/decode + UI)
MVP Phase 2手動存檔欄位、存檔列表 UI、版本遷移框架
MVP Phase 3補充 map + estate + chronicle 區塊
正式版切換到 IndexedDB、源血基因碼短碼系統
VIP 版本存檔編碼雲端備份、分享短網址、排行榜