refactor(event): centralize point event references

This commit is contained in:
unanmed 2026-09-09 12:53:37 +08:00
parent a297311ea6
commit 64efcf2555
14 changed files with 617 additions and 167 deletions

View File

@ -34,9 +34,11 @@ Decimal phases appear between their surrounding integers in numeric order.
3. 引擎能执行踩踏触发事件驱动的事件执行链路(对话/开门依赖 A2 内建函数清单,延后到收尾工作)
4. 事件系统保持面向初学者的简单抽象,未引入复杂场景的通用表达能力
**Plans**: 12/12 plans executed
**Plans**: 13/13 plans executed
Plans:
- [x] 01-13-PLAN.md
- [x] 01-04-PLAN.md
- [x] 01-05-PLAN.md
- [x] 01-06-PLAN.md
@ -145,7 +147,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. 事件系统 | 12/12 | Ready to complete | |
| 1. 事件系统 | 13/13 | In Progress| |
| 2. 寻路系统 | 0/TBD | Not started | - |
| 3. 数据端完成 | 0/TBD | Not started | - |
| 4. 渲染适配与双布局 | 0/TBD | Not started | - |

View File

@ -4,16 +4,16 @@ milestone: v1.0
current_phase: 01
current_phase_name: event
status: executing
stopped_at: Phase 01 verification passed; ready for phase completion
last_updated: "2026-09-09T03:53:57.000Z"
stopped_at: Completed 01-13-PLAN.md
last_updated: "2026-09-09T04:51:31.356Z"
last_activity: 2026-09-09
last_activity_desc: "Phase 01 verification passed: user confirmed compile-cache reuse and setRaw invalidation with no issue reported"
state_head: b9213dd
state_head: a297311ea60082fd4c2f1454e1c6bee0286356be
progress:
total_phases: 6
completed_phases: 0
total_plans: 12
completed_plans: 12
total_plans: 13
completed_plans: 13
milestone_name: milestone
---
@ -29,9 +29,9 @@ See: .planning/PROJECT.md (updated 2026-09-07)
## Current Position
Phase: 01 (event) — EXECUTING
Plan: 12 of 12 (gap closure complete)
Status: Phase verification passed; ready for completion transition
Last activity: 2026-09-09 — Phase 01 verification passed; focused tests/type/lint evidence is green in phase scope
Plan: 13 of 13 (point-event reference/storage refactor complete)
Status: Plan 01-13 verification complete; ready for phase completion transition
Last activity: 2026-09-09 — Plan 01-13 focused tests/lint passed; repository-wide type diagnostics remain deferred outside plan scope
Progress: [█████████░] 92%
@ -68,6 +68,7 @@ Progress: [█████████░] 92%
| Phase 01 P09 | 8min | 2 tasks | 2 files |
| Phase 01 P11 | 13min | 2 tasks | 3 files |
| Phase 01 P12 | 20min | 2 tasks | 6 files |
| Phase 01 P13 | 30 | 3 tasks | 11 files |
## Accumulated Context
@ -97,6 +98,8 @@ Recent decisions affecting current work:
- [Phase 01]: Gap-closure plan 01-12 repairs only the missing IBlockEventEnv import, focused fixture typing, and reported CRLF/Prettier errors; it preserves all locked deferrals and public contracts.
- [Phase 01]: Gap-closure Plan 01-12 imports IBlockEventEnv and types only the focused event/map fixtures without changing runtime behavior or public contracts.
- [Phase 01]: Gap-closure Plan 01-12 preserves serialized registration/map-id binding, rawEvent/cache/Promise/as, and eventStore-cycle deferrals.
- [Phase 01]: Plan 01-13: LayerEventView owns point-event refs with O(1) dirty state; MapLayer uses flat index storage and ref-first overlays; MapTileBase centralizes default restoration.
- [Phase 01]: Plan 01-13 preserves the approved save shape, resize semantics, locked deferrals, and leaves types.ts and executor.ts user edits untouched.
### Pending Todos
@ -108,15 +111,17 @@ None yet.
- 01-08 rawEvent cache-safety and no-as implementation assumptions are explicitly removed from the revised executable scope; the 01-05 current contract remains unchanged.
- eventStore circular paths are explicitly preserved as the Phase 01 baseline; the revised 01-08 regression does not require those paths to disappear.
- Plan 01-12 leaves the repository-wide type gate blocked only by pre-existing diagnostics outside the plan-owned files; these are recorded in the phase deferred-items ledger.
- Plan 01-13 records the same repository-wide type gate diagnostics outside its implementation and test files in the phase deferred-items ledger.
## Deferred Items
| Category | Item | Status | Deferred At | Milestone |
|----------|------|--------|-------------|-----------|
| quality gate | Pre-existing TypeScript diagnostics outside Plan 01-12 files | deferred | 2026-09-09 | v1.0 |
| quality gate | Pre-existing TypeScript diagnostics outside Plan 01-13 files | deferred | 2026-09-09 | v1.0 |
## Session Continuity
Last session: 2026-09-09T03:36:51.913Z
Stopped at: Phase 01 verification passed; ready for completion
Last session: 2026-09-09T04:51:31.227Z
Stopped at: Completed 01-13-PLAN.md
Resume file: None

View File

