# ARCHITECTURE.md — 方寸仙門 技術架構

> 本文件是方寸仙門的**技術架構權威**。所有 code 決策、模組劃分、性能 budget、測試策略以本檔為準。設計層面對應 [GAME_DESIGN.md](GAME_DESIGN.md)。
>
> **版本**:對齊 v1.1.0 (M77 ship) + M79 idle pilot 規劃中。最後更新 2026-05-13。

---

## 一、一句話定位

**方寸仙門**是一款桌寵 idle 修仙遊戲。**Electron 透明置頂視窗 + React 主面板 + PixiJS 桌寵 canvas**,弟子住在你的桌面上,當你做別的事時自動修煉、突破、飛升。

**技術上的三條決策性質**:
1. **桌寵 + 主面板 = 雙視窗**(M17 落地):透明置頂的桌寵視窗 + 可拖移的 1024×720 主管理視窗,共享同一個 Zustand store 透過 IPC 同步
2. **數值用 break_eternity.js**:後期 multiplier 連乘可達 ×10^150+,native number 必爆,從 day 1 用 Decimal
3. **永不重置**:沒有 prestige reset/rebirth/歸零換倍率。任何「飛升」「升界」都是疊加新內容,舊內容繼續運轉。架構上這條鐵律影響 save schema、tick 設計、模組依賴方向

---

## 二、技術棧(已鎖定)

| 層 | 技術 | 版本 | 角色 |
|---|---|---|---|
| Runtime | Electron | 31 | 跨平台桌面 app + 透明視窗 |
| 打包 | electron-vite | 2 | dev HMR / build,管 main + preload + renderer |
| 語言 | TypeScript | 5.4 | strict,`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 全開 |
| UI | React | 18 | 主管理面板(`src/renderer/`) |
| Canvas | PixiJS | 8 | 桌寵動畫、粒子特效(突破光柱、飛升光點) |
| State | Zustand | 4.5 | sect store + 5 sub-stores |
| 數字 | break_eternity.js | 1.4 | `Decimal` 包裝為 `BigNumber`,所有資源量、倍率、產出走它 |
| i18n | i18next + react-i18next | 26 / 17 | zh-TW(主)/ zh-CN / en |
| 音效 | Howler.js | 2.2 | BGM + SFX(M20 procedural placeholder + M20.1 real swap) |
| RNG | seedrandom | 3 | seeded RNG,seed 進存檔保 replay |
| UUID | uuid | 10 | 弟子 ID 等 |
| 測試 | Vitest + fast-check | 2 / 3 | 單元 + property |
| E2E | Playwright(Electron mode) | (TBA) | smoke + screenshot |
| 打包發行 | electron-builder | 25 | mac dmg / win nsis / linux AppImage |
| 套件管理 | npm | 11 | package-lock.json 為準 |

> **重要**:設計文檔(`docs/RELEASE_NOTES_v1.1.0.md`)曾提到原始計畫用 pnpm + steamworks.js + better-sqlite3,實作後改為 **npm + localStorage(JSON 序列化)+ steamworks 延後**。Steam 整合在 M15 延後到 v1.0.1+ patch。

---

## 三、模組分層

```
src/
├── main/               Electron 主行程 — windows / IPC / tray / save manager
├── preload/            contextBridge — renderer 唯一對 main 的入口
├── renderer/           React UI + PixiJS canvas + 14 系統頁
│   ├── components/     共用元件(SettingsModal / QuestTracker / ConfirmDialog ...)
│   ├── views/          14 系統頁(Buildings / Disciples / DaoTree / Bonds / ...)
│   ├── progression/    階段性 UI(PerformanceModal / NumberDisplay)
│   ├── styles/         theme.css(M75-M78 美術線維護)
│   └── App.tsx / App.css
├── core/               純函數遊戲邏輯(無 store 副作用,可獨立測試)
│   ├── bignumber.ts    BigNumber 包裝
│   ├── production-loop.ts  生產 tick 主迴圈
│   ├── auto-*.ts       9 個 auto- 機制(assign / breakthrough / refine / research ...)
│   ├── disciple/       bonus-calc / recruitment / ascension / breakthrough / stacking-bonus
│   ├── building/       upgrade / 升級節點
│   ├── dao/            research / 230 節點 tech tree
│   ├── realm/          境界 / 升界
│   ├── balance/        simulator / player-profile / auto-playtest
│   ├── fortune/        天機 8 子系統
│   ├── seasonal/       季節事件
│   ├── tide/           12 时辰潮汐
│   ├── local-social/   弟子羈絆 + 6 NPC 訊息流
│   ├── mystic-realm/   秘境派遣
│   └── tribulation/    天劫戰鬥
├── data/               純資料常數(無 logic)
│   ├── buildings.ts    12 凡 + 8 仙 建築 spec
│   ├── recipes.ts      25+ 配方
│   ├── items.ts        85 物品(T0-T9)
│   ├── disciples-pool.ts STARTING_DISCIPLES + 抽卡池
│   ├── dao-techs.ts    230 道學節點
│   ├── achievements.ts 33 成就
│   ├── quests.ts       88 任務(M74)
│   └── ...
├── store/              Zustand state
│   ├── sect.ts         主 store(1700+ LOC,所有 reducer / 派發)
│   ├── sect-types.ts   SectState type
│   ├── event-log.ts    事件流(toast 來源)
│   ├── onboarding/     14 系統漸進解鎖 + FTUE 步驟
│   ├── quest/          88 任務 engine + trigger-detector
│   ├── quest-points/   任務點貨幣
│   ├── settings/       語言 / 音量 / 主題
│   └── tutorial/       25 步教學
├── save-system/        持久化
│   ├── migrations.ts   v0.x → v1.x → v1.1 → v1.2 遷移鏈
│   └── cloud-save-adapter.ts  Steam Cloud / iCloud / WebDAV stub
├── shared/             type 共用(SavePayload / SettingsTypes)
├── i18n/locales/       zh-TW.json / zh-CN.json / en.json
└── telemetry/          PostHog / Mixpanel stub(M58)
```

**模組依賴方向(鐵律)**:
```
data → core → store → renderer
              ↑
              save-system
              ↑
              shared (types only)

