mirror of
https://github.com/motajs/template.git
synced 2026-09-16 20:28:55 +08:00
docs: map existing codebase
This commit is contained in:
parent
7eeab32134
commit
e439200e4a
250
.planning/codebase/ARCHITECTURE.md
Normal file
250
.planning/codebase/ARCHITECTURE.md
Normal file
@ -0,0 +1,250 @@
|
||||
<!-- refreshed: 2026-09-07 -->
|
||||
# Architecture
|
||||
|
||||
**Analysis Date:** 2026-09-07
|
||||
|
||||
## System Overview
|
||||
|
||||
This is **`mota-ts`** — an HTML5 "魔塔" (Mota / Tower of the Sorcerer) game engine plus a sample game, organized as a **pnpm monorepo**. The core engine lives in `packages/` (scoped `@motajs/*`), user-facing game code lives in `packages-user/` (scoped `@user/*`), and the game entry point lives in `src/`. Legacy "mota-js" sample content (uncompiled game data and the old runtime) lives in `public/`.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Game Entry (src/) │
|
||||
│ src/main.ts (render/client entry) src/data.ts (data entry, replay) │
|
||||
│ src/App.vue (Vue UI root) src/content/ (JSONC game data) │
|
||||
└───────────────────────────────┬──────────────────────────────────────────┘
|
||||
│ depends on (@user/*)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ User Layer (packages-user/ → @user/*) │
|
||||
│ entry-client / entry-data ← composition root + module registry │
|
||||
│ client-base (系统层) client-modules (实现层) [render end] │
|
||||
│ data-common(L0) data-base(L1) data-system(L2) data-state(L3) [data end] │
|
||||
│ data-fallback / legacy-plugin-client / legacy-plugin-data │
|
||||
└───────────────────────────────┬──────────────────────────────────────────┘
|
||||
│ depends on (@motajs/*)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Core Engine (packages/ → @motajs/*) │
|
||||
│ common legacy-common types client client-base system │
|
||||
│ render render-vue animate audio loader legacy-* │
|
||||
└───────────────────────────────┬──────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Legacy runtime & assets (public/) + Build tooling (script/, vite) │
|
||||
│ public/main.js (legacy mota-js core), public/project/*, public/libs/* │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Dependency direction is strictly one-way:** `src` → `packages-user` → `packages`. `packages` and `packages-user` are independently buildable as libraries; `src` is the game entry code. This is stated in `dev.md`.
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
| Component (package) | Scope | Responsibility | Key file |
|
||||
|---------------------|-------|----------------|----------|
|
||||
| `src` (`@user/main`) | Game entry | Composes the game: `createGame()` + mounts Vue `App` | `src/main.ts` |
|
||||
| `@user/entry-client` | Composition | Registers client-side modules into `Mota`, then runs `create()` on each | `packages-user/entry-client/src/create.ts` |
|
||||
| `@user/entry-data` | Composition | Defines the `Mota` module registry (`IMota`/`MotaSystem`) and registers data-side modules | `packages-user/entry-data/src/mota.ts` |
|
||||
| `@user/client-base` | Render system layer | Render-side core: asset loading + material/autotile management | `packages-user/client-base/src/index.ts` |
|
||||
| `@user/client-modules` | Render impl layer | Concrete renderer, UI, weather, action (hotkey/move) | `packages-user/client-modules/src/index.ts` |
|
||||
| `@user/data-common` | Data Layer 0 | Common/utility interfaces (face, mover), event, replay, save, store | `packages-user/data-common/src/index.ts` |
|
||||
| `@user/data-base` | Data Layer 1 | Saveable game data: maps, hero, enemy, flag, loading/hook | `packages-user/data-base/src/index.ts` |
|
||||
| `@user/data-system` | Data Layer 2 | Game logic: combat/damage + trigger registry/collector | `packages-user/data-system/src/index.ts` |
|
||||
| `@user/data-state` | Data Layer 3 | `CoreState` singleton that wires 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*
|
||||
192
.planning/codebase/CONCERNS.md
Normal file
192
.planning/codebase/CONCERNS.md
Normal file
@ -0,0 +1,192 @@
|
||||
<!-- refreshed: 2026-09-07 -->
|
||||
# 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<string, any>` 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` (`<span innerHTML={...}>`), `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 = '<md5-hash>';` — 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*
|
||||
202
.planning/codebase/CONVENTIONS.md
Normal file
202
.planning/codebase/CONVENTIONS.md
Normal file
@ -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*
|
||||
108
.planning/codebase/INTEGRATIONS.md
Normal file
108
.planning/codebase/INTEGRATIONS.md
Normal file
@ -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=<tower>`) — 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*
|
||||
110
.planning/codebase/STACK.md
Normal file
110
.planning/codebase/STACK.md
Normal file
@ -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*
|
||||
217
.planning/codebase/STRUCTURE.md
Normal file
217
.planning/codebase/STRUCTURE.md
Normal file
@ -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/<package>/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>/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*
|
||||
113
.planning/codebase/TESTING.md
Normal file
113
.planning/codebase/TESTING.md
Normal file
@ -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*
|
||||
Loading…
Reference in New Issue
Block a user