template/.planning/codebase/ARCHITECTURE.md

251 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- refreshed: 2026-09-07 -->
# Architecture
**Analysis Date:** 2026-09-07
## System Overview
This is **`mota-ts`** — an HTML5 "魔塔" (Mota / Tower of the Sorcerer) game engine plus a sample game, organized as a **pnpm monorepo**. The core engine lives in `packages/` (scoped `@motajs/*`), user-facing game code lives in `packages-user/` (scoped `@user/*`), and the game entry point lives in `src/`. Legacy "mota-js" sample content (uncompiled game data and the old runtime) lives in `public/`.
```text
┌──────────────────────────────────────────────────────────────────────────┐
│ Game Entry (src/) │
│ src/main.ts (render/client entry) src/data.ts (data entry, replay) │
│ src/App.vue (Vue UI root) src/content/ (JSONC game data) │
└───────────────────────────────┬──────────────────────────────────────────┘
│ depends on (@user/*)
┌──────────────────────────────────────────────────────────────────────────┐
│ User Layer (packages-user/ → @user/*) │
│ entry-client / entry-data ← composition root + module registry │
│ client-base (系统层) client-modules (实现层) [render end] │
│ data-common(L0) data-base(L1) data-system(L2) data-state(L3) [data end] │
│ data-fallback / legacy-plugin-client / legacy-plugin-data │
└───────────────────────────────┬──────────────────────────────────────────┘
│ depends on (@motajs/*)
┌──────────────────────────────────────────────────────────────────────────┐
│ Core Engine (packages/ → @motajs/*) │
│ common legacy-common types client client-base system │
│ render render-vue animate audio loader legacy-* │
└───────────────────────────────┬──────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ Legacy runtime & assets (public/) + Build tooling (script/, vite) │
│ public/main.js (legacy mota-js core), public/project/*, public/libs/* │
└──────────────────────────────────────────────────────────────────────────┘
```
**Dependency direction is strictly one-way:** `src``packages-user``packages`. `packages` and `packages-user` are independently buildable as libraries; `src` is the game entry code. This is stated in `dev.md`.
## Component Responsibilities
| Component (package) | Scope | Responsibility | Key file |
|---------------------|-------|----------------|----------|
| `src` (`@user/main`) | Game entry | Composes the game: `createGame()` + mounts Vue `App` | `src/main.ts` |
| `@user/entry-client` | Composition | Registers client-side modules into `Mota`, then runs `create()` on each | `packages-user/entry-client/src/create.ts` |
| `@user/entry-data` | Composition | Defines the `Mota` module registry (`IMota`/`MotaSystem`) and registers data-side modules | `packages-user/entry-data/src/mota.ts` |
| `@user/client-base` | Render system layer | Render-side core: asset loading + material/autotile management | `packages-user/client-base/src/index.ts` |
| `@user/client-modules` | Render impl layer | Concrete renderer, UI, weather, action (hotkey/move) | `packages-user/client-modules/src/index.ts` |
| `@user/data-common` | Data Layer 0 | Common/utility interfaces (face, mover), event, replay, save, store | `packages-user/data-common/src/index.ts` |
| `@user/data-base` | Data Layer 1 | Saveable game data: maps, hero, enemy, flag, loading/hook | `packages-user/data-base/src/index.ts` |
| `@user/data-system` | Data Layer 2 | Game logic: combat/damage + trigger registry/collector | `packages-user/data-system/src/index.ts` |
| `@user/data-state` | Data Layer 3 | `CoreState` singleton that wires L0L3 together | `packages-user/data-state/src/core.ts` |
| `@user/data-fallback` | Compatibility | Patches legacy globals onto new state (`patchAll`) | `packages-user/data-fallback/src/index.ts` |
| `@motajs/common` | Core utility | `utils`, `logger`, `hook`/`Hookable`, `dirtyTracker` | `packages/common/src/index.ts` |
| `@motajs/legacy-common` | Legacy util | `Patch` system, legacy `EventEmitter`, utils | `packages/legacy-common/src/index.ts` |
| `@motajs/system` | Input + UI sys | `Hotkey`/keyboard (`action`) + `UIController`/`GameUI` (`ui`) | `packages/system/src/index.ts` |
| `@motajs/render` | Graphics engine | `MotaRenderer` WebGL/Canvas render tree, assets, style | `packages/render/src/core/render.ts` |
| `@motajs/render-vue` | Vue renderer | Custom Vue `createRenderer` over `IRenderItem` | `packages/render-vue/src/renderer.ts` |
| `@motajs/animate` | Animation | `RafExcitation`, `ExcitationDivider`, transitions | `packages/animate/src/index.ts` |
| `@motajs/audio` | Audio | `MotaAudioContext`, BGM/effect/sound, decoders | `packages/audio/src/index.ts` |
| `@motajs/loader` | Loader | `LoadTask`, `LoadProgressTotal`, stream | `packages/loader/src/index.ts` |
## Pattern Overview
**Overall:** Layered monorepo + **inversion-of-control module registry** + **event-driven lifecycle** + strict **dual-end separation** (data end vs render end).
**Key Characteristics:**
- **Module registry (`Mota`) instead of static imports across layers.** `window.Mota.register(key, ns)` / `Mota.require(key)` is a runtime DI container that lets the data end reference render-side code lazily without creating bundle-level circular imports (see `packages-user/entry-data/src/mota.ts`).
- **No side effects at module scope.** Packages only declare functions/classes/constants; initialization happens via `createXxx()` functions bubbled up to the composition root (`dev.md` module principles).
- **Event-driven loading.** `loading` (a `GameLoading` `EventEmitter`) and `hook` (a `GameEvent` `EventEmitter`) in `packages-user/data-base/src/game.ts` coordinate startup and gameplay events.
- **Dual-end separation.** The **data end** (`src/data.ts`) runs standalone in Node for replay verification and contains zero rendering; the **render end** (`src/main.ts`) only sends input and never computes logic.
- **Legacy bridge via `Patch`.** `@motajs/legacy-common`'s `Patch` class monkey-patches the legacy `main.js` globals (`core`, `main`, `data`, `enemys`, …) so new TypeScript code coexists with the uncompiled mota-js sample.
## Layers
**Data end (three layers, per `dev.md` and `CoreState`):**
- **Layer 0 — 公共层 (`@user/data-common`):**
- Purpose: dependency-free common interfaces/utilities (`IDataCommon`); no saveable state.
- Location: `packages-user/data-common/src/`
- Contains: `common/` (face, faceManager, indexer, mover), `event/`, `replay/` (`ReplaySystem`), `save/` (`SaveSystem`, Dexie), `store/` (tile/item/map/event stores).
- Depends on: `@motajs/common`, `@motajs/loader`, `@motajs/types` only.
- Used by: Layers 13 and the render end.
- **Layer 1 — 数据层 (`@user/data-base`):**
- Purpose: all saveable game data and its interfaces (`IDataBase`).
- Location: `packages-user/data-base/src/`
- Contains: `game.ts` (`loading`/`hook`/`gameListener`), `map/` (`MapState`, `MapLayer`, `Tile`), `hero/`, `enemy/`, `flag/`, `load/` (`MotaDataLoader`).
- Depends on: `@user/data-common`, `@motajs/common`, `@motajs/types`, `@motajs/loader`.
- Used by: Layer 2, Layer 3, and render modules.
- **Layer 2 — 系统层 (`@user/data-system`):**
- Purpose: game-logic actions that mutate Layer 1 state but are themselves not saved (`IDataSystem`).
- Location: `packages-user/data-system/src/`
- Contains: `combat/` (`DamageSystem`, `EnemyContext`, `MapDamage`), `trigger/` (`TriggerRegistry`, `TriggerCollector`).
- Depends on: `@user/data-base`, `@motajs/common`.
- Used by: Layer 3.
- **Layer 3 — 顶层模块 (`@user/data-state`):**
- Purpose: composition/initialization only; exposes `CoreState` (singleton `state`) to the render end.
- Location: `packages-user/data-state/src/`
- Contains: `core.ts` (`CoreState` wires L0L3), `enemy/` (calculators/comparers/specials), `hero/`, `legacy/`, `content/`, `ins.ts` (`state = new CoreState()`).
- Depends on: L0L2 plus `lodash-es`, `@motajs/loader`.
- Used by: `@user/entry-data` (and via `Mota.require('@user/data-state')`, the render end).
**Render end (two layers):**
- **系统层 (`@user/client-base`):** render-side core — asset loading (`load/`) and material/autotile managers (`material/`). Entry `create()` in `packages-user/client-base/src/index.ts` calls `createMaterial()`.
- **实现层 (`@user/client-modules`):** depends on the system layer to implement actual rendering and interaction — `render/` (map renderer, UI panels, weather, fx), `action/` (hotkey, move), `fallback/`.
## Data Flow
### Startup / Composition Path
1. **Render entry** `src/main.ts` calls `createGame()` (from `@user/entry-client`), then `createApp(App).mount('#root')`, then legacy `main.init('play')` + `main.listen()`.
2. `createGame()` (`packages-user/entry-client/src/index.ts`) calls `createData()` then `create()`.
3. `createData()` (`packages-user/entry-data/src/index.ts`) calls `createMota()` (installs `window.Mota`), `patchAll(state)`, and `create()`.
4. `create()` (`entry-data/src/create.ts`) registers data-side namespaces into `Mota`, then emits `loading.emit('dataRegistered')`.
5. Client `create()` (`entry-client/src/create.ts`) registers client namespaces into `Mota`, emits `loading.emit('clientRegistered')`.
6. `GameLoading.checkRegistered()` (`packages-user/data-base/src/game.ts`) emits `registered` once **both** ends are registered.
7. On `registered`, `createModule()` runs `UserClientBase.create()`, `ClientModules.create()`, `LegacyUI.create()`; then async-imports Ant Design CSS, sets `main.renderLoaded`, emits `hook.emit('renderLoaded')`.
### Gameplay Loop
1. Input (keyboard/mouse) → `@motajs/system` `Hotkey` (`gameKey`) dispatches (see `packages/system/src/action/hotkey.ts`, DOM listeners at bottom).
2. Action handlers (e.g. `@user/client-modules/src/action/move.ts`) send intents to the data end.
3. Data end (`@user/data-system` combat/trigger + `@user/data-state` `CoreState`) mutates Layer 1 state (`maps`, `hero`, `enemyManager`, `flags`).
4. `hook` events (e.g. `moveOneStep`, `afterBattle`, `setBlock`) notify render modules.
5. Render end reads state reactively and re-renders via the WebGL `MotaRenderer` / custom Vue renderer.
**State Management:**
- Single source of truth is the data-end `CoreState` (`packages-user/data-state/src/core.ts`), exposed as singleton `state` (`ins.ts`). It holds saveable stores (`tileStore`, `itemStore`, `mapStore`, `maps`, `hero`, `enemyManager`, `flags`) plus execution objects (`enemyContext`, `triggerRegistry`, `triggerCollector`).
- Persistence via `SaveSystem` (`packages-user/data-common/src/save/system.ts`) over **Dexie** (IndexedDB), with undo/redo stacks and compression levels.
- Render state is derived/passive — the render end never pushes updates to the data end (arch constraint #17 in `.agents/code.md`).
## Key Abstractions
**`Mota` module registry:**
- Purpose: runtime DI container bridging the data end and render end without static import cycles.
- Interface: `IMota` with `require(key)` / `register(key, data)`, plus `r(fn)` / `rf(fn)` helpers (see `packages-user/entry-data/src/mota.ts`).
- Pattern: `Mota.register('@user/data-state', DataState)``Mota.require('@user/data-state')`.
- **`r()` / `rf()` are critical**: they wrap code that must run only in the render process and never during replay verification (`main.replayChecking`). Use `rf` to wrap a function, `r` to run a block.
**`CoreState` (data-end singleton):**
- Purpose: top-level object that wires Layer 03 and is the single data-end state.
- Files: `packages-user/data-state/src/core.ts` (class), `ins.ts` (`state` singleton).
- Pattern: constructor initializes `#region L0``L1``L2``L3`, registering saveable content (`addSaveableContent('@system/hero', this.hero)`, …).
**`loading` / `hook` event emitters:**
- Purpose: startup coordination (`GameLoading` in `game.ts`) and gameplay lifecycle (`GameEvent` in `game.ts`).
- Pattern: typed `EventEmitter` from `eventemitter3`; events declared as interface maps (`GameLoadEvent`, `GameEvent`, `ListenerEvent`).
**`Patch` (legacy bridge):**
- Purpose: monkey-patch legacy mota-js globals (`core`, `main`, `data`, `enemys`, `events`, `icons`, `items`, `loader`, `maps`, `ui`, `utils`, …) via `PatchClass` enum.
- Files: `packages/legacy-common/src/patch.ts`, applied in `packages-user/entry-data/src/index.ts` (`Patch.patchAll()`) and `packages-user/data-fallback/src/index.ts` (`patchAll`).
**`MotaRenderer` + custom Vue renderer:**
- Purpose: WebGL/Canvas rendering tree, plus a Vue `createRenderer` that renders Vue VNodes onto `IRenderItem` (so Vue reactivity drives the game canvas).
- Files: `packages/render/src/core/render.ts` (renderer), `packages/render-vue/src/renderer.ts` (Vue adapter), `packages-user/client-modules/src/render/renderer.ts` (instantiation, `mainRenderer`, `createApp`).
**`SaveSystem` / `ReplaySystem`:**
- Purpose: persistence (Dexie, undo/redo, compression) and replay verification (command recording + sandbox for deterministic replay in Node).
- Files: `packages-user/data-common/src/save/system.ts`, `packages-user/data-common/src/replay/system.ts`.
## Entry Points
**Render/client entry:**
- Location: `src/main.ts`
- Triggers: browser page load (`index.html` loads `main.js` then `/src/main.ts` as module).
- Responsibilities: `createGame()`, mount Vue `App`, start legacy `main.init('play')` / `main.listen()`.
**Data entry (replay verification):**
- Location: `src/data.ts`
- Triggers: `pnpm build:game` builds it separately via `script/build-game.ts` (`buildData`), run in Node.
- Responsibilities: `createData()` only — no rendering, no DOM.
**HTML entry:**
- Location: `index.html`
- Responsibilities: defines `#render-main` canvas, `#root` Vue mount, legacy third-party scripts, and legacy `main.js`.
**Editor/dev servers:**
- Location: `script/dev.ts`
- Responsibilities: Vite dev server (game), Express static/file API server (editor at `/editor.html`), WebSocket hot reload. Proxies `/readFile`, `/writeFile`, etc.
**Build pipeline:**
- Location: `script/build-game.ts` (game zip), `script/build-lib.ts`, `script/build-packages.ts`, `script/declare.ts` (type generation), `script/pack-template.ts`.
## Architectural Constraints
- **One-way dependency:** `src``packages-user``packages`. Never invert.
- **No circular imports:** enforced by convention (`dev.md`) and `pnpm check:circular` (madge on `src/main.ts`). If a cycle is tempting, use the `Mota` registry or refactor the interface design.
- **No module side effects:** packages must only export declarations; initialize via `createXxx()` functions.
- **No `import type`:** use regular imports (only very exceptional cases allowed) — `dev.md` module principles.
- **Render end is passive:** it never pushes updates to the data end; it only reacts via hooks (`.agents/code.md` rule #17).
- **Threading / process model:** the render end is single-threaded browser JS; the data end is a separate bundle designed to run standalone in Node (for replay verification). No web workers used in the data path.
- **Global state:** the legacy mota-js runtime maintains globals `core`, `main`, and hashed data globals (`data_a1e2fb4a…`, `enemys_fcae963b…`, `icons_4665ee12…`). The new engine adds `window.Mota` (`IMota`) and `state` (`CoreState`). These globals are intentional bridge points, not free-for-all state.
## Anti-Patterns
### Putting render code in the data end
**What happens:** Adding DOM/rendering calls directly on data-end objects (e.g. inside `CoreState` or data-system logic).
**Why it's wrong:** The data end runs in Node during replay verification and has no DOM; such code breaks replay determinism and will error. This is explicitly documented in `packages-user/data-state/src/ins.ts`.
**Do this instead:** Wrap render-only effects with `Mota.r(() => { ... })` / `rf(...)` (see `packages-user/entry-data/src/mota.ts`), or route through `hook` events and let the render end subscribe.
### Creating a module with top-level side effects
**What happens:** A package file runs initialization code at module scope (e.g. instantiating a singleton and wiring it immediately).
**Why it's wrong:** Breaks the "no side effects" principle (`dev.md`), makes import order load-bearing, and risks duplicate/incorrect initialization across the client/data bundles.
**Do this instead:** Export a `createXxx()` function and call it from the package `index.ts`, bubbled up to `entry-client`/`entry-data`.
### Referencing classes instead of interfaces as member types
**What happens:** Declaring a member as `map: GameMap` instead of `map: IGameMap`.
**Why it's wrong:** Violates `dev.md` type rules and `.agents/code.md` rule #16; couples consumers to concrete implementations and breaks the layered abstraction.
**Do this instead:** Declare the interface (e.g. `IGameMap`, `IEnemyManager`) and type members with it.
### Using `as` casts / silent error handling
**What happens:** Type assertions (`as`, `as unknown as X`) or swallowing errors with `return`.
**Why it's wrong:** `.agents/code.md` forbids `as` and requires errors to be reported through `logger` with a meaningful code.
**Do this instead:** Use `logger.error(code, ...)` / `logger.warn(code, ...)` (from `@motajs/common`) with a non-zero, non-reused code; avoid assertions.
## Error Handling
**Strategy:** Centralized `logger` interface from `@motajs/common` (`packages/common/src/logger.ts`). Errors/warnings are reported with numeric codes; `logger` never throws or halts the game.
**Patterns:**
- `logger.warn(code, ...args)` for non-fatal issues (e.g. duplicate registration warnings, unknown lookups).
- `logger.error(code, ...args)` for unexpected states; the game continues.
- Direct `throw new Error(...)` only where the contract genuinely requires it (e.g. `Mota.require` of an unregistered module, `Realize nonexistent key`).
- Non-null checks: `if (!object)` for objects, `isNil(value)` (lodash-es) for literals — per `.agents/code.md` rule #13.
## Cross-Cutting Concerns
**Logging:** `@motajs/common` `logger` (`packages/common/src/logger.ts`) with numeric codes; documented under `docs/logger/`.
**Validation / type safety:** TypeScript strict mode (`tsconfig.json`), `vue-tsc --noEmit` (`check:type`). Generated legacy typings live in `src/types/source/*.d.ts` (regenerated by `script/declare.ts` from `public/project/*.js`).
**Authentication:** Not applicable (client-side game; no auth). The editor file API (`script/dev.ts`) does path-safety checks (`resolvePath`/`withSafeCheck`) but is a local dev tool, not a secured service.
**Persistence:** `SaveSystem` over Dexie/IndexedDB (data end), with compression (`SaveCompression`) and undo/redo stacks.
**Replay/determinism:** `ReplaySystem` records commands into typed arrays and replays in a sandbox; render-only code must be gated by `main.replayChecking`/`main.mode` (see `r()`/`rf()`).
---
*Architecture analysis: 2026-09-07*