if/docs/map-turn-spec.md
2026-04-23 11:05:03 +08:00

304 lines
15 KiB
Markdown
Raw 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.

# 地图回合与技能状态伪代码规范
## 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` 的怪不在列表中。