# 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 L0–L3 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 1–3 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 L0–L3), `enemy/` (calculators/comparers/specials), `hero/`, `legacy/`, `content/`, `ins.ts` (`state = new CoreState()`). - Depends on: L0–L2 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 0–3 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*