main ←→ preload(via contextBridge)→ renderer
main → save-system(directly,FS access)
```

`core/` **絕不依賴** `store/` 或 `renderer/`,所有遊戲邏輯都是純函數,輸入 state、輸出新 state。這讓 simulator 可以無 UI 跑 1000 小時模擬(`scripts/balance-sim.ts`)。

---

## 四、State 管理(Zustand)

### 主 store:`src/store/sect.ts` 的 `useSectStore`

唯一一個全域 game state,~1700 LOC。`SectState`(`sect-types.ts`)涵蓋:

```typescript
interface SectState {
  // 資源
  inventory: Record<ItemId, string>      // BigNumber 序列化為 string
  spirit_qi 等等

  // 弟子
  disciples: Record<DiscipleId, Disciple>
  recruitCounts: Record<Rank, number>    // 招募 5 階各買過幾次
  bonds: Record<BondId, number>          // 羈絆強度

  // 建築
  buildings: Record<BuildingId, BuildingState>
    // BuildingState = { level, slots: SlotAssignment[] }
    // SlotAssignment = { recipeId, discipleId, progress, recipeQueue }

  // 道學
  unlockedNodes: NodeId[]
  daoPoints: Record<DaoBranch, string>   // 6 流派各自累計
  researchedTechs: TechId[]
  activeEchoes: ActiveEcho[]             // 飛升殘影 啟用槽

  // 境界 / 飛升
  realm: 'mortal' | 'immortal' | 'divine' | 'true_divine' | 'dao'
  realmAscensionAt: number | null
  ascendedCount: number
  ascensionRecords: AscensionRecord[]
  ascensionPoints: number                // 仙緣點(M43)

  // 自動化 — 11 個 auto-* flag(全 false default)
  autoAssignEnabled, autoBreakthroughEnabled, autoResearchEnabled,
  autoRulesEnabled, autoByproductsEnabled, autoQualityEnabled,
  autoRecipeQueueEnabled, autoTeleportEnabled, autoRefineEnabled,
  autoDispatchEnabled, autoPriorityChainEnabled

  // M79 新增(規劃中)
  idlePilotEnabled: boolean              // master toggle, default true
  lastIdlePilotTickAt: number | null

  // 天機 / 季節 / 秘境 / NPC
  fortune: FortuneState
  seasonalState: SeasonalState
  mysticRealmDispatches: ...
  // ...

  // Meta
  rngSeed: string                        // 進存檔保 replay
  lastTickAt: number                     // wall clock
  ftueCompleted: boolean
  eventLog: EventLogEntry[]
  notificationMode: 'ambient' | 'silent' | 'dnd24h'
}
```

### Sub-stores(5 個)

| Store | 路徑 | 用途 |
|---|---|---|
| `useOnboardingStore` | `src/store/onboarding/` | 14 系統漸進解鎖 + FTUE 步驟 + system-unlock-rules |
| `useTutorialStore` | `src/store/tutorial/` | 25 步教學進度,emit completion event |
| `useQuestStore` | `src/store/quest/` | 88 任務 engine + trigger-detector |
| `useQuestPointsStore` | `src/store/quest-points/` | 任務點貨幣 + shop history |
| `useSettingsStore` | `src/store/settings/` | 語言 / 音量 / 主題 / 通知 |

Sub-stores 各自有獨立 persist key(localStorage),透過 event 跟主 store 溝通,**不直接讀寫 SectState**。

### 為什麼 Zustand 而不是 Redux

- Zustand subscribe by selector,自動 shallow compare → 配合 BigNumber 用 `.toString()` 字串比較避免 deep compare
- 沒有 reducer / dispatcher boilerplate,reducer 直接是 method
- 跟 React 18 concurrent mode 親兄弟,沒有過去 Redux 的 batching 痛點

---

## 五、Tick 引擎(production-loop)

### 主迴圈

```typescript
// useSectStore.tick(deltaSec) action
tick(deltaSec: number) {
  const state = get()
  const rng = createSeededRng(state.rngSeed)

  // 1. production-loop tick(主生產)
  const afterProduction = productionLoopTick(state, deltaSec, rng)

  // 2. M79 idle pilot tick(若 idlePilotEnabled)
  const afterIdlePilot = applyIdlePilotTick(afterProduction, rng, elapsedSec)

  // 3. autoRules / autoByproducts / autoQuality 等獨立 auto- 邏輯(若各自 flag on)
  const afterAutos = applyAutos(afterIdlePilot, rng)

  // 4. fortune / seasonal / tide tick
  const afterMeta = tickMetaSystems(afterAutos, deltaSec, rng)

  // 5. 提交
  set(afterMeta)
}
```

### `production-loop.ts` — 心臟

```typescript
export function tick(state: SectState, deltaSec: number, rng: SeededRng): SectState {
  for (const [buildingId, building] of Object.entries(state.buildings)) {
    const spec = BUILDING_SPECS[buildingId]
    if (!spec) continue

    // 整棟 stacking bonus(M68)— 按 building 而非 per slot
    const stacking = computeStackingBonus(building, state.disciples)

    building.slots = building.slots.map(slot =>
      tickSlot(slot, building, spec, deltaSec, state.inventory, state.disciples, stacking, rng)
    )
  }
  return newState
}

