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

15 KiB
Raw Blame History

地图回合与技能状态伪代码规范

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(与道具/战斗等配置的 timeCostbattleFinalTimeCost 一致)。 可一次从 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+=1advanceMapTurnOne 必须被调用恰好 n以保证「0→3 的时间变化」对应「mapTurn 连续三步」的语义。

实现落点:调度函数挂在 project/plugins.jscore.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 拆成 nadvanceMapTurnOne§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 维护契约(性能)

  1. 建表:进入某楼层且地图块就绪后,调用 rebuildActiveEnemies(floorId),扫描该层 blocks(或等价 API仅加入「怪物图块且对应 core.material.enemys[id].timeCost > 0」的项,写入 flags.mapTurnState.activeEnemiesByFloor[floorId],并 activeEnemiesVersion++
  2. 局部更新:当 地图上的怪物集合或坐标 发生变化时(如 removeBlocksetBlock、怪物 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 战斗耗时结算

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),包含 consumeTimeadvanceMapTurnOneresolveEnemyActionsForSingleTickrebuildActiveEnemiespatchActiveEnemiesForBlockChangeclearOnDeath 等;所有「推进多少时间就结算多少次 tick」的逻辑集中于此
  • project/functions.js -> moveOneStep:移动完成后调用 core.plugin.mapTurn.consumeTime(1, "move")(或封装名一致即可)。
  • project/functions.js -> afterBattle:成功结算后先 consumeTime(settleBattleTimeCost(), "battle")(内部按 §2.1 拆 tickapplyStatusAfterBattle("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 > 0actTypetimeCost === 0 的怪不得进入 activeEnemies
  • 地图变更挂钩(实现时择优):在会改变怪物位置/存亡的 API 之后调用 patchActiveEnemiesForBlockChange(若短期无法统一挂钩,可退化为每次变更后对该层 rebuildActiveEnemies)。

9. 存读档一致性要求

  • 所有运行态必须在 flags.mapTurnStateflags.skillState 下可序列化。
  • 读档后不重置状态层数、不重置时间计量(clock / mapTurn / enemyActionGauge)、不重置技能状态。
  • 切楼不触发状态清空,仅切换当前楼层;换层后须使用对应 activeEnemiesByFloor[floorId]
  • activeEnemiesByFloor 为派生缓存:读档后若列表缺失或与地图不一致,在楼层就绪时 重建一次§6.3),不依赖旧缓存跨版本兼容。

10. 最小验收清单

  • 使用状态技能后不立即推进时间,战斗成功后层数 -1
  • 远程/恢复技能释放立即推进时间,怪物按时间响应。
  • 战斗耗时按 max 规则而不是累加。
  • 多怪捕捉触发多场战斗时,状态层数逐场扣减。
  • Boss contactBattleOnly=true 时单次接触仅结算一场。
  • 死亡后状态与临时态清理符合预期。
  • 存档读档后状态层数与地图时间连续。
  • 设置怪物 timeCost=0 时,该怪不参与调度,性能无明显回退。
  • consumeTime(3, ...) 触发恰好 3 次 advanceMapTurnOnemapTurn 连续 +3,且敌人行动槽/行为按 3 个独立 tick 结算(可用日志或计数器断言)。
  • activeEnemies 路径:每 tick 不调用全图 extractBlocks;进楼 rebuild 后,增删怪仅 patch 或单次 rebuildtimeCost=0 的怪不在列表中。