@ -0,0 +1,178 @@
---
phase: 01-event
plan: 13
type: execute
wave: 1
depends_on: []
files_modified:
- packages-user/data-base/src/map/eventView.ts
- packages-user/data-base/src/map/mapLayer.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/eventView.test.ts
- packages-user/data-base/src/map/mapLifecycle.test.ts
- packages-user/data-base/src/map/eventPath.test.ts
- packages-user/data-system/src/event/eventDispatch.test.ts
autonomous: true
requirements: [EVENT-REF-01]
estimate:
tokens: 32000
raw_tokens: 32000
tasks: 3
confidence: low
must_haves:
truths:
- "LayerEventView owns its reference snapshot and returns dirty() in O(1) through a maintained flag."
- "MapLayer stores point events as a flat index-to-view map, saves the approved index-to-priority-to-eventId shape, and never owns a second point-event baseline store."
- "Loading restores each existing point-event view from ref() before applying save overlays, without changing the reference through markPure()."
- "resize preserves in-range point events, crop removes out-of-range entries, and resize2 clears point events while retaining existing resize semantics."
- "Static and dynamic tile default-event restoration is implemented once in MapTileBase without expanding the public interfaces."
- "Source-aware point/tile event dispatch and the existing executor behavior remain unchanged, including the user's uncommitted executor change."
artifacts:
- path: "packages-user/data-base/src/map/eventView.ts"
provides: "LayerEventView reference snapshot, O(1) dirty flag, and mutation bookkeeping"
- path: "packages-user/data-base/src/map/mapLayer.ts"
provides: "Flat point-event storage, ref-based load/reset behavior, save overlays, resize handling, and grouped event region"
- path: "packages-user/data-base/src/map/tile.ts"
provides: "Shared non-public default-event restoration for tile subclasses"
- path: "packages-user/data-base/src/map/eventView.test.ts"
provides: "Focused dirty/ref mutation coverage"
- path: "packages-user/data-base/src/map/mapLifecycle.test.ts"
provides: "Point-event load, save, index, resize, and tile-default regression coverage"
- path: "packages-user/data-base/src/map/eventPath.test.ts"
provides: "Raw point-event baseline and event-view reference coverage"
- path: "packages-user/data-system/src/event/eventDispatch.test.ts"
provides: "Source-aware dispatch regression coverage"
key_links:
- from: "MapLayer.pointEvents"
to: "LayerEventView.ref()"
via: "flat index lookup followed by reset-to-reference copy before save overlay application"
- from: "MapState.fromRaw"
to: "LayerEventView.markPure()"
via: "raw event import establishes the view-owned baseline; MapLayer load paths do not replace it"
- from: "MapLayer.savePointEvents()"
to: "IMapLayerSave.pointEvents"
via: "index key and copied priority-to-eventId map"
- from: "MapTileBase.restoreDefaultEvents()"
to: "StaticTile and DynamicTile"
via: "shared protected implementation called by constructors, set, and loadState"
---
<objective>
Refactor point-event ownership and tile-event restoration so event references live in LayerEventView, MapLayer uses flat index storage, and loading composes reference state with save overlays while preserving all approved event and resize behavior.
Purpose: Make point-event dirtiness and persistence explicit and efficient without changing the user's existing public ref() contract, source-aware dispatch behavior, or deferred event-system decisions.
Output: One executable implementation-and-regression plan with a final scoped commit. The existing uncommitted changes in packages-user/data-base/src/map/types.ts and packages-user/data-system/src/event/executor.ts remain untouched and unstaged by this plan.
</objective>
<execution_context>
@dev.md
@packages-user/data-base/src/map/types.ts
@packages-user/data-base/src/map/eventView.ts
@packages-user/data-base/src/map/mapLayer.ts
@packages-user/data-base/src/map/mapState.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/mapLifecycle.test.ts
@packages-user/data-base/src/map/eventPath.test.ts
@packages-user/data-system/src/event/executor.ts
@packages-user/data-system/src/event/eventDispatch.test.ts
@package.json
</execution_context>
<context>
The working tree already contains user edits: types.ts adds IReadonlyEventView.ref(), and executor.ts contains the user's event reduction/execution changes. Preserve both files exactly as user-owned changes; do not reformat, revert, or stage them.
Use the repository's ESNext/ES2026 runtime assumptions and existing Map.getOrInsertComputed usage. Keep the approved point-event save contract index -> priority -> eventId, resize/resize2 semantics, source-aware dispatch, and the locked boundaries: no production event registration, no rawEvent immutability/Promise/no-as cleanup, and no eventStore cycle repair.
Task dependencies are intentional and sequential inside this single traceable plan: Task 1 establishes the view-owned reference and flat-load path; Task 2 centralizes tile defaults and completes lifecycle coverage on that path; Task 3 locks dispatch regressions, runs the full verification set, and creates the final scoped implementation commit.
</context>
<tasks>
<task type="tracer">
<name>Task 1: Trace point-event reference ownership through save and load</name>
<files>packages-user/data-base/src/map/eventView.ts, packages-user/data-base/src/map/mapLayer.ts, packages-user/data-base/src/map/eventView.test.ts, packages-user/data-base/src/map/mapLifecycle.test.ts</files>
<precondition>The existing IReadonlyEventView.ref() declaration in packages-user/data-base/src/map/types.ts is present and the user's edits in types.ts and packages-user/data-system/src/event/executor.ts are not part of this task's change set.</precondition>
<action>Implement the EVT-REF-01 point-event tracer end to end. In LayerEventView, keep the reference snapshot private, expose it through the already-declared ref() contract, and maintain a dedicated dirty flag whose value is updated by set, delete, clear, markPure, and any reference transition; dirty() itself must only return the flag. Preserve snapshot semantics so restoring the current entries to ref() makes the view clean without calling markPure. In MapLayer, remove the point-event baseline map and all y-to-x point-event traversal, store pointEvents as Map<number, ILayerEventView>, and use getOrInsertComputed for index lookup/creation where it prevents nullable values. Group the point-event field plus event-specific helpers and public methods in one #region, while leaving unrelated dynamic-tile storage and behavior alone. Make savePointEvents emit copied index-to-priority-to-eventId maps. Make loadPointEvents first restore every existing view by reading eventView.ref() and then apply save overlays by flat index; do not mark views pure during loading and do not add a new public reset API unless compilation and the existing interface prove it is required. Cache repeated tileEvent() calls in any MapLayer method changed by this task. Add focused tests for dirty transitions, ref stability, load-without-overlay reset, load-with-overlay dirty state, and the flat save key shape.
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts</automated>
<automated>pnpm check:type</automated>
</verify>
<done>LayerEventView dirty() is O(1) by implementation, point-event references exist only inside views, MapLayer uses flat index storage, load restores ref() before overlays without markPure(), and focused tests prove clean reset, dirty overlay, immutable saved copies, and index-shaped persistence.</done>
</task>
<task type="auto">
<name>Task 2: Centralize tile defaults and finish map lifecycle edge cases</name>
<depends_on>Task 1</depends_on>
<files>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/mapLifecycle.test.ts, packages-user/data-base/src/map/eventPath.test.ts</files>
<action>Implement the EVT-REF-01 expansion without changing public API visibility. Move restoreDefaultEvents into MapTileBase as a non-public subclass helper, use one local tile-event view for repeated calls in each affected method, and have static/dynamic constructors, set, and loadState use the shared restoration path so defaults are rebuilt and marked clean exactly as before. Keep the existing tile-specific raw/num implementations and transfer behavior. Complete lifecycle tests for tile default restoration after set/load and static-dynamic conversion, point-event ref() values loaded from raw data, preserving in-range events during resize, cropping out-of-range indexes, and clearing all point events with resize2. Verify that resize and resize2 continue to mark the layer dirty according to their existing contracts and that no unrelated event-system cleanup is introduced.
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts</automated>
<automated>pnpm lint:custom packages-user/data-base/src/map/eventView.ts packages-user/data-base/src/map/mapLayer.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/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts</automated>
</verify>
<done>MapTileBase owns the single non-public default restoration implementation, repeated tileEvent() calls use local constants where applicable, and lifecycle tests cover defaults, ref/load, crop, clear-resize, and approved resize semantics.</done>
</task>
<task type="auto">
<name>Task 3: Lock dispatch regressions and create the final scoped commit</name>
<depends_on>Task 2</depends_on>
<files>packages-user/data-system/src/event/eventDispatch.test.ts</files>
<action>Implement the EVT-REF-01 regression fence by retaining and, where needed, extending the focused source-aware dispatch assertions for point, static-tile, and dynamic-tile sources, their ordering, trigger filtering, source metadata, and sequential awaiting. Do not modify packages-user/data-system/src/event/executor.ts or any production registration/event-store code. Run the complete focused suite, type check, lint, and diff hygiene checks. Before committing, inspect the staged file list and stage only the implementation and test files listed in this plan; explicitly leave the pre-existing user changes in packages-user/data-base/src/map/types.ts and packages-user/data-system/src/event/executor.ts unstaged. Create the required final commit with message refactor(event): centralize point event references.
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts packages-user/data-system/src/event/eventDispatch.test.ts</automated>
<automated>pnpm check:type</automated>
<automated>pnpm lint:custom packages-user/data-base/src/map/eventView.ts packages-user/data-base/src/map/mapLayer.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/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts packages-user/data-system/src/event/eventDispatch.test.ts</automated>
<automated>git diff --check -- packages-user/data-base/src/map/eventView.ts packages-user/data-base/src/map/mapLayer.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/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts packages-user/data-system/src/event/eventDispatch.test.ts</automated>
</verify>
<done>All focused event and lifecycle regressions pass, type/lint/diff checks pass, the final commit contains only this refactor's implementation/tests, the two pre-existing user-edited files remain unchanged and unstaged, and the commit message is exactly refactor(event): centralize point event references.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| raw/save event data -> MapLayer | Event indexes, priorities, and IDs cross from serialized/raw structures into mutable in-memory views. |
| working tree -> final commit | Existing user-owned edits must not be accidentally included in the refactor commit. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-01-event | Tampering | MapLayer point-event load | medium | mitigate | Retain in-map checks through event(indexToLocator), restore only from the view-owned ref(), apply copied save overlays, and cover malformed/out-of-range behavior through existing raw/event-path tests. |
| T-02-event | Tampering | LayerEventView reference state | medium | mitigate | Keep the reference snapshot private, expose it only through the existing readonly ref() contract, and test that save overlays do not replace the reference baseline. |
| T-03-event | Tampering | Git staging and final commit | high | mitigate | Inspect the staged path list before commit and stage only plan-owned implementation/test files; explicitly exclude types.ts and executor.ts, which contain current user edits. |
</threat_model>
<verification>
Run the focused Vitest command, pnpm check:type, and the grounded lint command from Task 3. Review the diff for one MapLayer point-event #region, absence of a MapLayer point-event baseline collection, flat point-event storage, no markPure call in MapLayer loading, shared MapTileBase restoration, cached repeated tileEvent() calls, unchanged save shape, and untouched user edits. Confirm the final commit contains only the plan-owned refactor/test paths.
</verification>
<success_criteria>
- Point-event reference data is owned exclusively by LayerEventView and dirty() is an O(1) flag read.
- MapLayer point events are index -> ILayerEventView, load restores ref() then overlays saves, and resize/resize2 behavior is preserved.
- MapTileBase contains the shared non-public default-event restoration path without new undocumented public API.
- Focused tests cover dirty/ref/reset/load, index persistence, crop/clear resize, tile defaults, and source-aware dispatch regressions.
- The final implementation commit is scoped to this refactor and leaves the existing user changes in types.ts and executor.ts untouched and unstaged.
</success_criteria>
## Source audit
| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
|--------|----|--------------------|------|--------|-------|
| GOAL | — | User-requested Phase 01 event reference/storage refactor | 01-13 | COVERED | Objective and all three tasks trace the end-to-end path. |
| REQ | EVENT-REF-01 | All seven requested scope groups, including tests and final commit | 01-13 | COVERED | Must-haves and task acceptance criteria cover each item. |
| RESEARCH | — | ESNext/ES2026 Map.getOrInsertComputed and repository patterns | 01-13 | COVERED | Grounded in tsconfig target/lib and existing repository usage; no new dependency. |
| CONTEXT | — | Existing user edits and locked deferrals supplied in the request | 01-13 | COVERED | types.ts and executor.ts are preserved; production registration, rawEvent cleanup, and eventStore repair are outside the change set. |
<output>
After execution, create .planning/phases/01-event/01-13-SUMMARY.md and report the final commit hash.
</output>

View File

@ -0,0 +1,193 @@
---
phase: 01-event
plan: 13
subsystem: event
tags: [typescript, map, point-events, tile-events, vitest]
requires:
- phase: 01-event
provides: Source-aware event dispatch, raw map event loading, and tile event persistence
provides:
- LayerEventView-owned reference snapshots with O(1) dirty reads
- Flat MapLayer point-event storage with ref-first save overlay loading
- Shared MapTileBase default-event restoration and regression coverage
affects: [event, map, save-load, resize]
actuals:
tokens: 5379
tasks: 3
commits: 1
plan_head_before: a297311ea60082fd4c2f1454e1c6bee0286356be
tech-stack:
added: []
patterns:
- LayerEventView maintains a mismatch count so dirty() is a flag read
- MapLayer stores point events by serialized flat index and composes ref plus save overlay
- MapTileBase owns protected default-event restoration for static and dynamic tiles
key-files:
created:
- packages-user/data-base/src/map/eventView.test.ts
- .planning/phases/01-event/01-13-PLAN.md
modified:
- packages-user/data-base/src/map/eventView.ts
- packages-user/data-base/src/map/mapLayer.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/mapLifecycle.test.ts
- packages-user/data-base/src/map/eventPath.test.ts
- packages-user/data-system/src/event/eventDispatch.test.ts
key-decisions:
- "Point-event references remain exclusively in LayerEventView; MapLayer has no second baseline collection."
- "MapLayer load restores each view from ref() without markPure(), then applies copied save overlays."
- "The native getOrInsertComputed path is used when available with a compatibility fallback for the repository's supported Node runtime."
- "The two pre-existing user edits in types.ts and executor.ts remain untouched and unstaged."
patterns-established:
- "Flat point-event index keys are reindexed during resize before new dimensions are exposed."
- "Tile subclasses call one shared protected restoration path and cache repeated tileEvent() access locally."
requirements-completed: [EVENT-REF-01]
coverage:
- id: D1
description: "LayerEventView owns stable refs and maintains O(1) dirty state across mutation and restoration."
requirement: EVENT-REF-01
verification:
- kind: unit
ref: packages-user/data-base/src/map/eventView.test.ts
status: pass
human_judgment: false
- id: D2
description: "MapLayer flat point-event save/load, ref reset, resize reindexing, crop, and resize2 clearing are covered."
requirement: EVENT-REF-01
verification:
- kind: unit
ref: packages-user/data-base/src/map/mapLifecycle.test.ts
status: pass
- kind: unit
ref: packages-user/data-base/src/map/eventPath.test.ts
status: pass
human_judgment: false
- id: D3
description: "Static and dynamic tile default restoration and source-aware dispatch regressions remain green."
requirement: EVENT-REF-01
verification:
- kind: unit
ref: packages-user/data-system/src/event/eventDispatch.test.ts
status: pass
- kind: other
ref: "pnpm lint:custom [all Plan 01-13 implementation/test files]"
status: pass
human_judgment: false
duration: 30min
completed: 2026-09-09
status: complete
---
# Phase 01 Plan 13 Summary
**Point-event references are now view-owned with flat index persistence, ref-first load composition, shared tile defaults, and focused dispatch/lifecycle regressions.**
## Performance
- **Duration:** 30 min
- **Started:** 2026-09-09T04:20:00Z
- **Completed:** 2026-09-09T04:50:25Z
- **Tasks:** 3
- **Files modified:** 11 implementation/test/planning files, plus required planning metadata
## Accomplishments
- Added private LayerEventView references, stable ref(), and mutation-maintained O(1) dirty state
- Replaced nested MapLayer point-event storage and baseline tracking with flat index-to-view storage, ref restoration, copied save overlays, and resize reindexing
- Centralized static/dynamic tile default restoration in MapTileBase and extended lifecycle, raw-reference, and source-aware dispatch regression tests
- Preserved the approved save shape, resize/resize2 behavior, locked event-system deferrals, and both user-owned edits
## Task Commits
The user required exactly one final commit for this plan, so Tasks 13 are represented by the single scoped final commit:
1. **Task 1: Trace point-event reference ownership through save and load** — included in the final scoped commit
2. **Task 2: Centralize tile defaults and finish map lifecycle edge cases** — included in the final scoped commit
3. **Task 3: Lock dispatch regressions and create the final scoped commit** — included in the final scoped commit
The final commit message is exactly `refactor(event): centralize point event references`.
## Files Created/Modified
- `packages-user/data-base/src/map/eventView.ts` - View-owned reference snapshot and O(1) dirty bookkeeping
- `packages-user/data-base/src/map/mapLayer.ts` - Flat point-event storage, ref-first load, save overlays, and resize handling
- `packages-user/data-base/src/map/tile.ts` - Shared protected default-event restoration
- `packages-user/data-base/src/map/staticTile.ts` - Shared restoration and cached event-view usage
- `packages-user/data-base/src/map/dynamicTile.ts` - Shared restoration and cached event-view usage
- `packages-user/data-base/src/map/eventView.test.ts` - Focused dirty/ref regression coverage
- `packages-user/data-base/src/map/mapLifecycle.test.ts` - Load, save, resize, crop, and tile-default coverage
- `packages-user/data-base/src/map/eventPath.test.ts` - Raw point-event reference coverage
- `packages-user/data-system/src/event/eventDispatch.test.ts` - Source metadata dispatch assertions
- `.planning/phases/01-event/01-13-PLAN.md` - Executed plan retained for traceability
## Decisions Made
- Point-event reference state is owned only by LayerEventView; MapLayer no longer captures a second baseline.
- Loading copies ref() into existing views before applying save entries and never calls markPure() from MapLayer load paths.
- Flat indexes are reindexed from old dimensions during resize so in-range coordinates survive width changes.
- Runtime support for environments without Map.prototype.getOrInsertComputed uses a local compatibility fallback without changing the storage contract.
- `packages-user/data-base/src/map/types.ts` and `packages-user/data-system/src/event/executor.ts` were hash-verified unchanged and remain unstaged.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Runtime compatibility] Added a fallback for unavailable `Map.getOrInsertComputed`**
- **Found during:** Task 1 focused Vitest run
- **Issue:** The repository's current Node/Vitest runtime does not expose `Map.prototype.getOrInsertComputed`, although the project targets the API and uses it where available.
- **Fix:** Retained the getOrInsertComputed path and added a local get/set fallback for MapLayer event creation.
- **Files modified:** `packages-user/data-base/src/map/mapLayer.ts`
- **Verification:** Focused lifecycle tests and the complete focused suite pass.
**2. [Rule 3 - Test setup] Added the existing replay-checking global setup to the new event-view test**
- **Found during:** Task 1 focused Vitest run
- **Issue:** Importing LayerEventView directly initializes the shared logger, which requires the test suite's `main` and `location` globals.
- **Fix:** Added the same `vi.hoisted` globals used by the adjacent map tests.
- **Files modified:** `packages-user/data-base/src/map/eventView.test.ts`
- **Verification:** The event-view test passes.
---
**Total deviations:** 2 auto-fixed (Rule 1: 1, Rule 3: 1)
**Impact on plan:** Both fixes were local compatibility/test-infrastructure corrections and did not expand the approved architecture or public API.
## Issues Encountered
- `pnpm check:type` was run as required but remains red only for pre-existing diagnostics outside the plan-owned files. The exact scope is recorded in `.planning/phases/01-event/deferred-items.md`; no diagnostics were reported in the changed implementation or test files.
- Focused Vitest runs emit existing logger warnings for duplicate priorities and missing fixture event IDs; all focused assertions pass.
## Verification
- `pnpm exec vitest run packages-user/data-base/src/map/eventView.test.ts packages-user/data-base/src/map/mapLifecycle.test.ts` — PASS (7 tests)
- `pnpm exec vitest run packages-user/data-base/src/map/mapLifecycle.test.ts packages-user/data-base/src/map/eventPath.test.ts` — PASS (14 tests)
- Complete focused Vitest suite across all four plan test files — PASS (22 tests)
- Grounded `pnpm lint:custom ...` command — PASS
- `git diff --check` for all plan-owned implementation/test paths — PASS
- `pnpm check:type` — deferred repository-wide diagnostics only; documented above
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
The event reference/storage refactor is ready for phase verification. The repository-wide type gate remains blocked by the pre-existing legacy/client diagnostics documented in the phase deferred-items ledger.
## Self-Check: PASSED
- All listed implementation, test, plan, summary, and deferred-item files exist
- The pre-existing `types.ts` and `executor.ts` hashes match their start-of-plan hashes
- The final scoped commit is prepared to contain one commit for all three tasks

