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