From 41cdd187610103fd95e3469d0fd64698e55303ef Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 7 Sep 2026 22:08:35 +0800 Subject: [PATCH] docs(01): create phase 1 plans --- .planning/REQUIREMENTS.md | 2 +- .planning/ROADMAP.md | 35 +- .planning/STATE.md | 12 +- .planning/phases/01-event/01-01-PLAN.md | 253 +++++++++++++ .planning/phases/01-event/01-02-PLAN.md | 253 +++++++++++++ .planning/phases/01-event/01-03-PLAN.md | 195 ++++++++++ .planning/phases/01-event/01-PATTERNS.md | 440 +++++++++++++++++++++++ .planning/phases/01-event/01-RESEARCH.md | 7 +- 8 files changed, 1187 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/01-event/01-01-PLAN.md create mode 100644 .planning/phases/01-event/01-02-PLAN.md create mode 100644 .planning/phases/01-event/01-03-PLAN.md create mode 100644 .planning/phases/01-event/01-PATTERNS.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 39a2031..b63c646 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -10,7 +10,7 @@ Requirements for the engine's completion. Each maps to roadmap phases. ### 事件系统(Event) - [ ] **EVT-01**: 引擎提供事件系统的数据/序列化接口,支持 blockly 式低代码定义(编辑器在外部项目) -- [ ] **EVT-02**: 事件系统能驱动简单场景的事件流程(如踩踏触发、对话、开门) +- [ ] **EVT-02**: 事件系统能驱动简单场景的事件流程(踩踏触发链路;对话、开门依赖 A2 内建函数清单,延后到收尾工作) - [ ] **EVT-03**: 事件系统定位为初学者抽象,仅覆盖简单场景,不为复杂场景追求通用表达 ### 寻路(Pathfinding) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 587180e..b6803f8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -7,6 +7,7 @@ ## Phases **Phase Numbering:** + - Integer phases (1, 2, 3): Planned milestone work - Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) @@ -22,70 +23,100 @@ Decimal phases appear between their surrounding integers in numeric order. ## Phase Details ### Phase 1: 事件系统 + **Goal**: 引擎能以 blockly 式低代码定义事件,并驱动简单场景的事件流程 **Depends on**: Nothing (first phase) **Requirements**: EVT-01, EVT-02, EVT-03 **Success Criteria** (what must be TRUE): + 1. 开发者能通过数据/序列化接口定义事件(blockly 式低代码可序列化为引擎事件数据,编辑器在外部项目) 2. 引擎能执行踩踏触发事件(角色踩上地板触发对应事件) - 3. 引擎能执行对话事件与开门事件,打通简单场景事件全链路 + 3. 引擎能执行踩踏触发事件驱动的事件执行链路(对话/开门依赖 A2 内建函数清单,延后到收尾工作) 4. 事件系统保持面向初学者的简单抽象,未引入复杂场景的通用表达能力 -**Plans**: TBD + +**Plans**: 3 plans +Plans: +**Wave 1** + +- [ ] 01-01-PLAN.md — L0/L1 事件数据契约落地 + 图块/点位事件视图 + 存读档迁移 + +**Wave 2** *(blocked on Wave 1 completion)* + +- [ ] 01-02-PLAN.md — 执行器接口用户确认(checkpoint)+ 执行器实现 + 删除旧 ITrigger(D-13)+ CoreState 装配 + +**Wave 3** *(blocked on Wave 2 completion)* + +- [ ] 01-03-PLAN.md — moverImpl 踩踏触发重写为事件执行 + 阶段验收 ### Phase 2: 寻路系统 + **Goal**: 引擎支持自动寻路,移动端点击地图即可触发移动 **Depends on**: Phase 1 **Requirements**: PATH-01, PATH-02 **Success Criteria** (what must be TRUE): + 1. 角色能在地图上自动寻路移动到指定格 2. 移动端点击地图上的可达格时,角色自动寻路移动到该格 3. 寻路正确避开不可通行格(碰撞/障碍/墙体) + **Plans**: TBD **UI hint**: yes ### Phase 3: 数据端完成 + **Goal**: 数据端 L0–L3 接口实现完成,数据层各系统可用并可在 Node 环境独立运行回放验证 **Depends on**: Phase 2 **Requirements**: DATA-01 **Success Criteria** (what must be TRUE): + 1. 用户设计的 L0–L3 数据层接口全部落地,地图/角色/敌人/flag/战斗/触发器/存档/回放各系统可用 2. 数据端可在 Node 环境独立运行回放验证,无 DOM 依赖 3. 数据端与渲染端保持双端分离,渲染相关代码经 `r()`/`rf()` 门控或走 hook,渲染端不向数据端推送更新 4. 接口设计由用户主导,AI 仅负责实现 + **Plans**: TBD ### Phase 4: 渲染适配与双布局 + **Goal**: 渲染端通过新数据层接口驱动,并同时支持移动端与桌面端布局 **Depends on**: Phase 3 **Requirements**: REND-01, REND-02 **Success Criteria** (what must be TRUE): + 1. 用新数据层接口加载一张地图后,已重构的渲染端能正确渲染该地图场景 2. 桌面端布局下,地图、角色、界面元素正常显示并可操作 3. 移动端(窄屏)布局下,同一场景正常显示且可操作 4. 数据端与渲染端保持双端分离——数据端无 DOM,仍可在 Node 环境跑回放验证 + **Plans**: TBD **UI hint**: yes ### Phase 5: Legacy 移植 + **Goal**: 删除被新接口覆盖的 legacy 系统,迁移仍需要的内容 **Depends on**: Phases 1-4 **Requirements**: LEGACY-01, LEGACY-02 **Success Criteria** (what must be TRUE): + 1. 被新接口覆盖的 legacy 系统已删除,代码中无残留引用 2. 仍需要的 legacy 内容已迁移到新接口 3. 仅当无新接口覆盖时才新增接口,且接口设计经用户 review 4. 移植后引擎仍能完整跑通一部魔塔,无功能回归 + **Plans**: TBD ### Phase 6: 单元测试 + **Goal**: 为核心系统(数据层等)补齐单元测试 **Depends on**: Phases 1-5 **Requirements**: TEST-01 **Success Criteria** (what must be TRUE): + 1. 核心数据层系统有单元测试覆盖 2. 测试覆盖关键行为(战斗伤害、触发器、寻路、事件等) 3. 测试在本地可运行且全部通过 4. 测试由 AI 编写,经用户 review 后提交 + **Plans**: TBD ## Progress diff --git a/.planning/STATE.md b/.planning/STATE.md index 5ceab5b..a069348 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: "1.0" current_phase: 1 current_phase_name: 事件系统 -status: planning +status: executing stopped_at: Phase 1 context gathered -last_updated: "2026-09-07T10:26:54.629Z" +last_updated: "2026-09-07T14:06:31.386Z" last_activity: 2026-09-07 last_activity_desc: Roadmap revised (6 phases, 11 requirements mapped) -state_head: 9cfe7ebea827cc13fbc2cac6d1440aa96db36a80 +state_head: 6f06b686560de391f0c76bf0b0e72658a94e3e37 progress: total_phases: 6 completed_phases: 0 - total_plans: 0 + total_plans: 3 completed_plans: 0 percent: 0 --- @@ -27,9 +27,9 @@ See: .planning/PROJECT.md (updated 2026-09-07) ## Current Position -Phase: 1 of 6 (事件系统) +Phase: 1 (事件系统) — READY TO EXECUTE Plan: 0 of TBD in current phase -Status: Ready to plan +Status: Ready to execute Last activity: 2026-09-07 — Roadmap revised (6 phases, 11 requirements mapped) Progress: [░░░░░░░░░░] 0% diff --git a/.planning/phases/01-event/01-01-PLAN.md b/.planning/phases/01-event/01-01-PLAN.md new file mode 100644 index 0000000..2e5cf17 --- /dev/null +++ b/.planning/phases/01-event/01-01-PLAN.md @@ -0,0 +1,253 @@ +--- +phase: 01-event +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-common/src/event/event.ts + - packages-user/data-common/src/store/index.ts + - packages-user/data-base/src/map/eventView.ts + - packages-user/data-base/src/map/tile.ts + - packages-user/data-base/src/map/staticTile.ts + - packages-user/data-base/src/map/dynamicTile.ts + - packages-user/data-base/src/map/mapLayer.ts + - packages-user/data-base/src/map/mapState.ts +autonomous: true +requirements: [EVT-01, EVT-03] + +estimate: + tokens: 70000 + raw_tokens: 35000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "开发者能通过 IGameEvent 数据接口定义事件(EventTrigger 枚举,D-02),GameEvent.compile() 编译一次后写回 compiled 缓存(D-07)" + - "GameEventStore 与 MapStore 均可通过桶导出 import { GameEventStore, MapStore } from '@user/data-common' 访问;事件存储走 id 引用、不进存档(D-11)" + - "图块实例通过 tileEvent() 返回 ILayerEventView,通过 pointEvent() 返回所在位置的 ILayerEventView 或 null,对象+地图格混合绑定(D-01/D-03/D-04)" + - "ILayerEventView 的 set/delete/clear/get/markPure/dirty 全部可用,set 遇到重复优先级走 logger.warn(136, ...)(D-06/D-12)" + - "StaticTile/DynamicTile 的 saveState/loadState 读写 events(ReadonlyMap)而非 triggers(IMapBlockSaveBase.events),图块默认事件同样走 eventStore id 引用(D-05)" + - "MapLayer.event(x,y)/getPointEvent(x,y) 返回点事件视图,点事件不随图块移动(D-03)" + - "MapState.fromRaw 从 raw.events 装配点位事件(不再读 blockData/trigger)" + artifacts: + - packages-user/data-base/src/map/eventView.ts + - "store/index.ts 的 mapStore 桶导出" + key_links: + - "MapTileBase.tileEvent()/pointEvent() 连接 LayerEventView 与 IMapLayer.event(x,y)" + - "StaticTile/DynamicTile.saveState/loadState 连接 tileEvent().dirty()/get()/set() 与 IMapBlockSaveBase.events" + - "MapState.fromRaw 连接 IMapRawData.events 与 layer 静态图块 tileEvent().set()" +--- + + +把用户已设计的 L0/L1 事件数据接口落地为实现:L0 的事件编译缓存与存储桶导出,L1 的图块/点位事件视图、存读档迁移、点位事件装配。本计划不触碰 L2 事件执行器(执行器接口已由用户设计完成,Plan 02 直接实现)。 + +Purpose: 打通「事件定义 → 编译 → 图块/点位绑定 → 存读档」的 L0→L1 垂直切片,为 Plan 02 的执行器(实现用户已设计的 IGameEventExecutor/IGameEventSystem)与 Plan 03 的踩踏触发重写提供已落地的数据层骨架。 +Output: `GameEvent.compile` 缓存回写、`store/index.ts` 导出 mapStore、`LayerEventView` 实现、`MapTileBase.tileEvent/pointEvent`、`StaticTile/DynamicTile` 存读档 `events`、`MapLayer.event/getPointEvent` + `syncStaticEvent`、`MapState.fromRaw` 从 `raw.events` 装配。 + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-event/01-CONTEXT.md +@.planning/phases/01-event/01-RESEARCH.md +@.planning/phases/01-event/01-PATTERNS.md + +# 已设计接口(只读,禁止修改) +@packages-user/data-common/src/event/types.ts +@packages-user/data-common/src/event/event.ts +@packages-user/data-common/src/store/types.ts +@packages-user/data-common/src/store/eventStore.ts +@packages-user/data-common/src/store/mapStore.ts +@packages-user/data-base/src/map/types.ts +@packages-user/data-system/src/event/types.ts + +# 本计划要修改/新建的实现文件 +@packages-user/data-base/src/map/tile.ts +@packages-user/data-base/src/map/staticTile.ts +@packages-user/data-base/src/map/dynamicTile.ts +@packages-user/data-base/src/map/mapLayer.ts +@packages-user/data-base/src/map/mapState.ts +@packages-user/data-common/src/store/index.ts +@packages-user/data-common/src/event/event.ts + + + + + + 端到端切片:事件「定义→编译→图块/点位绑定」路径跑通(L0 编译缓存 + L1 事件视图) + + packages-user/data-common/src/event/event.ts + packages-user/data-common/src/store/index.ts + packages-user/data-base/src/map/eventView.ts + packages-user/data-base/src/map/tile.ts + packages-user/data-base/src/map/mapLayer.ts + + + - packages-user/data-common/src/event/types.ts(EventTrigger 枚举 + IGameEvent/IReadonlyGameEvent,只读) + - packages-user/data-common/src/event/event.ts(GameEvent 现状) + - packages-user/data-common/src/store/eventStore.ts(GameEventStore 现状) + - packages-user/data-common/src/store/index.ts(桶导出现状:缺 mapStore) + - packages-user/data-base/src/map/types.ts(IReadonlyEventView/ILayerEventView/ITileBase/IReadonlyTileBase/IMapLayer.event/getPointEvent/IMapBlockSaveBase.events,只读) + - packages-user/data-base/src/map/tile.ts(MapTileBase 现状:triggers/block 待清) + - packages-user/data-base/src/map/mapLayer.ts(现状:syncStaticTrigger/createDynamic/transferTo*) + - .planning/phases/01-event/01-PATTERNS.md 的 Pattern 2/5/9 与「错误处理」段(logger 数字错误码、isNil、getOrInsertComputed) + + + 实现一条「事件定义到图块/点位绑定」的最小可运行路径,逐层如下(只实现已设计接口,不改 types.ts): + + L0 — `packages-user/data-common/src/event/event.ts`:把 `GameEvent.compile()` 由「直接返回 `this.interpreter.compile(this.rawEvent)`」改为「先 `this.compiled = this.interpreter.compile(this.rawEvent)` 再 `return this.compiled`」,使 `execute()` 的 `if (this.compiled)` 分支真正命中缓存(setRaw 已正确置 `this.compiled = null`,勿改)。 + + L0 — `packages-user/data-common/src/store/index.ts`:补一行桶导出 `export * from './mapStore';`(当前缺 mapStore,导致 core.ts 只能相对路径绕过)。保持现有 eventStore/itemStore/tileStore/types 导出不变。 + + L1 — `packages-user/data-base/src/map/eventView.ts`(新建):实现 `class LayerEventView implements ILayerEventView`,内部持有 `private readonly store: Map` 与一个 `private reference: Map | null`(dirty 基准,D-12)。方法:`get()` 返回 `ReadonlyMap`;`set(priority, event)` 遇 `store.has(priority)` 时 `logger.warn(136, priority.toString())` 后覆盖;`delete`/`clear` 直接操作 store;`markPure()` 把当前 store 内容拷贝进 reference;`dirty()` 在 reference 为 null 时对空 store 判断,否则逐项比对 store 与 reference。私有成员必须加 jsDoc(code.md 规则 8)。 + + L1 — `packages-user/data-base/src/map/tile.ts`:删除 `triggers: Set | null` 成员及 `addTrigger`/`deleteTrigger`/`clearTrigger`/`useEmptyTrigger`,新增 `private tileEvents: LayerEventView`(构造器初始化 `new LayerEventView()` 并以空 Map `markPure()`),实现 `tileEvent(): ILayerEventView` 返回该视图,实现 `pointEvent(): ILayerEventView | null` 委托 `this.layer.event(this.locator.x, this.locator.y)`。移除引用未定义类型 `IMapPointRawData` 的 `block()` 方法与对应 import(该类型已不在 @user/data-common,见「Blockers」)。`MapTileBase` 现需 `import { LayerEventView } from './eventView';` 且 `implements ITileBase` 应补齐 `tileEvent`/`pointEvent`。 + + L1 — `packages-user/data-base/src/map/mapLayer.ts`:新增 `event(x, y): ILayerEventView | null` 与 `getPointEvent(x, y): ReadonlyMap | null`。点事件视图需要一份图层级存储(点事件不随图块移动,D-03):新增私有成员 `private readonly pointEvents: Map>`(外层 key = y,内层 key = x),`event(x, y)` 惰性创建并返回 `ILayerEventView`(`getOrInsertComputed` 惯例);`getPointEvent` 返回 `event(x, y)?.get() ?? null`。私有成员加 jsDoc。 + + 约束:禁止出现 `as` 关键字、禁止 `value === undefined`(用 `isNil`)、公共方法必须先在接口定义再 implements、对象成员用接口类型、所有私有成员加 jsDoc。 + + + - `grep -c "mapStore" packages-user/data-common/src/store/index.ts` 命中 `export * from './mapStore'` + - `grep -n "triggers" packages-user/data-base/src/map/tile.ts` 无 `Set` 命中 + - `grep -n "tileEvent\|pointEvent" packages-user/data-base/src/map/tile.ts` 各至少命中一次 + - `grep -n "event(x: number, y: number)" packages-user/data-base/src/map/mapLayer.ts` 命中一次 + - `grep -n "getPointEvent(x: number, y: number)" packages-user/data-base/src/map/mapLayer.ts` 命中一次 + + + pnpm check:type 2>&1 | Select-String -Pattern "eventView\.ts|map/tile\.ts|map/mapLayer\.ts|event/event\.ts|store/index\.ts" + 命中以上 L0/L1 文件的任何 TS 错误(有输出即失败)。staticTile/dynamicTile/mapState 的 triggers 迁移在 Task 2/3 处理,core.ts 缺失 eventStore/eventSystem、moverImpl 引用旧 trigger、client-modules、tileStore.getTrigger 等属跨计划/阶段外残差,不计入本任务失败 + + 一条事件从 IGameEvent 定义、经 compile 缓存、经 tileEvent/pointEvent 视图绑定到图块/点位的路径,类型层面成立;旧 triggers 成员已从 MapTileBase 移除。 + + + + 图块事件存读档:StaticTile/DynamicTile 从 triggers 迁移到 events + + packages-user/data-base/src/map/staticTile.ts + packages-user/data-base/src/map/dynamicTile.ts + + + - packages-user/data-base/src/map/types.ts(IMapBlockSaveBase.events / IStaticBlockSave / IDynamicBlockSave,只读) + - packages-user/data-base/src/map/staticTile.ts(现状) + - packages-user/data-base/src/map/dynamicTile.ts(现状) + - packages-user/data-base/src/map/tile.ts(Task 1 改造后的 tileEvent/pointEvent) + + + 把两个类的存读档字段从旧 `triggers` 迁到 `events`(`IMapBlockSaveBase.events?: ReadonlyMap`),结构保持不变、仅换字段与存取方式: + + `staticTile.ts`:`shouldSave()` 由 `return !!this.triggers` 改为 `return this.tileEvent().dirty();`;`saveState()` 由 `save.triggers = this.triggers` 改为「当 `this.tileEvent().dirty()` 为 true 时 `save.events = this.tileEvent().get()`」;`loadState(save)` 由 `save.triggers.forEach(v => this.addTrigger(v))` 改为「读 `save.events`:无则 `this.tileEvent().clear()`,有则遍历 `[priority, id]` 调用 `this.tileEvent().set(priority, id)`,读完 `this.tileEvent().markPure()`」。 + + `dynamicTile.ts`:`saveState()` 同样把 `save.triggers = this.triggers` 改为 dirty 时 `save.events = this.tileEvent().get()`;`loadState(save)` 同上迁移到 `tileEvent().set/clear/markPure`。 + + 注意:`this.triggers` / `useEmptyTrigger` / `addTrigger` / `clearTrigger` 已在 Task 1 从 `MapTileBase` 移除,本任务不得再引用它们;`save` 对象字面量继续用接口类型 `IStaticBlockSave`/`IDynamicBlockSave`(其 `events` 为可选只读字段)。禁用 `as`、私有成员加 jsDoc。 + + + - `grep -n "triggers" packages-user/data-base/src/map/staticTile.ts packages-user/data-base/src/map/dynamicTile.ts` 无命中 + - `grep -n "\.events" packages-user/data-base/src/map/staticTile.ts packages-user/data-base/src/map/dynamicTile.ts` 各至少命中一次 + - `grep -n "tileEvent()" packages-user/data-base/src/map/staticTile.ts packages-user/data-base/src/map/dynamicTile.ts` 各至少命中一次 + + + pnpm check:type 2>&1 | Select-String -Pattern "map/staticTile\.ts|map/dynamicTile\.ts" + 命中 staticTile.ts/dynamicTile.ts 的任何 TS 错误(有输出即失败)。mapState 的迁移在 Task 3,core.ts/moverImpl/client-modules/tileStore.getTrigger 属跨计划/阶段外残差,不计入本任务失败 + + StaticTile/DynamicTile 存读档写 `events` Map(dirty 门控),不再引用 `triggers` 或已删除的 trigger 方法。 + + + + 点位事件装配与图层事件同步:MapLayer 静态/动态转换 + MapState.fromRaw + + packages-user/data-base/src/map/mapLayer.ts + packages-user/data-base/src/map/mapState.ts + + + - packages-user/data-base/src/map/types.ts(IMapLayer.event/getPointEvent/transferToDynamic/transferToStatic 的 keepEvent 参数,只读) + - packages-user/data-common/src/store/types.ts(IMapRawData.events 的 Record>> 结构,只读) + - packages-user/data-base/src/map/mapLayer.ts(Task 1 已加 event/getPointEvent,本任务补 syncStaticEvent/transferTo*) + - packages-user/data-base/src/map/mapState.ts(现状:fromRaw 中 raw.blockData/data.trigger/addTrigger) + + + `mapLayer.ts`(在 Task 1 已加 event/getPointEvent 基础上继续): + 1. `createDynamic(num, x, y)`:把 `location.static.triggers` 的复制改为「从 `location.static.tileEvent()` 视图读取 id Map,逐项 `tile.tileEvent().set(priority, id)`」。 + 2. `syncStaticTrigger(tile, keepTrigger)` 重命名为 `syncStaticEvent(tile, keepEvent)`:`keepEvent` 为 true 时把 `tile.tileEvent().get()` 逐项 `staticTile.tileEvent().set(priority, id)`,否则 `staticTile.tileEvent().clear()`;`transferToStatic`/`transferToStaticIfSafe` 同步改用 `keepEvent` 参数(参数名按 map/types.ts 已定义的 `keepEvent`,替换原 `keepTrigger`)。 + 3. `transferToDynamic(x, y, keepEvent)` 中 `staticTile.clearTrigger()` 改为 `staticTile.tileEvent().clear()`,`tile.clearTrigger()` 改为 `tile.tileEvent().clear()`。 + + `mapState.ts` `fromRaw`:把「读 `raw.blockData[z]` + `data.trigger` + `addTrigger`」整段替换为「读 `raw.events[z]`:对每层 z、每个 pos 索引、每个 `[priority, id]`,由 pos 索引算出 x/y(`index % raw.width` / `Math.floor(index / raw.width)`),经 `layer.getLocationData(x, y)` 拿到静态图块,调用 `static.tileEvent().set(priority, id)`」。 + + 禁用 `as`、`isNil` 判空、logger 数字错误码、私有成员 jsDoc。 + + + - `grep -n "syncStaticTrigger" packages-user/data-base/src/map/mapLayer.ts` 无命中(已改名 syncStaticEvent) + - `grep -n "blockData\|\.trigger\b" packages-user/data-base/src/map/mapState.ts` 无命中 + - `grep -n "raw\.events" packages-user/data-base/src/map/mapState.ts` 命中一次 + - `grep -n "keepEvent" packages-user/data-base/src/map/mapLayer.ts` 命中 + + + pnpm check:type 2>&1 | Select-String -Pattern "map/mapLayer\.ts|map/mapState\.ts" + 命中 mapLayer.ts/mapState.ts 的任何 TS 错误(有输出即失败)。core.ts/moverImpl/client-modules/tileStore.getTrigger 属跨计划/阶段外残差,不计入本任务失败 + + 点事件视图经 `IMapLayer.event/getPointEvent` 落地,静态/动态转换同步 `events`,`MapState.fromRaw` 从 `raw.events` 装配点位事件。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 地图原始数据 `IMapRawData.events` → 图块/点位事件绑定 | 优先级与事件 id 来自外部序列化,可能是未知/损坏的 id | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|----------------| +| T-01-01 | Tampering | `LayerEventView.set(priority, id)` 收到重复优先级 | low | mitigate | `logger.warn(136, priority.toString())` 后覆盖,不抛异常 | +| T-01-02 | Tampering | `MapState.fromRaw` 读取 `raw.events` 中损坏的 pos 索引/优先级 | low | mitigate | `getLocationData(x,y)` 越界返回 null 时跳过;优先级/坐标为数值,`isNaN` 时走 logger 错误码跳过 | +| T-01-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本计划无新依赖安装;`anon-tokyo@0.0.0-alpha.0` 为已锁定既有依赖(RESEARCH Package Legitimacy Audit 标记 SUS,仅提示不阻断) | + + + +本计划验证聚焦「本计划涉及文件类型成立 + 旧 triggers 字段在本计划涉及文件内清零」。整体阶段绿门(`check:type` + 旧 ITrigger 引用清零)在 Plan 03 统一验收;执行器装配在 Plan 02 处理。 + + + +- `GameEvent.compile()` 写回 `compiled` 缓存;`store/index.ts` 导出 `mapStore` +- `LayerEventView` 实现 `ILayerEventView`(含 dirty/markPure) +- `MapTileBase` 提供 `tileEvent()`/`pointEvent()`,移除 `triggers`/`block()` +- `StaticTile`/`DynamicTile` 存读档 `events`;`MapLayer.event/getPointEvent` + `syncStaticEvent` + `MapState.fromRaw` 落地 + + + +Create `.planning/phases/01-event/01-01-SUMMARY.md` when done + + +## Artifacts this phase produces + +- `GameEvent.compile()` 缓存回写逻辑(修改 `data-common/src/event/event.ts`) +- `store/index.ts` 新增 `mapStore` 桶导出 +- `LayerEventView` 类(`data-base/src/map/eventView.ts`,新建,implements `ILayerEventView`) +- `MapTileBase.tileEvent()` / `pointEvent()` 方法(`data-base/src/map/tile.ts`,替换 `triggers`/`addTrigger`/`clearTrigger`/`useEmptyTrigger`/`block`) +- `StaticTile.shouldSave/saveState/loadState`、`DynamicTile.saveState/loadState` 迁移到 `events` +- `MapLayer.event(x,y)` / `getPointEvent(x,y)` + `syncStaticEvent`(替换 `syncStaticTrigger`) +- `MapState.fromRaw` 从 `raw.events` 装配点事件 + +## Blockers / Deferred(非本计划落地项) + +1. **执行器接口 `IGameEventExecutor`(A3 / D-10)**:已由用户设计完成(`data-system/src/event/types.ts` 中 `EventExecuteMode`/`IGameEventExecutor`/`IGameEventSystem`/`IGameEventBuiltinFunction` 均已定义),Plan 02 直接实现,无需 checkpoint。本计划不导出/实现执行器,仅在本计划 context 中把它列为只读接口。 +2. **`IMapPointRawData`(A1)**:`tile.ts` 原 `block()` 引用的 `IMapPointRawData` 已不在 `@user/data-common`,本计划移除该残留方法与 import;点事件的承载字段已在 `IMapRawData.events`(三级结构)落地,本计划实现。 +3. **`ITileStore.getTrigger(num): number[]`**:`store/types.ts:85` 接口仍声明 `getTrigger` 返回 `number[]`,但 `ITileRawData` 已改为 `events: Record`,实现 `tileStore.ts:27-29` 读 `.trigger` 已失效。此为用户接口遗留,删除/改签名需用户决定,本计划不动 `store/types.ts` 与 `tileStore.ts`。 +4. **内建函数清单(A2)**:本阶段完全不落地(已移出本阶段,归入收尾工作),本计划不涉及。 +5. **移动四钩子 → EventTrigger 映射(A4)**:本计划不落地(Plan 03)。 + +## 错误码 + +- 复用:`136`(重复优先级)。执行器的 `171`(事件 id 未找到)在 Plan 02 与执行器装配一并落地。 diff --git a/.planning/phases/01-event/01-02-PLAN.md b/.planning/phases/01-event/01-02-PLAN.md new file mode 100644 index 0000000..50ffbe4 --- /dev/null +++ b/.planning/phases/01-event/01-02-PLAN.md @@ -0,0 +1,253 @@ +--- +phase: 01-event +plan: 02 +type: execute +wave: 2 +depends_on: [01-01] +files_modified: + - packages-user/data-system/src/event/types.ts + - packages-user/data-system/src/event/executor.ts + - packages-user/data-system/src/event/system.ts + - packages-user/data-system/src/event/index.ts + - packages-user/data-system/src/index.ts + - packages-user/data-system/src/types.ts + - packages-user/data-state/src/core.ts + - packages/common/src/logger.json +files_deleted: + - packages-user/data-system/src/trigger/types.ts + - packages-user/data-system/src/trigger/trigger.ts + - packages-user/data-system/src/trigger/registry.ts + - packages-user/data-system/src/trigger/collector.ts + - packages-user/data-system/src/trigger/collection.ts + - packages-user/data-system/src/trigger/index.ts + - packages-user/data-state/src/content/triggers.ts +autonomous: true +requirements: [EVT-01, EVT-02, EVT-03] + +estimate: + tokens: 50000 + raw_tokens: 25000 + tasks: 2 + confidence: low + +must_haves: + truths: + - "data-system/src/event/executor.ts 实现 IGameEventExecutor 接口:execute(events: string[], param, env): Promise 接收预先收集的事件 id 数组,await 每个事件后按 EventExecuteMode 合并返回值(D-10 专门对象;execute 返回 Promise 为用户批准的接口变更)" + - "data-system/src/event/system.ts 实现 IGameEventSystem:executor + store + useStore() 完整装配,不出现只有 { executor } 的残缺装配" + - "IStateSystem 不再含 triggerRegistry/triggerCollector,保留已有 eventSystem(IGameEventSystem)成员(D-13)" + - "CoreState 实例化 GameEventStore 赋值 this.eventStore,装配完整 eventSystem(含 executor/store/useStore)并赋值 this.eventSystem" + - "data-system/src/trigger/ 目录与 data-state/src/content/triggers.ts(ChangeFloorTrigger)已删除" + - "@user/data-system 桶导出 event 执行器与事件系统(export * from './event')" + - "logger.json 新增错误码 171 文案,执行器取回未知事件 id 时 logger.warn(171, id) 而非 throw(D-08)" + artifacts: + - packages-user/data-system/src/event/types.ts + - packages-user/data-system/src/event/executor.ts + - packages-user/data-system/src/event/system.ts + - "IStateSystem 移除 trigger 成员(data-system/src/types.ts)" + - "删除清单(files_deleted)全部移除" + key_links: + - "IGameEventSystem.executor 连接 EventExecutor 实现与 CoreState.eventSystem 装配(含 store/useStore)" + - "EventExecutor.execute 连接 storeRef().getEvent(id) 与 await GameEvent.execute(param, env),按 mode 合并返回值" + - "data-system/src/index.ts 桶导出连接 @user/data-system 消费方(moverImpl/combat)" +--- + + +实现 `IGameEventExecutor`/`IGameEventSystem` 接口(D-10 专门对象)并接入 L2/L3 装配,同时删除旧 `ITrigger` 体系(D-13)。接口本身仅做一处用户批准的单行变更——`IGameEventExecutor.execute` 返回类型由 `R` 改为 `Promise`(消除 sync-R vs async 事件的 tension),其余接口成员一律不动。`mode`/`store` 两个 `readonly` 接口成员以普通类字段实现(`.agents/code.md` 禁用 getter),仅经 `setMode`/`useStore` 变更。本计划不实现任何内建函数(对话/开门/道具/战斗)——内建函数已移出本阶段,归入收尾工作。 + +> **Scope note(回应 scope_sanity):** 本计划文件操作中,7 个为机械目录删除(`data-system/src/trigger/` 整目录 6 文件 + `data-state/src/content/triggers.ts`),不含逻辑改写;另含 1 个用户批准的单行接口签名变更(`event/types.ts` 的 `execute` → `Promise`),其余为常规实现/装配/桶导出改动。 + +Purpose: 完成「旧体系删除 + 新执行器接入」的切换,使 `@user/data-system` 只暴露事件执行器接口,`CoreState` 完整实现 `IStateSystem`(含 `eventStore` 与 `eventSystem`);这是 EVT-02(踩踏触发链路,由 Plan 03 的 moverImpl 收集+排序后调用 executor.execute)与 EVT-03(初学者抽象、无旧残留)的执行器骨架。 +Output: `EventExecutor`(implement `IGameEventExecutor`,execute 返回 `Promise`)、`GameEventSystem`(implement `IGameEventSystem`)、`data-system/src/event/types.ts` 单行签名变更(execute → Promise)、`data-system/src/event/index.ts` + `src/index.ts` 桶导出、`IStateSystem` 移除 trigger 成员、`CoreState` 装配 eventStore/eventSystem(含 store/useStore)、trigger/ 目录与 content/triggers.ts 删除、logger.json 171。 + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/01-event/01-CONTEXT.md +@.planning/phases/01-event/01-RESEARCH.md +@.planning/phases/01-event/01-PATTERNS.md +@.planning/phases/01-event/01-01-PLAN.md + +@packages-user/data-system/src/types.ts +@packages-user/data-system/src/index.ts +@packages-user/data-system/src/event/types.ts +@packages-user/data-system/src/event/index.ts +@packages-user/data-system/src/trigger/types.ts +@packages-user/data-system/src/trigger/collector.ts +@packages-user/data-system/src/trigger/collection.ts +@packages-user/data-system/src/trigger/registry.ts +@packages-user/data-system/src/trigger/trigger.ts +@packages-user/data-state/src/content/triggers.ts +@packages-user/data-state/src/core.ts +@packages-user/data-common/src/event/types.ts +@packages-user/data-base/src/map/types.ts +@packages/common/src/logger.json + + + + + + 实现 EventExecutor + GameEventSystem(implement 已有接口,execute 返回 Promise)+ event 桶导出 + logger 错误码 171 + + packages-user/data-system/src/event/types.ts + packages-user/data-system/src/event/executor.ts + packages-user/data-system/src/event/system.ts + packages-user/data-system/src/event/index.ts + packages-user/data-system/src/index.ts + packages/common/src/logger.json + + + - packages-user/data-system/src/event/types.ts(EventExecuteMode/IGameEventBuiltinFunction/IGameEventExecutor/IGameEventSystem;本计划仅把 IGameEventExecutor.execute 返回类型改为 Promise(用户批准),其余成员只读、禁止修改) + - packages-user/data-system/src/event/index.ts(现状:仅 export * from './types') + - packages-user/data-system/src/index.ts(现状:combat/trigger/types) + - packages-user/data-common/src/event/types.ts(IReadonlyGameEvent.execute(param, env): Promise,只读) + - packages-user/data-common/src/store/types.ts(IGameEventStore.getEvent,只读) + - packages-user/data-base/src/map/types.ts(IBlockEventParam/IBlockEventEnv/BlockEventType,只读) + - packages-user/data-base/src/types.ts(IStateBase/IDataBaseExtended,只读) + - .planning/phases/01-event/01-PATTERNS.md Pattern 1/2 与「错误处理」段(logger 数字错误码、isNil) + + + 实现 `IGameEventExecutor` 与 `IGameEventSystem` 接口。除下述用户批准的单行变更(`execute` 返回 `Promise`)外,不得新增/修改其他公共接口签名。执行器接收「预先收集好的事件 id 数组」,不负责收集事件(收集 + D-06 排序是 Plan 03 moverImpl 的职责): + + 0. `packages-user/data-system/src/event/types.ts`(用户批准的单行接口变更):把 `IGameEventExecutor.execute` 的返回类型由 `R` 改为 `Promise`(`execute(events: string[], param: IBlockEventParam, env: IBlockEventEnv): R` → `...: Promise`),其余接口成员(`mode`/`interpreter`/`setMode` 及整个 `IGameEventSystem`)一概不动。 + + 1. `packages-user/data-system/src/event/executor.ts`(新建):实现 `class EventExecutor implements IGameEventExecutor`。 + - 构造器 `constructor(readonly interpreter: AnonTokyoInterpreter, private readonly storeRef: () => IGameEventStore | null)`:`interpreter` 为公共只读字段;`storeRef` 是惰性取 store 的回调,供 `execute` 解析事件 id(避免与 `IGameEventSystem.useStore` 换 store 后脱节)。 + - 用普通类字段满足接口的 `readonly mode`(`.agents/code.md` 禁用 getter,不用 `get mode()`):`mode: EventExecuteMode = EventExecuteMode.ListReturn;`(普通非 readonly 字段即可满足 `readonly mode` 接口成员),`setMode(mode)` 直接赋值 `this.mode = mode`。 + - `async execute(events: string[], param: IBlockEventParam, env: IBlockEventEnv): Promise`:遍历 `events`,对每个 id 调 `storeRef()?.getEvent(id)`;返回 null 时 `logger.warn(171, id)` 后跳过(D-08,绝不 throw);命中则 `await event.execute(param, env)` 取到实际返回值。按 `this.mode` 合并各事件 `await` 后的返回值: + - `ListReturn`:收集所有返回值组成数组作为整体返回值。 + - `OrReduce`:顺序短路取或;遇非布尔值按短路运算符规则输出并 `logger.warn`。 + - `AndReduce`:顺序短路取与;非布尔值同上。 + - `CutIfFalsy`:任意事件返回 falsy 立即停止,把前面返回值组成数组返回。 + - 返回值:`execute` 为 `async`,最终 `return` 按 `this.mode` 合并后的结果(`ListReturn`/`CutIfFalsy` 返回数组,`OrReduce`/`AndReduce` 返回短路运算结果),类型满足 `Promise`。 + - 约束:禁用 `as`;`isNil` 判空;公共方法先定义于接口再 implements;私有成员加 jsDoc;本阶段不实现任何内建函数。 + + 2. `packages-user/data-system/src/event/system.ts`(新建):实现 `class GameEventSystem implements IGameEventSystem`。 + - `readonly state: IStateBase`(构造器注入,满足 `IDataBaseExtended`)。 + - `readonly executor: IGameEventExecutor`:构造时 `new EventExecutor(interpreter, () => this.store)`,`interpreter` 用 `new AnonTokyoInterpreter({ builtInFunctions: [], globalFunctions: [] })`(内建函数本阶段留空骨架)。 + - 用普通类字段满足接口的 `readonly store`(`.agents/code.md` 禁用 getter,不用 `get store()`):`store: IGameEventStore | null;`(普通非 readonly 字段即可满足 `readonly store` 接口成员);`useStore(store)` 直接赋值 `this.store = store`(executor 的 `storeRef` 闭包随之读到新 store)。构造时 `this.store = state.eventStore`。 + - 约束同上。 + + 3. `packages-user/data-system/src/event/index.ts`:追加 `export * from './executor';` 与 `export * from './system';`(保留 `export * from './types';`)。 + + 4. `packages-user/data-system/src/index.ts`:把 `export * from './trigger';` 替换为 `export * from './event';`(保留 `export * from './combat';` 与 `export * from './types';`)。 + + 5. `packages/common/src/logger.json`:在 `warn` 段追加 `"171": "Event id '$1' not found in event store, event will be skipped."`(英文文案,沿用现有风格)。 + + + - `grep -n "class EventExecutor" packages-user/data-system/src/event/executor.ts` 命中 + - `grep -n "class GameEventSystem" packages-user/data-system/src/event/system.ts` 命中 + - `grep -n "Promise" packages-user/data-system/src/event/types.ts` 命中(execute 返回 Promise,用户批准的单行接口变更) + - `grep -n "execute`) + - `grep -n "get mode\|get store" packages-user/data-system/src/event/executor.ts packages-user/data-system/src/event/system.ts` 无命中(code.md 禁用 getter,mode/store 用普通字段) + - `grep -n "export \* from './executor'" packages-user/data-system/src/event/index.ts` 命中 + - `grep -n "export \* from './system'" packages-user/data-system/src/event/index.ts` 命中 + - `grep -n "export \* from './event'" packages-user/data-system/src/index.ts` 命中 + - `grep -n "171" packages/common/src/logger.json` 命中一次(warn 段) + - `Test-Path packages-user/data-system/src/event/builtin.ts` 返回 False(本阶段不实现内建函数) + + + pnpm check:type 2>&1 | Select-String -Pattern "event/types\.ts|event/executor\.ts|event/system\.ts|event/index\.ts|data-system/src/index\.ts" + 命中 event/types.ts、event/executor.ts、event/system.ts、event/index.ts、data-system/src/index.ts 的任何 TS 错误(有输出即失败)。IStateSystem/core.ts 的 trigger 成员删除在 Task 2,moverImpl 引用旧 trigger、client-modules、tileStore.getTrigger 属跨计划/阶段外残差,不计入本任务失败 + + EventExecutor 实现 IGameEventExecutor(execute 返回 Promise,await 每个事件并按 mode 合并返回值,mode 为普通字段由 setMode 赋值),GameEventSystem 实现 IGameEventSystem(executor+store+useStore,store 为普通字段由 useStore 赋值),`@user/data-system` 桶导出 event,错误码 171 文案可用。 + + + + 删除旧 ITrigger 体系 + CoreState 装配完整 eventSystem(executor+store+useStore)(D-13) + + packages-user/data-system/src/types.ts + packages-user/data-state/src/core.ts + + + - packages-user/data-system/src/types.ts(IStateSystem 现状:triggerRegistry/triggerCollector + eventSystem) + - packages-user/data-system/src/index.ts(Task 1 已切 trigger→event) + - packages-user/data-system/src/event/types.ts 与 event/system.ts、event/executor.ts(Task 1 产出) + - packages-user/data-state/src/core.ts(现状 L0/L2 初始化区) + - packages-user/data-common/src/store/eventStore.ts(GameEventStore,只读) + - packages-user/data-common/src/types.ts(IDataCommon.eventStore,只读) + - packages-user/data-state/src/hero/moverImpl.ts(确认其 import 的 TriggerType/ITriggerCollector/ITriggerHandler 将在本计划移除后失效,Plan 03 重写) + + + 1. 删除 `packages-user/data-system/src/trigger/` 整个目录(types.ts/trigger.ts/registry.ts/collector.ts/collection.ts/index.ts,共 6 文件),删除 `packages-user/data-state/src/content/triggers.ts`(ChangeFloorTrigger 及其自引用的 TriggerType 枚举)。这些文件中的 `TriggerType`(Enter/Leave/Hit/CannotEnter 与 ChangeFloor)均为旧体系,D-13 要求一并删除。 + + 2. `packages-user/data-system/src/types.ts`:`IStateSystem` 删除 `triggerRegistry: ITriggerRegistry` 与 `triggerCollector: ITriggerCollector` 两个成员及其 `import { ITriggerCollector, ITriggerRegistry } from './trigger';`。保留已有 `eventSystem: IGameEventSystem`(其 import 已存在)。 + + 3. `packages-user/data-state/src/core.ts`: + - L0 区新增 `const eventStore = new GameEventStore(); this.eventStore = eventStore;` 并补 `import { GameEventStore } from '@user/data-common';`(同时把第 79 行 `import { MapStore } from '../../data-common/src/store/mapStore'` 改为桶导出 `import { MapStore } from '@user/data-common';`,Plan 01 已补 mapStore 桶导出)。 + - L2 区删除 `ITriggerCollector/ITriggerRegistry/TriggerCollector/TriggerRegistry` import(第 42-46 行)、`triggerRegistry`/`triggerCollector` 成员声明(第 98-99 行)、装配段(第 202-207 行),改为 `const eventSystem = new GameEventSystem(this); this.eventSystem = eventSystem;` 并补 `import { GameEventSystem } from '@user/data-system';`。注意装配的是完整 `IGameEventSystem`(含 executor/interpreter/mode/setMode 与 store/useStore),绝不装配 `{ executor }` 单独对象。 + + 约束:禁用 `as`;删除后 `grep` 在 `packages-user/data-system` 与 `data-state/src/{core.ts,content}` 内不得残留旧符号;`EventTrigger`(新枚举)与 `BlockEventType`(新枚举)不受影响。 + + + - `grep -rn "ITrigger\|TriggerRegistry\|TriggerCollector\|TriggerCollection\|BaseTrigger\|TriggerType" packages-user/data-system packages-user/data-state/src/core.ts packages-user/data-state/src/content 2>$null` 无命中(注意 `EventTrigger`/`BlockEventType` 不属匹配目标) + - `Test-Path packages-user/data-system/src/trigger` 返回 False + - `Test-Path packages-user/data-state/src/content/triggers.ts` 返回 False + - `grep -n "eventStore" packages-user/data-state/src/core.ts` 命中 + - `grep -n "eventSystem" packages-user/data-system/src/types.ts packages-user/data-state/src/core.ts` 各命中 + - `grep -n "GameEventSystem" packages-user/data-state/src/core.ts` 命中(装配完整系统,而非 `{ executor }`) + + + pnpm check:type 2>&1 | Select-String -Pattern "data-system/src/types\.ts|data-state/src/core\.ts" + 命中 data-system/types.ts、data-state/core.ts 的任何 TS 错误(有输出即失败)。moverImpl 引用旧 trigger、client-modules、tileStore.getTrigger 属跨计划/阶段外残差,不计入本任务失败 + + 旧 ITrigger 体系删除完毕,`@user/data-system` 只暴露 event 执行器与事件系统,`CoreState` 装配 `eventStore` + 完整 `eventSystem`(executor+store+useStore)并完整实现 IStateSystem。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 外部 blockly 编辑器 → `Statement[]` → AnonTokyoInterpreter | 事件数据为不可信输入,执行器须容忍未知 id 与非法参数 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|----------------| +| T-02-01 | Tampering | `EventExecutor` 对 `getEvent(id)` 返回 null 的未知 id | low | mitigate | `logger.warn(171, id)` 跳过,绝不 throw 中断游戏 | +| T-02-02 | DoS | 恶意/损坏事件引用被删除的旧 trigger 概念 | low | accept | D-13 删除后旧体系无残留,无对应执行面 | +| T-02-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本计划无新依赖;`anon-tokyo` 为已锁定 SUS 依赖,仅提示不阻断 | + + + +本计划切换后,`check:type` 的预期残差仅剩 `moverImpl.ts`(仍 import 已删除的旧 trigger 符号,Plan 03 重写)与阶段外残差(client-modules、tileStore.getTrigger)。完整 eventSystem 装配、IStateSystem、CoreState、删除项在 `check:type` 中应无新增错误。 + + + +- 旧 ITrigger 体系(trigger/ 6 文件 + content/triggers.ts)全部删除,无残留引用 +- `EventExecutor` 实现 `IGameEventExecutor`(`execute` 返回 `Promise`,`await` 每个事件并按 mode 合并返回值,`mode` 为普通字段由 `setMode` 赋值);`GameEventSystem` 实现 `IGameEventSystem`(executor + store + useStore,`store` 为普通字段) +- `IStateSystem` 只含 `eventSystem`(无 triggerRegistry/triggerCollector) +- `CoreState` 装配 `eventStore` + 完整 `eventSystem`,完整实现 IStateSystem +- logger.json 171 就位;无内建函数实现(内建函数移出本阶段) + + + +Create `.planning/phases/01-event/01-02-SUMMARY.md` when done + + +## Artifacts this phase produces + +- 删除:`data-system/src/trigger/`(6 文件)、`data-state/src/content/triggers.ts`(ChangeFloorTrigger + ChangeFloor TriggerType) +- `data-system/src/event/types.ts` 单行变更:`IGameEventExecutor.execute` 返回 `Promise`(用户批准) +- `data-system/src/event/executor.ts`(EventExecutor,实现 IGameEventExecutor;execute 返回 Promise,mode 为普通字段由 setMode 赋值) +- `data-system/src/event/system.ts`(GameEventSystem,实现 IGameEventSystem;store 为普通字段由 useStore 赋值) +- `data-system/src/event/index.ts` 追加 executor/system 导出;`src/index.ts` 桶导出 trigger→event +- `IStateSystem` 移除 trigger 成员(保留 eventSystem) +- `CoreState.eventStore` + `CoreState.eventSystem` 装配(删除 TriggerRegistry/TriggerCollector,装配完整 eventSystem) +- `logger.json` 错误码 171 + +## Blockers / Deferred + +1. **内建函数清单(A2,用户 deferred)**:对话/开门/道具/战斗的具体 `BuiltInFunction` 清单与命名已移出本阶段,归入收尾工作。本计划不实现、不新建 builtin 文件;`AnonTokyoInterpreter` 以空 `LanguageFeature`(builtInFunctions/globalFunctions 均空)构造。 +2. **执行器 sync-R vs 异步事件(已解决)**:`IGameEventExecutor.execute` 已按用户批准改为返回 `Promise`(本计划在 `event/types.ts` 落地该单行签名变更),执行器 `async execute` `await` 每个事件并按 mode 合并返回值。原「sync-R 声明 vs async 事件」的 tension 已消除。 +3. **moverImpl.ts 暂时编译失败**:删除旧 trigger 导出后 `moverImpl.ts` 的 import 失效,属预期过渡态,Plan 03 重写修复(重写后负责收集 + D-06 排序并调用 executor.execute)。 + +## 错误码 + +- 新增:`171`(事件 id 未在 eventStore 中找到)——文案在本计划落地。 diff --git a/.planning/phases/01-event/01-03-PLAN.md b/.planning/phases/01-event/01-03-PLAN.md new file mode 100644 index 0000000..00b8105 --- /dev/null +++ b/.planning/phases/01-event/01-03-PLAN.md @@ -0,0 +1,195 @@ +--- +phase: 01-event +plan: 03 +type: execute +wave: 3 +depends_on: [01-02] +files_modified: + - packages-user/data-state/src/hero/moverImpl.ts +autonomous: true +requirements: [EVT-02, EVT-03] + +estimate: + tokens: 50000 + raw_tokens: 25000 + tasks: 2 + confidence: low + +must_haves: + truths: + - "英雄移动的踩踏触发从旧 TriggerCollector/TriggerType 改为:moverImpl 收集点事件+图块事件、按 D-06(先点后块、各自优先级降序)排序后 await state.eventSystem.executor.execute(events[], param, env)(D-06/D-08)" + - "enter 映射 EventTrigger.OnEnter、leave 映射 EventTrigger.OnLeave、hit 映射 EventTrigger.OnTouch(映射依据 OnTouch 注释,触碰触发,直接走入不触发)" + - "moverImpl.ts 不再 import 任何 @user/data-system 的旧 trigger 符号(ITriggerCollector/ITriggerHandler/TriggerType),也不再访问 ILayerLocation.raw(改用 location.static.raw())" + - "全仓 packages-user 内 ITrigger/TriggerRegistry/TriggerCollector/TriggerCollection/BaseTrigger/TriggerType 引用清零(EventTrigger/BlockEventType 为合法新符号,除外)" + - "pnpm check:type(本阶段涉及文件无报错)、pnpm check:circular、pnpm lint:user 全绿(阶段外残差另记)" + artifacts: + - packages-user/data-state/src/hero/moverImpl.ts + key_links: + - "DefaultHeroMoveTopImpl.commonTrigger 收集点事件+图块事件并按 D-06 排序后,连接 state.eventSystem.executor.execute(events[], param, env)" + - "EventTrigger.OnEnter/OnLeave/OnTouch 连接移动四钩子 enter/leave/hit" +--- + + +把英雄移动的踩踏触发从旧触发器收集器重写为:moverImpl 收集点事件+图块事件并按 D-06 排序后,委托新事件执行器的 `execute(events[], param, env)` 执行,同时同步修复 moverImpl 对已变更 `ILayerLocation`(`raw` → `static.raw()`)的引用,完成事件系统与移动流程的对接,随后执行阶段级验收(旧体系引用清零 + 门禁)。 + +Purpose: EVT-02「事件系统能驱动踩踏触发链路」的关键集成点;重写后 `moverImpl` 只负责「收集 → D-06 排序 → 委托执行器」,返回值合并语义(EventExecuteMode)集中在执行器(Plan 02 已落地),同时让 `check:type` 中 moverImpl 相关报错清零。内建函数(对话/开门/道具/战斗)已移出本阶段,不在本计划范围。 +Output: `moverImpl.ts` 重写(含 raw 迁移 + 收集/排序/委托 execute)+ 阶段级验证结论。 + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/01-event/01-CONTEXT.md +@.planning/phases/01-event/01-RESEARCH.md +@.planning/phases/01-event/01-PATTERNS.md +@.planning/phases/01-event/01-01-PLAN.md +@.planning/phases/01-event/01-02-PLAN.md +@.planning/phases/01-event/01-02-SUMMARY.md + +@packages-user/data-state/src/hero/moverImpl.ts +@packages-user/data-base/src/hero/types.ts +@packages-user/data-base/src/map/types.ts +@packages-user/data-common/src/event/types.ts +@packages-user/data-system/src/event/types.ts + + + + + + moverImpl 重写:raw 迁移 + 踩踏触发委托事件执行器(enter/leave/hit → EventTrigger) + + packages-user/data-state/src/hero/moverImpl.ts + + + - packages-user/data-state/src/hero/moverImpl.ts(现状:canPass/shouldHit 用 location.raw,commonTrigger 用 collector) + - packages-user/data-base/src/hero/types.ts(IHeroMoveTopImpl 的 enter/leave/hit/cannotEnter 契约,只读) + - packages-user/data-base/src/map/types.ts(ILayerLocation.static.raw(),只读) + - packages-user/data-common/src/event/types.ts(EventTrigger 枚举:OnEnter/OnLeave/OnTouch 注释,只读) + - packages-user/data-system/src/event/types.ts(IGameEventExecutor.execute(events: string[], param, env): Promise 签名 + EventExecuteMode,只读) + + + 重写 `packages-user/data-state/src/hero/moverImpl.ts`,分两部分: + + A. 修复已变更的 `ILayerLocation` 引用(`canPass`/`shouldHit`): + - `canPass` 中 `curr.raw.pass.outPass`/`next.raw.pass.inPass` 改为 `curr?.static.raw()?.pass.outPass`/`next?.static.raw()?.pass.inPass`(`ILayerLocation` 已无 `raw`,改经 `static.raw()` 取 `ITileRawData`,raw() 可能为 null,用 `isNil`/可选链判空后默认 true 可通行)。外层 `if (curr && curr.raw)` 相应改为对 `static.raw()` 结果判空。 + - `shouldHit` 中 `next.raw.eventPass` 改为 `next?.static.raw()?.eventPass`(raw 为 null 时返回 false)。 + + B. 触发器行为改为「收集 + D-06 排序 → 委托执行器 execute」: + 1. 顶部 import:删除 `ITriggerCollector/ITriggerHandler/TriggerType`(来自 `@user/data-system`);改为从 `@user/data-common` 引入 `EventTrigger`,从 `@user/data-base` 引入 `IBlockEventParam/IBlockEventEnv/BlockEventType`,从 `@user/data-system` 引入 `IGameEventExecutor`,从 `@motajs/common` 引入 `ITileLocator`;`state` 类型仍是 `IStateSystem`。 + 2. 构造器:把 `private readonly collector: ITriggerCollector` 与 `this.collector = state.triggerCollector` 改为 `private readonly executor: IGameEventExecutor` 与 `this.executor = state.eventSystem.executor`。 + 3. `commonTrigger` 改为签名 `commonTrigger(trigger: EventTrigger, handler: IHeroMoveTopHandler, heroLoc: ITileLocator, x: number, y: number): Promise`,方法体(D-06:先点事件、后图块事件,各自 priority 降序): + - `if (isNil(handler.floorId)) return Promise.resolve();` → `const map = this.maps.getMap(handler.floorId); if (!map) return Promise.resolve(); const event = map.eventLayer; if (!event) return Promise.resolve();` + - 收集点事件:`const point = event.getPointEvent(x, y);`(`ReadonlyMap | null`)。 + - 收集图块事件:`const loc = event.getLocationData(x, y);`,`const tileEvents = loc?.static.tileEvent().get();`(`ReadonlyMap`;`loc` 为 null 时按空处理)。 + - 排序合并:把 `point` 与 `tileEvents` 各自的 `[priority, id]` 条目按 priority 降序排成 id 数组,先点后块拼成 `const sortedIds: string[]`(D-06)。 + - 构造 `const param: IBlockEventParam = { custom: {} };` 与 `const env: IBlockEventEnv = { type: BlockEventType.TileEvent, trigger, heroLocator: heroLoc, triggerLocator: { x, y }, tile: loc?.static ?? null, layer: event, map };` + - `await this.executor.execute(sortedIds, param, env);`(执行器 `execute` 返回 `Promise`,直接 `await`,D-09 长事件 await)。不再构造 `ITriggerHandler` 对象。 + 4. 四个钩子改为映射(映射为规划期最佳解读,A4 已延后确认,见「Blockers」): + - `enter(handler)` → `this.commonTrigger(EventTrigger.OnEnter, handler, handler.nextLoc, handler.nextLoc.x, handler.nextLoc.y)` + - `leave(handler)` → `this.commonTrigger(EventTrigger.OnLeave, handler, handler.currLoc, handler.currLoc.x, handler.currLoc.y)` + - `hit(handler)` → `this.commonTrigger(EventTrigger.OnTouch, handler, handler.currLoc, handler.nextLoc.x, handler.nextLoc.y)` + - `cannotEnter(handler)` → `return Promise.resolve();`(新 EventTrigger 无 CannotEnter 对应,A4 已延后,先空实现并加 jsDoc 注明) + + 约束:禁用 `as`;`isNil`/可选链判空;公共方法不新增(接口契约未变);保留 4 钩子的 async 签名与返回类型。 + + + - `grep -n "TriggerType\|ITriggerCollector\|ITriggerHandler\|triggerCollector" packages-user/data-state/src/hero/moverImpl.ts` 无命中 + - `grep -n "\.raw\b" packages-user/data-state/src/hero/moverImpl.ts` 无命中(已改 static.raw()) + - `grep -n "EventTrigger.OnEnter\|EventTrigger.OnLeave\|EventTrigger.OnTouch" packages-user/data-state/src/hero/moverImpl.ts` 各命中一次 + - `grep -n "getPointEvent\|tileEvent()\|executor.execute" packages-user/data-state/src/hero/moverImpl.ts` 命中(收集点事件/图块事件并委托 execute) + - `grep -n "eventSystem\|executor" packages-user/data-state/src/hero/moverImpl.ts` 命中 + - `pnpm check:type` 中无 moverImpl.ts 报错 + + + pnpm check:type 2>&1 | Select-String -Pattern "hero/moverImpl\.ts" + 命中 hero/moverImpl.ts 的任何 TS 错误(有输出即失败)。client-modules、tileStore.getTrigger 属阶段外残差,不计入本任务失败 + + moverImpl 三个移动钩子(enter/leave/hit)收集点事件+图块事件并按 D-06 排序后委托 executor.execute,踩踏触发走 OnEnter/OnLeave/OnTouch,不再引用旧 trigger 符号或 ILayerLocation.raw。 + + + + 阶段验收:旧体系引用清零 + 门禁 + + packages-user/data-state/src/hero/moverImpl.ts + + + - .planning/phases/01-event/01-RESEARCH.md 的「Validation Architecture → Phase gate」 + - .planning/phases/01-event/01-01-PLAN.md 与 01-02-PLAN.md 的 must_haves + + + 执行阶段级验收(本任务无源码改动,只跑门禁并记录结论): + + 1. 旧体系引用清零:`grep -rn "ITrigger\|TriggerRegistry\|TriggerCollector\|TriggerCollection\|BaseTrigger" packages-user` 应为空;`grep -rn "\bTriggerType\b" packages-user` 应为空(旧枚举已全删)。注意:`EventTrigger`(新枚举,data-common/src/event/types.ts)与 `BlockEventType`(data-base/src/map/types.ts)是合法符号,不得被误判为残留。 + + 2. `pnpm check:type`:本阶段涉及文件(eventView/tile/staticTile/dynamicTile/mapLayer/mapState/event/executor/system/types/index/core/moverImpl)应无报错;阶段外残差(client-modules 的 HeroMover/degradeFace/FaceDirection 等、tileStore.getTrigger)记入 SUMMARY 的 blockers,注明「非本阶段引入」。 + + 3. `pnpm check:circular`:无新增循环依赖(event 执行器只依赖 IStateBase,不可反向)。 + + 4. `pnpm lint:user`:packages-user 无 lint 错误(eventView/executor/moverImpl 等新文件符合 code.md)。 + + 把四项结论(命令 + 通过/失败 + 残差)写进 01-03-SUMMARY.md。若 check:type 出现非 mover 残留的既有错误,不静默忽略——在 SUMMARY 的 blockers 记录并注明「非本阶段引入」。 + + + - 旧 trigger 符号 grep 三组命令输出均为空 + - 本阶段涉及文件在 `pnpm check:type` 中无报错(阶段外残差记录在案) + - `pnpm check:circular` 退出码 0 + - `pnpm lint:user` 退出码 0 + + + pnpm check:circular; if ($?) { pnpm lint:user } + 任一命令非零退出(check:type 的全仓退出码因阶段外 client-modules/tileStore 残差可能非零,故以「本阶段文件无报错」为准,见 Task 1 的 verify) + + 阶段验收通过:旧 ITrigger 体系无残留,事件系统骨架类型(本阶段文件)/循环依赖/lint 门禁通过,阶段外残差已在 SUMMARY 记录。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 英雄移动钩子 → 事件执行器 → `Statement[]` 执行 | 移动触发的图块/点事件数据为不可信输入,执行器须容忍未知 id 与非法参数 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|----------------| +| T-03-01 | Tampering | `moverImpl` 委托执行器触发含未知事件 id 的图块 | low | mitigate | 执行器 `getEvent` 为 null 时 `logger.warn(171)` 跳过,不阻断移动 | +| T-03-02 | DoS | 踩踏触发执行长事件阻塞移动循环 | low | accept | D-09 长事件 `await` 为既定语义,属设计范围,不序列化执行状态 | +| T-03-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本计划无新依赖安装;`anon-tokyo` 已锁定(SUS,仅提示) | + + + +阶段门禁:`grep` 旧 trigger 符号三组命令均为空;`pnpm check:circular` + `pnpm lint:user` 全绿;`check:type` 以「本阶段文件无报错」为准(阶段外 client-modules/tileStore 残差记录)。 + + + +- 踩踏触发(enter/leave/hit)经事件执行器驱动 +- 旧 ITrigger 体系在全仓无残留引用 +- moverImpl 不再访问 ILayerLocation.raw +- 阶段门禁通过(本阶段文件类型成立 + circular + lint),阶段可进入 review/transition + + + +Create `.planning/phases/01-event/01-03-SUMMARY.md` when done + + +## Artifacts this phase produces + +- `moverImpl.ts` 踩踏触发重写:`commonTrigger(EventTrigger, handler, heroLoc, x, y)` 收集点事件+图块事件、按 D-06 排序、委托 `executor.execute(events[], param, env)` + `raw` → `static.raw()` 迁移 +- 阶段验收结论(旧体系引用清零 + 门禁),记录于 01-03-SUMMARY.md + +## Blockers / Deferred + +1. **移动四钩子 → EventTrigger 映射(A4,已延后)**:`enter→OnEnter`/`leave→OnLeave`/`hit→OnTouch` 为规划期最佳解读(依据 OnTouch 注释),`cannotEnter` 无 EventTrigger 对应(空实现)。OnTouch/OnEnter 边界与 cannotEnter 去留须用户后续确认。 +2. **内建函数(对话/开门/道具/战斗,A2,已移出本阶段)**:EVT-02 的「对话/开门」端到端验证不在本阶段,归入收尾工作。 +3. **执行器 sync-R vs 异步事件(已解决)**:`execute` 已按用户批准改为返回 `Promise`(Plan 02 在 `event/types.ts` 落地签名变更),moverImpl 以 `execute` 消费并 `await`(D-09 长事件 await)。 +4. **执行器 env 的 `type`/heroLocator 精确值**:`type` 暂统一为 `BlockEventType.TileEvent`(踩踏触发以图块为触发位置);`heroLocator` 已按 enter/hit=nextLoc、leave=currLoc 传入,`triggerLocator={x,y}`。接入 hero.location 后如需区分点事件类型再核对(不阻塞骨架)。 + +## 错误码 + +- 复用 `171`(执行器未找到事件 id)——Plan 02 已落地文案。 diff --git a/.planning/phases/01-event/01-PATTERNS.md b/.planning/phases/01-event/01-PATTERNS.md new file mode 100644 index 0000000..05f1755 --- /dev/null +++ b/.planning/phases/01-event/01-PATTERNS.md @@ -0,0 +1,440 @@ +# Phase 1: 事件系统 (event) - Pattern Map + +**Mapped:** 2026-09-07 +**Files analyzed:** 18 (create + modify + delete) +**Analogs found:** 18 / 18 (all have a codebase analog; no true "no-analog" files) + +## File Classification + +> 说明:接口层(L0/L1)已由用户设计完成(`EventTrigger`/`IGameEvent`/`GameEventStore`/`ITileBase.events`/`ILayerEventView`/`IBlockEvent*` 等)。本阶段是「落地已设计接口 + 删除旧 `ITrigger` 体系」。因此多数改造文件是**自我重构**(同一文件内 `triggers` → `events`),其 analog 即其自身。 + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `data-system/src/event/types.ts` (新建) | service (接口) | event-driven | `data-system/src/trigger/types.ts` | exact | +| `data-system/src/event/executor.ts` (新建) | service | event-driven | `data-system/src/trigger/collector.ts` + `collection.ts` | exact | +| `data-system/src/event/index.ts` (新建) | config (桶导出) | — | `data-system/src/trigger/index.ts` | exact | +| `data-system/src/event/builtin.ts` (新建) | service (内建函数) | event-driven | `data-state/src/content/triggers.ts` (`ChangeFloorTrigger`) | role-match | +| `data-base/src/map/eventView.ts` (新建) | model (Map 视图 + dirty) | CRUD | `packages/common/src/dirtyTracker.ts` + `tile.ts` triggers 方法 | role-match | +| `data-common/src/store/index.ts` (改) | config (桶导出) | — | 自身 + `data-common/src/index.ts` | exact | +| `data-common/src/event/event.ts` (改) | model | transform | 自身 (`GameEvent.compile`) | exact | +| `data-common/src/store/tileStore.ts` (改) | store | CRUD | `store/itemStore.ts` | exact | +| `data-base/src/map/tile.ts` (改) | model | CRUD | 自身 (`MapTileBase` triggers 方法) | exact | +| `data-base/src/map/staticTile.ts` (改) | model | CRUD (存读档) | 自身 (`saveState`/`loadState`) | exact | +| `data-base/src/map/dynamicTile.ts` (改) | model | CRUD (存读档) | 自身 (`saveState`/`loadState`) | exact | +| `data-base/src/map/mapLayer.ts` (改) | model/service | CRUD (存读档) | 自身 (`syncStaticTrigger` + `compareWith`) | exact | +| `data-system/src/index.ts` (改) | config (桶导出) | — | 自身 | exact | +| `data-system/src/types.ts` (改) | config (接口) | — | 自身 (`IStateSystem`) | exact | +| `data-state/src/core.ts` (改) | provider (装配) | — | 自身 (L2 初始化 region) | exact | +| `data-state/src/hero/moverImpl.ts` (改) | controller | event-driven | 自身 (`commonTrigger`) | exact | +| `data-system/src/trigger/*` (删, 6 文件) | — | — | — (删除) | n/a | +| `data-state/src/content/triggers.ts` (删) | — | — | — (删除) | n/a | + +--- + +## Pattern Assignments + +### 1. `data-system/src/event/types.ts` (新建, service 接口, event-driven) + +**Analog:** `data-system/src/trigger/types.ts` + +这是「专门事件执行器」的接口层(D-10)。必须遵循项目「接口先行 + `implements`」的刚性约定(code.md 规则 14/15/16:公共方法必须先定义在接口、类成员必须 `readonly`、对象成员用接口类型不用类类型)。 + +**导入与依赖方向**(trigger/types.ts:1-7 为样板): +```typescript +// 事件执行器依赖 L1 IStateBase,与旧 ITriggerHandler 同向 +import { ITileLocator } from '@motajs/common'; +import { IGameMap, IMapLayer, IStateBase, IDataBaseExtended } from '@user/data-base'; +import { EventTrigger } from '@user/data-common'; // 注意:事件相关类型来自 L0,不是本层 +``` + +**接口命名与结构样板**(trigger/types.ts:20-29 `ITriggerHandler`): +```typescript +// 事件执行上下文 —— 替换旧 ITriggerHandler。字段用 readonly。 +export interface ITriggerHandler { + readonly state: IStateBase; + readonly layer?: IGameMap; + readonly mapLayer?: IMapLayer; + readonly locator?: ITileLocator; +} +``` + +**before 类返回语义的关键参照**——「执行器如何表达 `OnBeforeBattle`/`OnBeforeOpenDoor`/`OnBeforeChangeFloor` 返回 `R` 决定是否继续」已经在 combat 层有现成样板,直接照抄其返回约定(combat/types.ts:767-790 `ICombatScript`): +```typescript +// combat/types.ts:772-779 —— before 返回 boolean 表示「是否继续」的既定语义 +before( + info: IEnemyDamageInfo, + handler: ICombatFlowHandler +): Promise; +// 注释约定:返回 false 会立刻停止后续执行并放弃本次战斗 +``` + +**错误/缺省处理**:执行器接口方法若拿不到 `eventStore` 中对应 id 的事件,按项目惯例 `logger.warn(code)` 后跳过(不抛异常)。参照 `eventStore.ts` 的 `getEvent` 返回 `null`,调用侧做空判。 + +--- + +### 2. `data-system/src/event/executor.ts` (新建, service, event-driven) + +**Analog:** `data-system/src/trigger/collector.ts`(收集+排序) + `trigger/collection.ts`(异步派发) + +D-06/D-10 要求「先点事件、后图块事件,各自按优先级降序,before 返回值集中解析」。旧 `TriggerCollector.collect` 就是这套排序/去重的唯一现成实现,直接沿用其结构。 + +**收集与优先级排序样板**(trigger/collector.ts:15-107)——核心逻辑是「优先级降序 + 重复优先级 warn」: +```typescript +// trigger/collector.ts:54-79(动态图块 > 2 的通用排序,含重复优先级警告) +const usedPriority = new Set(); +const duplicate = new Set(); +// ... 收集时 +if (usedPriority.has(trigger.priority)) { + duplicate.add(trigger.priority); +} +usedPriority.add(trigger.priority); +// ... 排序 +triggers.sort((a, b) => b.priority - a.priority); +if (duplicate.size > 0) { + logger.warn(136, [...duplicate].join(',')); // 136 = duplicate trigger priority +} +``` + +**异步顺序派发样板**(trigger/collection.ts:20-27)——事件执行是 `await` 顺序执行(D-09 长事件直接 await,不并行): +```typescript +async trigger(condition: TriggerType, handler: ITriggerHandler): Promise { + for (const trigger of this.triggerList) { + await this.dispatch(trigger, condition, handler); + } +} +``` + +**核心差异点(执行器 vs 旧 collector)**: +- 旧 collector 输入 `ITriggerCollection`,新执行器输入「点事件 `ReadonlyMap` + 图块事件 `ReadonlyMap`」的 id,经 `state.eventStore.getEvent(id)` 取出 `IReadonlyGameEvent`,再 `event.execute(param, env)`(D-04/D-07/D-08)。 +- `param` = `IBlockEventParam`(`{ custom: Record }`,map/types.ts:35-38);`env` = `IBlockEventEnv`(map/types.ts:40-55,含 `type`/`trigger`/`heroLocator`/`triggerLocator`/`tile`/`layer`/`map`)。 +- before 类触发器(`EventTrigger.OnBeforeBattle/OnBeforeOpenDoor/OnBeforeChangeFloor`)的 `execute` 返回 `R`(boolean),由执行器统一解析后决定是否继续对应动作。 + +**执行器持有解释器**:参照 `EnemyContext` 的「attach + constructor(state)」装配风格(combat/context.ts:103),执行器构造器注入 `state`,内部从 `state` 取 `eventStore`。若执行器负责编译,则持有 `AnonTokyoInterpreter` 实例(anon-tokyo `.d.ts:19-29`)。 + +--- + +### 3. `data-system/src/event/index.ts` (新建, 桶导出) + +**Analog:** `data-system/src/trigger/index.ts`(5 行)与 `combat/index.ts`(5 行) + +```typescript +// trigger/index.ts:1-5 样板 +export * from './builtin'; +export * from './executor'; +export * from './types'; +``` + +--- + +### 4. `data-system/src/event/builtin.ts` (新建, 内建函数, event-driven) + +**Analog:** `data-state/src/content/triggers.ts`(`ChangeFloorTrigger`,领域动作实现) + +内建函数是 `BuiltInFunction[]`(anon-tokyo `.d.ts:61-65`),对应「对话 / 开门 / 道具 / 战斗」等游戏动作(EVT-02)。旧的 `ChangeFloorTrigger` 是唯一现成的「领域动作」实现,其 `trigger(handler)` 方法(content/triggers.ts:140-161)展示了如何从 `handler` 取 `layer`/`locator`/`mapLayer` 并驱动游戏状态(`hero.changeFloor`)。 + +**内建函数签名样板**(anon-tokyo `.d.ts:61-65` + `LanguageFeature` 153-156): +```typescript +export declare interface BuiltInFunction { + name: string; + save?: boolean; + func: (parameters: Record, env: Record) => any; +} +export declare interface LanguageFeature { + builtInFunctions: BuiltInFunction[]; + globalFunctions: [name: string, Block][]; +} +``` + +**参数校验惯例**:内建函数参数来自外部编辑器(不可信输入,RESEARCH Security Domain V5)。用 `isNil`(lodash-es)+ `logger.warn(code)` 缺省跳过,**绝不抛异常中断**(参照 content/triggers.ts:141-150 的 `if (!handler.layer || !handler.locator) { logger.warn(164); return; }` 模式)。 + +> ⚠️ A2 待定:具体内建函数清单/命名由用户主导(blockly 编辑器约定)。清单未定前,此文件仅含最小集(如 `openDoor`),其余留空。 + +--- + +### 5. `data-base/src/map/eventView.ts` (新建, ILayerEventView 实现, CRUD+dirty) + +**Analog:** `packages/common/src/dirtyTracker.ts`(`PrivateMapDirtyTracker`)+ `tile.ts`(triggers 增删改方法) + +`IReadonlyEventView` / `ILayerEventView`(map/types.ts:83-119)已定义但无实现。核心是「priority → eventId 的 Map」+ `dirty()`/`markPure()`。dirty 语义(D-12:初始 dirty=false,变动且与基准不同则 true)与 `PrivateMapDirtyTracker` 完全同构。 + +**dirty 追踪样板**(dirtyTracker.ts:92-133 `PrivateMapDirtyTracker`): +```typescript +// mark() 返回一个 symbol 快照;dirtySince(mark) 比较 dirtyFlag 判断是否变化 +private dirtyFlag: number = 0; +protected dirty(data: T): void { + this.dirtyFlag++; + this.markMap.set(data, this.dirtyFlag); +} +``` + +**增删改方法样板**(tile.ts:47-65,旧 `addTrigger`/`deleteTrigger`/`clearTrigger`,新实现直接改名/换类型为 `set`/`delete`/`clear` 对应 `ILayerEventView`): +```typescript +clearTrigger(): void { this.triggers = null; } +addTrigger(trigger: number): void { + if (!this.triggers) this.triggers = new Set(); + this.triggers.add(trigger); +} +// → 新接口为 set(priority, eventId) / delete(priority) / clear() / markPure() +``` + +> 注:`ILayerEventView` 用 `set(priority, event)` 带「优先级冲突则覆盖并 warn」语义(map/types.ts:96-102),冲突警告沿用 `logger.warn(136, ...)`(duplicate priority)惯例。 + +--- + +### 6. `data-common/src/store/index.ts` (改, 桶导出) + +**Analog:** 自身 + `data-common/src/index.ts` + +当前内容(store/index.ts:1-4)已含 `eventStore` 导出,**缺失 `mapStore`**(`core.ts:79` 只能写相对路径 `'../../data-common/src/store/mapStore'` 绕过桶导出——RESEARCH Pitfall 2)。 + +```typescript +// 需补为(参照 data-common/src/index.ts:1-7 的完整桶导出风格) +export * from './eventStore'; +export * from './itemStore'; +export * from './mapStore'; // ← 缺失,补上 +export * from './tileStore'; +export * from './types'; +``` + +--- + +### 7. `data-common/src/event/event.ts` (改, compile 缓存回写) + +**Analog:** 自身 (`GameEvent.compile`) + +RESEARCH Pitfall 4:`compile()` 返回编译结果但未写回 `this.compiled`,导致 `execute()` 每次走 compile 分支。修复样板: + +```typescript +// event/event.ts:24-26 现状 +compile(): AnonTokyoExecutable | null { + return this.interpreter.compile(this.rawEvent); +} +// → 改为写回缓存(this.compiled 是 readonly 接口成员,但类内可写): +compile(): AnonTokyoExecutable | null { + this.compiled = this.interpreter.compile(this.rawEvent); + return this.compiled; +} +``` +`setRaw`(event.ts:45-48)已正确置 `this.compiled = null`,缓存回写后即可生效。 + +--- + +### 8. `data-common/src/store/tileStore.ts` (改, 类型对齐) + +**Analog:** `store/itemStore.ts`(`getData`/`getCategory` 的 `??` 缺省取值模式) + +当前 `tileStore.ts:27-29` 的 `getTrigger` 返回 `dataMap.get(num)?.trigger ?? -1`,但 `ITileRawData` 已把 `trigger: number[]` 改为 `events: Record`(store/types.ts:53),`ITileStore.getTrigger` 签名声明返回 `number[]`(store/types.ts:85)——实现与接口脱节,属「落地已设计接口」必改项。 + +**getter 缺省样板**(itemStore.ts:20-22): +```typescript +getCategory(num: number): ItemCategory { + return this.dataMap.get(num)?.category ?? ItemCategory.Unknown; +} +``` + +--- + +### 9. `data-base/src/map/tile.ts` (改, MapTileBase triggers → events) + +**Analog:** 自身(`MapTileBase` 现有 `triggers`/`addTrigger`/`clearTrigger`/`useEmptyTrigger`,tile.ts:18,47-65) + +`ITileBase` 接口(map/types.ts:148-170)已定义 `tileEvent(): ILayerEventView` 与 `pointEvent(): ILayerEventView | null`,但 `MapTileBase` 未实现(`implements ITileBase` 会报缺成员)。改造方向: + +- `triggers: Set | null` → 图块事件视图(`ILayerEventView`,含 priority→id Map + dirty 标记) +- `addTrigger`/`deleteTrigger`/`clearTrigger`/`useEmptyTrigger` → 委托给 `tileEvent()` 视图的 `set`/`delete`/`clear` +- 新增 `tileEvent()`/`pointEvent()` 实现(pointEvent 来自 `layer.event(x, y)`,map/types.ts:573) + +私有成员必须加 jsDoc 注释(code.md 规则 8)。 + +--- + +### 10/11. `data-base/src/map/staticTile.ts` / `dynamicTile.ts` (改, 存读档 events) + +**Analog:** 自身(现有 `saveState`/`loadState`/`shouldSave` 结构) + +`IStaticBlockSave`/`IDynamicBlockSave` 继承 `IMapBlockSaveBase.events?: ReadonlyMap`(map/types.ts:71-74),但实现仍写 `save.triggers = this.triggers`(staticTile.ts:41-43、dynamicTile.ts:105-107)。改造样板(保持现有结构,只换字段类型与存取方式): + +```typescript +// staticTile.ts:34-58 现状(triggers),改 events 后结构不变 +shouldSave(): boolean { return !!this.triggers; } // → 图块事件视图 dirty() 或非空判断 +saveState(): Readonly { + const save: IStaticBlockSave = {}; + if (this.triggers) save.triggers = this.triggers; // → save.events = 视图.get() 的 ReadonlyMap + return save; +} +loadState(save: Readonly): void { + // triggers.size === 0 → useEmptyTrigger(),否则逐条 addTrigger + // → events 为空 Map → clear(),否则逐条 set(priority, id) +} +``` + +> dirty 标记(D-12)语义:图块事件视图初始以「原始图块 `ITileRawData.events`」为基准(`markPure()`),存档时仅当 `dirty()` 为 true 才写入 `events`。参照 `MapLayer.saveStatics`(mapLayer.ts:580-589)里 `tile.shouldSave()` 决定是否写存档的门控。 + +--- + +### 12. `data-base/src/map/mapLayer.ts` (改, 事件视图 + 点事件) + +**Analog:** 自身(`syncStaticTrigger`、`compareWith`、`event(x,y)`/`getPointEvent` 待实现) + +接口 `IMapLayer.event(x,y): ILayerEventView | null`(map/types.ts:573)与 `getPointEvent`(581)已声明未实现。改造点: + +- `syncStaticTrigger`(mapLayer.ts:118-134):`triggers` 复制 → 图块事件视图的 `set`/`clear`(对应 `transferToDynamic`/`transferToStatic`/`transferToStaticIfSafe` 的 `keepEvent` 参数,map/types.ts:409-435 已定义) +- `createDynamic`(mapLayer.ts:314-329):从 `location.static.triggers` 复制 → 复制图块事件视图 +- 新增 `event(x,y)` / `getPointEvent(x,y)`:点事件存于 `IMapRawData.events`(store/types.ts:310,`Record>>`),不随图块移动(D-03) + +**脏标记对照**(mapLayer.ts:481-503 `dirty()`/`markDirty`/`compareWith`/`isEqualToRef`):点事件的 dirty 应与图层 dirty 协同(RESEARCH A1),沿用 `layerDirty` + `refArray` 对比模式。 + +--- + +### 13/14. `data-system/src/index.ts` / `types.ts` (改, 桶导出 + IStateSystem) + +**Analog:** 自身 + +- `index.ts`(data-system/index.ts:1-4):`export * from './trigger'` → `export * from './event'` +- `types.ts`(data-system/types.ts:6-13):`IStateSystem` 删除 `triggerRegistry`/`triggerCollector` 两个成员,替换为事件执行器成员(如 `readonly eventExecutor: IEventExecutor`,接口名由用户定/待确认) + +```typescript +// data-system/types.ts:6-13 现状 +export interface IStateSystem extends IStateBase { + readonly enemyContext: IEnemyContext; + readonly triggerRegistry: ITriggerRegistry; // ← 删 + readonly triggerCollector: ITriggerCollector; // ← 删 + // ← 增:事件执行器成员 +} +``` + +--- + +### 15. `data-state/src/core.ts` (改, 装配) + +**Analog:** 自身(L2 初始化 region,core.ts:184-209) + +- 删 `TriggerRegistry`/`TriggerCollector` 装配(core.ts:203-207) +- L0 增 `eventStore` 实例化:`const eventStore = new GameEventStore(); this.eventStore = eventStore;`(`IDataCommon.eventStore` 已声明,types.ts:54;`GameEventStore` 已实现,store/eventStore.ts:5-28) +- 修 `import { MapStore } from '../../data-common/src/store/mapStore'`(core.ts:79)为桶导出 `@user/data-common` +- L2 增事件执行器装配(构造器注入 `this`),参照 combat 的 `EnemyContext`/`DamageSystem` 装配风格(core.ts:187-200) + +**装配样板**(core.ts:202-207 现状 → 替换): +```typescript +// 触发器注册与收集器(待删) +const triggerRegistry = new TriggerRegistry(this); +const triggerCollector = new TriggerCollector(); +triggerCollector.attachRegistry(triggerRegistry); +// → 事件执行器(新) +const eventExecutor = new EventExecutor(this); // 类名/接口名待定 +``` + +--- + +### 16. `data-state/src/hero/moverImpl.ts` (改, 踩踏触发重写) + +**Analog:** 自身(`commonTrigger` + enter/leave/hit/cannotEnter,moverImpl.ts:141-184) + +`commonTrigger` 统一「收集 → 执行」流程(moverImpl.ts:141-164)是执行器委托的唯一入口。重写方向: + +- `this.collector`(moverImpl.ts:19,23 `state.triggerCollector`)→ `state.eventExecutor`(或直接访问 `state.eventStore`) +- `triggers.trigger(type, handler)` → 委托执行器,`type`(旧 `TriggerType.Enter/Leave/Hit/CannotEnter`)→ `EventTrigger.OnEnter/OnLeave/OnTouch`(映射待用户确认,RESEARCH A4) + +**委托样板**(moverImpl.ts:166-184): +```typescript +async enter(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.nextLoc; + return this.commonTrigger(TriggerType.Enter, handler, x, y); // → OnEnter +} +async hit(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.nextLoc; + return this.commonTrigger(TriggerType.Hit, handler, x, y); // → OnTouch(待确认) +} +``` + +--- + +### 17/18. 删除 `data-system/src/trigger/*` 与 `data-state/src/content/triggers.ts` + +纯代码删除(D-13)。`content/triggers.ts` 的 `ChangeFloorTrigger`(楼层切换)语义改由 `OnBeforeChangeFloor`/`OnAfterChangeFloor` 事件承载,其 `getFloorTarget`/`getPosTarget` 逻辑(content/triggers.ts:47-134)可作为新「切换楼层」内建函数(builtin.ts)的实现参照,而非直接丢弃。 + +--- + +## Shared Patterns + +### 分层与依赖方向(本项目核心架构) +**Source:** `core.ts` 的 L0/L1/L2/L3 分区注释(core.ts:82-103)+ `data-base/types.ts`/`data-system/types.ts` + +- **L0 `@user/data-common`**:无依赖的序列化契约与 store(`event/types.ts`、`store/eventStore.ts`)。事件类型只依赖 `anon-tokyo`。 +- **L1 `@user/data-base`**:可存档数据(`ITileBase.events`、`IMapBlockSaveBase.events`、`ILayerEventView`),`IDataBaseExtended.state: IStateBase`。 +- **L2 `@user/data-system`**:游戏逻辑系统(事件执行器),`IStateSystem extends IStateBase`(含 `IStateSystemExtended.state: IStateSystem`)。 +- **L3 `@user/data-state`**:装配(`core.ts`)与顶层集成(`moverImpl`)。 + +依赖**只向下**:event 执行器(L2)依赖 `IStateBase`(L1),绝不可反向。`check:circular` 是硬门禁。 + +### 接口先行 + `implements` + `readonly`(code.md 规则 14/15/16,刚性) +**Source:** 全部接口文件(`store/types.ts`、`map/types.ts`、`trigger/types.ts`) +**Apply to:** 所有新建/修改文件 + +```typescript +// 公共方法必须在 interface 定义,class 用 implements 实现 +// 接口成员一律 readonly;对象成员用接口类型不用类类型 +export interface IGameEventStore { + addEvent(id: string, event: IReadonlyGameEvent<...>): void; // 接口签名 + getEvent(id: string): IReadonlyGameEvent | null; +} +// class GameEventStore implements IGameEventStore { ... } +``` + +### 错误处理:`logger` 数字错误码,绝不抛异常中断(code.md 规则 12/13) +**Source:** `packages/common/src/logger.ts:112-171` + `logger.json` +**Apply to:** 所有文件 + +```typescript +import { logger } from '@motajs/common'; +import { isNil } from 'lodash-es'; + +// warn(可恢复,继续运行):logger.warn(code, ...params) +// error(严重,记录但继续):logger.error(code, ...params) +// 每个错误码唯一,不得用 0 或复用无关 code;params 为字符串 + +// 非空判断:对象 if (!obj);字面量 if (isNil(value)) —— 禁止 if (value === undefined) +``` + +本阶段新增/复用的事件相关错误码(logger.json 已知占用):`132` registry 重复、`135` 缺 registry、`136` 重复优先级、`170` eventStore 重复 id。执行器「event id 未找到」需分配**新** code。 + +### Map 工具方法 `getOrInsert` / `getOrInsertComputed`(全局 Map 原型扩展) +**Source:** 用法遍布 `mapLayer.ts:82-83,598`、`combat/context.ts:138,159,405` +**Apply to:** 执行器收集/排序、事件视图内部映射 + +```typescript +const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); +const list = blocks.getOrInsert(index, []); +``` + +### 桶导出 `index.ts` +**Source:** `data-common/src/index.ts`、`data-system/src/index.ts` +**Apply to:** 所有新模块 + +每个模块目录有 `index.ts`,`export * from './xxx'`;改动文件导出时同步改 `index.ts`(否则 `check:type` 失败)。 + +### Hooks(Hookable/HookController) +**Source:** `packages/common/src/hook.ts` + `gameMap.ts`/`mapLayer.ts` 的 `forEachHook` +**Apply to:** 若执行器/内建函数需对外通知(对话/开门动画),沿用 `Hookable` + `createController` 模式(gameMap.ts:213-217);若不需要,则不用(D-09 长事件直接 await,无需挂起状态)。 + +### 存读档 `ISaveableContent` + `SaveCompression` +**Source:** `mapLayer.ts:694-810`(saveState/loadState 三分支 No/Low/HighCompression) +**Apply to:** `staticTile`/`dynamicTile`/`mapLayer` 的 events 存档(事件本体不进存档,只存 id Map) + +--- + +## No Analog Found + +无。本阶段是「落地已设计接口 + 删除旧体系」的 refactor,所有文件的模式都能在代码库中找到同构参照。唯二的**语义待用户确认**项(非「无 analog」,而是接口未定)如下,planner 应标注为待定而非自行拍板(code.md 规则 3「歧义提问」): + +| 文件 | 待定项 | 依据 | +|------|--------|------| +| `data-system/src/event/builtin.ts` | 内建函数清单/命名(对话/开门/道具/战斗的块名与参数结构) | RESEARCH A2 / Open Question 2 | +| `data-base/src/map/mapLayer.ts` (点事件) | 点事件承载字段(`IMapPointRawData` 尚不存在,`IMapRawData.events` 已有但未落地) | RESEARCH A1 / Open Question 1 | +| `data-state/src/hero/moverImpl.ts` | 移动四钩子 → `EventTrigger` 映射(`cannotEnter` 是否有对应、`OnTouch` 与 `OnEnter` 边界) | RESEARCH A4 / Open Question 3 | + +## Metadata + +**Analog search scope:** `packages-user/{data-common,data-base,data-system,data-state}`, `packages/common`, `node_modules/anon-tokyo/dist/index.d.ts` +**Files scanned:** ~40(事件/存储/地图/触发器/战斗/核心装配/移动/日志/dirty-tracker/解释器类型) +**Pattern extraction date:** 2026-09-07 diff --git a/.planning/phases/01-event/01-RESEARCH.md b/.planning/phases/01-event/01-RESEARCH.md index 0451fb1..a4d52a2 100644 --- a/.planning/phases/01-event/01-RESEARCH.md +++ b/.planning/phases/01-event/01-RESEARCH.md @@ -329,7 +329,12 @@ export class GameEventStore implements IGameEventStore { **Note:** A1/A2/A4 是「接口设计由用户主导」下 AI 无法自行拍板的点,应在计划阶段前由用户确认或明确标注为待定。它们不阻塞「删除旧体系 + 落地已设计接口」的骨架工作,但阻塞 EVT-02 对话/开门的端到端验证。 -## Open Questions +## Open Questions (RESOLVED) + +> 以下三个开放问题已在规划阶段前由用户决策(2026-09-07): +> - **A1 已解决**:点事件承载字段已在接口中落地(`IMapRawData.events` 三级结构、`IMapBlockSaveBase.events` 可选、`ITileRawData` 旧 `trigger` 已替换为 `events`)。 +> - **A2 已延后**:Anon Tokyo 内建函数清单(对话/开门/道具/战斗)延后到「收尾工作」,本阶段不实现,`builtInFunctions` 留空骨架。 +> - **A4 已延后**:不处理旧 `ITrigger` 移动四钩子的兼容映射,本阶段直接删除旧体系,不实现 `cannotEnter` 对应触发。 1. **点事件的承载字段(A1)** - What we know: D-03 区分点事件(不随图块移动)与图块事件;图块事件已设计在 `ITileBase.events`;`IMapPointRawData` 目前仅有 `trigger?: number[]` 与 `changeFloor`。