# 地图回合与技能状态伪代码规范 实现进度与代码接线台账见 [map-turn-implementation-status.md](map-turn-implementation-status.md)。**角色分工**:本规范写冻结规则与实现契约(尤其 §6);台账写「已接线文件 / API 清单 / 待办勾选项」,二者交叉引用,避免重复维护大段表格。 ## 1. 目标与范围 本规范用于在当前项目中引入“地图时间(Map Turn)”和“四类技能状态”机制,确保: - 规则口径唯一,避免实现歧义。 - 变量命名可直接映射到代码实现。 - 与现有流程兼容:移动、战斗、道具、捕捉、存读档。 - 先支持玩家侧,后续可平滑扩展到“敌方技能”。 ## 2. 规则优先级(冻结版) 当规则冲突时,按以下优先级从高到低执行: 1. 死亡流程优先:战斗失败或致死效果触发后,立即进入死亡清理。 2. 战斗成功后扣层:只有成功结算的战斗才扣除“按战斗次数持续”的状态层数。 3. 时间推进来源:仅由推进时间的行为触发(移动/拾取/tools/战斗/远程/恢复)。 4. 状态技能时间固定:状态技能 `timeCost = 0`,开启/关闭不直接推进地图时间。 5. 战斗耗时结算:`battleFinalTimeCost = max(baseBattleTimeCost, statusBattleTimeCostMax)`。 6. 多战逐场结算:同一帧内多场战斗(如多怪捕捉)逐场独立结算。 7. 存读档一致:状态容器完整写入 `flags`,读档恢复后行为一致。 8. 切楼不清状态:切楼、读档不影响状态持续;死亡按机制清临时状态。 9. 事件默认不打断状态:除非事件显式调用清理接口。 ## 2.1 时间增量(`timeCost` / `deltaTime`)与地图回合(`mapTurn`) | 概念 | 含义 | 典型变化 | |------|------|----------| | **时间增量** | 一次玩家行为产生的「要结算的时间单位」总数,记为 `deltaTime`(与道具/战斗等配置的 `timeCost` 或 `battleFinalTimeCost` 一致)。 | 可一次从 `0` 跳到 `3`(例如 `consumeTime(3, ...)`)。 | | **`mapTurn`(离散 tick)** | 每完成 **1** 个时间单位的完整地图回合结算,记数 `+1`。 | 必须 **逐一** 经历 `… → k → k+1 → k+2 → …`,不可把 3 个 tick 合并成一次模糊批处理。 | **结算顺序(写死)**:对外入口 `consumeTime(deltaTime, reason)` 在单帧内收到 `deltaTime = n` 时: 1. 先将累计量一次性加上:`flags.mapTurnState.clock += n`(表示「时间池」或总经过时间,允许与 `mapTurn` 不同步增长策略见下)。 2. 再 **循环 `n` 次** 调用 `advanceMapTurnOne(reason)`:每次只做 **一个** tick——`mapTurn += 1`,并在该 tick 内完成敌人槽位/行动等(见 §6)。 **推荐关系**:若采用「整数时间模型」,也可令 `clock` 仅在循环内每 tick `+1`,与 `mapTurn` 同步递增;**无论 `clock` 是一次 `+=n` 还是分 `n` 次 `+=1`,`advanceMapTurnOne` 必须被调用恰好 `n` 次**,以保证「0→3 的时间变化」对应「mapTurn 连续三步」的语义。 **实现落点**:调度函数挂在 [project/plugins.js](project/plugins.js) 的 `core.plugin.mapTurn`(或等价模块名);`items.js` / `enemys.js` 仅提供数值字段 `timeCost` 等,不在图块脚本里写复杂循环。 ## 3. 统一字段与运行时数据模型 ### 3.1 技能定义(items 扩展) 技能品类通过 **`cls`** 区分(写入 `project/items.js`,无需另行注册「技能类型枚举」): | `cls` | 含义 | `timeCost` 与地图回合 | |-------|------|----------------------| | **`skillbattle`** | 战斗技能(开关型) | 使用瞬间 **不** `consumeTime`;数值参与战后 **`settleBattleTimeCost`** 与基础耗时的 **max** | | **`skillremote`** | 远程技能 | 使用后立即按 **`timeCost`** `consumeTime`(规范 §4) | | **`skillrecover`** | 恢复技能 | 同上 | | **`skillupdate`** | 状态技能 | **`timeCost` 固定为 `0`**;开关 **不** `consumeTime`;持续层数等走 **`skillState`** | ```js // project/items.js 示例字段(按需) { cls: "skillbattle" | "skillremote" | "skillrecover" | "skillupdate", timeCost: number, // 战斗/远程/恢复语义见上表;状态技能为 0 canCancel: boolean, // 可否取消(主要用于 skillupdate) exclusiveGroup: string | null, // 战斗技能互斥组,例如 "battleSkill" durationBattles: number | null,// 状态技能持续战斗次数;null表示非按战斗计数 stackPolicy: "refresh" | "stack" | "replace" // 后续扩展 } ``` 与运行时 **`flag:skill`** 对齐时:约定道具 **`id`** 为 **`skill` + 编号**(如 `skill1` → `flag:skill === 1`),地图回合侧据此同步 **`activeBattleSkillId`**。 ### 3.2 怪物定义(enemys/enemy48 扩展) ```js // project/enemys.js + enemy48 对应定义建议字段 { timeCost: number, // 0=不参与地图时间调度;>0=参与 actType: "chase" | "patrol" | "skill" | "idle", contactBattleOnly: boolean, // Boss可设true:单次接触仅一场战斗 actArgs: object | null } ``` ### 3.3 运行时状态(flags) ```js flags.mapTurnState = { // 累计时间单位(可与 mapTurn 同步策略二选一,见 §2.1) clock: 0, // 已完成的离散地图回合 tick 数;仅能通过 advanceMapTurnOne 每次 +1 mapTurn: 0, enemyActionGauge: { // [floorId]: { [enemyRuntimeId]: gaugeValue } }, // 仅收录「怪物定义 timeCost > 0」的实例;按楼层缓存,供每 tick 遍历,避免全图 extractBlocks activeEnemiesByFloor: { // [floorId]: [ // { runtimeId, x, y, enemyId, def, ... } // runtimeId 约定如 "floorId:x:y:enemyId";def 为 material.enemys[id] 引用,供 resolve 读 timeCost / actType // ] }, // 可选:每次 rebuild / patch 后自增,便于调试与断言缓存有效 activeEnemiesVersion: 0 } flags.skillState = { activeStatusSkills: { // [skillId]: { // remainBattles: number, // battleTimeCost: number, // 对战斗耗时影响值,状态技能可为0或设计值 // sourceItemId: string // } }, activeBattleSkillId: null, // 当前战斗技能(互斥) temp: { // 临时运行态,死亡可清空 inBattle: false } } ``` ### 3.4 插件模块职责(`project/plugins.js`) - 实现 `core.plugin.mapTurn`(名称可调整,全文保持一致),至少包含: - `consumeTime(deltaTime, reason)` - `advanceMapTurnOne(reason)` - `rebuildActiveEnemies(floorId)`、`patchActiveEnemiesForBlockChange(floorId, hint)`(见 §6.3) - `resolveEnemyActionsForSingleTick(reason)`(仅遍历当前层 `activeEnemies`) - `performEnemyAction(...)`、`settleBattleTimeCost()`、`applyStatusAfterBattle` / `clearOnDeath` 等与 §6 对齐的辅助入口 - **总开关**:持久化在 **`flags.mapTurnEnabled`**(随存档);由 `isEnabled` / `setEnabled` / `bootstrapPersistedState` 维护。开关为假时,`consumeTime` / `advanceMapTurnOne` 等推进与敌调度为空操作。勿再用闭包 `__enable` 与上述 flag 混用。 ## 4. 行为触发矩阵(是否推进地图时间) - 移动一格(含事件强制移动):推进,默认 `+1`。 - 轻按拾取(不移动):推进,默认 `+1`。 - 使用 tools 类道具(如 bigKey):按道具 `timeCost` 推进。 - 单场战斗:推进,按 `battleFinalTimeCost`。 - 远程技能:立即按 `timeCost` 推进;工程上对应道具 **`cls: "skillremote"`**(见 §3.1)。 - 恢复技能:视同远程;对应 **`cls: "skillrecover"`**。 - 状态技能:`timeCost=0`,不推进;对应 **`cls: "skillupdate"`**。 - 战斗技能(开关型):使用瞬间不推进;战后按 **`settleBattleTimeCost`** 与基础耗时取 max;对应 **`cls: "skillbattle"`**。 - 纯 UI 操作(菜单、手册、预览模拟):不推进。 **说明**:矩阵中的「推进 `+n`」均指调用 `consumeTime(n, ...)`;引擎内部必须把 `n` 拆成 `n` 次 `advanceMapTurnOne`(§2.1)。 ## 5. 核心状态机 ```mermaid flowchart TD playerAction[PlayerAction] --> classifyAction[ClassifyActionType] classifyAction -->|move/getItem/tool/ranged/recover| consumeTime[consumeTime_delta] classifyAction -->|statusSkillToggle| updateStatus[upsertStatusSkillState] classifyAction -->|battleTrigger| settleBattleTime[settleBattleTimeCost] settleBattleTime --> consumeTime consumeTime --> loopTicks["loop_delta_times"] loopTicks --> advanceOne[advanceMapTurnOne] advanceOne --> resolveEnemy[resolveEnemyActionsForSingleTick] resolveEnemy --> maybeBattle[maybeTriggerBattles] maybeBattle -->|battleSuccess| afterBattle[applyStatusAfterBattle] maybeBattle -->|battleFail| deathClear[clearOnDeath] afterBattle --> persist[saveStateToFlags] updateStatus --> persist deathClear --> persist ``` ## 6. 伪代码规范(变量名级别) ### 6.1 时间推进入口(一次可变多格,tick 逐一结算) ```js function consumeTime(deltaTime, reason) { if (!deltaTime || deltaTime <= 0) return; // 累计时间可一次加上 deltaTime(与 §2.1 一致);若采用「clock 与 mapTurn 同步每 tick +1」则改为在 advanceMapTurnOne 内 +1 flags.mapTurnState.clock += deltaTime; for (let i = 0; i < deltaTime; i++) { advanceMapTurnOne(reason); // 内部 mapTurn += 1,且只处理单 tick } } function advanceMapTurnOne(reason) { flags.mapTurnState.mapTurn = (flags.mapTurnState.mapTurn || 0) + 1; resolveEnemyActionsForSingleTick(reason); } ``` ### 6.2 怪物行动调度(单 tick;只遍历 `activeEnemies`) ```js function resolveEnemyActionsForSingleTick(reason) { const floorId = core.status.floorId; const list = flags.mapTurnState.activeEnemiesByFloor[floorId] || []; for (const enemyRef of list) { const runtimeId = enemyRef.runtimeId; const enemyDef = enemyRef.def; // 每 mapTurn tick:行动槽 +1;达到怪物 timeCost(怪物参与调度阈值)则行动一次 addEnemyGauge(floorId, runtimeId, 1); while (getEnemyGauge(floorId, runtimeId) >= enemyDef.timeCost) { subEnemyGauge(floorId, runtimeId, enemyDef.timeCost); performEnemyAction(enemyRef); // chase/patrol/skill/idle } } } ``` **注意**:列表中的怪物已保证 `enemyDef.timeCost > 0`(见 §6.3);此处 **禁止** 每 tick 调用 `extractBlocks` 全图扫描。 **`performEnemyAction`(当前契约)**:达阈时调用;`actType === "idle"`(或未配置)不产生额外地图效果。`chase` / `patrol` / `skill` 等在本阶段可为占位——若在单 tick 内同步调用会改写图块的引擎路径(如经 `removeBlock` 间接触发全图块整理),与上条性能约束冲突;后续应改为异步动作链或引擎侧无全表扫描的迁移 API 再接。 ### 6.3 `activeEnemies` 维护契约(性能) 1. **建表**:进入某楼层且地图块就绪后,调用 `rebuildActiveEnemies(floorId)`,扫描该层 `blocks`(或等价 API),仅加入「怪物图块且对应 `core.material.enemys[id].timeCost > 0`」的项,写入 `flags.mapTurnState.activeEnemiesByFloor[floorId]`,并 `activeEnemiesVersion++`。 2. **局部更新**:当 **地图上的怪物集合或坐标** 发生变化时(如 `removeBlock`、`setBlock`、怪物 `moveBlock` 结束、`hideBlock` 等),调用 `patchActiveEnemiesForBlockChange(floorId, hint)` 增删或更新对应条目,**不** 全量重建(除非实现成本过高,可退化为对该层 `rebuildActiveEnemies`)。`hint.op` 语义(与本仓库实现一致): - **`removeCell`**:按坐标 `(x,y)` 从列表移除实例,并删除 `enemyActionGauge[floorId][runtimeId]`(删怪、隐藏怪等)。 - **`syncCell`**:先清除该格在列表中的旧项及对应 gauge,再按当前地图块若存在可调度怪则 **新建** 条目;**不继承**旧槽(同格召唤、替换、显示后出现新怪等均为默认槽位)。 - **`migratePoint`**:将 **同一实例** 从 `(fromX,fromY)` 迁到 `(toX,toY)`:更新条目中 `x,y` 与 `runtimeId`,并把 gauge 从旧 `runtimeId` 键迁到新键;与引擎在块移动落点后调用的 **`moveEnemyOnPoint`** 配套。 - **`rebuild`**(或缺省 `hint`):等价于对该层执行一次 **`rebuildActiveEnemies`**:本路径内允许 **单次** `extractBlocks`,重建列表并按仍存在的 `runtimeId` 修剪 gauge。 - **`moveBlock` / `jumpBlock`**:块位移动画期间,对中途触发的 `removeBlock` / `setBlock` **抑制**上述 patch,避免与 **`migratePoint`** 重复或乱序;动画 **`keep === false`**(块消失不落点)结束时,须补一次 **`removeCell`** 清理起点格上的调度数据。 - **`removeBlockByIndexes`**(等多点批量删除):本仓库采用 **一次调用结束后** 对该层 **`rebuild`** 的退化策略(规范允许)。 3. **遍历**:`resolveEnemyActionsForSingleTick` **仅** 遍历当前层缓存列表;`deltaTime > 1` 时,重复 `deltaTime` 次单 tick 调度,等价于 mapTurn 从 `k` 逐步走到 `k+deltaTime`。 4. **换层**:`core.status.floorId` 切换后,下一 tick 使用新层的 `activeEnemiesByFloor[floorId]`;未访问过的楼层可无列表,首次结算前 `rebuild`。 5. **读档**:`activeEnemiesByFloor` 为派生缓存,**允许** 在读档结束、当前楼层已 `drawMap` 就绪后执行一次 `rebuildActiveEnemies(currentFloorId)`;`enemyActionGauge` 等需持久化的数据仍放在 `flags.mapTurnState` 内随存档走。 ### 6.4 战斗耗时结算 ```js function settleBattleTimeCost() { const baseBattleTimeCost = 1; // 仅统计当前生效状态技能中的“战斗耗时影响” let statusBattleTimeCostMax = 0; for (const skillId in flags.skillState.activeStatusSkills) { const state = flags.skillState.activeStatusSkills[skillId]; statusBattleTimeCostMax = Math.max(statusBattleTimeCostMax, state.battleTimeCost || 0); } return Math.max(baseBattleTimeCost, statusBattleTimeCostMax); } ``` ### 6.5 战斗后状态扣减 ```js function applyStatusAfterBattle(battleResult) { if (battleResult !== "success") return; // 失败走死亡流程 for (const skillId in flags.skillState.activeStatusSkills) { const state = flags.skillState.activeStatusSkills[skillId]; if (typeof state.remainBattles === "number") { state.remainBattles -= 1; if (state.remainBattles <= 0) { delete flags.skillState.activeStatusSkills[skillId]; } } } } ``` ### 6.6 死亡清理 ```js function clearOnDeath() { // 预留:后续可细分清理范围 flags.skillState.temp = {}; flags.skillState.activeBattleSkillId = null; flags.skillState.activeStatusSkills = {}; } ``` ### 6.7 本仓库实现锚点(与当前代码一致) 以下与 [project/plugins.js](project/plugins.js) 及关联工程文件一致,便于对照 §8;**文件级清单仍以** [map-turn-implementation-status.md](map-turn-implementation-status.md) **为准**。 - **`core.plugin.mapTurn`** 定义于 [project/plugins.js](project/plugins.js) 插件 **`"mapTurn"`**:`consumeTime`、`advanceMapTurnOne`、`resolveEnemyActionsForSingleTick`、`performEnemyAction`、`rebuildActiveEnemies`、`patchActiveEnemiesForBlockChange`、`settleBattleTimeCost`、`applyStatusAfterBattle`、`clearOnDeath`、`bootstrapPersistedState` 等均挂在此对象上。 - **地图与块移动挂钩**:在 **`mapTurn`** 插件末尾 IIFE 内对 **`core.maps`** 原型包装 `removeBlock`、`setBlock`、`hideBlock`、`showBlock`、`removeBlockByIndexes`、`moveBlock`、`jumpBlock`;对 **`core.events`** 包装 **`moveEnemyOnPoint`**,从而触发 §6.3 所列 `patch` / `rebuild` 行为(含 `moveBlock` / `jumpBlock` 深度计数与 `keep === false` 补清)。 - **换层**:在 **`序章追击`** 等对 **`events.prototype.afterChangeFloor`** 的包装中调用 **`rebuildActiveEnemies`**;**普通换层**将当前层 **`mapTurn` 归零**、**读档换层**不归零 `mapTurn` 的口径见台账 §1.6。 - **读档 / 开局**:`loadData` 中 **`bootstrapPersistedState`** 见 [project/functions.js](project/functions.js);开局 **`mapTurnEnabled`** 与 `bootstrapPersistedState` 见 [project/data.js](project/data.js)(台账 §1.6)。 - **移动与战后时间**:在 [project/plugins.js](project/plugins.js) 的 **`序章追击`** 等插件段内对 **`control.prototype.moveOneStep` / `moveDirectly`** 与 **`events.prototype.afterBattle`** 包装,调用 **`consumeTime`**(含战后 **`settleBattleTimeCost`** 与 **`applyStatusAfterBattle`**);**非** [project/functions.js](project/functions.js) 默认空壳上的直接编辑。 - **道具推进时间**:`useItem` 内按 `timeCost` 调用 `consumeTime` 见 [libs/items.js](libs/items.js)(与台账 §1.3 一致)。 ## 7. 关键业务分支 ### 7.1 多怪捕捉导致多场战斗 - 逐场调用战斗结算。 - 每场成功后都执行一次 `applyStatusAfterBattle("success")`。 - 任一场失败则进入死亡流程并终止后续结算。 ### 7.2 Boss 单次接触 - `contactBattleOnly=true` 时,每次接触仅触发一场战斗。 - 该场战斗仍视作 1 场独立结算,按 `settleBattleTimeCost()` 推进时间。 - 不做“连战自动推进”,给玩家留操作窗口。 ### 7.3 战斗技能互斥 ```js function activateBattleSkill(skillId) { const skill = getSkillDef(skillId); if (skill.exclusiveGroup === "battleSkill") { flags.skillState.activeBattleSkillId = skillId; } } ``` ## 8. 工程接入点映射 - `project/plugins.js`:实现 `core.plugin.mapTurn`(§3.4)及 §6.7 所列原型挂钩;**所有「推进多少时间就结算多少次 tick」及单 tick 敌调度入口集中于此**。 - **移动 / 战后 / 换层与地图回合**:见 **§6.7**(对 `control` / `events` / `maps` 的包装;**非** `project/functions.js` 默认模板中的 `moveOneStep` / `afterBattle` / `afterChangeFloor` 直接改法)。 - `project/functions.js` -> `loadData`:读档后调用 **`bootstrapPersistedState`**(§6.7);默认 **`afterChangeFloor`** 仍可由项目模板保留,地图回合换层 **`rebuild`** 由 plugins 内对 **`afterChangeFloor`** 的包装完成。 - `libs/items.js` -> `useItem`:正数 `timeCost` 时调用 `consumeTime`(§6.7);状态技能只更新 `flags.skillState`,不调用 `consumeTime`。 - `project/enemys.js`(及 enemy48):按需为参与地图回合的怪物设置 `timeCost > 0` 与 `actType`;`timeCost === 0` 的怪不得进入 `activeEnemies`。 - **地图变更挂钩**:本仓库已通过 §6.7 对 `maps` / `events` 的包装触发 **`patchActiveEnemiesForBlockChange`** 或 **`rebuildActiveEnemies`**;若日后增加其它改图 API(如未包装的 `swapBlock` 等),可再补挂钩或对该层退化为 **`rebuild`**。 ## 9. 存读档一致性要求 - 所有运行态必须在 `flags.mapTurnState` 与 `flags.skillState` 下可序列化。 - 读档后不重置状态层数、不重置时间计量(`clock` / `mapTurn` / `enemyActionGauge`)、不重置技能状态。 - 切楼不触发状态清空,仅切换当前楼层;换层后须使用对应 `activeEnemiesByFloor[floorId]`。 - `activeEnemiesByFloor` 为派生缓存:读档后若列表缺失或与地图不一致,在楼层就绪时 **重建一次**(§6.3),不依赖旧缓存跨版本兼容。 ## 10. 最小验收清单 - 使用状态技能后不立即推进时间,战斗成功后层数 `-1`。 - 远程/恢复技能释放立即推进时间,怪物按时间响应。 - 战斗耗时按 `max` 规则而不是累加。 - 多怪捕捉触发多场战斗时,状态层数逐场扣减。 - Boss `contactBattleOnly=true` 时单次接触仅结算一场。 - 死亡后状态与临时态清理符合预期。 - 存档读档后状态层数与地图时间连续。 - 设置怪物 `timeCost=0` 时,该怪不参与调度,性能无明显回退。 - **`consumeTime(3, ...)` 触发恰好 3 次 `advanceMapTurnOne`**:`mapTurn` 连续 `+3`,且敌人行动槽/行为按 **3 个独立 tick** 结算(可用日志或计数器断言)。 - **`activeEnemies` 路径**:每 tick 不调用全图 `extractBlocks`;进楼 `rebuild` 后,增删怪以 **`patch`(`removeCell` / `syncCell` / `migratePoint`)或单次 `rebuild`** 维护;`moveBlock` / `jumpBlock` 与深度计数规则见 §6.3;`timeCost=0` 的怪不在列表中。