function tickSlot(slot, building, spec, deltaSec, inventory, disciples, stacking, rng): SlotAssignment {
  if (!slot.recipeId) return { ...slot }
  const recipe = RECIPES[slot.recipeId]
  const disciple = slot.discipleId ? disciples[slot.discipleId] : null

  // M62 coupling formula:建築 × 弟子 雙軸對等
  const rateMul = calcRateMultiplier(building, spec, disciple)
    .mul(stacking)              // M68 同派 stacking
    .mul(seasonalMultiplier)    // M44 季節 buff
    .mul(tideMultiplier)        // M12 12 时辰
    .toNumber()

  const safeMul = Math.max(0.01, Math.min(1e9, rateMul))  // 防爆 cap
  let progress = slot.progress + (deltaSec / recipe.durationSec) * safeMul

  while (progress >= 1) {
    if (!completeRecipe(inventory, recipe, rng)) { progress = 0.99; break }
    progress -= 1
  }

  return { recipeId: slot.recipeId, progress, discipleId: slot.discipleId ?? null }
}
```

### Tick 頻率

- **真 wall clock**:`requestAnimationFrame` → 邏輯 tick 固定 **100ms**(`deltaSec = 0.1`)
- **離線追算**:`offline.ts`,以 1 秒一 tick 補齊,**離線封頂 24 小時**(M23 設計,M71 校準)
- **simulator**:`scripts/balance-sim.ts` 用同樣 tick 邏輯,**no wall clock**,以 `(0.1 sec × 36000 = 1 hour) × 240` 在數秒內跑出 240 分鐘進度

### M62 公式(coupling 核心)

```text
rateMultiplier =
    buildingLevelMul                            // UPGRADE_RATE_MULTIPLIER^(level-1),M64 1.15→2 by M71
  × additiveBaseline                            // 1.0 + max(0, (avgAttr-20)/100) × 0.5(M62)
  × (1 + discipleMultBonus)                     // cultivation + category attr + spiritualRoot
  × affinityMatchCoef                           // 1.5 主派 / 1.2 副派 / 0.8 不匹配 / 1.0 無 daoBranch
  × realmMultiplier                             // 1.5^REALM_INDEX(M62 verdict v2,凡 1 → 飛升 11.39)
```

**重要設計約束**:
- 弟子缺席時 `additiveBaseline = 1.0`,建築獨立可跑(baseRate × levelMul × 1.0)
- `affinityMatchCoef = 1.0` 當建築無 daoBranch,避免懲罰所有基礎操作
- `realmMul` 用 1.5^n 而非 10^n / 2^n — 經 M62 兩輪 verdict 後固化(`docs/BALANCE_DESIGN.md` §3)
- `safeMul` clamp 到 [0.01, 1e9] 防運算溢出

---

## 六、生產製造管線

```
配方(Recipe spec)         弟子(Disciple)          建築(BuildingState)
        |                          |                          |
        ↓                          ↓                          ↓
    slot.recipeId         slot.discipleId             slot, building.level
        |__________________________|__________________________|
                                   ↓
                          calcRateMultiplier()
                                   ↓
                  rateMul × stackingBonus × seasonal × tide
                                   ↓
                          tickSlot:progress += deltaSec/durationSec × rateMul
                                   ↓
                       (progress >= 1) → completeRecipe
                                   ↓
                       inventory 變動(扣 input + 加 output)
                                   ↓
                          副產物(byproducts.ts,5% T4+)
                                   ↓
                          配方熟練 +1(proficiency.ts)
                                   ↓
                       (proficiency milestone) → 觸發 buff
