docs(phase-1): add research and validation strategy

This commit is contained in:
unanmed 2026-09-07 19:44:34 +08:00
parent f35590ca42
commit 6f06b68656
2 changed files with 523 additions and 0 deletions

View File

@ -0,0 +1,447 @@
# Phase 1: 事件系统 - Research
**Researched:** 2026-09-07
**Domain:** 数据驱动游戏引擎的脚本事件系统blockly 式低代码 → 序列化 `Statement[]` → 解释执行)
**Confidence:** HIGH
## Summary
本阶段将旧的 `ITrigger` 触发器体系(数字类型 + 工厂注册 + 收集器 + 四条件方法)整体替换为事件驱动体系:事件内容是 Anon Tokyo 解释器的 `Statement[]`,经 `interpreter.compile()` 编译为 `AnonTokyoExecutable`,执行时调用 `execute(param, env)`。事件绑定粒度是「对象 + 地图格」混合,对象/图块上只存 `eventStore` 的 id优先级 → id 的 Map不存事件本体。
关键发现:**接口层已被用户设计完成**(符合 PROJECT.md「接口设计由用户主导」的协作模型实现层落后于接口层。具体地`EventTrigger` 枚举、`IGameEvent`/`IReadonlyGameEvent`、`GameEvent`、`IGameEventStore`/`GameEventStore`、`IBlockEvent`/`IBlockEventParam`/`IBlockEventEnv`、`ITileBase.events`、`IMapBlockSaveBase.events` 均已定义;但实现侧 `MapTileBase`/`StaticTile`/`DynamicTile` 仍使用旧的 `triggers: Set<number>``CoreState` 未实例化 `eventStore``IDataCommon` 已声明该成员),`store/index.ts` 桶导出缺失 `eventStore`/`mapStore`。本阶段的实现工作就是把这些已设计的接口落地,删除旧 `ITrigger` 体系,并把英雄移动流程从「收集触发器」改为「收集并执行事件」。
**Primary recommendation:** 以「已设计接口落地 + 旧体系删除」为主线组织计划:先补齐 L0 存储与桶导出、实例化 `eventStore`,再实现 L1 图块/点位的事件绑定与存档(`events` Map + dirty 标记),然后在 L2 实现事件执行器(专门对象,含 before 类触发器的返回值语义),最后重写 `moverImpl` 的踩踏触发并删除旧 `ITrigger` 体系。
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| EVT-01 | 引擎提供事件系统的数据/序列化接口,支持 blockly 式低代码定义(编辑器在外部项目) | 已设计:`EventTrigger` 枚举 + `IGameEvent``Statement[]` → `compile()``AnonTokyoExecutable`+ `IGameEventStore`Anon Tokyo 解释器在 node_modules 中。需落地实现与桶导出 |
| EVT-02 | 事件系统能驱动简单场景的事件流程(踩踏触发、对话、开门) | 英雄移动流程 `moverImpl.ts` 的 enter/leave/hit/cannotEnter 需从旧 collector 重写为事件执行;对话/开门对应 `Statement` 内建函数调用与 `openDoor`/对话钩子 |
| EVT-03 | 事件系统定位为初学者抽象,仅覆盖简单场景,不为复杂场景追求通用表达 | `EventTrigger` 11 种内置枚举固定;长事件直接 `await`D-09不引入通用脚本表达式Anon Tokyo 已提供控制流但无需扩展) |
</phase_requirements>
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** 事件绑定粒度:对象 + 地图格混合,两者都可绑定事件
- **D-02:** 触发方式使用 `EventTrigger` 枚举11 种内置 + 可自行扩展),定义于 `packages-user/data-common/src/event/types.ts`
- **D-03:** 绑定对象分两类:点事件(不随脚下图块移动)与图块事件(随图块移动)
- **D-04:** 对象上存储的是 eventStore 的 id而非事件本身
- **D-05:** 图块默认事件(怪物→战斗、道具→捡拾、可自定义)也走 eventStoreid 引用),可被自定义事件覆盖
- **D-06:** 执行顺序:先点事件、后图块事件,各自按优先级从高到低执行
- **D-07:** 事件内容 = Anon Tokyo 的 `Statement[]``interpreter.compile()` 编译为 `AnonTokyoExecutable`
- **D-08:** 执行 = `execute(param, env)``param` 为本次执行参数、`env` 为本次执行的环境对象
- **D-09:** 长事件(如对话)直接用 `await` 等待,不需要序列化执行状态;现阶段不聚焦解释执行的细节
- **D-10:** before 类触发器OnBeforeBattle/OnBeforeOpenDoor/OnBeforeChangeFloor的返回值处理抽象一个专门对象负责事件执行含 before 返回值的语义)
- **D-11:** 事件存储 `GameEventStore`id → event不进存档代码生成的事件生成后立即执行、不落存档
- **D-12:** 点/图块事件的存档采用 dirty 标记:初始化时 dirty=false后续变动且与原始数据不同则标记为 true
- **D-13:** 旧的 `ITrigger` 体系在本次阶段一并删除
### the agent's Discretion
接口设计由用户主导PROJECT.md 协作模型AI 仅在已设计接口上实现、不自行设计接口。
### Deferred Ideas (OUT OF SCOPE)
None — 讨论保持在阶段范围内,未提出跨阶段新增能力。
</user_constraints>
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| 事件数据类型(`EventTrigger`/`IGameEvent`/`IReadonlyGameEvent` | L0 `@user/data-common` | — | 无依赖的序列化契约blockly 编辑器生成的 `Statement[]` 直接落入此层 |
| 事件存储 `GameEventStore`id → event | L0 `@user/data-common` | — | 纯 Map不进存档D-11可被任意高层引用 |
| 事件绑定到图块(`ITileBase.events` | L1 `@user/data-base` | — | 属于可存档地图数据,随静态/动态图块存读档 |
| 事件绑定到点位(`IMapPointRawData` | L1 `@user/data-base` | L0 | 点事件不随图块移动D-03存于地图原始点位数据 |
| 事件执行器专门对象before 返回值语义) | L2 `@user/data-system` | — | 依赖 `IStateBase`L1驱动战斗/开门/楼层切换D-10 |
| Anon Tokyo 解释器 + 内建函数(对话/开门/道具等) | L2 `@user/data-system` | L3 `@user/data-state` | 内建函数是游戏动作,需访问 L1/L2 状态;解释器按 `LanguageFeature` 注册 |
| 英雄移动踩踏触发集成 | L3 `@user/data-state``moverImpl` | L2 | `moverImpl``IHeroMoveTopImpl` 的默认实现,重写 enter/leave/hit/cannotEnter |
| 核心装配(`eventStore` 实例化) | L3 `@user/data-state``core.ts` | L0 | `CoreState` 构造器统一装配 L0→L3 |
## Standard Stack
本阶段**不新增外部依赖**。技术栈固定PROJECT.md Constraints事件系统的「库」是既有 in-repo 模块 + 已安装的 Anon Tokyo 解释器。
### Core
| Library / Module | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `anon-tokyo` | 0.0.0-alpha.0 | blockly 式低代码 `Statement[]` 的解释执行(`compile`/`exec` | 已在 `package.json` 锁定D-07/D-08 指定的解释器,用户选型 |
| `@user/data-common``event/types.ts` + `store/eventStore.ts` | in-repo | `EventTrigger`/`IGameEvent`/`GameEventStore` | 用户已设计的 L0 事件契约 |
| `@user/data-base``map/types.ts` | in-repo | `IBlockEvent*`、`ITileBase.events` | 用户已设计的 L1 图块事件绑定接口 |
### Supporting
| Library / Module | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `@user/data-system` | in-repo | 事件执行器(专门对象)+ 内建函数注册 | L2 层新建模块,替代旧 `trigger/` |
| `@user/data-state``core.ts`/`moverImpl.ts` | in-repo | 装配与移动触发集成 | 替换旧 `triggerRegistry`/`triggerCollector` |
| `vitest` | 4.0.18 | 单测框架 | 已配置 `pnpm test`Phase 6 才正式补单测,本阶段可选冒烟 |
| `vue-tsc` / `madge` / `eslint` | — | 类型 / 循环依赖 / lint 门禁 | `pnpm check:type` / `check:circular` / `lint:user` 为项目既定验证手段 |
**Installation:** 无新依赖安装。`anon-tokyo` 已在 `node_modules``.pnpm/anon-tokyo@0.0.0-alpha.0`),无需重新安装。
**Version verification:** `anon-tokyo` 版本经 `node_modules/anon-tokyo/package.json` 确认为 `0.0.0-alpha.0`,发布日期 2024-05-31npm registry发布者仓库 `github.com/tocque/AnonTokyo.git`。`vitest` 4.0.18 经根 `package.json` devDependencies 确认。
## Package Legitimacy Audit
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|---------|----------|-----|-----------|-------------|---------|-------------|
| anon-tokyo | npm | ~2.3 yrs (pub 2024-05-31) | 7/wk | github.com/tocque/AnonTokyo.git | SUS | 已锁定依赖(非本阶段新装),无需 checkpointflag 供用户知悉 |
**Packages removed due to [SLOP] verdict:** none
**Packages flagged as suspicious [SUS]:** `anon-tokyo` — 低下载量7/wk但它是用户本人的领域解释器`tocque` 仓库),已作为 `dependencies` 写入根 `package.json` 并安装于 `node_modules`。本阶段不安装新包,故无 `checkpoint:human-verify` 安装门槛;该依赖是 D-07 锁定的解释器,仅提示其为预发布 `alpha` 版本,`Statement` 类型签名以 `.d.ts` 为准。
*`anon-tokyo` 非通过 WebSearch/训练数据发现的新包——它已存在于 `package.json``node_modules`,且类型经直接读取 `node_modules/.pnpm/anon-tokyo@0.0.0-alpha.0/node_modules/anon-tokyo/dist/index.d.ts` 验证故不适用「WebSearch 发现包必须 tag `[ASSUMED]`」规则。*
## Architecture Patterns
### System Architecture Diagram
```
[外部 blockly 编辑器] (不在本仓库)
│ 生成序列化事件数据: Statement[]
┌───────────────────────────────────────────────┐
│ GameEventStore (L0, id → IReadonlyGameEvent) │ ← 不进存档 (D-11)
│ addEvent(id, event) / getEvent(id) │
└───────────────────────────────────────────────┘
▲ 对象/图块/点位仅存 eventStore id (D-04) │ getEvent(id)
│ ▼
┌───────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐
│ ITileBase │ │ IMapPointRawData (blockData) │ │ IGameEvent │
│ .events │ │ 点位事件 (不随图块移动, D-03) │ │ trigger │
│ Map<pri,id> │ └──────────────────────────────┘ │ rawEvent │
│ (随图块移动) │ │ compile() │
└───────────────┘ │ execute(p,e) │
│ └────────┬────────┘
▼ │
英雄移动 moverImpl (L3) │
enter/leave/hit/cannotEnter ──收集──▶ 事件执行器(专门对象,L2) ◀─┘
│ 先点事件、后图块事件 (D-06)
│ 各自按 priority 降序
│ before 触发器解析返回值 R (D-10)
┌─────────────────────────────────┐
│ AnonTokyoInterpreter (内建函数) │
│ compile(Statement[])→Executable │
│ exec(param, env) → Promise<R>
└─────────────────────────────────┘
┌─────────────────────┼──────────────────────┐
▼ ▼ ▼
对话(await) 开门 openDoor 战斗/楼层切换
(长事件, D-09) (OnBeforeOpenDoor (OnBeforeBattle/
返回是否可开门) OnBeforeChangeFloor)
```
### Recommended Project Structure
```
packages-user/
├── data-common/src/
│ ├── event/
│ │ ├── types.ts # EventTrigger + IGameEvent/IReadonlyGameEvent ✅已定义
│ │ ├── event.ts # GameEvent 实现 ✅已定义compile 缓存待修)
│ │ └── index.ts # 导出 ✅已定义
│ └── store/
│ ├── eventStore.ts # GameEventStore ✅已定义(桶导出缺失)
│ ├── types.ts # IGameEventStore ✅已定义
│ └── index.ts # ❌ 缺失 eventStore / mapStore 导出
├── data-base/src/map/
│ ├── types.ts # IBlockEvent* + ITileBase.events ✅已定义
│ ├── tile.ts # MapTileBase ❌ 仍用 triggers待改 events
│ ├── staticTile.ts # ❌ 存档仍用 triggers
│ └── dynamicTile.ts # ❌ 存档仍用 triggers
├── data-system/src/
│ ├── trigger/ # ❌ 旧 ITrigger 体系,本阶段删除 (D-13)
│ └── event/ # 新建:事件执行器(专门对象)+ 内建函数
└── data-state/src/
├── core.ts # ❌ 删除 triggerRegistry/triggerCollector实例化 eventStore
├── content/triggers.ts # ❌ 删除 ChangeFloorTrigger
└── hero/moverImpl.ts # ❌ 重写为事件执行
```
### Pattern 1: 事件执行器专门对象D-10
**What:** L2 层一个专门对象封装「收集事件 → 排序 → 执行 → before 返回值解析」。执行顺序先点事件后图块事件,各自按 `events` Map 的 keypriority降序D-06。对 `OnBeforeBattle`/`OnBeforeOpenDoor`/`OnBeforeChangeFloor` 三类 before 触发器,其 `execute` 返回的 `R` 语义为「是否继续执行对应动作」,由该对象解析后决定是否调用战斗/开门/楼层切换。
**When to use:** 英雄移动踩踏、战斗前、开门前、楼层切换前等一切事件派发入口统一走此对象,避免各调用点重复实现排序与 before 语义。
**Example接口层已就绪实现为规划目标:**
```typescript
// 已定义接口VERIFIED—— map/types.ts
export interface IReadonlyBlockEvent<R = void> extends IReadonlyGameEvent<
IBlockEventParam,
IBlockEventEnv,
R
> {}
// IReadonlyGameEvent<P,E,R>.execute(param: P, env: E): Promise<R>
// before 类事件: R 为「是否继续」,如 OnBeforeOpenDoor 返回 false 则不开门
```
### Pattern 2: 图块事件绑定 = 优先级 Map
**What:** 图块/点位上存 `ReadonlyMap<number, string>`key 为优先级数值越大越优先value 为 `eventStore` 中的 idD-04。已设计在 `ITileBase.events`,实现侧需把 `MapTileBase.triggers: Set<number>` 替换为 `events: Map<number, string>`
**When to use:** 所有静态/动态图块与点位的事件绑定;存档序列化时写入 `IMapBlockSaveBase.events`
**Example:**
```typescript
// map/types.tsVERIFIED
export interface IMapBlockSaveBase {
/** 当前图块的事件,键表示优先级,值表示事件在 `IGameEventStore` 中的 id */
readonly events: ReadonlyMap<number, string>;
}
// ITileBase 已有 setEvent(priority, eventId) / deleteEvent(priority) / clearEvent()
```
### Anti-Patterns to Avoid
- **事件本体存进图块/点位:** 违反 D-04/D-11会导致事件重复序列化进存档且无法统一覆盖默认事件D-05。图块上只能存 id。
- **把事件执行逻辑散落在 moverImpl 各处:** 违反 D-06/D-10排序与 before 返回值语义应集中在专门执行器对象moverImpl 只负责「收集 → 委托执行器」。
- **为解释执行细节过度设计:** 违反 D-09/EVT-03长事件直接 `await`,不要引入执行状态序列化/挂起恢复。
- **残留旧 `ITrigger` 引用:** D-13 要求一并删除,`core.ts` 的 `triggerRegistry`/`triggerCollector` 与 `data-system/trigger/` 必须清空,否则 `check:type`/`check:circular` 会失败。
- **手写解释器/表达式求值:** Anon Tokyo 已提供 `compile`/`exec`,不要自造。
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| blockly 脚本的解释执行 | 自定义 AST 解释器 | `anon-tokyo``AnonTokyoInterpreter.compile` → `exec` | 用户锁定的领域解释器含控制流if/loop/switch与异步调用语义 |
| 事件存储 | 自定义事件注册表 | `GameEventStore``store/eventStore.ts`id → event Map | 已实现 `addEvent`/`getEvent`D-11 明确不进存档 |
| 图块/点位事件绑定 | 自定义绑定结构 | `ReadonlyMap<number, string>`priority → id | 已设计在 `ITileBase.events`/`IMapBlockSaveBase.events` |
| 优先级排序 | 手写排序 + 冲突去重 | 执行器内按 Map key 降序遍历 | D-06 指定顺序,逻辑集中在专门对象 |
| 事件 id 生成 | 手写唯一 id | 沿用 `@user/` 既有的字符串 id 约定(图块/道具/地图均用字符串 id | 与 eventStore 的 `id → event` 键类型一致 |
**Key insight:** 本阶段的「库」几乎全部是 in-repo 已设计接口与已安装的 Anon Tokyo。唯一需要「新建」的是 L2 的事件执行器对象D-10 专门对象)与内建函数注册表——这是把「已设计接口」与「游戏动作」粘合的最小胶水层,不应扩展出额外的自定义框架。
## Runtime State Inventory
> 本阶段属 refactor删除旧 `ITrigger` 体系,替换为事件体系)。旧触发器体系为纯内存代码,无外部运行时状态承载。
| Category | Items Found | Action Required |
|----------|-------------|------------------|
| Stored data | None — 旧 `ITrigger` 仅存于代码/地图图块 `triggers: Set<number>`(内存,随存档序列化但无独立存储服务) | code edit存档格式 `ITrigger``events` Map |
| Live service config | None — 无外部 UI/DB 配置承载触发器类型 | 无 |
| OS-registered state | None — 无 Task Scheduler / 服务注册 | 无 |
| Secrets/env vars | None — 触发器不涉及密钥 | 无 |
| Build artifacts | None — `pnpm build:lib` 产物随源码重建,无带旧名的持久产物 | 无 |
**Nothing found in category各分类均已显式核对** 旧触发器体系是纯 TypeScript 代码 + 地图图块的 `triggers` 集合(经 `grep` 确认仅存在于 `data-system/src/trigger/`、`data-base/src/map/*`、`data-state/src/{core.ts,content/triggers.ts,hero/moverImpl.ts}`,无数据库/服务/OS/密钥/构建产物承载)。删除是纯代码编辑,不涉及数据迁移。
## Common Pitfalls
### Pitfall 1: 实现层仍引用旧 `triggers` 集合,与已设计的 `events` Map 脱节
**What goes wrong:** 接口 `ITileBase.events: ReadonlyMap<number, string>` 已设计,但 `MapTileBase`/`StaticTile`/`DynamicTile` 仍用 `triggers: Set<number>``addTrigger`/`clearTrigger`,导致 `check:type``implements` 不匹配,或存档仍写旧格式。
**Why it happens:** 用户先提交了接口层types.ts实现层未同步。
**How to avoid:** 计划中显式列出 `tile.ts`/`staticTile.ts`/`dynamicTile.ts` 的 `triggers``events` 改造项,含 `saveState`/`loadState`/`syncStaticTrigger` 的存档路径。
**Warning signs:** `vue-tsc --noEmit``MapTileBase` 缺少 `events`/`setEvent` 等成员;存档 JSON 中仍出现 `triggers` 字段。
### Pitfall 2: `eventStore` 声明了但未实例化,`CoreState` 类型不完整
**What goes wrong:** `IDataCommon` 已声明 `readonly eventStore: IGameEventStore``data-common/src/types.ts`),但 `CoreState` 构造器未创建 `GameEventStore``CoreState implements ICoreState` 缺成员。
**Why it happens:** 接口先行,实现滞后。
**How to avoid:** 计划中明确「`core.ts` 实例化 `GameEventStore` 并赋值 `this.eventStore`」;同时补 `store/index.ts` 导出 `eventStore`(当前桶导出缺 `eventStore`/`mapStore``core.ts` 甚至用相对路径 `'../../data-common/src/store/mapStore'` 绕过桶导出)。
**Warning signs:** `vue-tsc``CoreState` 缺少属性 `eventStore``import { GameEventStore } from '@user/data-common'` 解析失败。
### Pitfall 3: before 触发器的返回值语义散落各处
**What goes wrong:** `OnBeforeBattle`/`OnBeforeOpenDoor`/`OnBeforeChangeFloor` 的 `execute` 返回 `R`(是否继续),若由各调用点自行解析,会导致「是否开门/是否战斗/是否换层」的判定重复且不一致。
**Why it happens:** D-10 要求抽象专门对象,但实现时容易图快在各入口内联。
**How to avoid:** 事件执行器对象统一解析 before 返回值,战斗/开门/楼层切换入口只消费执行器的布尔结论。
**Warning signs:** 出现多处 `if (result === false) return` 分散在 `moverImpl`/战斗/开门代码中。
### Pitfall 4: `GameEvent.compile()` 未缓存导致重复编译
**What goes wrong:** `GameEvent.compile()` 返回 `interpreter.compile(this.rawEvent)` 但未写回 `this.compiled``execute()` 每次走 `compile` 分支,`compiled` 成员形同虚设(`setRaw` 里置 null 也无从触发缓存命中)。
**Why it happens:** 已实现但未完成缓存回写。
**How to avoid:** 计划中列入 `GameEvent.compile()` 缓存回写(`this.compiled = interpreter.compile(...)`),或由执行器持有 `AnonTokyoExecutable`
**Warning signs:** 每次事件执行都重新 compile性能与语义一致性问题
## Code Examples
### 事件定义与执行已定义接口VERIFIED
```typescript
// Source: packages-user/data-common/src/event/types.ts直接读取
export const enum EventTrigger {
None, // 无触发器,事件需要手动执行
OnTouch, // 当玩家触碰指定图块时触发,如果直接走入则不触发
OnEnter, // 当玩家进入指定图块时触发
OnLeave, // 当玩家离开指定图块时触发
OnBeforeBattle, // 与怪物战斗前触发,返回值表示是否与怪物战斗
OnAfterBattle, // 与怪物战斗后触发
OnBeforeOpenDoor, // 开启门之前触发,返回值表示是否能够成功开启门
OnAfterOpenDoor, // 开启门之后触发
OnAfterGetItem, // 成功拾取指定道具时触发
OnBeforeChangeFloor, // 楼层切换前触发,返回值表示是否执行切换操作
OnAfterChangeFloor // 楼层切换后触发
}
// IReadonlyGameEvent<P, E, R>:
// readonly interpreter: AnonTokyoInterpreter;
// readonly trigger: EventTrigger;
// readonly rawEvent: Statement[];
// readonly compiled: AnonTokyoExecutable | null;
// compile(): AnonTokyoExecutable | null;
// execute(param: P, env: E): Promise<R>;
```
### 解释器执行语义VERIFIED来自 .d.ts
```typescript
// Source: node_modules/.pnpm/anon-tokyo@0.0.0-alpha.0/node_modules/anon-tokyo/dist/index.d.ts
// class AnonTokyoInterpreter {
// constructor(lang: LanguageFeature);
// compile(script: Statement[]): AnonTokyoExecutable;
// exec(script: Statement[], parameters, env): Promise<unknown>;
// }
// class AnonTokyoExecutable {
// exec(parameters: Record<string, any>, env: Record<string, any>): Promise<unknown>;
// }
// interface LanguageFeature {
// builtInFunctions: BuiltInFunction[]; // { name, func(parameters, env) }
// globalFunctions: [name: string, Block][];
// }
```
### 事件存储已实现VERIFIED
```typescript
// Source: packages-user/data-common/src/store/eventStore.ts直接读取
export class GameEventStore implements IGameEventStore {
private readonly store: Map<string, IReadonlyGameEvent<...>> = new Map();
addEvent(id, event): void { /* 重复 id 时 logger.warn(170, id) 后覆盖 */ }
getEvent<P, E, R>(id): IReadonlyGameEvent<P, E, R> | null { ... }
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `ITrigger` 四条件方法(`onEnter`/`onLeave`/`onHit`/`onCannotEnter`+ 数字类型工厂注册 | `EventTrigger` 11 枚举 + 事件 `Statement[]` 解释执行 | 本阶段D-13 删除旧体系) | 触发语义从「方法」变为「数据」blockly 编辑器可直接序列化 |
| 触发器类型存为 `number[]``ITileRawData.trigger` | 图块/点位存 `eventStore` id`Map<number,string>` | 本阶段 | 默认事件可被自定义覆盖D-05事件内容与图块解耦 |
| 触发器实例随收集即时创建(`TriggerCollector.collect` | 事件预编译为 `AnonTokyoExecutable`,执行时 `execute(param, env)` | 本阶段 | 编译与执行分离,长事件可 `await`D-09 |
**Deprecated/outdated:**
- `@user/data-system/trigger/``ITrigger`/`ITriggerRegistry`/`ITriggerCollector`/`ITriggerCollection`):本阶段删除。
- `@user/data-state/src/content/triggers.ts``ChangeFloorTrigger`):本阶段删除,楼层切换改由 `OnBeforeChangeFloor`/`OnAfterChangeFloor` 事件承载。
- `ITileRawData.trigger: number[]` / `MapTileBase.triggers`:将被 `events: Map<number,string>` 取代。
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | 点事件绑定落在 `IMapPointRawData``blockData`),与图块事件 `ITileBase.events` 分离D-03「不随图块移动」语义 | Architecture | 若点事件应另建存储结构,`blockData` 改造范围需调整;已设计接口里 `IMapPointRawData` 目前仅有 `trigger?: number[]`/`changeFloor`,需用户确认点事件的承载字段 |
| A2 | Anon Tokyo 内建函数集(对话、开门、道具、战斗等 blockly 块对应的 `BuiltInFunction`的清单与命名由用户主导AI 只实现已定函数 | Standard Stack / Architecture | 若内建函数清单未定,执行器与 `LanguageFeature` 组装无法落地;这是 EVT-02对话/开门)的关键前提,需用户提供 |
| A3 | 事件执行器D-10 专门对象)归属 L2 `@user/data-system`,并在此新建模块(`data-system/src/event/` | Architecture | 若用户期望执行器在 data-stateL3模块位置与依赖方向需调整 |
| A4 | 英雄移动四个钩子enter/leave/hit/cannotEnter`EventTrigger` 的映射enter→`OnEnter`、hit`eventPass=false` 撞击)→`OnTouch`、leave→`OnLeave``cannotEnter` 无对应事件 | Architecture | 旧 `TriggerType` 四条件与新 `EventTrigger` 枚举并非一一对应,`cannotEnter` 是否保留、`OnTouch` 与 `OnEnter` 的边界需用户确认 |
| A5 | `GameEvent.compile()` 缓存回写与 `compiled` 成员语义需在实现时修正 | Pitfalls | 若不修正,事件重复编译,虽功能可用但浪费;若用户有更明确语义需遵循 |
| A6 | 本阶段无新增外部依赖,`anon-tokyo` 作为既有依赖直接使用 | Package Legitimacy | 若需升级/替换 `anon-tokyo` 版本,`Statement` 类型签名可能变化 |
**Note:** A1/A2/A4 是「接口设计由用户主导」下 AI 无法自行拍板的点,应在计划阶段前由用户确认或明确标注为待定。它们不阻塞「删除旧体系 + 落地已设计接口」的骨架工作,但阻塞 EVT-02 对话/开门的端到端验证。
## Open Questions
1. **点事件的承载字段A1**
- What we know: D-03 区分点事件(不随图块移动)与图块事件;图块事件已设计在 `ITileBase.events``IMapPointRawData` 目前仅有 `trigger?: number[]``changeFloor`
- What's unclear: 点事件的优先级 → eventStore id 应挂在 `IMapPointRawData` 的哪个字段(新增 `events`?),以及点事件的存档 dirty 标记如何与图层 dirty 协同。
- Recommendation: 计划中把「点事件字段」标为用户待定项,先落地图块事件路径,点事件按用户后续接口定义接入。
2. **Anon Tokyo 内建函数清单A2**
- What we know: `LanguageFeature.builtInFunctions: BuiltInFunction[]``BuiltInFunction.func(parameters, env)`;对话/开门/道具/战斗是 EVT-02 必需动作。
- What's unclear: 具体块名(如「对话」「开门」「给道具」「战斗」)与参数结构,由 blockly 编辑器与引擎约定。
- Recommendation: 请用户提供内建函数清单;在清单未定前,先实现「执行器 + eventStore + 图块绑定」骨架,内建函数注册表留空或仅含最小集(`openDoor`)。
3. **`EventTrigger` 与旧移动条件的映射A4**
- What we know: 旧 `TriggerType` = Enter/Leave/Hit/CannotEnter`EventTrigger` = None/OnTouch/OnEnter/OnLeave/OnBeforeBattle/OnAfterBattle/OnBeforeOpenDoor/OnAfterOpenDoor/OnAfterGetItem/OnBeforeChangeFloor/OnAfterChangeFloor。
- What's unclear: 撞击(`eventPass=false`)映射到 `OnTouch` 还是 `OnEnter``cannotEnter`(地形阻挡)在新体系中是否有对应触发。
- Recommendation: 请用户确认移动四钩子与 `EventTrigger` 的映射;`OnTouch` 注释「触碰指定图块,直接走入则不触发」暗示撞击即 `OnTouch`,需确认。
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js | 构建/运行 | ✓ | v22.18.0 | —(满足 `^20 || >=22` |
| pnpm | 依赖管理 | ✓ | 10.15.0 | —(满足 `>=10` |
| anon-tokyo | 事件解释执行 | ✓ | 0.0.0-alpha.0 | —(已安装) |
| vitest | 单测 | ✓ | 4.0.18 | —(`pnpm test` 可用) |
| vue-tsc | 类型门禁 | ✓ | 2.2.12 | — |
| madge | 循环依赖检查 | ✓ | 8.0.0 | — |
| eslint | lint 门禁 | ✓ | 9.39.4 | — |
**Missing dependencies with no fallback:** none
**Missing dependencies with fallback:** none
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | vitest 4.0.18 |
| Config file | none — 根 `package.json``"test": "vitest"`,无 `vitest.config.*` |
| Quick run command | `pnpm check:type`< 30s类型门禁 |
| Full suite command | `pnpm test`(当前无测试文件) |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| EVT-01 | `EventTrigger`/`IGameEvent`/`GameEventStore` 定义可序列化、`compile`/`execute` 可用 | unit可选 | `pnpm test``pnpm check:type` | ❌ Wave 0 |
| EVT-02 | 踩踏触发enter/hit驱动事件执行对话 await、开门 openDoor 链路 | integration / manual | `pnpm check:type` + 手动验证 | ❌ Wave 0 |
| EVT-03 | 抽象简单(无复杂通用表达式),旧 ITrigger 无残留引用 | staticgrep + type | `pnpm check:type` + `pnpm check:circular` | ✅ 门禁已存在 |
### Sampling Rate
- **Per task commit:** `pnpm check:type`(本项目既定门禁,类型缺失会立即暴露接口/实现脱节)
- **Per wave merge:** `pnpm check:circular`(循环依赖门禁)+ `pnpm lint:user`
- **Phase gate:** `pnpm check:type` 全绿 + 旧 `ITrigger` 引用清零(`grep -r "ITrigger\|TriggerCollector\|TriggerRegistry" packages-user` 无匹配)
### Wave 0 Gaps
- [ ] `packages-user/data-common/src/event/event.test.ts`(或等价)— 覆盖 `GameEvent``compile`/`execute`/缓存回写可选TEST-01 在 Phase 6
- [ ] `packages-user/data-common/src/store/eventStore.test.ts` — 覆盖 `addEvent`/`getEvent`(可选)
- [ ] 内建函数冒烟测试(对话/开门)— 依赖 A2 清单,清单未定前搁置
*(说明TEST-01 单测补齐为 Phase 6本阶段按项目惯例以 `check:type`/`check:circular`/`lint:user` 为主要验证门禁行为验证EVT-02 端到端)在清单确定后补可选 vitest 冒烟。)*
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | 引擎无账号体系 |
| V3 Session Management | no | 无会话概念(存档由 SaveSystem/Dexie 管理,不涉本阶段) |
| V4 Access Control | no | 无用户权限边界 |
| V5 Input Validation | yes | 事件 `Statement[]` 来自外部编辑器序列化数据,执行前须容忍非法块名/非法参数Anon Tokyo 内建函数应校验参数、缺省时 `logger.warn` 而非抛异常) |
| V6 Cryptography | no | 不涉密钥 |
### Known Threat Patterns for {Anon Tokyo 脚本事件}
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| 恶意/损坏的 `Statement[]` 引用未注册的内建函数名 | DoS / Tampering | `AnonTokyoInterpreter` 对未注册函数调用应安全失败;内建函数用白名单注册,未知名 `logger.warn` 后跳过 |
| 事件参数类型越界(如负数坐标、非法图块 id | Tampering | 内建函数内部用 `isNil`/边界校验(沿用 `logger` 数字错误码惯例),不抛异常中断游戏 |
| 事件执行无限循环/长任务阻塞主循环 | DoS | D-09 已限定长事件 `await`、不序列化状态Anon Tokyo 解释器自身控制流语义,超出本阶段范围 |
**Note:** 本阶段不处理用户输入的身份/授权,安全面集中在「外部编辑器生成的事件数据」这一不受信输入边界——事件数据与事件内建函数调用应视为不可信输入,遵循项目既有的 `logger` 数字错误码 + 非异常中断的处理惯例。
## Sources
### Primary (HIGH confidence) — 直接读取源码文件
- `packages-user/data-common/src/event/types.ts:7-74``EventTrigger` 枚举11 值)+ `IGameEvent`/`IReadonlyGameEvent``[VERIFIED]`
- `packages-user/data-common/src/event/event.ts:8-53``GameEvent` 实现(`[VERIFIED]`
- `packages-user/data-common/src/store/eventStore.ts:5-28` + `store/types.ts:338-360``GameEventStore`/`IGameEventStore``[VERIFIED]`
- `packages-user/data-common/src/types.ts:46-61``IDataCommon.eventStore` 声明(`[VERIFIED]`
- `packages-user/data-common/src/store/index.ts:1-3` — 桶导出缺失 `eventStore`/`mapStore``[VERIFIED]`
- `packages-user/data-base/src/map/types.ts:26-54,58-68,79-127``IBlockEvent*`、`IMapBlockSaveBase.events`、`ITileBase.events`/`setEvent`/`deleteEvent`/`clearEvent``[VERIFIED]`
- `packages-user/data-base/src/map/tile.ts:18,47-65` + `staticTile.ts` + `dynamicTile.ts` — 实现侧仍用 `triggers``[VERIFIED]`
- `packages-user/data-system/src/trigger/*` — 旧 `ITrigger` 体系待删除(`[VERIFIED]`
- `packages-user/data-state/src/core.ts:97-99,202-207``triggerRegistry`/`triggerCollector` 引用待删(`[VERIFIED]`
- `packages-user/data-state/src/content/triggers.ts:26-29` — 旧 `TriggerType.ChangeFloor``[VERIFIED]`
- `packages-user/data-state/src/hero/moverImpl.ts:141-184` — 移动钩子旧 collector 集成(`[VERIFIED]`
- `node_modules/.pnpm/anon-tokyo@0.0.0-alpha.0/node_modules/anon-tokyo/dist/index.d.ts``AnonTokyoInterpreter`/`AnonTokyoExecutable`/`Statement`/`LanguageFeature``[VERIFIED]`
- `node_modules/anon-tokyo/package.json` — 版本 0.0.0-alpha.0`[VERIFIED]`
### Secondary (MEDIUM confidence)
- `docs/dev/map/trigger.md`、`trigger-impl.md`、`system/trigger-refactor.md` — 旧 ITrigger 体系的历史设计文档(已确认其描述的是**待删除**的旧体系,非新事件系统)
- `.planning/phases/01-event/01-CONTEXT.md` + `01-DISCUSSION-LOG.md` — 用户决策 D-01..D-13 与讨论轨迹
### Tertiary (LOW confidence)
- 无 WebSearch 依赖——本阶段为内部代码重构,所有关键事实均来自直接读取源码,未引入外部检索结论。
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — 全部 in-repo 模块 + `anon-tokyo` 均经直接读取源码/`.d.ts`/`package.json` 验证
- Architecture: HIGH — 接口层已设计实现层差距经逐文件核对D-01..D-13 决策直接引用
- Pitfalls: HIGH — 四个陷阱均来自「接口已设计/实现滞后」的具体代码事实(`check:type` 可复现)
**Research date:** 2026-09-07
**Valid until:** 2026-09-21接口设计由用户主导若用户补充点事件字段/内建函数清单A1/A2/A4 相关结论需刷新)

View File

@ -0,0 +1,76 @@
---
phase: "1"
slug: "event"
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
status: draft
nyquist_compliant: false
wave_0_complete: false
created: "2026-09-07"
---
# Phase 1 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | vitest 4.0.18 |
| **Config file** | none — 根 `package.json` `"test": "vitest"`,无 `vitest.config.*` |
| **Quick run command** | `pnpm check:type` |
| **Full suite command** | `pnpm test` |
| **Estimated runtime** | ~30 seconds (quick) |
---
## Sampling Rate
- **After every task commit:** Run `pnpm check:type`
- **After every plan wave:** Run `pnpm check:circular` + `pnpm lint:user`
- **Before `/gsd-verify-work`:** Full suite must be green
- **Max feedback latency:** ~30 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| -01-01 | 01 | 1 | EVT-01 | — | 事件数据容忍非法块名/参数logger.warn 非抛异常) | unit | `pnpm check:type` | ❌ W0 | ⬜ pending |
| -01-02 | 01 | 1 | EVT-02 | T-1-01 | 踩踏触发→事件执行、对话 await、开门链路 | integration | `pnpm check:type` + 手动验证 | ❌ W0 | ⬜ pending |
| -01-03 | 01 | 1 | EVT-03 | — | 旧 ITrigger 无残留引用、无复杂通用表达式 | static | `pnpm check:type` + `pnpm check:circular` | ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `packages-user/data-common/src/event/event.test.ts``GameEvent` compile/execute/缓存回写冒烟可选TEST-01 在 Phase 6
- [ ] `packages-user/data-common/src/store/eventStore.test.ts``addEvent`/`getEvent`(可选)
*说明TEST-01 单测补齐为 Phase 6本阶段以 `check:type`/`check:circular`/`lint:user` 为主要验证门禁。*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| 对话/开门端到端事件链路 | EVT-02 | 依赖 A2 内建函数清单(暂缓),无自动断言 | 清单落地后补 vitest 冒烟或手动走查 |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < ~30s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending