15 KiB
15 KiB
地图回合与技能状态伪代码规范
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 扩展)
// 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 扩展)
// 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, 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. 核心状态机
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 全图扫描。
6.3 activeEnemies 维护契约(性能)
- 建表:进入某楼层且地图块就绪后,调用
rebuildActiveEnemies(floorId),扫描该层blocks(或等价 API),仅加入「怪物图块且对应core.material.enemys[id].timeCost > 0」的项,写入flags.mapTurnState.activeEnemiesByFloor[floorId],并activeEnemiesVersion++。 - 局部更新:当 地图上的怪物集合或坐标 发生变化时(如
removeBlock、setBlock、怪物moveBlock结束、hideBlock等),调用patchActiveEnemiesForBlockChange(...)增删或更新对应条目,不 全量重建(除非实现成本过高,可退化为对该层rebuildActiveEnemies)。 - 遍历:
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 = {};
}
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),包含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的怪不在列表中。