```

### 配方鏈(T0-T9)

| Tier | 用途 | 範例 | durationSec |
|---|---|---|---|
| T0 | 採集 | spirit_herb, fan_iron, wood | 3-5s |
| T1 | 基礎鍛料 | rough_iron, jade_dust | 15s |
| T2 | 進階煉材 | qi_pill, jade_ingot | 30s |
| T3 | 法器/丹藥 | flying_sword_3, gathering_pill | 60s |
| T4 | 一品法器 | premium_sword, premium_pill | 300s |
| T5-T9 | 跨界至寶 | 仙界 / 神界 / 真神 / 道境 物品 | 900-30000s |

完整資料在 `src/data/recipes.ts` + `src/data/items.ts`。設計依據見 [GAME_DESIGN.md §5](GAME_DESIGN.md#五生產系統t0-t9--12-殿堂--配方鏈)。

---

## 七、Save / Load 系統

### 結構

- **存放**:`localStorage`,key `fangcun-sect`(主 state)+ 各 sub-store keys
- **格式**:JSON,BigNumber 透過 `Decimal.toJSON()` 序列化為 string
- **3 slot**:玩家可命名,各有縮圖(M23)
- **5 backup**:每 slot 自動輪轉備份,corrupt fallback
- **import / export**:JSON 文字輸出,size cap 防 OOM(M23.1)
- **path traversal 防護**:IPC slotId validation(M23.1)

### Migration chain

```
v0.x baseline
  → v0.6 secondaryDao migration(M62)
  → v0.7 道學節點重排(M67)
  → v0.8 stacking bonus 新欄位(M68)
  → v1.0 ship time 凍結
  → v1.1 (現行) 派別匹配 / breakthrough cost / cosmicMul
  → v1.2 (M79) idlePilotEnabled + lastIdlePilotTickAt
```

每個 migration 在 `src/save-system/migrations.ts` **idempotent** + **保留未知欄位**(轉發給下個 migration)。重跑同 save 結果完全一致。

### Cloud save stub(M55)

`cloud-save-adapter.ts` 預留 interface,目前只有本地 ack。實作待 Steam EA + Steam Cloud API。

### Anti-cheat 三層(M23 設計)

- L1:JSON base64 + custom header,擋 90% 玩家
- L2:HMAC-SHA256 簽章,key 衍生自 machine-id + Steam ID
- L3:AES-256-GCM 整檔加密
- **不硬幹 anti-cheat** — Electron asar 拆 5 分鐘破解,接受

---

## 八、IPC + 雙視窗架構

### 三個 BrowserWindow

| 視窗 | 大小 | 透明 | 置頂 | 用途 |
|---|---|---|---|---|
| **桌寵視窗** | 350×350 | ✅ | ✅(動態) | 主桌寵 PixiJS 渲染,點擊穿透 |
| **主管理視窗** | 1024×720 resizable | ❌ | ❌ | 14 系統頁、Settings、Quest 等 |
| (Future) tutorial overlay | 全螢幕透明 | ✅ | ✅(暫時) | M60 教學 spotlight |

### 點擊穿透邏輯

```typescript
// 桌寵預設「穿透模式」
petWindow.setIgnoreMouseEvents(true, { forward: true })

// 滑鼠進入 hitbox → forward 事件偵測 → 切「互動模式」
petWindow.setIgnoreMouseEvents(false)

// 離開 hitbox 0.5 秒後 → 切回穿透
```

**邊角 case 處理**(`src/main/window.ts`):
- Win11 + Intel GPU forward 事件可能 hang → 備援用 `screen.getCursorScreenPoint()` 每 50ms 輪詢
- 主視窗顯示時,桌寵 `setAlwaysOnTop(false)` 避免擋管理介面
- 主視窗隱藏時,桌寵 re-asserts alwaysOnTop

### IPC channel

`preload/index.ts` 透過 `contextBridge.exposeInMainWorld('api', ...)` 暴露:
- `save.*` — read/write/list/rename/import/export/cloud-status
- `window.*` — openMain / hideMain / dragMain
- `settings.*` — read/write
- `pet.*` — setSize / setPosition / show / hide

**Renderer 永遠用 `window.api.*` 而非 `ipcRenderer`**(`contextIsolation` + `nodeIntegration: false`)

---

## 九、大數字系統

### `BigNumber` 封裝

```typescript
import { Decimal } from 'break_eternity.js'

export type BigNumberInput = string | number | Decimal | BigNumber
export class BigNumber { ... }

export function bn(input: BigNumberInput): BigNumber
```

**所有資源量、倍率、產出**走 `BigNumber`。**計數器、UI 中間值** 仍是 native `number`,**禁止與 BigNumber 混用無轉換**(`src/core/bignumber.ts` 提供 helpers)。

### 三層數值體驗(M27)

詳細設計見 `.bridge/NUMBERS_DESIGN.md`。摘要:

- **Visceral**(秒):數字微跳 + tier 跨越慶祝 + 顏色梯度
- **Cognitive**(分):tooltip 預測 +X/sec、N 分回本、效率排序
- **Strategic**(時):多倍率源疊加、永不重置、prestige 節奏

### 中文單位(M27)

`format.ts` 把 `BigNumber.toString()` 轉成 「萬 / 億 / 兆 / 京 / 垓 / 秭 / 穰 / 溝 / 澗」。超過 10^20 改科學記號 `1.23×10^45`。i18n locale 切英文時改 `K / M / B / T / Q`。

### `Decimal` vs Zustand `===`

**重要陷阱**:Zustand subscribe 用 `===` 比較,`Decimal` 實例每次 tick 新建會誤判 dirty 觸發 re-render。

**解法**(Q3 in original tech Q&A):
```typescript
// UI subscribe 時取 .toString() 字串比較
const power = useSectStore(s => s.power.toString())

