17 KiB
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_modulesare 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
coreobject +patch.add(...)shims, producing pervasiveanycasts and@ts-expect-errormarkers. Comments literally say// @ts-expect-error todoand// 为了防止逆天样板出问题(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§"双端分离"); replacecore.*global access with typedIStateBase/IStateSystemcalls, deletingpatch.addshims as each legacy surface is migrated.
@ts-expect-error / @ts-ignore debt:
- Issue: ~43 suppression markers across
packagesandpackages-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.tsaugmentation instead of@ts-expect-error.
Widespread any types (explicitly allowed):
- Issue:
@typescript-eslint/no-explicit-anyis set to'off'ineslint.config.js:59, soanyis unchecked across the codebase (86 matches inpackages, 74 inpackages-user). The WebGL layer and the module registry are the heaviest users. - Files:
packages/render/src/core/gl2.ts(buffer/subtakeany),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 unnecessaryany, yet the registry and WebGL bindings are entirely untyped; regressions slip pastvue-tsc. - Fix approach: Re-enable
no-explicit-anyselectively (per-directory overrides) onceMota.require/registerand WebGL wrappers are typed; start withentry-data/src/mota.tsandrender/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 globalwindow.Motaregistry inpackages-user/entry-data/src/mota.ts:152and the staticGameStorage.listregistry inpackages/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/IClientCorethrough constructors/parameters per the TODOs; removeGameStorage.liststatic accumulation or scope it.
Oversized source files:
- Issue: Multiple files exceed ~800 lines with no
#regionsegmentation despitedev.mdrecommending 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.tsinto layer/draw/camera modules); use#regionas an intermediate step.
Planned deprecations/refactors (from task.md):
- Issue:
task.mdlists an explicit backlog: deprecategetMappedName,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.mdas 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
localStorageentry throws duringGameStorage.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
localStoragekey 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
.h5savefile 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)logsconsole.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 + '')intype()),packages-user/data-state/src/legacy/item.ts:40,48,55(new Function('state','item', legacy.itemEffect)etc.), and the gitignoredindex.cjs(root) which uses Node'svmmodule 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.cjsis gitignored and local-only (replay validation), butitem.ts/utils.tsrun in the player's browser on live game data. - Recommendations: Sandbox
new Functionbodies (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-htmlwith text interpolation where markup isn't required.
Hardcoded credentials in working tree:
- Risk:
user.tsat the repo root containsexport 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.tsis 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.ymlruns only the defaultjavascriptquery pack (queries: security-extended,security-and-qualityis commented out) and uses deprecatedactions/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
@v4actions, enablesecurity-extended,security-and-quality, and ensureeval/new Functionfindings are triaged.
Performance Bottlenecks
beforeunload/blur writes every storage instance:
- Problem: On every window blur (tab switch, devtools focus, dialog), all
GameStorageinstances are serialized and written tolocalStoragesynchronously. - Files:
packages/legacy-system/src/storage.ts:111-116 - Cause: Global
GameStorage.listregistry iterated without debounce; writes are synchronouslocalStorage.setItemcalls on the main thread. - Improvement path: Debounce/coalesce writes; only persist dirty storages (track a dirty flag in
setValue); avoidbluras a write trigger or userequestIdleCallback.
Large in-memory replay buffers:
- Problem:
ReplayArraymaintainscommandBuffer,paramBuffer,indexBufferasArrayBuffers 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 (
dexieis 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 newstateobject 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.addhandler before touching; keep old/new writes atomic (mirrorcore.status.hero.locandstate.hero.movertogether, as insetHeroLoc). - 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
anyWebGL bindings, manual buffer/sub-offset arithmetic, and shader compilation error paths (logger.jsoncodes 9/10/13/17/18/28/29). Rendering regressions are visually subtle. - Safe modification: Keep shader/layout changes isolated; validate against
logger.jsonerror 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/addedSaveablesmaps),packages-user/data-base/src/flag/field.ts(toStructured/fromStructuredreturnany),packages-user/client-modules/src/render/utils/saves.ts - Why fragile: Save schema is spread across
toStructured/fromStructuredmethods returningany; a field rename or type change silently corrupts saves. This is the "存档系统" refactor target intask.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), oneClientCore(client-modules/src/core.ts), one globalwindow.Motaregistry. - Limit: Cannot host two independent game sessions (e.g. editor + preview, or side-by-side replay) in one page; the static
GameStorage.listalso grows unbounded across instances. - Scaling path: Convert singletons to injected instances (the already-filed TODOs), and scope
GameStorage.listper game context.
Replay buffer memory growth:
- Current capacity: In-memory
ReplayArrayfor the whole session. - Limit: Multi-hour sessions produce large buffers; growth-by-multiplier causes repeated copies.
- Scaling path: Segmented/streaming storage backed by IndexedDB (
dexiedependency 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 globalcoreobject 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/IStateBaseand deprecate rawcoreaccess viapatch.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) andtypescript-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-tscandtypescript-eslintofficially support.
Missing Critical Features
Automated test suite (blocking):
- Problem:
vitestis configured (package.json:8,"test": "vitest") but zero test files exist anywhere inpackages,packages-user,src, orscript(no*.test.*/*.spec.*/__tests__), and there is novitest.config.*. - Blocks: Safe refactoring of the save system, pathfinding, and legacy bridge; regression prevention for render/data split; the
pnpm testscript currently does nothing useful.
CI for lint/type/build/tests:
- Problem:
.github/workflows/only containscodeql.yml(security scan) andpage.yml(docs deploy). No workflow runspnpm lint:packages,pnpm lint:user,pnpm check:type,pnpm check:circular, orpnpm test. - Blocks: Automated gating of the quality rules documented in
dev.md; nothing prevents a bad commit from reachingmaster.
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.mdsave/pathfinding items and the in-flight map interface refactor on branchrefactor/data) is unguarded against regressions; the data/render split's "data side must run headlessly in Node" guarantee (seedev.md§"双端分离") is untested and can silently regress. - Priority: High — add at minimum unit tests for
data-common(replay, save) anddata-system(combat), which are the headless, deterministic, high-value layers.
Concerns audit: 2026-09-07