From e439200e4a3e6df6038d7c278c7a347a76d629eb Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 7 Sep 2026 16:22:05 +0800 Subject: [PATCH] docs: map existing codebase --- .planning/codebase/ARCHITECTURE.md | 250 +++++++++++++++++++++++++++++ .planning/codebase/CONCERNS.md | 192 ++++++++++++++++++++++ .planning/codebase/CONVENTIONS.md | 202 +++++++++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 108 +++++++++++++ .planning/codebase/STACK.md | 110 +++++++++++++ .planning/codebase/STRUCTURE.md | 217 +++++++++++++++++++++++++ .planning/codebase/TESTING.md | 113 +++++++++++++ 7 files changed, 1192 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..da77b07 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,250 @@ + +# 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* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..7725b5a --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,192 @@ + +# Codebase Concerns + +**Analysis Date:** 2026-09-07 + +> Project: `mota-ts` (魔塔/Magic Tower game engine monorepo). Scope: full repo (`packages`, `packages-user`, `src`, `script`, plus root config files). `public`, `template`, `graphify-out`, `_bundle`, `node_modules` are gitignored/generated and were treated as out of scope for source-level findings. + +--- + +## Tech Debt + +**Legacy bridge layer (`legacy-*` packages):** +- Issue: The old engine surface is bridged to the new data-side via global `core` object + `patch.add(...)` shims, producing pervasive `any` casts and `@ts-expect-error` markers. Comments literally say `// @ts-expect-error todo` and `// 为了防止逆天样板出问题` (to guard against pathological third-party plugins). +- Files: `packages-user/legacy-plugin-data/src/fallback.ts` (630 lines), `packages-user/data-state/src/legacy/item.ts`, `packages-user/data-state/src/legacy/hero.ts`, `packages-user/data-state/src/legacy/tile.ts`, `packages-user/data-fallback/src/hero.ts` +- Impact: Legacy compatibility is maintained through untyped, hard-to-reason-about shims; type-safety guarantees of the new layer are bypassed wherever the old API is touched. +- Fix approach: Continue the planned Layer-0/1/2 data-side refactor (see `dev.md` §"双端分离"); replace `core.*` global access with typed `IStateBase`/`IStateSystem` calls, deleting `patch.add` shims as each legacy surface is migrated. + +**`@ts-expect-error` / `@ts-ignore` debt:** +- Issue: ~43 suppression markers across `packages` and `packages-user`, several explicitly deferred (`// @ts-expect-error 之后修` = "fix later", `// @ts-expect-error 遗留问题` = "legacy issue", `// @ts-expect-error todo`, `// @ts-ignore`). +- Files (notable): `packages/legacy-ui/src/tools/fixed.ts:32,35`, `packages/legacy-ui/src/preset/ui.ts:94`, `packages/legacy-ui/src/ui/equipbox.vue:248,258`, `packages/legacy-common/src/eventEmitter.ts:122`, `packages/render/src/core/gl2.ts:703,706`, `packages-user/legacy-plugin-data/src/fallback.ts:146,344,346,399,430`, `packages-user/client-modules/src/render/components/textbox.tsx:620` +- Impact: Suppressions mask real type gaps and drift; new code written against these modules has no reliable type contract. +- Fix approach: Resolve each suppression (not blanket-disable); where a third-party declaration is wrong, contribute a local `.d.ts` augmentation instead of `@ts-expect-error`. + +**Widespread `any` types (explicitly allowed):** +- Issue: `@typescript-eslint/no-explicit-any` is set to `'off'` in `eslint.config.js:59`, so `any` is unchecked across the codebase (86 matches in `packages`, 74 in `packages-user`). The WebGL layer and the module registry are the heaviest users. +- Files: `packages/render/src/core/gl2.ts` (`buffer`/`sub` take `any`), `packages/render/src/core/graphics.ts` (`prevValue: any, nextValue: any`), `packages-user/entry-data/src/mota.ts:87` (`Record` registry), `packages/legacy-ui/src/controller.ts:129` (`[x: string]: any`) +- Impact: `dev.md` §"类型规范" mandates avoiding unnecessary `any`, yet the registry and WebGL bindings are entirely untyped; regressions slip past `vue-tsc`. +- Fix approach: Re-enable `no-explicit-any` selectively (per-directory overrides) once `Mota.require`/`register` and WebGL wrappers are typed; start with `entry-data/src/mota.ts` and `render/src/core/gl2.ts`. + +**Singleton/global-state patterns flagged for refactor:** +- Issue: Three module-level singletons are explicitly marked `// TODO: 逐渐弱化 … 单例概念` (gradually weaken singleton concept, pass instances via parameters). +- Files: `packages-user/data-state/src/ins.ts:3` (`state = new CoreState()`), `packages-user/client-modules/src/core.ts:4` (`client = new ClientCore(state)`), plus the global `window.Mota` registry in `packages-user/entry-data/src/mota.ts:152` and the static `GameStorage.list` registry in `packages/legacy-system/src/storage.ts:4`. +- Impact: Global mutable singletons make the data-side non-reentrant, hard to test, and couple the render/data split the project is trying to enforce. +- Fix approach: Inject `ICoreState`/`IClientCore` through constructors/parameters per the TODOs; remove `GameStorage.list` static accumulation or scope it. + +**Oversized source files:** +- Issue: Multiple files exceed ~800 lines with no `#region` segmentation despite `dev.md` recommending it for long files. +- Files: `packages-user/client-modules/src/render/map/renderer.ts` (1642 lines), `packages/render/src/core/types.ts` (1526), `packages-user/client-modules/src/render/components/textboxTyper.ts` (1295), `packages/render/src/core/gl2.ts` (1289), `packages-user/client-modules/src/render/map/vertex.ts` (1090), `packages/render/src/core/item.ts` (1043), `packages/render/src/core/graphics.ts` (950), `packages-user/data-system/src/combat/context.ts` (818) +- Impact: High cognitive load, difficult review; the renderer/vertex/gl2 trio is the core hot path and also the hardest to change safely. +- Fix approach: Split by responsibility (e.g. `renderer.ts` into layer/draw/camera modules); use `#region` as an intermediate step. + +**Planned deprecations/refactors (from `task.md`):** +- Issue: `task.md` lists an explicit backlog: deprecate `getMappedName`, `getNextLvUpNeed`, `getLvName`, `getHeroLoc`, `setHeroLoc`, `getNakedStatus`, `getStatusLabel`, `setBuff`, `addBuff`, `getBuff`, `setStatus`, `addStatus`, `getStatus`, `getStatusOrDefault`, `getRealStatus`, `getRealStatusOrDefault`; refactor 存档系统 (save system), 寻路系统 (pathfinding), `core.status.hero`, `core.status.hero.flags`. +- Impact: The save system and pathfinding are singled out as needing rework while still in active use; changes in these areas are risk-prone until the refactor lands. +- Fix approach: Treat `task.md` as the authoritative debt backlog; sequence save-system and pathfinding refactors before adding features that depend on them. + +--- + +## Known Bugs + +**Unguarded `JSON.parse` on persisted storage (startup crash risk):** +- Symptoms: A corrupted/malformed `localStorage` entry throws during `GameStorage.read()`, which runs in the constructor (`storage.ts:11`), breaking module initialization and the whole game load. +- Files: `packages/legacy-system/src/storage.ts:18-21` +- Trigger: Any prior crash mid-write, manual tampering, or a schema change leaves invalid JSON under a `HumanBreak_*` / `{author}@{key}` key. +- Workaround: Manually clear the offending `localStorage` key via devtools. + +**Unguarded decompress+parse on save/swap data:** +- Symptoms: `JSON.parse(decompressFromBase64(...))` can throw on truncated/invalid save blobs with no recovery UI. +- Files: `packages/legacy-ui/src/utils.ts:284` (`swapChapter`), `packages-user/client-modules/src/render/utils/saves.ts:82,88` +- Trigger: Loading a corrupt `.h5save` file or a failed network response during chapter swap. +- Workaround: None in-app; error surfaces as an unhandled rejection. + +**Duplicate module registration silently overwrites:** +- Symptoms: `Mota.register(key, data)` logs `console.warn('模块注册重复: …')` and overwrites the previous module, which can mask load-order bugs (the last registrant wins). +- Files: `packages-user/entry-data/src/mota.ts:100-105` +- Trigger: Two entry bundles or a plugin re-registering the same `@user/...` / `@motajs/...` key. +- Workaround: None; the overwrite is silent beyond the console warning. + +--- + +## Security Considerations + +**Dynamic code execution from game data (`eval` / `new Function`):** +- Risk: Untrusted game data (item effect scripts, typewriter strings) is executed as JavaScript, allowing arbitrary code execution if a game archive/plugin is malicious or compromised. +- Files: `packages/legacy-ui/src/utils.ts:155` (`eval('`' + str + '`')` in `type()`), `packages-user/data-state/src/legacy/item.ts:40,48,55` (`new Function('state','item', legacy.itemEffect)` etc.), and the gitignored `index.cjs` (root) which uses Node's `vm` module to run game/replay code for headless replay validation. +- Current mitigation: None. This is inherent to the "魔塔" plugin model where plugins supply raw JS strings. `index.cjs` is gitignored and local-only (replay validation), but `item.ts`/`utils.ts` run in the player's browser on live game data. +- Recommendations: Sandbox `new Function` bodies (e.g. `vm`/WebWorker/`with`-scoped whitelist), or migrate legacy effect strings to a declarative effect DSL. At minimum, document that loading a project = executing its code. + +**Unsafe HTML injection (`v-html` / `innerHTML`):** +- Risk: XSS if any interpolated string originates from game data, plugin output, or player-provided text. +- Files: `packages/legacy-ui/src/ui/settings.vue:52`, `packages/legacy-ui/src/ui/shop.vue:12`, `packages/legacy-ui/src/ui/toolbox.vue:101`, `packages/legacy-ui/src/ui/equipbox.vue:155`, `packages/legacy-ui/src/tools/book.tsx:43` (``), `packages/legacy-system/src/keyboard.vue:17` +- Current mitigation: Content largely originates from first-party game data (`descText`, item descriptions), but the sink is unguarded. +- Recommendations: Sanitize or escape before rendering; replace `v-html` with text interpolation where markup isn't required. + +**Hardcoded credentials in working tree:** +- Risk: `user.ts` at the repo root contains `export const id = 2691; export const password = '';` — a static credential pattern for game upload/auth. +- Files: `E:\github\template\user.ts` +- Current mitigation: `user.ts` is listed in `.gitignore`, so it is not committed — but it exists in the working tree and is easy to accidentally force-add or copy. +- Recommendations: Move to an untracked `.env`/config loaded at runtime; never hardcode credentials in source; rotate the credential. + +**CodeQL configured but minimal:** +- Risk: `.github/workflows/codeql.yml` runs only the default `javascript` query pack (`queries: security-extended,security-and-quality` is commented out) and uses deprecated `actions/checkout@v3` / `codeql-action@v2`. +- Files: `.github/workflows/codeql.yml:17,41,45,53,59,72` +- Current mitigation: Weekly + push-to-master CodeQL scan exists. +- Recommendations: Pin `@v4` actions, enable `security-extended,security-and-quality`, and ensure `eval`/`new Function` findings are triaged. + +--- + +## Performance Bottlenecks + +**`beforeunload`/`blur` writes every storage instance:** +- Problem: On every window blur (tab switch, devtools focus, dialog), all `GameStorage` instances are serialized and written to `localStorage` synchronously. +- Files: `packages/legacy-system/src/storage.ts:111-116` +- Cause: Global `GameStorage.list` registry iterated without debounce; writes are synchronous `localStorage.setItem` calls on the main thread. +- Improvement path: Debounce/coalesce writes; only persist dirty storages (track a dirty flag in `setValue`); avoid `blur` as a write trigger or use `requestIdleCallback`. + +**Large in-memory replay buffers:** +- Problem: `ReplayArray` maintains `commandBuffer`, `paramBuffer`, `indexBuffer` as `ArrayBuffer`s grown by a multiplier; long sessions accumulate large buffers that are copied on resize. +- Files: `packages-user/data-common/src/replay/array.ts` (823 lines) +- Cause: Resize likely re-allocates and copies the whole buffer; full replay is held in memory for step/seek. +- Improvement path: Use growable ring/segmented buffers; consider streaming to IndexedDB (`dexie` is already a dependency) for long replays. + +--- + +## Fragile Areas + +**`packages-user/legacy-plugin-data/src/fallback.ts` (legacy → new bridge):** +- Files: `packages-user/legacy-plugin-data/src/fallback.ts` (630 lines), `packages-user/legacy-plugin-data/src/shop.ts`, `packages-user/legacy-plugin-data/src/hook.ts` +- Why fragile: Every shim reaches into both the old `core.*` global and the new `state` object simultaneously; a change to either side silently breaks the other. Heavily decorated with `@ts-expect-error todo`. +- Safe modification: Add regression coverage for each `patch.add` handler before touching; keep old/new writes atomic (mirror `core.status.hero.loc` and `state.hero.mover` together, as in `setHeroLoc`). +- Test coverage: None (see Test Coverage Gaps). + +**`packages/render/src/core/gl2.ts` + `graphics.ts` (WebGL core):** +- Files: `packages/render/src/core/gl2.ts` (1289 lines), `packages/render/src/core/graphics.ts` (950 lines), `packages/render/src/core/render.ts` (822 lines) +- Why fragile: Untyped `any` WebGL bindings, manual buffer/sub-offset arithmetic, and shader compilation error paths (`logger.json` codes 9/10/13/17/18/28/29). Rendering regressions are visually subtle. +- Safe modification: Keep shader/layout changes isolated; validate against `logger.json` error codes; add render smoke tests if a headless GL context becomes feasible. +- Test coverage: None. + +**Save/load round-trip (`ISaveableContent` system):** +- Files: `packages-user/data-state/src/core.ts` (`saveables`/`addedSaveables` maps), `packages-user/data-base/src/flag/field.ts` (`toStructured`/`fromStructured` return `any`), `packages-user/client-modules/src/render/utils/saves.ts` +- Why fragile: Save schema is spread across `toStructured`/`fromStructured` methods returning `any`; a field rename or type change silently corrupts saves. This is the "存档系统" refactor target in `task.md`. +- Safe modification: Version the save format; add a save round-trip test before modifying `toStructured`/`fromStructured`. +- Test coverage: None. + +--- + +## Scaling Limits + +**Singleton architecture limits reentrancy/multi-instance:** +- Current capacity: One `CoreState` (`data-state/src/ins.ts`), one `ClientCore` (`client-modules/src/core.ts`), one global `window.Mota` registry. +- Limit: Cannot host two independent game sessions (e.g. editor + preview, or side-by-side replay) in one page; the static `GameStorage.list` also grows unbounded across instances. +- Scaling path: Convert singletons to injected instances (the already-filed TODOs), and scope `GameStorage.list` per game context. + +**Replay buffer memory growth:** +- Current capacity: In-memory `ReplayArray` for the whole session. +- Limit: Multi-hour sessions produce large buffers; growth-by-multiplier causes repeated copies. +- Scaling path: Segmented/streaming storage backed by IndexedDB (`dexie` dependency available). + +--- + +## Dependencies at Risk + +**`anon-tokyo` (version `0.0.0-alpha.0`):** +- Risk: Pinned to a pre-release alpha version in `package.json:28`; API may change without notice. +- Impact: Whatever it powers (likely a font/typeface or UI preset) could break on upgrade. +- Migration plan: Pin to a stable release or vendor the needed subset. + +**Legacy engine dependency on global `core` (not npm):** +- Risk: The `legacy-*` packages rely on a runtime-injected global `core` object rather than typed imports; it is not represented as a dependency and cannot be type-checked. +- Impact: Refactors of the data-side risk breaking an invisible contract with third-party 魔塔 plugins. +- Migration plan: Continue the Layer migration; expose a typed `ICoreState`/`IStateBase` and deprecate raw `core` access via `patch.add`. + +**TypeScript `6.0.3` (bleeding edge):** +- Risk: `typescript: 6.0.3` (`package.json:90`) is a very new major; `vue-tsc` (`^2.2.12`) and `typescript-eslint` (`^8.58.2`) may lag on full compatibility. +- Impact: Type-check results may differ between editor and CI; possible false positives/negatives. +- Migration plan: Pin to the latest stable that `vue-tsc` and `typescript-eslint` officially support. + +--- + +## Missing Critical Features + +**Automated test suite (blocking):** +- Problem: `vitest` is configured (`package.json:8`, `"test": "vitest"`) but zero test files exist anywhere in `packages`, `packages-user`, `src`, or `script` (no `*.test.*`/`*.spec.*`/`__tests__`), and there is no `vitest.config.*`. +- Blocks: Safe refactoring of the save system, pathfinding, and legacy bridge; regression prevention for render/data split; the `pnpm test` script currently does nothing useful. + +**CI for lint/type/build/tests:** +- Problem: `.github/workflows/` only contains `codeql.yml` (security scan) and `page.yml` (docs deploy). No workflow runs `pnpm lint:packages`, `pnpm lint:user`, `pnpm check:type`, `pnpm check:circular`, or `pnpm test`. +- Blocks: Automated gating of the quality rules documented in `dev.md`; nothing prevents a bad commit from reaching `master`. + +--- + +## Test Coverage Gaps + +**Entire codebase is untested:** +- What's not tested: Save/load round-trip (`ISaveableContent`), replay array encode/decode, combat calculation (`packages-user/data-system/src/combat/*`), the legacy→new bridge (`fallback.ts`), map layer/vertex generation, storage persistence. +- Files: No test files present. `pnpm test` (`vitest`) has nothing to run. +- Risk: Every refactor (esp. the `task.md` save/pathfinding items and the in-flight map interface refactor on branch `refactor/data`) is unguarded against regressions; the data/render split's "data side must run headlessly in Node" guarantee (see `dev.md` §"双端分离") is untested and can silently regress. +- Priority: **High** — add at minimum unit tests for `data-common` (replay, save) and `data-system` (combat), which are the headless, deterministic, high-value layers. + +--- + +*Concerns audit: 2026-09-07* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..c8932a0 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,202 @@ +# Coding Conventions + +**Analysis Date:** 2026-09-07 + +> Note: this is a Chinese-language game-engine monorepo (魔塔 / Mota). All in-code comments and docs are written in Chinese, and convention rules are codified in `dev.md` and `.agents/code.md`. Those two files are the source of truth; this document distills them into prescriptive rules for the executor. + +## Naming Patterns + +The canonical naming table is in `dev.md` under "命名规则". + +**Files:** +- Source files (`.ts`, `.tsx`, `.vue`): **camelCase** — e.g. `dirtyTracker.ts`, `faceManager.ts`, `mapStore.ts` +- Markdown doc files: **kebab-case** — e.g. `face-manager.md`, `hero-equipment.md` +- Barrel/entry files are always `index.ts` and `types.ts` (a package's public types live in `types.ts`) + +**Functions / Methods / Variables / Members / general constants:** **camelCase** +- `getDamageInfo()`, `setPos()`, `markAllDirty()`, `moveQueue`, `dirtyFlag` + +**Classes / Interfaces / Type aliases / Namespaces / Generics / Enums / Components:** **PascalCase** +- `DamageSystem`, `IObjectMover`, `ObjectMoveType`, `LogLevel`, `IDataCommon` + +**Immutable constants:** **UPPER_SNAKE_CASE** — e.g. `MAX_COUNT` + +**Acronyms (HTTP, URI, etc.):** all-caps + +**Interfaces intended to be `implements`-ed:** prefixed with capital `I` — e.g. `IObjectMover`, `IDamageSystem`, `IHookable`, `IDataCommon` + +**HTML/CSS `id` / `class`:** kebab-case — e.g. `box-main`, `ui-list`, `border-vertical` + +**Never** use underscore prefix for private members/methods. Unused variables/methods use a leading `_` (e.g. `_param`) so they pass the `no-unused-vars` rule. + +## Code Style + +**Formatting (Prettier 3.8.1) — config in `.prettierrc`:** +```json +{ + "printWidth": 80, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "bracketSpacing": true, + "vueIndentScriptAndStyle": false, + "arrowParens": "avoid", + "trailingComma": "none", + "endOfLine": "crlf" +} +``` +- 4-space indent, single quotes, no trailing commas, `arrowParens: avoid` (e.g. `v => v.x`), **CRLF line endings** +- `.prettierignore` excludes generated/build files (`dist/`, `public/project/*.js`, `script/**/*.js`, `docs/.vitepress/dist`, etc.) + +**Linting (ESLint 9 flat config) — `eslint.config.js`:** +- Uses `@eslint/js` recommended, `typescript-eslint` recommended, `eslint-plugin-vue` `flat/recommended`, and `eslint-plugin-prettier/recommended` (prettier as the last rule set, so prettier wins) +- `eslint-plugin-react` is loaded for `**/*.{ts,tsx,vue}` files (for JSX/TSX) +- Key rules applied across `**/*.{js,mjs,cjs,vue}`: + - `no-console`: `warn` + - `eqeqeq`: `['error', 'always']` (always `===`) +- Key rules for `**/*.{ts,tsx,vue}`: + - `@typescript-eslint/no-empty-object-type`: `off` + - `@typescript-eslint/no-explicit-any`: `off` + - `@typescript-eslint/no-namespace`: `off` + - `@typescript-eslint/no-this-alias`: `off` + - `@typescript-eslint/no-unused-vars`: `error` with `argsIgnorePattern: '^_'`, `caughtErrorsIgnorePattern: '^_'`, `varsIgnorePattern: '^_'`, `ignoreRestSiblings: true` + - `vue/multi-word-component-names`: `off` + - `vue/no-mutating-props`: `error` with `shallowOnly: true` + - `react/jsx-boolean-value`: `['error', 'never']` +- Ignores: `node_modules`, `dist`, `public` + +**Lint scripts** (`package.json`): `pnpm lint:packages` (`eslint packages/`), `pnpm lint:user` (`eslint packages-user/`), `pnpm lint:custom` (bare `eslint`) + +## Import Organization + +**Order (not enforced by a plugin, but observed in practice):** third-party libraries first, then aliased `@motajs/*` / `@user/*` imports, then relative `./` imports. Example from `packages-user/data-system/src/combat/damage.ts`: +```ts +import { clamp } from 'lodash-es'; +import { ITileLocator, logger } from '@motajs/common'; +import { ... } from './types'; +import { ... } from '@user/data-base'; +``` + +**Path Aliases** (defined in `tsconfig.json` and `vite.config.ts`): +- `@motajs/*` → `packages/*/src` (core engine) +- `@user/*` → `packages-user/*/src` (user code) + +**No `import type`:** per `dev.md` "无类型导入", all imports are normal value imports. The only sanctioned exception is the module-interface registration file `packages-user/entry-data/src/mota.ts`, which uses `import type * as X` deliberately (it only needs types to build the `ModuleInterface` map). Do not introduce `import type` in new code. + +**Barrel exports:** each package exposes `index.ts` with `export * from './...'` for its subfolders. Do not re-export content from outside the current package (`dev.md` "不转发导出"). + +## Error Handling + +**Core principle:** errors/warnings are reported through the `logger` singleton — never silently swallowed via `return null` / `return false`. + +**Logger** (`packages/common/src/logger.ts`): +- `logger.error(code, ...params)` — fatal-adjacent errors, each with a unique numeric `code` +- `logger.warn(code, ...params)` — warnings, unique numeric `code` +- `logger.log(text)` — informational +- `logger.catch(fn)` — runs `fn` while capturing any errors/warnings it emits, returns `{ ret, info }` without throwing (see `packages/common/src/logger.ts:189`) +- `logger.disable()` / `logger.enable()` + +**Error codes are data, not code:** all messages live in `packages/common/src/logger.json`, keyed by `error` / `warn` maps of `code -> message`. Messages use `$1`, `$2` positional placeholders substituted by the params passed to `error`/`warn`. Codes are unique and never reused; do not use code `0`. Internal meta-error for a missing message is code `16` (`logger.error(16, ...)`). + +Example usage: +```ts +if (!this.calculator) { + logger.warn(106); + return null; +} +``` +```ts +if (!obj) { + logger.warn(85); + return; +} +``` + +**The logger never throws and never interrupts execution.** It is designed so a warning/error does not break the game loop or replay verification. + +## Logging + +**Framework:** the custom `logger` (above), plus `console` directly for debug/tooling in `script/` files. `no-console` is `warn`-level so plain `console.log` in scripts is tolerated but discouraged in library code. + +**Patterns:** +- Library/engine code: use `logger.error/warn/log` with a registered code. Do not `throw`. +- A genuine programming fault that must halt (e.g. unknown module in `Mota.require`) may `throw new Error(...)` — see `packages-user/entry-data/src/mota.ts:96`. + +## Comments + +All comments are written in **Chinese**. Guidelines from `dev.md` "注释规范" and `.agents/code.md` "注释": + +- **Public methods/interfaces/members** get jsDoc comments **at the source** (usually the `interface`). Inherited / `implements`-ed members do **not** repeat the comment unless the semantics change. +- **Private methods and private members must be commented** (jsDoc), and private method params must be commented. Exception: constructor parameter-property declarations. +- **Method jsDoc uses multi-line style**; **member jsDoc uses single-line style** when short. +- **No comment on constructors.** No comment on the `interface`/`type alias`/`enum`/`class` itself (only its members). +- **TODO format:** `// TODO:` or `// todo:`. +- Single-line comments: `//` followed by one space. No non-jsDoc multi-line comments — use multiple single-line comments instead. +- **`#region` / `#endregion`** partition long files by function — see `packages/common/src/types.ts`, `packages-user/data-common/src/common/mover.ts`, `packages/common/src/utils/types.ts`. +- Wrap comments reasonably (Chinese chars are wide): ~40–60 chars per line, break at punctuation, keep lines roughly even, no mid-sentence breaks. +- Comments must add value (explain *why* the next line exists), not restate the code (e.g. `// 清空 Xxx` is disallowed). + +Example jsDoc (member, single-line): +```ts +/** 怪物生命值 */ +hp: number; +``` +Example jsDoc (method, multi-line): +```ts +/** + * 创建只读信息对象 + * @param enemy 怪物对象 + * @param locator 怪物位置 + * @param hero 勇士属性对象 + */ +``` + +## Function Design + +**Size:** no hard limit, but single-responsibility is expected. Long classes are partitioned with `#region`. + +**Parameters:** +- More than 2 optional params → switch to an object param. +- Unused trailing params are omitted, not named `_` (in method implementations). +- `{@link}` references used in jsDoc to cross-reference related members. + +**Return Values:** +- Builder-style chaining methods return `this` (e.g. `step()`, `speed()`, `face()` in `packages-user/data-common/src/common/mover.ts`). +- "May not be available" results return `T | null` and call `logger.warn` rather than throwing. + +**Design rules (from `.agents/code.md`):** +- Complete `if - else` when both branches must do work — no early `return` to fake an `else` for same-level conditions. +- Minimal abstraction: local repetition is allowed; do not add indirection just to reduce line count. +- Do not define local functions inside a function unless a function argument is required. +- Avoid `getter`/`setter` (only for operator-method scenarios). +- Avoid `?.` except (1) side-effect calls like `this.obj?.func()`, (2) object "Required"-ification like `{ value: obj?.value ?? 0 }`. +- Do not line-break ternary expressions or `private readonly` members. +- Single-property destructuring is disallowed — write `const value = obj.value` instead of `const { value } = obj`. + +## Module Design + +**Exports:** barrel `index.ts` with `export * from './subdir'` and `export * from './types'`. Each package's public types are in `types.ts`. + +**Module principles (`dev.md` "模块原则"):** +- **No side effects** in modules: only function/class/constant declarations; no exported `let`/`var`, no top-level execution. +- **No circular imports** (checked by `pnpm check:circular` via `madge`; config `.madgerc`). +- **No re-export** of content outside the current package. +- **One class per file.** Multiple small implementations of the same interface may share a file only with explicit approval. + +**Type rules (`dev.md` "类型规范"):** +- No unnecessary `any` (though `no-explicit-any` is `off`, it's still discouraged). +- All class members have explicit type annotations. +- Unavoidable type errors → `// @ts-expect-error` + explanation (see `packages-user/entry-data/src/mota.ts:137`). +- Avoid `as`; never chain `as unknown as`. +- Function types → separate `type` alias (unless <20 chars). +- Object types → separate `interface`, never an inline object type. +- Object members use interface types, not class types (`map: IGameMap` not `map: GameMap`). +- Enums use `const enum` for zero-runtime-cost (e.g. `LogLevel`, `ObjectMoveType`, `ObjectSpecialStep`). + +**Architecture constraint:** rendering side never pushes updates to the data side; it only observes via hooks. Data-side code calling render-side code must wrap it in `Mota.r(() => {})` / `Mota.rf(fn)` (see `packages-user/entry-data/src/mota.ts`). + +--- + +*Convention analysis: 2026-09-07* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..08f6f66 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,108 @@ +# External Integrations + +**Analysis Date:** 2026-09-07 + +## APIs & External Services + +**h5mota.com (H5魔塔 tower publishing platform) — the only external HTTP service:** + +The project integrates with the H5魔塔 community platform (`h5mota.com`) in three distinct places: + +1. **Tower metadata scraper** — `script/special.ts` + - `GET https://h5mota.com/backend/towers/query.php` (`mode=list`) — list all published towers + - `GET https://h5mota.com/backend/admin/tower/info.php` (`name=`) — fetch tower metadata + - `GET https://h5mota.com/games/{name}/project/{functions|enemys|floors.min|maps}.js` — download raw game source files for offline analysis + - Auth: hardcoded `Cookie: id=2691; password=...` header (a session credential embedded in source — treat as sensitive; see Security Considerations below) + +2. **Danmaku (barrage) proxy** — `script/dev.ts` + - Vite dev proxy route `/danmaku` → `https://h5mota.com/backend/tower/barrage.php` + +3. **Cloud save sync** — `packages-user/client-modules/src/render/utils/saves.ts` + - `POST /games/sync.php` (relative path, resolved against the deployed `h5mota.com` origin) + - Request body: `FormData` with `type=load`, `name`, `id`, `password` + - Response: `SyncSaveFromServerResponse` — JSON with `code`/`msg`; `msg` is `lz-string` base64-compressed save data + - Auth: identifier string (`存档编号` + `密码`) split into `id`/`password` by `parseIdPassword()` + +## Data Storage + +**Databases:** +- IndexedDB via **Dexie** (`dexie ^4.4.2`) + - Implementation: `packages-user/data-common/src/save/system.ts` (`SaveSystem`) + - Schema (v1): `saves` table (`id` key) and `global` table (`key` key) + - Used for local save/autosave slots, undo/redo stack persistence, and global key-value state +- `localStorage` (legacy) — `packages/legacy-system/src/storage.ts` +- **localforage** (legacy fallback) — vendored at `public/libs/thirdparty/localforage.min.js`, typed in `src/types/declaration/util.d.ts` + +**File Storage:** +- Local filesystem only. The dev/editor server exposes a file CRUD API over Express (`script/dev.ts`): `POST /listFile`, `/makeDir`, `/readFile`, `/writeFile`, `/deleteFile`, `/moveFile`, `/writeMultiFiles`; `GET /all/__all_floors__.js`, `/all/__all_animates__`, `/esm`, `/getPort`. All paths are confined to the `public/` base directory (`resolvePath()` safety check). + +**Caching:** +- None (no external cache service). In-browser `localStorage`/IndexedDB are used for persistence only. + +## Authentication & Identity + +**Auth Provider:** +- Custom / none. There is no OAuth or third-party identity provider. +- Cloud save uses a bare `id` + `password` pair (split from a user-entered save code). See `parseIdPassword()` in `packages-user/client-modules/src/render/utils/saves.ts`. +- The scraper in `script/special.ts` authenticates to the admin API using a hardcoded session cookie. + +**Native bridge (mobile packaging):** +- `window.jsinterface` global is called for orientation control (`requestPortrait()` / `requestLandscape()`) in `packages/legacy-ui/src/utils.ts` (`triggerFullscreen()`). This is the interface exposed by the native app shell (Android/iOS) that wraps the HTML5 game. + +## Monitoring & Observability + +**Error Tracking:** +- None (no Sentry/Bugsnag/etc.) + +**Logs:** +- Custom in-repo logger: `packages/common/src/logger.ts` (with error/warn code tables surfaced in `docs/logger/`). Uses `console` output; `no-console` is `warn` in `eslint.config.js`. + +## CI/CD & Deployment + +**Hosting:** +- GitHub Pages (static) — `.github/workflows/page.yml` builds on push to `master` and deploys the `dist/` folder to the `gh-pages` branch using `JamesIves/github-pages-deploy-action`. +- The built game is also distributed as a self-contained static bundle (`dist/`) and `dist.zip`. + +**CI Pipeline:** +- GitHub Actions only: + - `page.yml` — install deps (`pnpm@7.27.0`), `pnpm i`, `pnpm build`, deploy to Pages (uses secret `ACCESS_TOKEN`) + - `codeql.yml` — CodeQL static analysis (JavaScript), runs on push/PR to `master` + weekly cron + +## Environment Configuration + +**Required env vars:** +- None at runtime. The project uses no `.env` files. + +**Secrets location:** +- GitHub Actions secret: `ACCESS_TOKEN` (referenced in `.github/workflows/page.yml`) +- Editor server config: `public/_server/config.json` (gitignored; auto-created as `{}` by `script/dev.ts`) +- Hardcoded admin cookie in `script/special.ts` (should be externalized, see Security Considerations) + +## Webhooks & Callbacks + +**Incoming:** +- Dev-time Express server routes (`script/dev.ts`): file CRUD endpoints listed above, plus `GET /getPort` (returns the hot-reload WebSocket port to the client, `packages-user/legacy-plugin-client/src/dev/hotReload.ts`). +- WebSocket server (`ws`) on the editor HTTP server for hot reload; client connects to `ws://127.0.0.1:{port}` and receives `reload`, `floorHotReload`, `dataHotReload`, `cssHotReload` messages. + +**Outgoing:** +- h5mota.com tower query/info/game-file endpoints (`script/special.ts`) +- h5mota.com barrage endpoint via dev proxy (`script/dev.ts`) +- h5mota.com cloud save sync `/games/sync.php` (`packages-user/client-modules/src/render/utils/saves.ts`) +- Local asset streaming via `window.fetch` (`packages/loader/src/task.ts`, `packages/loader/src/stream.ts`) + +--- + +## Security Considerations + +**Hardcoded session credential in `script/special.ts`** +- The admin API calls embed `Cookie: id=2691; password=26e631510147c1d0b71a368a3729df5a` directly in source. This is a live-looking session credential checked into the repository. +- Impact: if the credential is valid, it grants the scraper access to h5mota.com's admin/tower endpoints and leaks on any code share. +- Recommendation: move the cookie value to a local, gitignored config or environment variable; rotate the credential. + +**No auth on editor file API (`script/dev.ts`)** +- The Express routes (`/readFile`, `/writeFile`, `/deleteFile`, etc.) require no authentication and are bound to the local server; the only protection is a path-traversal check (`resolvePath()`). +- Recommendation: keep the editor server loopback-only in production; do not expose port 3000 publicly. + +--- + +*Integration audit: 2026-09-07* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..9288ea3 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,110 @@ +# Technology Stack + +**Analysis Date:** 2026-09-07 + +## Languages + +**Primary:** +- TypeScript 6.0.3 - All engine/package source, build scripts, and docs config (monorepo `packages/`, `packages-user/`, `src/`, `script/`, `docs/`) +- Vue 3 SFC (Single File Components) - Client UI (`*.vue` in `packages-user/client-modules/`, `src/App.vue`) + +**Secondary:** +- JavaScript (legacy runtime) - The legacy H5 runtime layer under `public/libs/*.js` and game content `public/project/*.js` (data.js, enemys.js, events.js, floors, maps, etc.) +- Less - Stylesheets (`src/styles.less`, `javascriptEnabled: true` in Vite config) +- CSS - Editor/assets styles (`public/styles.css`, `public/_server/**`) + +## Runtime + +**Environment:** +- Node.js `^20.0.0 || >=22.0.0` (per `dev.md`) +- Browsers supporting ESNext; production build targets `Chrome >= 56`, `Firefox >= 51`, `Edge >= 79`, `Safari >= 15`, `Opera >= 43` via `@vitejs/plugin-legacy` (`script/build-game.ts`) + +**Package Manager:** +- pnpm `>= 10.0.0` (per `dev.md`) +- Lockfile: `pnpm-lock.yaml` (present) +- Workspace: `pnpm-workspace.yaml` — globs `packages/*`, `packages-user/*`, and `src/`; `onlyBuiltDependencies`: `core-js`, `esbuild`, `ttf2woff2`, `vue-demi` + +## Frameworks + +**Core:** +- Vue `^3.5.29` - UI framework for the client render side (`src/main.ts`, `src/App.vue`) +- Vite `^7.3.1` - Dev server and production bundler (`vite.config.ts`, `script/dev.ts`, `script/build-game.ts`) +- Ant Design Vue `^3.2.20` + `@ant-design/icons-vue ^6.1.0` - UI component library (bundled as manual chunk `antdv`) + +**Rendering (custom, in-repo):** +- WebGL2 - Hand-written render engine in `packages/render/src/core/gl2.ts`, `assets/composer.ts` (no external rendering framework) +- `gl-matrix ^3.4.4` - Matrix/vector math for WebGL (`packages/render/src/core/transform.ts`, `core/item.ts`) +- `maxrects-packer ^2.7.3` - Texture atlas packing (`packages/render/src/assets/composer.ts`, `streamComposer.ts`) + +**Audio:** +- Web Audio API (`AudioContext`) via `packages/audio/src/context.ts` with wasm decoders: + - `@wasm-audio-decoders/ogg-vorbis ^0.1.20` + - `ogg-opus-decoder ^1.7.3` + - `opus-decoder ^0.7.11` + - `codec-parser ^2.5.0` (codec stream parsing in `packages/audio/src/source.ts`) + +**Testing:** +- Vitest `^4.0.18` - Test runner (`package.json` script `"test": "vitest"`) + +**Docs:** +- VitePress `^1.6.4` - Documentation site (`docs/.vitepress/config.ts`, output to `public/_docs`) +- Mermaid `^11.12.3` + `vitepress-plugin-mermaid ^2.0.17` + `markdown-it-mathjax3 ^4.3.2` + +**Build/Dev:** +- Rollup `^4.59.0` (+ `@rollup/plugin-*` family) - Programmatic bundling in build scripts +- `@babel/core ^7.29.0` / `@babel/preset-env ^7.29.0` / `@babel/cli ^7.28.6` - Post-build script minification (`script/build-game.ts`) +- `vue-tsc ^2.2.12` - Type checking for `.vue` files +- `tsx ^4.21.0` - Execute TypeScript build/dev scripts +- `fontmin ^2.0.3` - CJK font subsetting at build time (`script/build-game.ts`) +- Express `^5.2.1` - Local editor/dev HTTP server (`script/dev.ts`) +- `ws ^8.19.0` - WebSocket server for hot reload (`script/dev.ts`) +- `chokidar ^3.6.0` - File watching (`script/dev.ts`) +- `archiver ^7.0.1` + `compressing ^1.10.4` - Zip packaging (`script/build-game.ts`) +- `madge ^8.0.0` - Circular dependency check (`"check:circular"` script) + +## Key Dependencies + +**Critical:** +- `dexie ^4.4.2` - IndexedDB wrapper for game saves (`packages-user/data-common/src/save/system.ts`) +- `lz-string ^1.5.0` - Save compression (`packages-user/client-modules/src/render/utils/saves.ts`, `packages/legacy-ui/src/utils.ts`) +- `jszip ^3.10.1` - Zip handling (`packages/loader/`) +- `axios ^1.13.6` - HTTP client (`script/special.ts`, `packages/legacy-ui/src/utils.ts`) +- `lodash-es ^4.17.23` - Utility functions (used widely across packages) +- `eventemitter3 ^5.0.4` - Event emitter +- `mutate-animate ^1.4.2` - Animation tweening (`packages/legacy-ui/src/utils.ts`) +- `anon-tokyo 0.0.0-alpha.0` - "high performance interpreter" (declared dependency; no import found in `packages/`/`src/`/`packages-user/` source) +- `chart.js ^4.5.1` - Charts (declared; not detected in source, likely editor-facing) + +**Infrastructure:** +- `fs-extra ^11.3.4`, `glob ^11.1.0` - File system utilities in build scripts +- `less ^4.5.1`, `postcss-preset-env ^9.6.0` - CSS preprocessing + +## Configuration + +**Environment:** +- No `.env` / `.env.*` files present — the project does not use runtime environment variables. +- `import.meta.env.BASE_URL` is used for asset path prefixing (`packages/loader/src/task.ts`, `packages/legacy-ui/src/utils.ts`). Vite `base` is set to `./` (`vite.config.ts`). +- Editor config: `public/_server/config.json` (gitignored; auto-generated as `{}` by `ensureConfig()` in `script/dev.ts`). + +**Build:** +- `tsconfig.json` — `strict`, `moduleResolution: "bundler"`, `jsx: "preserve"` (`jsxImportSource: "vue"`), path aliases `@motajs/*` → `packages/*/src` and `@user/*` → `packages-user/*/src`. +- `tsconfig.node.json` — covers `vite.config.ts`, `script/`, and `docs/.vitepress/*`. +- `vite.config.ts` — dynamic aliases generated from `packages/*/src` and `packages-user/*/src`; Less `javascriptEnabled`; `postcss-preset-env`. +- `.prettierrc` — `tabWidth: 4`, `singleQuote`, `semi`, `trailingComma: "none"`, `endOfLine: "crlf"`. +- `eslint.config.js` — flat config combining `@eslint/js`, `typescript-eslint`, `eslint-plugin-vue`, `eslint-plugin-react`, `eslint-plugin-prettier`. +- `.madgerc` — madge config for circular-dependency detection (`.ts`/`.tsx`, skips type imports). + +## Platform Requirements + +**Development:** +- Node.js 20/22+, pnpm 10+, VSCode (recommended extensions: `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `vue.volar`, `slevesque.shader`, `tobermory.es6-string-html` in `.vscode/extensions.json`). +- Run `pnpm dev` (Vite on 5173 + Express editor server on 3000) or `pnpm test`. + +**Production:** +- Static HTML5 game: `pnpm build:game` produces `dist/` (deployable static bundle) and `dist.zip`. +- Deployment target: GitHub Pages via `.github/workflows/page.yml` (builds `dist` folder to `gh-pages` branch). +- The data layer (`src/data.ts`) is built separately as an IIFE bundle (`data.process.js`) usable for replay verification in Node. + +--- + +*Stack analysis: 2026-09-07* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..2cdc971 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,217 @@ +# Codebase Structure + +**Analysis Date:** 2026-09-07 + +## Directory Layout + +``` +mota-ts/ +├── src/ # Game entry code (@user/main): entry points + content data +│ ├── main.ts # Render/client entry +│ ├── data.ts # Data entry (replay verification, runs in Node) +│ ├── App.vue # Vue UI root (legacy UI stack) +│ ├── data.ts # (data-end entry) +│ ├── styles.less # Global styles +│ ├── content/ # New JSONC game content (core/enemy/item/tile/maps) +│ ├── types/ # Generated + declared typings (source/, declaration/) +│ └── package.json # name: @user/main +├── packages/ # Core engine monorepo (@motajs/*) +│ ├── common/ # utils, logger, hook, dirtyTracker +│ ├── legacy-common/ # Patch system, legacy EventEmitter, utils +│ ├── types/ # shared types (enemy, utils) +│ ├── client/ # re-export of client-base +│ ├── client-base/ # glUtils, keyCodes, types (KeyCode) +│ ├── system/ # action (hotkey/keyboard) + ui (UIController/GameUI) +│ ├── render/ # WebGL/Canvas MotaRenderer + assets + style +│ ├── render-vue/ # custom Vue renderer over IRenderItem +│ ├── animate/ # excitation/animation +│ ├── audio/ # audio context, decoders, bgm/effect/sound +│ ├── loader/ # LoadTask, progress, stream +│ ├── legacy-client/ # re-export of legacy-system + legacy-ui +│ ├── legacy-system/ # keyboard.vue, storage +│ └── legacy-ui/ # Vue components/panels/presets/tools/ui +├── packages-user/ # User game code monorepo (@user/*) +│ ├── entry-client/ # composition root (render): createGame() +│ ├── entry-data/ # composition root (data): Mota registry, createData() +│ ├── client-base/ # render system layer: load/ + material/ +│ ├── client-modules/ # render impl layer: render/ + action/ + fallback/ +│ ├── data-common/ # data L0: common/ event/ replay/ save/ store/ +│ ├── data-base/ # data L1: game/ map/ hero/ enemy/ flag/ load/ +│ ├── data-system/ # data L2: combat/ + trigger/ +│ ├── data-state/ # data L3: CoreState singleton + enemy/hero/legacy +│ ├── data-fallback/ # patch legacy globals onto new state +│ ├── legacy-plugin-client/ # dev hot reload +│ └── legacy-plugin-data/ # legacy plugins: shop/replay/fiveLayer/hook +├── public/ # Legacy mota-js sample content + editor assets +│ ├── main.js # legacy mota-js runtime (core/main globals) +│ ├── editor.html # legacy editor +│ ├── project/ # data.js/enemys.js/events.js/items.js/maps.js + assets +│ ├── libs/ # thirdparty libs (lz-string, lodash, localforage…) +│ ├── extensions/ # legacy extensions +│ ├── _server/ # editor server config +│ └── _docs/ # editor-embedded docs +├── script/ # Build/dev tooling (tsx scripts) +│ ├── dev.ts # Vite + Express + WS dev servers +│ ├── build-game.ts # full game build → dist.zip +│ ├── build-lib.ts # library build for packages + packages-user +│ ├── build-packages.ts # library build for packages only +│ ├── build-resource.ts # resource splitting/compression +│ ├── declare.ts # regenerate src/types/source/*.d.ts from public/project/*.js +│ ├── pack-template.ts # pack the template/ directory +│ ├── lines.ts # line-count utility +│ ├── special.ts / types.ts / utils.ts +│ └── template/ # legacy template runtime (main.js, data.js, 启动服务.exe) +├── template/ # Copy of a fresh template project (for pack:template) +├── docs/ # Vitepress documentation site +├── .planning/ # GSD planning state (config.json, graphs/, codebase/) +├── graphify-out/ # Knowledge-graph output (manifest, graph.json/html) +├── _bundle/ # Dev rollup output (ignored) +├── index.html # HTML entry (canvas + Vue root + legacy scripts) +├── vite.config.ts # Vite config + @motajs/@user path aliases +├── tsconfig.json # TS project config + path aliases +├── tsconfig.node.json # Node-side TS config +├── eslint.config.js # ESLint flat config +├── pnpm-workspace.yaml # workspace: packages/*, packages-user/*, src/ +├── package.json # root scripts + shared deps +└── dev.md # Project dev conventions/architecture doc (read first) +``` + +## Directory Purposes + +**`src/`:** +- Purpose: The game entry point and game content. Package name `@user/main`. +- Contains: `main.ts` (client entry), `data.ts` (data entry), `App.vue`, `styles.less`, `content/` (JSONC data), `types/` (typings). +- Key files: `src/main.ts`, `src/data.ts`, `src/App.vue`, `src/package.json`. + +**`packages/` (core engine, `@motajs/*`):** +- Purpose: The reusable engine core — utilities, render system, audio, animation, input/UI systems, loader, and the legacy bridge. +- Contains: one directory per package, each with `src/` and its own `package.json`. +- Key files: `packages/render/src/core/render.ts`, `packages/system/src/action/hotkey.ts`, `packages/legacy-common/src/patch.ts`. + +**`packages-user/` (user code, `@user/*`):** +- Purpose: The game-specific implementation layered over the engine — data end (L0–L3) and render end (system + impl), plus composition roots. +- Contains: one directory per package; each `src/` mirrors its layer's responsibility. +- Key files: `packages-user/entry-data/src/mota.ts`, `packages-user/data-state/src/core.ts`, `packages-user/client-modules/src/index.ts`. + +**`public/`:** +- Purpose: The legacy mota-js sample game content and runtime, plus editor assets. Not TypeScript — these are the uncompiled game files the engine loads. +- Contains: `main.js` (legacy runtime), `project/` (data, enemys, events, items, maps, floors, images, sounds, bgms, autotiles, tilesets, materials, animates), `libs/thirdparty/`, `extensions/`, `_server/`, `_docs/`, `editor.html`, `styles.css`, `logo.png`. +- Key files: `public/main.js`, `public/project/data.js`, `public/project/maps.js`. + +**`script/`:** +- Purpose: Build/dev tooling run via `tsx` (`pnpm dev`, `pnpm build:game`, `pnpm declare`, …). +- Contains: `dev.ts` (dev servers + hot reload), `build-game.ts` (game packaging), `build-resource.ts` (asset splitting), `declare.ts` (type generation), `pack-template.ts`, and helpers. +- Key files: `script/dev.ts`, `script/build-game.ts`, `script/declare.ts`. + +**`docs/`:** +- Purpose: Vitepress documentation site (`docs:dev` / `docs:build`), including API docs per package, dev guides, and logger error-code reference. +- Contains: `.vitepress/`, `api/`, `dev/`, `guide/`, `logger/`. + +**`template/`:** +- Purpose: A standalone copy of a fresh template project, packaged by `script/pack-template.ts` (`pnpm pack:template`). +- Contains: its own `src/`, `script/`, `vite.config.ts`, `package.json`, etc. + +**`.planning/`:** +- Purpose: GSD workflow state — `config.json` (workflow toggles), `graphs/` (project knowledge graph), `codebase/` (these analysis docs). Do not hand-edit during normal development. + +## Key File Locations + +**Entry Points:** +- `src/main.ts`: Render/client entry — `createGame()` + Vue mount. +- `src/data.ts`: Data entry — replay verification, Node-only. +- `index.html`: HTML shell — canvas `#render-main`, Vue `#root`, legacy scripts. +- `packages-user/entry-client/src/create.ts`: Client composition (`createGame`). +- `packages-user/entry-data/src/mota.ts`: Module registry (`Mota`, `r`, `rf`). + +**Configuration:** +- `vite.config.ts`: Vite + auto-generated `@motajs/*`/`@user/*` aliases (from `packages/*/src` and `packages-user/*/src`). +- `tsconfig.json`: path aliases `@motajs/*` → `./packages/*/src`, `@user/*` → `./packages-user/*/src`. +- `pnpm-workspace.yaml`: workspace globs. +- `package.json`: root scripts (`dev`, `build:game`, `build:lib`, `build:packages`, `declare`, `check:circular`, `lint:*`). +- `eslint.config.js`, `.prettierrc`, `.madgerc`. + +**Core Logic:** +- `packages-user/data-state/src/core.ts`: `CoreState` (data-end composition). +- `packages-user/data-base/src/game.ts`: `loading`, `hook`, `gameListener`. +- `packages-user/data-common/src/save/system.ts`: `SaveSystem` (Dexie persistence). +- `packages-user/data-common/src/replay/system.ts`: `ReplaySystem`. +- `packages/render/src/core/render.ts`: `MotaRenderer`. +- `packages/system/src/action/hotkey.ts`: `Hotkey` (input). + +**Testing:** +- Root `package.json` defines `pnpm test` → `vitest`. (See `TESTING.md` for details; not the focus of this doc.) + +## Naming Conventions + +Conventions are defined in `dev.md` (authoritative) and escalated in `.agents/code.md`. + +**Files:** +- Code files: **camelCase** (e.g. `mapStore.ts`, `hotkey.ts`, `build-game.ts`). +- Markdown folders/files: **kebab-case** (e.g. `docs/dev/`, `my-notes.md`). +- One class per file; multiple trivial implementations of the same interface in one file only after confirmation (`.agents/code.md` #6). + +**Directories:** +- Package directories: lowercase single word (e.g. `client-modules`, `legacy-ui`), kebab-case for multi-word. +- Source subfolders group by feature/domain (e.g. `render/map/`, `data-state/enemy/`), not by modifier type. + +**Identifiers (from `dev.md`):** +- Variables, members, general constants, methods, functions: **camelCase**. +- Classes, interfaces, type aliases, namespaces, generics, enums, comments: **PascalCase**. +- Immutable constants: **UPPER_SNAKE_CASE** (e.g. `MAX_COUNT`); acronyms all-caps (`HTTP`, `URI`). +- Interfaces meant to be `implements`-ed: **prefixed with `I`** (e.g. `IGameMap`, `IEnemyManager`, `ICoreState`). +- HTML/CSS `id`/`class`: **kebab-case**. +- No underscore naming; private members/methods do **not** start with underscore. + +## Where to Add New Code + +**New engine feature (core, reusable):** +- Implementation: `packages//src/` under the appropriate package (e.g. render primitives in `packages/render/src/core/`). +- Export it from the package `index.ts` (e.g. `packages/render/src/index.ts`). +- Update `packages//package.json` dependencies if it now depends on another `@motajs/*` package. + +**New user/game feature:** +- Data logic → `packages-user/data-system/src/` (Layer 2) or extend `packages-user/data-state/src/` (Layer 3). +- Saveable data structures → `packages-user/data-base/src/` (Layer 1). +- Shared/utility interfaces → `packages-user/data-common/src/` (Layer 0). +- Rendering/UI → `packages-user/client-modules/src/render/` (impl layer) or `packages-user/client-base/src/` (system layer). +- Register new modules in `packages-user/entry-data/src/create.ts` and/or `packages-user/entry-client/src/create.ts` so they are available via `Mota`. + +**New content (game data):** +- New JSONC content: `src/content/` (e.g. `src/content/item.jsonc`, `src/content/maps/`). +- Legacy content editing: `public/project/` (regenerate types with `pnpm declare`). + +**Utilities:** +- Generic shared helpers → `packages/common/src/utils/` (or `packages/common/src/` for `logger`/`hook`). +- Legacy compatibility helpers → `packages/legacy-common/src/`. + +**Tests:** +- Co-located or under a `test`/`__tests__` folder as the existing `vitest` config expects; run with `pnpm test`. + +**Documentation:** +- API docs → `docs/api/` (one folder per package is generated); dev guides → `docs/dev/`; error codes → `docs/logger/`. + +## Special Directories + +**`node_modules/` (workspace + per-package):** +- Purpose: pnpm-installed dependencies; per-package `node_modules/@motajs/*` and `@user/*` are symlinks to sibling workspace packages. +- Generated: Yes. Committed: No. + +**`_bundle/`:** +- Purpose: Dev rollup output produced by `script/dev.ts` (`getEsmFile`). +- Generated: Yes. Committed: No. + +**`_temp/` / `dist/` / `dist.zip`:** +- Purpose: Build intermediates (`_temp/`) and game output (`dist/`, `dist.zip`) from `script/build-game.ts`. +- Generated: Yes. Committed: No. + +**`.planning/`:** +- Purpose: GSD workflow state (config, graphs, codebase docs). +- Generated: Partly (by GSD commands). Committed: Yes (config and docs are committed by GSD). + +**`graphify-out/`:** +- Purpose: Knowledge-graph build output (`graph.json`, `graph.html`, `manifest.json`). +- Generated: Yes. Committed: Varies (check `.gitignore`). + +--- + +*Structure analysis: 2026-09-07* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..1a279a3 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,113 @@ +# Testing Patterns + +**Analysis Date:** 2026-09-07 + +## Test Framework + +**Runner:** +- [Vitest](https://vitest.dev) `^4.0.18` — declared in `package.json` `devDependencies` + +**Config:** None present. There is **no** `vitest.config.ts`, `vitest.config.js`, `vitest.setup.*`, or any test-related config file anywhere in the repo. Vitest would run with its default configuration (files matching `**/*.{test,spec}.?(c|m)[jt]s?(x)`). + +**Assertion Library:** +- Vitest's bundled assertions (`expect`), plus Jest-compatible `describe`/`it`/`test` globals. No `@testing-library/*`, `jsdom`, or `happy-dom` is installed. + +**Run Commands (`package.json`):** +```bash +pnpm test # Run all tests (runs `vitest`) +``` + +There is **no** dedicated watch mode or coverage script. To run watch/coverage manually: +```bash +pnpm vitest --watch # Watch mode +pnpm vitest --coverage # Coverage (requires @vitest/coverage-* provider, not installed) +``` + +## Test File Organization + +**Location:** No test files currently exist in the repository. A repo-wide search for `*.test.ts`, `*.spec.ts`, `*.test.tsx`, `*.spec.tsx` returns zero matches. + +**Planned location (per `.agents/review.md`):** test-case design documents live in `docs/test/` (with subfolders where appropriate), following the example template `docs/test/template.md`. **Note:** the `docs/test/` directory and `docs/test/template.md` do not exist yet — the workflow is defined but no tests have been authored. + +**Naming:** No established on-disk convention yet. Follow the Vitest default: co-located `*.test.ts` (or `*.spec.ts`) next to the module under test, or a `__tests__/` directory. + +## Test Structure + +No test source exists to extract a concrete pattern from. The authoritative testing *workflow* is defined in `.agents/review.md` and is a **manual, human-in-the-loop** process: + +1. The user requests tests for a feature. +2. The agent analyzes the feature and proposes test cases in a markdown document (placed in `docs/test/`, following `docs/test/template.md`). +3. The user reviews the proposal over several rounds until the plan is finalized. +4. The agent writes the test cases from the document. The agent **must not** run the test command; the user runs it. +5. The user reports results; simple issues are fixed by the user, complex ones may be handed back to the agent. + +**Test-case design principle (from `.agents/review.md`):** test cases must cover **valid inputs AND invalid inputs / exception paths**. For invalid paths, the expectation is usually that the system either throws correctly or produces a sensible `logger` output (rather than silently returning a wrong value). + +**Document structure for a test-case proposal:** +```md +# 测试目的 + +测试 XXX 系统的基本功能及异常处理。 + +# 测试用例 + +## 测试用例 1 + +- 设计目的:为什么需要这一测试用例(其来源/推导),而非它做什么。 +- 针对接口:最重要的若干接口,最好五个以内。 + +### 测试内容 + +描述测试内容,并写出预期结果。 +``` + +## Mocking + +**Framework:** None configured. Vitest provides `vi.mock()`, `vi.fn()`, `vi.spyOn()` which are available without extra deps, but no project-specific mocking pattern exists yet. + +**Relevant for future tests — the `logger.catch` mechanism** (`packages/common/src/logger.ts:189`): the engine routes all errors/warnings through the `logger` singleton rather than throwing. Tests can therefore assert error behavior via `logger.catch(fn)` which returns `{ ret, info }` (captured messages) instead of expecting exceptions. The logger also exposes `disable()`/`enable()` to silence output during tests. + +**What to Mock (prospective):** browser globals (`document`, `window`, `main`, `Mota`) since much engine code references them at module load (e.g. `packages/common/src/logger.ts:24-40` references `main.replayChecking` and `document`). Data-layer packages (`@user/data-base`, `@user/data-system`, `@user/data-common`) are designed to run in Node for replay verification, so they are the most unit-testable without a DOM. + +**What NOT to Mock (prospective):** the data-layer interfaces themselves (`IDataCommon`, `IDataBase`, `IDataSystem`) — they are designed to be instantiated in Node and driven through their interfaces. + +## Fixtures and Factories + +**Test Data:** No fixtures or factory helpers exist yet. Note the engine's `createXxx` factory convention (`dev.md` "模块初始化"): if a module needs initialization, expose a `createXxx` function wired up through `index.ts`. Test setup would follow this pattern rather than relying on module side effects (which are forbidden by `dev.md` "无副作用"). + +**Location:** `docs/test/` (for design docs); no fixture directory established. + +## Coverage + +**Requirements:** None enforced. No coverage script, no coverage provider installed, no CI coverage gate. + +**View Coverage:** not available without installing a `@vitest/coverage-*` provider and running `pnpm vitest --coverage`. + +## Test Types + +**Unit Tests:** +- Not yet written. The layered data-side packages (`@user/data-common`, `@user/data-base`, `@user/data-system`) are explicitly designed to run in Node ("数据端可在 node 环境中单独运行" — `dev.md` "双端分离"), making them the natural first targets for unit tests. + +**Integration Tests:** +- Not present. The `IDataCommon` / `IDataBase` / `IDataSystem` layer interfaces (`packages-user/data-common/src/types.ts`, `packages-user/data-system/src/types.ts`) form a seam where integration tests could assemble a full data-side stack in Node. + +**E2E Tests:** +- Not used. No Playwright/Cypress. The closest is the replay-verification system (`packages-user/data-common/src/replay/`) which validates that gameplay is deterministic, but it is a runtime feature, not a test harness. + +## CI + +- `.github/workflows/page.yml` only builds and deploys static content to GitHub Pages; it runs `pnpm build`, **not** tests. +- `.github/workflows/codeql.yml` runs CodeQL static analysis; **not** unit tests. +- There is currently **no CI step that runs the test suite** (and no committed test suite to run). + +## Common Patterns + +**Async Testing (prospective):** the engine is heavily `async`/`await`-based (see `ObjectMover.moveProgress` in `packages-user/data-common/src/common/mover.ts:626`). Use `await` inside `it` blocks and `Promise.withResolvers()`/`expect(...).resolves` patterns as appropriate. + +**Error Testing (prospective):** prefer `logger.catch(() => { ... })` and assert on `info` (the captured `{ level, message, code }[]`) rather than expecting thrown exceptions — the engine is designed to never throw in normal operation. + +**Determinism (the engine's own "testing" philosophy):** the replay system in `packages-user/data-common/src/replay/` exists to guarantee that a gameplay run is reproducible. When writing tests for game logic, favor deterministic data-driven inputs so results can be asserted exactly. + +--- + +*Testing analysis: 2026-09-07*