// 真要算的時候才 bn(power).mul(...)
```

存到 store 的數字以 `.toJSON()` string 形式存,渲染時不轉 Decimal,只在 calculation hot path 才 `bn(...)` 包回。

---

## 十、自動化系統(15 機制)

15 機制由 M8 (1-7) + M9 (8-15) 分批 ship,每個機制獨立 module + 獨立 flag:

| # | 機制 | 模組 | flag |
|---|---|---|---|
| 1 | Auto Assign | `src/core/auto-assign.ts` | autoAssignEnabled |
| 2 | Auto Breakthrough | `src/core/auto-breakthrough.ts` | autoBreakthroughEnabled |
| 3 | Auto Recipe Queue | `src/core/recipe-queue.ts` | autoRecipeQueueEnabled |
| 4 | Auto Rules(IF-THEN DSL) | `src/core/auto-rules.ts` | autoRulesEnabled |
| 5 | Auto Byproducts | `src/core/byproducts.ts` | autoByproductsEnabled |
| 6 | Auto Quality | `src/core/quality-extended.ts` | autoQualityEnabled |
| 7 | Auto Research | `src/core/auto-research.ts` | autoResearchEnabled |
| 8 | Auto Teleport | `src/core/teleport.ts` | autoTeleportEnabled |
| 9 | Auto Refine | `src/core/auto-refine.ts` | autoRefineEnabled |
| 10 | Auto Dispatch | `src/core/auto-dispatch.ts` | autoDispatchEnabled |
| 11 | Auto Priority Chain | `src/core/priority-chain.ts` | autoPriorityChainEnabled |
| 12 | Synergy Enhance | `src/core/synergy.ts` | (passive, no flag) |
| 13 | Proficiency Milestone | `src/core/proficiency.ts` | (passive) |
| 14 | Batch Upgrade | `src/core/batch-upgrade.ts` | (UI button) |
| 15 | Efficiency Breakdown | `src/core/efficiency-breakdown.ts` | (UI) |

**M79 Idle Pilot master toggle** 將統一覆寫 1-7 自動化(規劃中)— 詳見 `openspec/changes/M79-idle-pilot/`。

### Auto Rules DSL(M8 #4)

簡化 IF-THEN:
```
when inventory.spirit_herb >= 80
then dispatch to building.alchemy_hall recipe.qi_pill
```

執行器每 tick 掃描,**cycle detection**(Kahn 拓撲排序 DAG)拒絕儲存循環規則。Runtime 加保險:每 tick 規則執行次數 ≤ 規則數 × 3。

---

## 十一、Balance Simulator + Auto-Playtest

### Simulator(M61)

`src/core/balance/simulator.ts` 提供 headless tick:

```typescript
function runSimulation(profile: PlayerProfile, durationMinutes: number, opts): SimulationResult {
  let state = createSimulationInitialState(profile)
  const rng = createSeededRng(opts.seed)

  for (let sec = 0; sec < durationMinutes * 60; sec += 0.1) {
    state = productionLoopTick(state, 0.1, rng)
    state = profile.decisionMode === 'optimal'
      ? applyOptimalDecisions(state, profile, rng, sec)
      : applyIdleDecisions(state, profile, rng, sec)  // M79 fix
  }

  return {
    samples: [{ at: 60, spiritQi, discipleCount, ... }, ...],
    anchors: { firstBreakthrough: 0, firstAscension: 32, ... }
  }
}
```

### 4 個 player profile(M61)

| Profile | decisionMode | tutorialMode | 用途 |
|---|---|---|---|
| `idle-no-tutorial` | idle | skip | 純掛機體驗 |
| `idle-with-tutorial` | idle | play | 教學成本對掛機影響 |
| `optimal-no-tutorial` | optimal | skip | 最佳化路徑 |
| `optimal-with-tutorial` | optimal | play | 含教學的最佳化 |

### Auto-Playtest(M66)

`scripts/auto-playtest.ts` 跑 4 × 240 分鐘 → 輸出 `.bridge/balance/playtest-report-v1.1.md`,verify anchors:

```
firstBreakthrough ≤ 2 min
firstNewBuilding ≤ 3 min
fifthDisciple ≤ 15 min
firstAscension ≤ 5 hour
stage5 ≤ 5 hour
stage7 ≤ 24 hour
feedbackDensity30m ≥ 5 events/window (連續 30 個窗口)
spiritQiMinute60Cap ∈ [3x, 10x] baseline
```

### M71 Auto-patch

`scripts/auto-patch.ts` 讀 playtest report fail criteria,自動產生「M71 patch」改 constants(buildings.baseRate、breakthrough cost、UPGRADE_RATE_MULTIPLIER 等)。**只動 constant,不動 logic**。

### 已知漏洞(v1.1.0 release notes 自承)

`applyIdleDecisions()` 是真空殼 → idle profile 8/8 FAIL。**M79 idle pilot 修這條**(`openspec/changes/M79-idle-pilot/`)。

---

## 十二、效能 Budget(M25 baseline)

### 目標

| 指標 | 上限 |
|---|---|
| Idle FPS | 60(視窗 active),30(背景) |
| Tick 邏輯耗時 | < 16ms P95(預留 RAF 一框) |
| 主視窗冷啟動 | < 3 秒(probe in M25) |
| 24 小時 stress test | RAM 不漏 + tick rate 不退化 |
| 存檔大小 | < 500KB after 100 小時 |
| Pixi sprite 總數 | < 200(突破 / 飛升瞬間 < 500 with `ParticleContainer`) |

### Hot path optimization

`src/renderer/progression/performance-analysis.ts` + `M25 createHotPathCache()`:
- 3 個 hot path(uiTotalQiPerSec / topProducerList / nextUpgradePreview)做 memo
- 任何 SectState change → invalidate 對應 cache
- React.memo + useMemo + Zustand selectorscw 三重防

### MutationObserver debounce

M21 polish:Settings 寫盤 debounce 500ms,避免每 tick 都寫 localStorage。

---

## 十三、測試策略

### 4 種層級

| 層級 | 工具 | 範圍 | coverage 目標 |
|---|---|---|---|
| Unit | Vitest | `src/core/**` + `src/data/**` + `src/store/**` | **lines 80%,functions 80%,branches 70%**(`vitest.config.ts` 強制) |
| Property | fast-check | RNG 分布、Decimal 序列化、formula invariants | 關鍵 invariant 4-6 個 |
| Integration | Vitest + JSDOM/happy-dom | renderer components(SettingsModal, QuestTracker ...) | UI 不強求 |
| E2E smoke | Playwright Electron | 啟動 → save → reload → quit | 0 crash |

### 測試檔案 layout

```
tests/
├── core/
│   ├── auto-idle-pilot.test.ts        (M79 新增 ≥ 12 case)
│   ├── bonus-calc.test.ts             (M62 ≥ 15 case)
│   ├── stacking-bonus.test.ts         (M68 ≥ 6 case)
│   └── ...
├── store/
│   ├── sect.test.ts
│   ├── quest/                         (M74 ≥ 20)
│   └── ...
├── renderer/components/
│   ├── QuestTracker.test.tsx          (需 happy-dom env)
│   ├── SettingsModal-idle-pilot.test.tsx  (M79 新增)
│   └── ...
├── balance/
│   ├── simulator.test.ts
│   ├── auto-playtest.test.ts
│   ├── idle-profile.test.ts           (M79 新增 ≥ 4)
│   └── ...
└── save-system/
    ├── migrations.test.ts
    └── idle-pilot-migration.test.ts   (M79 新增 ≥ 3)
```

### 已知測試環境債

5 個 renderer test 用 `@vitest-environment happy-dom` pragma,但 `happy-dom` 不在 `package.json` devDeps(只在 lockfile 標 `optional: true`)。後果:`npm test` 報「178 files passed」但有 5 個 unhandled error,**這 5 個套件實際沒跑**。M79 順手補裝。

### Headless 模擬

```bash
npm run balance-sim -- --profile=optimal-no-tutorial --duration=60 --output=/tmp/test.csv
npm run auto-playtest  # 4 profile × 240min,完整 report
```

---

## 十四、Build 與部署

### Scripts(`package.json`)

```bash
npm run dev          # electron-vite dev,HMR renderer + 重啟 main
npm run build        # vite build + tsc main
npm run typecheck    # tsc --noEmit
npm run lint         # eslint --ext .ts,.tsx src
npm run format       # prettier --write src
npm run test         # vitest run
npm run test:cov     # vitest run --coverage
npm run balance-sim  # tsx scripts/balance-sim.ts
npm run auto-playtest # tsx scripts/auto-playtest.ts
npm run build:mac    # electron-builder --mac dmg
npm run build:win    # electron-builder --win nsis
npm run build:internal # bash scripts/build-internal.sh
```

### electron-builder 設定

```yaml
appId: com.fangcun.ximen
productName: 方寸仙門
mac:
  category: public.app-category.games
  target: dmg
  icon: resources/icon.icns
  identity: null     # 暫不簽 macOS notarization
win:
  target: nsis
linux:
  target: AppImage
```

### CI

(待補)現有計畫:GitHub Actions on push / PR,跑 `lint + typecheck + test + build`。Steam pipe 上傳留給 manual。

---

## 十五、開發流程(三層分工)

### 角色鐵律(D-001)

| 角色 | 職責 | 不得做 |
|---|---|---|
| **使用者** | 產品設計、最終驗收、決策仲裁 | — |
| **Claude(Orchestrator)** | 寫 OpenSpec 四件組、寫 `/goal`、跑審計、維護 PROGRESS / REVIEW_QUEUE / CONVERSATION_DECISIONS | **不直接寫業務代碼** |
| **Codex(Executor)** | 嚴格依 spec + `/goal` 寫代碼、跑測試、卡住時 escalate | **不做架構自由發揮、不偏離 spec、不省略測試** |

### OpenSpec 四件組(每 milestone)

```
openspec/changes/M<n>-<name>/
├── proposal.md     為什麼 / 改什麼 / 商業價值 / 範圍 / 不動
├── design.md       技術方案 + Step 1-N 程式碼骨架 + MUST NOT
├── specs/<area>.md GIVEN/WHEN/THEN + SHALL/MUST NOT
└── tasks.md        Done When 1:1 對應
```

**M70+ 趨勢**:openspec 已簡化,有的只剩 `proposal.md + tasks.md + audit.md`。當前對 architecture-impact milestone(像 M62/M79)仍寫全套。

### Codex prompt 模板(`.bridge/prompt-template.md` §A-§F)

每 `.bridge/prompts/M<n>-exec.txt` **必含**:
- §A Header(read list)
- §B Risk Escalation 觸發表 + Echo Rule
- §C Token 預算 + 回報義務
- < milestone-specific tasks >
- §D Escalation Channels
- §E Stop if 通用 + milestone-specific
- §F 完工 stdout sentinel

### 雙向 escalation channel

| Channel | 用途 | 頻率 |
|---|---|---|
| A — `REVIEW_QUEUE.md` | async,預設 | 0-N 次 / goal |
| B — `ask-claude.sh` | sync headless,純查詢 | 0-3 次 / goal |
| C — `wake-orchestrator.sh` | 戳進 live chat,使用者也看到 | 上限 1 次 / goal |

每 `/goal` escalate 上限 3 次,第 4 次 = `BLOCKED: spec-quality`。

### 完工三層 sentinel

1. Codex stdout 最後三行:
   - `M<n>_DONE_AWAITING_VERIFICATION`
   - comparison summary
   - 執行 `wake-orchestrator.sh`
2. Orchestrator 跑 `verify.sh` + 寫 `audit.md`
3. PASS 則 `git tag M<n>-done`(慣例 2026-05-12 後歇,改用 commit hash)

### 美術線並行(2026-05-13 起)

當前 **兩條 Codex 並行**:
- **美術線**:M75-M78 ink-wash theme + light removal(已 ship `93e3dad`),動 `.css` / `theme.css` / token
- **邏輯線**:M79 idle pilot(等啟動),動 `.ts` / `.tsx` / `.json` / `core/`

**並行隔離鐵律**(`AGENTS.md` §11):
- 美術線 milestone 只動 `.css`,**不動 `.ts` / `.tsx` 結構**
- 邏輯線 milestone 不動任何 `.css`,**SettingsModal 等加 row 用既有 className**

**開新 milestone 編號前**:Orchestrator 必先 `git fetch origin` 看美術線跑到哪個 M,避免再撞號(2026-05-13 已撞過一次,M78-idle-pilot 改名 M79)。

---

## 十六、跨機器與多帳號

### 舊機器 vs 新機器

- **舊機器**(`/Users/fengbot/Desktop/方寸仙門/`):2026-05-08 ~ 05-12 主開發
- **新機器**(`/Users/zuofengwang/Desktop/方寸仙門/fangcun-ximen/`):2026-05-13 起新環境
- Codex CLI 0.129 在舊機器,新機器待裝
- `.bridge/prompts/M*-exec.txt` 早期(M1-M14)硬寫舊路徑,M78+ 改相對路徑(`prompt-template.md` 已更新)

### 路徑兼容

- 跑舊 milestone audit(極少需要)時臨時改 prompt 路徑
- 全域 `~/.codex/AGENTS.md` 仍在舊機器,新機器若要跑 Codex 需 setup

### 帳號

- GitHub:`Fengflay`(個人 private repo)
- Steam Partner(規劃):TBA
- gh CLI(新機器已裝 v2.92,OAuth device flow 已登入)
- git credentials:`gh auth setup-git`(2026-05-13 設好)

---

## 十七、維護鐵律與「不可動」

### 全局鐵律(`docs/BALANCE_DESIGN.md` §10)

1. **不引入 prestige / reset / rebirth** — 飛升與升界只疊加新內容,舊內容繼續運轉
2. **不讓無弟子建築產能歸零** — `additiveBaseline = 1.0` 保底
3. **不讓弱弟子壓 baseline 到 0** — `additiveBaseline ≥ 1.0`(屬性 < 20 仍 1.0)
4. **不把 controlled check 當標準 profile 結論** — controlled 是 micro,profile 是 macro
5. **不因單一 milestone 下界偏差就在禁止範圍外改核心公式** — 累積調整交給 M66-auto + M71

### 不可動檔案(`AGENTS.md` §11)

- `package-lock.json`(除非 milestone 本身是改 deps)
- `.env*`
- `docs/BALANCE_DESIGN.md` 與 `docs/RELEASE_NOTES_v*.md`(權威 + 已 ship 紀錄,修訂另開新檔)
- `CODEX指揮編排計畫_v*.md`
- `openspec/changes/*/proposal.md`(spec 由 Orchestrator 寫)
- M62 公式 / M64 buildings.ts / M65 突破 cost / M67 dao-techs / M68 stacking 等(各 milestone scope 內才動)
- 已 tag 完成的測試
- **美術並行線 M75-M77 涵蓋的所有 `.css` 檔**(2026-05-13 加)

### 可調 hook(`docs/BALANCE_DESIGN.md` §10)

| 類別 | 參數 | 注意 |
|---|---|---|
| 資料表倍率 | M64 baseRate / buildCost / buildTime / 仙界 ×1.2 / M69 breadth-depth-main rate | 保留分類意義 |
| 派別深度 | M62 1.5/1.2/0.8 / M67 affinity bonus / M68 stacking | 避免不匹配變不可用 |
| Progression cost | M65 breakthrough exp / world ascension req / cosmicMul / seclusion rate | 改前先跑 simulator |
| 供給端 | M63 招募保底 / 屬性區間 / 靈根分布 | 用 sample 分布驗證 |

---

## 十八、附:常見技術問題的解法(Q1-Q10)

### Q1. Win11 透明視窗點擊穿透 edge case

**問題**:`setIgnoreMouseEvents(true, { forward: true })` 在 Win11 + 部分 Intel GPU forward 事件延遲或掉。
**解法**:備援用 `screen.getCursorScreenPoint()` 每 50ms 輪詢,主動切 `setIgnoreMouseEvents`(CPU < 0.5%)。

### Q2. 開發機沒 Steam 怎麼測 Steam API

開發機裝 Steam client(免費)登入自己帳號,`steam_appid.txt` 寫 480(Spacewar)。
沒 Steam 環境:`initSteam()` try/catch 退化為 offline mode。
Feature flag `STEAM_DISABLED=1` 跑 fully offline。

### Q3. `break_eternity.js` 跟 React/Zustand 整合

```typescript
type DecimalValue = { d: Decimal; s: string }
function dec(n: Decimal): DecimalValue { return { d: n, s: n.toString() } }

