# 地图回合与技能状态伪代码规范 ## 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 扩展) ```js // project/items.js 每个可用技能道具(tools类)建议字段 { skillType: "battle" | "status" | "recover" | "ranged", timeCost: number, // 统一时间消耗,状态技能固定为0 canCancel: boolean, // 可否取消(主要用于status) exclusiveGroup: string | null, // 战斗技能互斥组,例如 "battleSkill" durationBattles: number | null,// 状态技能持续战斗次数;null表示非按战斗计数 stackPolicy: "refresh" | "stack" | "replace" // 后续扩展 } ``` ### 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, blockIndex?, ... } // runtimeId 实现阶段约定,如 "floorId:x:y:enemyId" // ] }, // 可选:每次 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(...)`(见 §6.6) - `resolveEnemyActionsForSingleTick(reason)`(仅遍历当前层 `activeEnemies`) - 可选:`__enable` 总开关,关闭时上述函数为空操作,便于分步接入游戏。 ## 4. 行为触发矩阵(是否推进地图时间) - 移动一格(含事件强制移动):推进,默认 `+1`。 - 轻按拾取(不移动):推进,默认 `+1`。 - 使用 tools 类道具(如 bigKey):按道具 `timeCost` 推进。 - 单场战斗:推进,按 `battleFinalTimeCost`。 - 远程技能(不可撤回):立即按技能 `timeCost` 推进。 - 恢复技能:视同远程技能,立即按 `timeCost` 推进。 - 状态技能:`timeCost=0`,不推进地图时间。 - 纯 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` 全图扫描。 ### 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(...)` 增删或更新对应条目,**不** 全量重建(除非实现成本过高,可退化为对该层 `rebuildActiveEnemies`)。 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 = {}; } ``` ## 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),包含 `consumeTime`、`advanceMapTurnOne`、`resolveEnemyActionsForSingleTick`、`rebuildActiveEnemies`、`patchActiveEnemiesForBlockChange`、`clearOnDeath` 等;**所有「推进多少时间就结算多少次 tick」的逻辑集中于此**。 - `project/functions.js` -> `moveOneStep`:移动完成后调用 `core.plugin.mapTurn.consumeTime(1, "move")`(或封装名一致即可)。 - `project/functions.js` -> `afterBattle`:成功结算后先 `consumeTime(settleBattleTimeCost(), "battle")`(内部按 §2.1 拆 tick),再 `applyStatusAfterBattle("success")`。 - `project/functions.js` -> `afterChangeFloor`(或 `changingFloor` 结束、楼层已绘制后):**一行** `core.plugin.mapTurn.rebuildActiveEnemies(core.status.floorId)`,建立当前层 `activeEnemies`(§6.3)。 - `libs/items.js` -> `useItem`(或道具 `useItemEffect` 末尾):恢复/远程/tools 按 `item.timeCost` 调用 `consumeTime(item.timeCost, ...)`;状态技能只更新 `flags.skillState`,不调用 `consumeTime`。 - `project/enemys.js`(及 enemy48):按需为参与地图回合的怪物设置 `timeCost > 0` 与 `actType`;`timeCost === 0` 的怪不得进入 `activeEnemies`。 - **地图变更挂钩(实现时择优)**:在会改变怪物位置/存亡的 API 之后调用 `patchActiveEnemiesForBlockChange`(若短期无法统一挂钩,可退化为每次变更后对该层 `rebuildActiveEnemies`)。 ## 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` 或单次 `rebuild`;`timeCost=0` 的怪不在列表中。