template/.planning/codebase/CONVENTIONS.md

11 KiB
Raw Blame History

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:

{
    "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:

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:

if (!this.calculator) {
    logger.warn(106);
    return null;
}
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): ~4060 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):

/** 怪物生命值 */
hp: number;

Example jsDoc (method, multi-line):

/**
 * 创建只读信息对象
 * @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