View File

@ -1,6 +1,15 @@
# Deferred Items
# Deferred Items — Phase 01 Plan 13
- `pnpm check:type` remains red on pre-existing files outside Plan 01-12,
including legacy client exports, the TileStore trigger contract, and legacy
hero/tile consumers. The plan-owned diagnostics are resolved; these unrelated
diagnostics are deferred without runtime or public-contract changes.
## Pre-existing repository-wide diagnostics
`pnpm check:type` remains blocked by diagnostics outside the files owned by
Plan 01-13. The failures are in the legacy/client integration surfaces and
the pre-existing `TileStore`/`ITileStore` trigger-shape mismatch, including:
- `packages-user/client-modules/**`
- `packages-user/data-common/src/store/tileStore.ts`
- `packages-user/data-state/**`
- `packages-user/legacy-plugin-data/**`
- `packages/legacy-ui/src/tools/equipbox.tsx`
No type errors were reported in the Plan 01-13 implementation or test files.

View File

@ -45,20 +45,6 @@ export class DynamicTile
this.restoreDefaultEvents();
}
/**
*
*/
private restoreDefaultEvents(): void {
this.tileEvent().clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
this.tileEvent().set(Number(priority), id);
}
}
this.tileEvent().markPure();
}
num(): number {
return this.tileNum;
}
@ -115,10 +101,11 @@ export class DynamicTile
saveState(): Readonly<IDynamicBlockSave> {
let save: IDynamicBlockSave;
if (this.tileEvent().dirty()) {
const eventView = this.tileEvent();
if (eventView.dirty()) {
save = {
num: this.num(),
events: new Map(this.tileEvent().get())
events: new Map(eventView.get())
};
} else {
save = {
@ -131,9 +118,10 @@ export class DynamicTile
loadState(save: Readonly<IDynamicBlockSave>): void {
this.restoreDefaultEvents();
if (save.events) {
this.tileEvent().clear();
const eventView = this.tileEvent();
eventView.clear();
for (const [priority, id] of save.events) {
this.tileEvent().set(priority, id);
eventView.set(priority, id);
}
}
}

View File

@ -61,6 +61,9 @@ describe('MapState raw event path', () => {
expect(layer?.getPointEvent(1, 0)).toEqual(
new Map([[5, 'point-event']])
);
expect(layer?.event(1, 0)?.ref()).toEqual(
new Map([[5, 'point-event']])
);
expect(layer?.getLocationData(1, 0)?.static.tileEvent().get()).toEqual(
new Map()
);

View File

@ -0,0 +1,48 @@
import { describe, expect, it, vi } from 'vitest';
import { LayerEventView } from './eventView';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
});
describe('LayerEventView reference tracking', () => {
it('keeps dirty state in sync with set, delete, and clear', () => {
const view = new LayerEventView();
expect(view.ref()).toEqual(new Map());
expect(view.dirty()).toBe(false);
view.set(10, 'first');
expect(view.dirty()).toBe(true);
view.delete(10);
expect(view.dirty()).toBe(false);
view.set(10, 'first');
view.markPure();
view.set(20, 'second');
expect(view.dirty()).toBe(true);
view.clear();
expect(view.dirty()).toBe(true);
view.set(10, 'first');
expect(view.dirty()).toBe(false);
});
it('exposes a stable reference snapshot and restores it without markPure', () => {
const view = new LayerEventView();
view.set(10, 'first');
view.markPure();
const reference = view.ref();
view.set(10, 'changed');
view.clear();
for (const [priority, id] of reference) {
view.set(priority, id);
}
expect(view.ref()).toBe(reference);
expect(view.ref()).toEqual(new Map([[10, 'first']]));
expect(view.get()).toEqual(new Map([[10, 'first']]));
expect(view.dirty()).toBe(false);
});
});

View File

@ -5,44 +5,70 @@ export class LayerEventView implements ILayerEventView {
/** 当前绑定的事件 */
private readonly store: Map<number, string> = new Map();
/** 用于判断事件是否变化的参考基准 */
private reference: Map<number, string> | null = null;
private reference: Map<number, string> = new Map();
/** 当前存储与参考基准不一致的条目数量 */
private dirtyEntries: number = 0;
get(): ReadonlyMap<number, string> {
return this.store;
}
ref(): ReadonlyMap<number, string> {
return this.reference;
}
/**
*
* @param priority
*/
private isEntryDirty(priority: number): boolean {
const storeHas = this.store.has(priority);
const referenceHas = this.reference.has(priority);
return (
storeHas !== referenceHas ||
(storeHas &&
this.store.get(priority) !== this.reference.get(priority))
);
}
/**
*
* @param priority
* @param before
*/
private updateDirtyEntry(priority: number, before: boolean): void {
const after = this.isEntryDirty(priority);
if (before !== after) {
this.dirtyEntries += after ? 1 : -1;
}
}
set(priority: number, event: string): void {
if (this.store.has(priority)) {
logger.warn(136, priority.toString());
}
const before = this.isEntryDirty(priority);
this.store.set(priority, event);
this.updateDirtyEntry(priority, before);
}
delete(priority: number): void {
const before = this.isEntryDirty(priority);
this.store.delete(priority);
this.updateDirtyEntry(priority, before);
}
clear(): void {
this.store.clear();
this.dirtyEntries = this.reference.size;
}
markPure(): void {
this.reference = new Map(this.store);
this.dirtyEntries = 0;
}
dirty(): boolean {
const reference = this.reference;
if (!reference) {
return this.store.size > 0;
}
if (this.store.size !== reference.size) {
return true;
}
for (const [priority, event] of this.store) {
if (reference.get(priority) !== event) {
return true;
}
}
return false;
return this.dirtyEntries > 0;
}
}

View File

@ -47,14 +47,6 @@ export class MapLayer
/** 坐标到动态图块集合的映射,外层 key = y内层 key = x */
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 点事件视图,外层 key = y内层 key = x */
private readonly pointEvents: Map<number, Map<number, ILayerEventView>> =
new Map();
/** 点事件的原始纯基准,外层 key = y内层 key = x */
private readonly pointEventBaselines: Map<
number,
Map<number, ReadonlyMap<number, string>>
> = new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
/** 图层脏标记 */
@ -62,6 +54,11 @@ export class MapLayer
/** 图层参考基准,用于存档压缩对比 */
private refArray: Uint32Array | null = null;
//#region 点事件操作
/** 点事件视图key = y * width + x */
private readonly pointEvents: Map<number, ILayerEventView> = new Map();
constructor(
array: Uint32Array,
width: number,
@ -138,80 +135,45 @@ export class MapLayer
if (!staticTile) return;
staticTile.set(staticTile.num());
if (keepEvent) {
staticTile.tileEvent().clear();
for (const [priority, id] of tile.tileEvent().get()) {
staticTile.tileEvent().set(priority, id);
const staticEvent = staticTile.tileEvent();
const dynamicEvent = tile.tileEvent();
staticEvent.clear();
for (const [priority, id] of dynamicEvent.get()) {
staticEvent.set(priority, id);
}
}
}
/**
*
* @param x
* @param y
* @param eventView
*/
private capturePointEventBaseline(
x: number,
y: number,
eventView: ILayerEventView
): void {
const yMap = this.pointEventBaselines.get(y);
if (yMap?.has(x)) return;
if (eventView.dirty()) return;
const target = yMap ?? new Map<number, ReadonlyMap<number, string>>();
target.set(x, new Map(eventView.get()));
if (!yMap) this.pointEventBaselines.set(y, target);
}
/**
*
*/
private resetPointEvents(): void {
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
this.capturePointEventBaseline(x, y, eventView);
}
}
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
eventView.clear();
const baseline = this.pointEventBaselines.get(y)?.get(x);
if (baseline) {
for (const [priority, id] of baseline) {
eventView.set(priority, id);
}
}
eventView.markPure();
for (const eventView of this.pointEvents.values()) {
const reference = eventView.ref();
eventView.clear();
for (const [priority, id] of reference) {
eventView.set(priority, id);
}
}
}
/**
*
*
* @param width
* @param height
*/
private cropPointEvents(width: number, height: number): void {
for (const [y, xMap] of this.pointEvents) {
if (y >= height) {
this.pointEvents.delete(y);
continue;
const pointEvents = new Map<number, ILayerEventView>();
for (const [index, eventView] of this.pointEvents) {
const x = index % this.width;
const y = Math.floor(index / this.width);
if (x < width && y < height) {
pointEvents.set(y * width + x, eventView);
}
for (const x of xMap.keys()) {
if (x >= width) xMap.delete(x);
}
if (xMap.size === 0) this.pointEvents.delete(y);
}
for (const [y, xMap] of this.pointEventBaselines) {
if (y >= height) {
this.pointEventBaselines.delete(y);
continue;
}
for (const x of xMap.keys()) {
if (x >= width) xMap.delete(x);
}
if (xMap.size === 0) this.pointEventBaselines.delete(y);
this.pointEvents.clear();
for (const [index, eventView] of pointEvents) {
this.pointEvents.set(index, eventView);
}
}
@ -220,12 +182,9 @@ export class MapLayer
*/
private savePointEvents(): Map<number, ReadonlyMap<number, string>> {
const pointEvents = new Map<number, ReadonlyMap<number, string>>();
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
if (!eventView.dirty()) continue;
const index = this.map.indexer.locToIndex(x, y);
pointEvents.set(index, new Map(eventView.get()));
}
for (const [index, eventView] of this.pointEvents) {
if (!eventView.dirty()) continue;
pointEvents.set(index, new Map(eventView.get()));
}
return pointEvents;
}
@ -240,7 +199,8 @@ export class MapLayer
this.resetPointEvents();
if (!save) return;
for (const [index, events] of save) {
const { x, y } = this.map.indexer.indexToLocator(index);
const x = index % this.width;
const y = Math.floor(index / this.width);
const eventView = this.event(x, y);
if (!eventView) continue;
eventView.clear();
@ -250,6 +210,31 @@ export class MapLayer
}
}
event(x: number, y: number): ILayerEventView | null {
if (!this.inMap(x, y)) return null;
const index = y * this.width + x;
const getOrInsertComputed = this.pointEvents.getOrInsertComputed;
if (getOrInsertComputed) {
return getOrInsertComputed.call(
this.pointEvents,
index,
() => new LayerEventView()
);
}
let eventView = this.pointEvents.get(index);
if (!eventView) {
eventView = new LayerEventView();
this.pointEvents.set(index, eventView);
}
return eventView;
}
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
return this.event(x, y)?.get() ?? null;
}
//#endregion
//#region 静态图层操作
inMap(x: number, y: number): boolean {
@ -432,9 +417,11 @@ export class MapLayer
const tile = new DynamicTile(num, x, y, this);
const location = this.getLocationData(x, y);
if (location) {
tile.tileEvent().clear();
for (const [priority, id] of location.static.tileEvent().get()) {
tile.tileEvent().set(priority, id);
const tileEvent = tile.tileEvent();
const staticEvent = location.static.tileEvent();
tileEvent.clear();
for (const [priority, id] of staticEvent.get()) {
tileEvent.set(priority, id);
}
}
this.addTileToPosMap(tile, x, y);
@ -580,27 +567,6 @@ export class MapLayer
this.setBlock(num, x, y);
}
event(x: number, y: number): ILayerEventView | null {
if (!this.inMap(x, y)) return null;
let xMap = this.pointEvents.get(y);
if (!xMap) {
xMap = new Map();
this.pointEvents.set(y, xMap);
}
let eventView = xMap.get(x);
if (!eventView) {
eventView = new LayerEventView();
xMap.set(x, eventView);
} else {
this.capturePointEventBaseline(x, y, eventView);
}
return eventView;
}
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
return this.event(x, y)?.get() ?? null;
}
//#endregion
//#region 图层操作
@ -616,10 +582,8 @@ export class MapLayer
dirty(): boolean {
if (this.layerDirty) return true;
for (const xMap of this.pointEvents.values()) {
for (const eventView of xMap.values()) {
if (eventView.dirty()) return true;
}
for (const eventView of this.pointEvents.values()) {
if (eventView.dirty()) return true;
}
return false;
}
@ -693,7 +657,6 @@ export class MapLayer
resize2(width: number, height: number): void {
this.layerDirty = true;
this.pointEvents.clear();
this.pointEventBaselines.clear();
if (this.width === width && this.height === height) {
this.empty = true;
this.mapArray.fill(0);

View File

@ -267,4 +267,33 @@ describe('MapLayer point event lifecycle', () => {
layer.resize2(1, 1);
expect(layer.getPointEvent(0, 0)).toEqual(new Map());
});
it('reindexes preserved point events when the layer width changes', () => {
const { layer } = createMapState({
1: { 1: 'top-right' },
3: { 1: 'bottom-right' }
});
layer.resize(3, 2);
expect(layer.getPointEvent(1, 0)).toEqual(new Map([[1, 'top-right']]));
expect(layer.getPointEvent(1, 1)).toEqual(
new Map([[1, 'bottom-right']])
);
layer.event(1, 1)!.set(2, 'runtime-event');
expect(
layer.saveState(SaveCompression.NoCompression).pointEvents
).toEqual(
new Map([
[
4,
new Map([
[1, 'bottom-right'],
[2, 'runtime-event']
])
]
])
);
});
});

View File

@ -16,20 +16,6 @@ export class StaticTile
this.restoreDefaultEvents();
}
/**
*
*/
private restoreDefaultEvents(): void {
this.tileEvent().clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
this.tileEvent().set(Number(priority), id);
}
}
this.tileEvent().markPure();
}
num(): number {
return this.layer.getBlock(this.locator.x, this.locator.y);
}
@ -48,14 +34,16 @@ export class StaticTile
}
shouldSave(): boolean {
return this.tileEvent().dirty();
const eventView = this.tileEvent();
return eventView.dirty();
}
saveState(): Readonly<IStaticBlockSave> {
let save: IStaticBlockSave;
if (this.tileEvent().dirty()) {
const eventView = this.tileEvent();
if (eventView.dirty()) {
save = {
events: new Map(this.tileEvent().get())
events: new Map(eventView.get())
};
} else {
save = {};
@ -66,9 +54,10 @@ export class StaticTile
loadState(save: Readonly<IStaticBlockSave>): void {
this.restoreDefaultEvents();
if (save.events) {
this.tileEvent().clear();
const eventView = this.tileEvent();
eventView.clear();
for (const [priority, id] of save.events) {
this.tileEvent().set(priority, id);
eventView.set(priority, id);
}
}
}

View File

@ -37,6 +37,21 @@ export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
abstract set(num: number): void;
/**
*
*/
protected restoreDefaultEvents(): void {
const eventView = this.tileEvent();
eventView.clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
eventView.set(Number(priority), id);
}
}
eventView.markPure();
}
setFaceDirection(direction: FaceDirection): number {
const cur = this.num();
const next = this.layer.faceBinder.getFaceOf(cur, direction);

View File

@ -301,6 +301,8 @@ describe('source-aware matching dispatch', () => {
expect(calls[0].tile).toBeNull();
expect(calls[1].tile).toBe(fixture.dynamic);
expect(calls[2].tile).toBe(fixture.layer.getLocationData(1, 0)!.static);
expect(calls[1].triggerLocator).toEqual({ x: 1, y: 0 });
expect(calls[2].triggerLocator).toEqual({ x: 1, y: 0 });
});
it('awaits each source before continuing to the next one', async () => {