20 KiB
地图回合与技能状态伪代码规范
实现进度与代码接线台账见 map-turn-implementation-status.md。角色分工:本规范写冻结规则与实现契约(尤其 §6);台账写「已接线文件 / API 清单 / 待办勾选项」,二者交叉引用,避免重复维护大段表格。
1. 目标与范围
本规范用于在当前项目中引入“地图时间(Map Turn)”和“四类技能状态”机制,确保:
- 规则口径唯一,避免实现歧义。
- 变量命名可直接映射到代码实现。
- 与现有流程兼容:移动、战斗、道具、捕捉、存读档。
- 先支持玩家侧,后续可平滑扩展到“敌方技能”。
2. 规则优先级(冻结版)
当规则冲突时,按以下优先级从高到低执行:
- 死亡流程优先:战斗失败或致死效果触发后,立即进入死亡清理。
- 战斗成功后扣层:只有成功结算的战斗才扣除“按战斗次数持续”的状态层数。
- 时间推进来源:仅由推进时间的行为触发(移动/拾取/tools/战斗/远程/恢复)。
- 状态技能时间固定:状态技能
timeCost = 0,开启/关闭不直接推进地图时间。 - 战斗耗时结算:
battleFinalTimeCost = max(baseBattleTimeCost, statusBattleTimeCostMax)。 - 多战逐场结算:同一帧内多场战斗(如多怪捕捉)逐场独立结算。
- 存读档一致:状态容器完整写入
flags,读档恢复后行为一致。 - 切楼不清状态:切楼、读档不影响状态持续;死亡按机制清临时状态。
- 事件默认不打断状态:除非事件显式调用清理接口。
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 时:
- 先将累计量一次性加上:
flags.mapTurnState.clock += n(表示「时间池」或总经过时间,允许与mapTurn不同步增长策略见下)。 - 再 循环
n次 调用advanceMapTurnOne(reason):每次只做 一个 tick——mapTurn += 1,并在该 tick 内完成敌人槽位/行动等(见 §6)。
推荐关系:若采用「整数时间模型」,也可令 clock 仅在循环内每 tick +1,与 mapTurn 同步递增;无论 clock 是一次 +=n 还是分 n 次 +=1,advanceMapTurnOne 必须被调用恰好 n 次,以保证「0→3 的时间变化」对应「mapTurn 连续三步」的语义。
实现落点:调度函数挂在 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 |
// 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 扩展)
// project/enemys.js + enemy48 对应定义建议字段
{
timeCost: number, // 0=不参与地图时间调度;>0=参与
actType: "chase" | "patrol" | "skill" | "idle",
contactBattleOnly: boolean, // Boss可设true:单次接触仅一场战斗
actArgs: object | null
}
3.3 运行时状态(flags)
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. 核心状态机
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 逐一结算)
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)
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 维护契约(性能)
- 建表:进入某楼层且地图块就绪后,调用
rebuildActiveEnemies(floorId),扫描该层blocks(或等价 API),仅加入「怪物图块且对应core.material.enemys[id].timeCost > 0」的项,写入flags.mapTurnState.activeEnemiesByFloor[floorId],并activeEnemiesVersion++。 - 局部更新:当 地图上的怪物集合或坐标 发生变化时(如
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的退化策略(规范允许)。
- 遍历:
resolveEnemyActionsForSingleTick仅 遍历当前层缓存列表;deltaTime > 1时,重复deltaTime次单 tick 调度,等价于 mapTurn 从k逐步走到k+deltaTime。 - 换层:
core.status.floorId切换后,下一 tick 使用新层的activeEnemiesByFloor[floorId];未访问过的楼层可无列表,首次结算前rebuild。 - 读档:
activeEnemiesByFloor为派生缓存,允许 在读档结束、当前楼层已drawMap就绪后执行一次rebuildActiveEnemies(currentFloorId);enemyActionGauge等需持久化的数据仍放在flags.mapTurnState内随存档走。
6.4 战斗耗时结算
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 战斗后状态扣减
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 死亡清理
function clearOnDeath() {
// 预留:后续可细分清理范围
flags.skillState.temp = {};
flags.skillState.activeBattleSkillId = null;
flags.skillState.activeStatusSkills = {};
}
6.7 本仓库实现锚点(与当前代码一致)
以下与 project/plugins.js 及关联工程文件一致,便于对照 §8;文件级清单仍以 map-turn-implementation-status.md 为准。
core.plugin.mapTurn定义于 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;开局mapTurnEnabled与bootstrapPersistedState见 project/data.js(台账 §1.6)。 - 移动与战后时间:在 project/plugins.js 的
序章追击等插件段内对control.prototype.moveOneStep/moveDirectly与events.prototype.afterBattle包装,调用consumeTime(含战后settleBattleTimeCost与applyStatusAfterBattle);非 project/functions.js 默认空壳上的直接编辑。 - 道具推进时间:
useItem内按timeCost调用consumeTime见 libs/items.js(与台账 §1.3 一致)。
7. 关键业务分支
7.1 多怪捕捉导致多场战斗
- 逐场调用战斗结算。
- 每场成功后都执行一次
applyStatusAfterBattle("success")。 - 任一场失败则进入死亡流程并终止后续结算。
7.2 Boss 单次接触
contactBattleOnly=true时,每次接触仅触发一场战斗。- 该场战斗仍视作 1 场独立结算,按
settleBattleTimeCost()推进时间。 - 不做“连战自动推进”,给玩家留操作窗口。
7.3 战斗技能互斥
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的怪不在列表中。