if/docs/map-turn-spec.md

337 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 地图回合与技能状态伪代码规范
实现进度与代码接线台账见 [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` 的怪不在列表中。