// Zustand selector 比較 s 字串
const power = useStore((s) => s.power.s)

// 真要算的時候才取 .d
```

### Q4. PixiJS 透明視窗效能優化

- `Application` 設 `backgroundAlpha: 0, antialias: false`
- 桌寵 idle 動畫 Ticker `minFPS: 30, maxFPS: 60`,沒互動降到 30
- 大量粒子用 `ParticleContainer`
- 沒桌寵動作時 `app.ticker.stop()`

### Q5. 多螢幕 DPI

```typescript
const { scaleFactor } = screen.getPrimaryDisplay()
const dpr = window.devicePixelRatio
pixiApp.renderer.resolution = dpr
```

存檔存「邏輯座標 + 顯示器 ID」。監聽 `display-added / display-removed / display-metrics-changed`。

### Q6. 存檔加密 / 防修改

見 §七 Anti-cheat 三層。**接受 Electron 5 分鐘破解現實**。

### Q7. 後期千萬個物品 UI 渲染

- 絕不用 `.map()` 渲染整 list
- React virtual scroll:`react-window` 或 `@tanstack/react-virtual`
- > 10k 物品自動切 summary mode
- 大量 Decimal 算離線收益用 Web Worker

### Q8. 自動化規則 cycle detection

- 規則建模為 DAG
- 建圖後 Kahn 拓撲排序,有 cycle 就拒儲存
- Runtime 每 tick 規則執行次數 ≤ N(N=規則數 × 3)
- UI 給玩家看「規則依賴圖」

### Q9. Steam Big Picture / 全螢幕應用

偵測 `isSteamInBigPictureMode()` 或讀全螢幕 foreground process。
全螢幕 active 時 → 「隱形 + 暫停 tick + 不消耗 CPU」。
設定加開關「全螢幕遊戲時自動隱藏」(預設開)。

### Q10. 螢幕生態系讀進程合規

- ✅ 讀:前景視窗 process name + window title prefix
- ❌ 不讀:視窗內容、剪貼簿、keystroke
- ❌ 不上傳:所有偵測本機處理
- 首次啟動 modal 明確告知 + 開關

---

## 十九、目錄:相關文件

- [GAME_DESIGN.md](GAME_DESIGN.md) — 玩法設計權威(對應 player-facing 體驗)
- [RELEASE_NOTES_v1.1.0.md](RELEASE_NOTES_v1.1.0.md) — v1.1.0 balance overhaul 落地實況
- [BALANCE_DESIGN.md](BALANCE_DESIGN.md) — v1.1 平衡設計 10 章節 + decisions log
- [MANUAL_TEST_PLAYBOOK.md](MANUAL_TEST_PLAYBOOK.md) — 人工測試 SOP
- [../.bridge/SYSTEM_DEPENDENCY_MAP.md](../.bridge/SYSTEM_DEPENDENCY_MAP.md) — 14 系統 directed graph
- [../.bridge/NUMBERS_DESIGN.md](../.bridge/NUMBERS_DESIGN.md) — 數值體驗總綱
- [../.bridge/FLOW_STATE_DESIGN.md](../.bridge/FLOW_STATE_DESIGN.md) — 心流 8 條件
- [../AGENTS.md](../AGENTS.md) — Codex 規則 + SoT 階層
- [../CLAUDE.md](../CLAUDE.md) — Orchestrator 守則
- [../PROGRESS.md](../PROGRESS.md) — milestone 進度
- [../openspec/changes/](../openspec/changes/) — M1-M79 全部 spec

**歷史 / 參考層**(衝突時以 SoT 為準,但作為設計演進與後續擴展參考保留):
- [../主框架設計_v0.1.md](../主框架設計_v0.1.md) ~ [v0.3.1.md](../主框架設計_v0.3.1.md) — 4 版設計迭代
- [../細節/01_物品配方表.md](../細節/01_物品配方表.md) ~ [06_自動化15機制.md](../細節/06_自動化15機制.md) — 6 系統初版 spec
- [../設計文檔說明.md](../設計文檔說明.md) — 導讀

---

**鐵律總結**:
- 模組依賴方向 `data → core → store → renderer`,`core` 絕不反向依賴
- 所有數字走 `BigNumber`,UI 比較用 `.toString()`
- 永不重置 — 飛升升界只疊加
- 三層分工:使用者決策、Claude 編排、Codex 執行
- 美術線 / 邏輯線並行,`.css` vs `.ts/.tsx` 物理隔離
