0.14地图回合构筑-各行动接口插入

This commit is contained in:
salt 2026-04-24 17:38:00 +08:00
parent b30cd88fd6
commit ba74ab06d9
16 changed files with 674 additions and 21 deletions

View File

@ -2379,6 +2379,10 @@ var terndefs_f6783a0a_522d_417e_8407_94c67b692e50 = [
"!doc": "连续前进,不撞南墙不回头<br/>例如core.moveHero(); // 连续前进<br/>direction: 可选,如果设置了就会先转身到该方向<br/>callback: 可选,如果设置了就只走一步<br/>【异步脚本,请勿在脚本中直接调用(而是使用对应的事件),否则可能导致录像出错】",
"!type": "fn(direction?: string, callback?: fn())"
},
"moveHeroMapTurn": {
"!doc": "事件用:移动勇者(每落一格推进地图回合)。勿直接调用 core.moveHeroMapTurn请用事件 JSON type: moveHeroMapTurn",
"!type": "fn()"
},
"getRealStatusOrDefault": {
"!doc": "从status中获得实际属性增幅后的如果不存在则从勇士属性中获取",
"!type": "fn(status?: ?, name?: string)"

View File

@ -887,6 +887,7 @@ action
| move_s
| moveAction_s
| moveHero_s
| moveHeroMapTurn_s
| jump_s
| jump_1_s
| jumpHero_s
@ -2430,6 +2431,23 @@ var code = '{"type": "moveHero"'+IntString_0+Bool_0+', "steps": ['+moveDirection
return code;
*/;
moveHeroMapTurn_s
: '移动勇者' '动画时间' IntString? '不等待执行完毕' Bool BGNL? moveDirection+ Newline
/* moveHeroMapTurn_s
tooltip : moveHeroMapTurn移动勇者同无视地形移动勇士但每落一格后推进地图回合并结算敌人见 project/plugins mapTurn
helpUrl : /_docs/#/instruction
default : ["",false,"上右3下2后4左前2"]
colour : this.mapColor
IntString_0 = IntString_0 ?(', "time": '+IntString_0):'';
Bool_0 = Bool_0?', "async": true':'';
var code = '{"type": "moveHeroMapTurn"'+IntString_0+Bool_0+', "steps": ['+moveDirection_0.trim().substring(2)+']},\n';
return code;
*/;
jump_s
: '跳跃事件' '起始 x' PosString? ',' 'y' PosString? '终止 x' PosString? ',' 'y' PosString? '动画时间' IntString? '不消失' Bool '不等待执行完毕' Bool Newline

View File

@ -504,6 +504,19 @@ ActionParser.prototype.parseAction = function() {
this.next = MotaActionBlocks['moveHero_s'].xmlText([
data.time,data.async||false,buildMoveDirection(data.steps),this.next]);
break;
case "moveHeroMapTurn": // 移动勇者(每格推进地图回合)
var buildMoveDirectionMT= function (obj) {
obj = MotaActionFunctions.processMoveDirections(obj||[]);
var res = null;
for(var ii=obj.length-1,one;one=obj[ii];ii--) {
var v = one.split(':');
res=MotaActionBlocks['moveDirection'].xmlText([v[0], parseInt(v[1]), res]);
}
return res;
}
this.next = MotaActionBlocks['moveHeroMapTurn_s'].xmlText([
data.time,data.async||false,buildMoveDirectionMT(data.steps),this.next]);
break;
case "jump": // 跳跃事件
data.from=data.from||['',''];
if (data.dxy) {

File diff suppressed because one or more lines are too long

View File

@ -158,6 +158,7 @@ editor_blocklyconfig=(function(){
MotaActionBlocks['setBlockFilter_s'].xmlText(),
MotaActionBlocks['turnBlock_s'].xmlText(),
MotaActionBlocks['moveHero_s'].xmlText(),
MotaActionBlocks['moveHeroMapTurn_s'].xmlText(),
MotaActionBlocks['move_s'].xmlText(),
MotaActionBlocks['jumpHero_s'].xmlText(),
MotaActionBlocks['jumpHero_1_s'].xmlText(),

77
claude.md Normal file
View File

@ -0,0 +1,77 @@
# Claude Project Memory
## 1. 项目定位(长期不变)
- 项目类型魔塔Like固定地图探索 + 数值驱动 + 剧情导向 + 解谜)。
- 目标体量:按原定体量推进,不主动缩减世界观与章节目标。
- 开发模式:代码主力由 AI 完成;人工主要负责美工、地图、剧情、体验验收与方向裁决。
- 当前基础:塔的基础结构已完成,支持正常战斗与可视化地图修改。
## 2. 当前开发优先级(默认顺序)
1. 地图回合时轴Map Turn剩余接线与稳定性。
2. 回档系统(状态继承、存读档一致性)。
3. 敌人可相互战斗与伤害。
4. 玩家多角色(队友/阵容)系统。
> 未经明确指令,不要跳级并行实现高风险模块。
## 3. 规则来源与冲突处理
- 机制规则唯一口径:`docs/map-turn-spec.md`
- 代码落地状态台账:`docs/map-turn-implementation-status.md`
- 协作与开发边界:本文件 `claude.md`
- 若规则冲突:先列出冲突点与影响,不擅自改规则,等待用户裁决。
## 4. 开发硬约束(来自 DEVELOPMENT_RULES 共识)
1. 新功能默认写在 `project/plugins.js`
2. 优先使用 `_docs/api.md` 中的样板接口实现需求。
3. 所有绘制基于 `core.createCanvas` 体系。
4. 所有异步流程基于 `core.insertAction`(含 function/async 事件流)。
5. 使用非样板接口时,必须就地注释:用途、原因、风险。
6. 不直接改动 `libs/``main.js` 等核心底层文件,除非用户明确授权。
7. 不直接破坏核心运行态结构(如整体替换 `core.status` / `core.material`)。
## 5. Map Turn 专项原则(必须遵守)
- 单一时间总线:时间推进统一走 `consumeTime(deltaTime, reason)`
- 单 tick 语义:`deltaTime = n` 必须触发恰好 `n``advanceMapTurnOne`
- 规则优先级:死亡流程优先、战后成功再扣层、状态技能 `timeCost=0` 不推进时间。
- 耗时规则:`battleFinalTimeCost = max(baseBattleTimeCost, statusBattleTimeCostMax)`。
- 性能底线:每 tick 禁止全图扫描;敌方调度仅遍历 `activeEnemiesByFloor` 缓存。
- 存档一致:`flags.mapTurnState` 与 `flags.skillState` 保持可序列化并可恢复。
- 缓存策略:`activeEnemiesByFloor` 作为派生缓存,读档或换层后允许重建。
## 6. AI 输出与执行格式
每次任务默认按以下顺序输出:
1. 先给结论(本次做什么/不做什么)。
2. 给改动范围(文件与模块)。
3. 对齐到规则(引用 `map-turn-spec.md` / 台账条目)。
4. 给最小验证步骤(可复现、可回归)。
5. 若有风险,给 1-2 条可选方案并标出推荐项。
## 7. 验收偏好
- 优先级:正确性与可验证性 > 写法优雅。
- 关键机制必须可回归验证(如 `consumeTime(3)` 的 3 tick 断言、战后扣层、读档连续、`timeCost=0` 过滤)。
- 遇到不确定行为,先加可观测日志/断言,再做进一步改动。
## 8. Git 与协作习惯
- 已使用 Git改动应保持小步、可读、可回退。
- 未经明确要求,不做破坏性历史操作(如强推、硬重置)。
- 不覆盖与当前任务无关的既有改动。
## 9. 调用短语约定
当用户说“按 claude 执行”时AI 必须默认执行:
1. 先复述目标与当前阶段优先级。
2. 明确本次任务对应哪个优先级模块。
3. 检查是否违反第 4、5 节约束。
4. 输出最小可执行步骤,再开始实现。
当用户说“先讨论不改代码”时AI 只给方案,不做实现。

View File

@ -0,0 +1,106 @@
# 地图回合实现进度(台账)
本文档记录**代码已落地内容**与**待接线任务**,便于在对话上下文变长时单独查阅。规则、伪代码与冻结优先级以 [map-turn-spec.md](map-turn-spec.md) 为唯一口径;本文不写新规则,只写状态与文件指针。**`patch` / `rebuild` / `performEnemyAction` 等与实现对齐的契约说明**见规范 **§6.2、§6.3、§6.7**。
---
## 1. 已完成(截至当前仓库)
### 1.1 数据表(`timeCost` 默认 `null`
| 区域 | 说明 |
|------|------|
| [project/items.js](../project/items.js) | 每条道具在 `"cls"` 下一行含 **`"timeCost": null`**;需消耗地图时间时改为正整数。 |
| [project/enemys.js](../project/enemys.js) | 每条怪物在 **`point``special` 之间**含 **`"timeCost": null`**。 |
| `enemy48` | 与 `enemys` 共用 **`core.material.enemys`** 数值,**无**独立 `libs/enemys48.js` 数据表。 |
### 1.2 引擎初始化缺省补全
| 文件 | 行为 |
|------|------|
| [libs/items.js](../libs/items.js) `items.prototype._init` | 若道具 **`timeCost === undefined`**,补 **`timeCost: null`**(含编辑器新注册道具漏字段)。 |
| [libs/enemys.js](../libs/enemys.js) `enemys.prototype._init` | 若怪物 **`timeCost === undefined`**,补 **`timeCost: null`**。 |
### 1.3 使用道具推进地图时间
| 文件 | 行为 |
|------|------|
| [libs/items.js](../libs/items.js) `items.prototype.useItem` | 在 **`_useItemEffect`** 之后:若 **`typeof timeCost === "number"``> 0`**,且存在 **`core.plugin.mapTurn.consumeTime`**,则调用 **`consumeTime(_tc, "item:" + itemId)`**。`null` 或非正数不推进。 |
### 1.4 插件:`core.plugin.mapTurn`
| 文件 | 说明 |
|------|------|
| [project/plugins.js](../project/plugins.js) 插件键 **`"mapTurn"`** | 挂载 **`this.mapTurn = { ... }`**,对外即 **`core.plugin.mapTurn.*`**。 |
已实现的 API名称与 [map-turn-spec.md](map-turn-spec.md) §3.4 / §6 对齐):
- **`isEnabled` / `setEnabled(v)`**:总开关读写 **`flags.mapTurnEnabled`**(随存档);为真时 **`consumeTime` / `advanceMapTurnOne`** 才会推进状态。`setEnabled(true)` 或 **`bootstrapPersistedState()`** 在开关为真时会补齐 `mapTurnState` / `skillState` 缺省结构。
- **`consumeTime(deltaTime, reason)`**`clock += floor(delta)`,再循环 **`deltaTime` 次** **`advanceMapTurnOne`**§2.1)。
- **`advanceMapTurnOne(reason)`**`mapTurn += 1`,调用 **`resolveEnemyActionsForSingleTick`**。
- **`resolveEnemyActionsForSingleTick(reason)`**:已按 §6.2 遍历 **`activeEnemiesByFloor[currentFloor]`**,维护 **`enemyActionGauge[floorId][runtimeId]`**,达阈调用 **`performEnemyAction`**;本函数路径内不调用 **`extractBlocks`**。
- **`performEnemyAction(enemyRef, def, floorId, reason)`**:已挂载;初版 **`actType === idle`** 无操作;`chase`/`patrol`/`skill` 占位(同步改图块会经引擎 `removeBlock` 触发全表扫描,故不在此 tick 内实现)。
- **`rebuildActiveEnemies(floorId)`**:对该层 **`extractBlocks` 一次** 后遍历 **`map.blocks`**,收录 **`cls``enemy` 开头** 且 **`enemys[id].timeCost` 为正数** 的实例(**`runtimeId` / `x` / `y` / `enemyId` / `def`**),写回 **`activeEnemiesByFloor[floorId]`****`activeEnemiesVersion++`**,并修剪 **`enemyActionGauge[floorId]`**。换层由 [project/plugins.js](../project/plugins.js) 对 **`afterChangeFloor`** 的包装调用。
- **`patchActiveEnemiesForBlockChange(floorId, hint)`**`hint.op` 为 **`removeCell`** / **`syncCell`** / **`migratePoint`** / **`rebuild`**(默认全量 **`rebuildActiveEnemies`**)。已在 **`maps.removeBlock` / `setBlock` / `hideBlock` / `showBlock` / `removeBlockByIndexes`** 与 **`events.moveEnemyOnPoint`** 挂钩;**`moveBlock` / `jumpBlock`** 用深度计数抑制中途 **`removeBlock`/`setBlock`** 的重复同步,**`keep===false`** 结束时补 **`removeCell`** 起点格;**`migratePoint`** 在 **`moveEnemyOnPoint`** 后迁移 **`runtimeId``enemyActionGauge`**。
- **`settleBattleTimeCost`**:已实现 **`max(1, 状态技能 battleTimeCost 最大值)`**§6.4)。
- **`applyStatusAfterBattle` / `clearOnDeath`**:已按规范 §6.56.6 操作 **`flags.skillState`**(经 **`core.getFlag` / `core.setFlag`** 维护的 **`mapTurnState` / `skillState`** 结构)。
### 1.5 编辑器 / 新素材注册
| 文件 | 说明 |
|------|------|
| [_server/table/comment.js](../_server/table/comment.js) | 道具表、怪物表增加 **`timeCost`** 列说明;**`items_template`** / **`enemys_template`** 含 **`timeCost: null`**,新建行时带该字段。 |
### 1.6 当前接线口径补充2026-04
- **总开关开启时机**:已在 [project/data.js](../project/data.js) 的 **`startText` 前两条****`setValue`** 写入 **`flag:mapTurnEnabled`** 为真,再 **`bootstrapPersistedState()`** 补齐结构。开关存于存档,读档后随存档恢复。
- **首次触发路径约束**:新开局统一经过 `startText` 写入总开关;读档后由 [project/functions.js](../project/functions.js) **`loadData`** 调用 **`bootstrapPersistedState()`** 与存档中的 **`mapTurnEnabled`** 对齐。
- **演出移动与地图回合**:事件编辑器「地图处理」中在「无视地形移动勇士」下增加图块 **「移动勇者」**JSON `type`: **`moveHeroMapTurn`**)。运行时由 [libs/events.js](../libs/events.js) **`_action_moveHeroMapTurn`** 将路径拆成每格一次 `eventMoveHero`,每格落点后 **`consumeTime(1, "event:moveHeroMapTurn")`**`speed:` 与 `:0` 段不扣时)。语法见 [_server/MotaAction.g4](../_server/MotaAction.g4) `moveHeroMapTurn_s`
- **分层相对回合约定**`mapTurn` 以楼层相对值使用;**普通切层**后当前层回合计数按 `0` 起算。**读档**导致的换层(`__fromLoad__` 为真)不归零,以保持与存档一致。`clock` 可继续作为全局累计观测值。
---
## 2. 待完成(接线清单,对齐 map-turn-spec §4 / §6 / §8
已完成接线见 **§1**(含 §1.4、§1.6)。以下为尚未实现的步骤说明;完成后在 §3 验收表对应行打勾。
- [x] **`resolveEnemyActionsForSingleTick`**:已在 [project/plugins.js](../project/plugins.js) **`mapTurn`** 中实现 §2 该条所列 14 步(含 **`performEnemyAction`**`idle` 已实现,其余 `actType` 占位)。
- [x] **`rebuildActiveEnemies`**:已在 **`mapTurn.rebuildActiveEnemies`** 实现 §2 该条 14 步;**`afterChangeFloor`** 中已调用 **`rebuildActiveEnemies(core.status.floorId || floorId)`**。
- [x] **`patchActiveEnemiesForBlockChange`**:已在 **`mapTurn.patchActiveEnemiesForBlockChange`** 实现 **`removeCell` / `syncCell` / `migratePoint` / `rebuild`**[project/plugins.js](../project/plugins.js) 在 **`mapTurn`** 插件末尾对 **`core.maps`** / **`core.events`** 安装挂钩(含 **`moveBlock`/`jumpBlock`** 与 **`keep===false`** 补清)。
### 2.1 后续扩展接线备忘
对应 [map-turn-spec.md](map-turn-spec.md) 中已有字段或流程、尚未在工程中专项接线时的操作指向:
- **四类技能与 `items`**:在 [project/items.js](../project/items.js) 为道具补 **`skillType` / `timeCost` 等**后,在道具使用效果与 **`flags.skillState`** 写入处接线;状态技能保持 **`timeCost === 0`** 且不调用 **`consumeTime`**;战斗耗时仍走 **`settleBattleTimeCost`** 与战后 **`applyStatusAfterBattle`** 调用链。
- **`contactBattleOnly` 与多怪捕捉**:在战斗触发逻辑(如 **`beforeBattle`** 或等价入口)读取怪物字段分支;多怪连续战斗时每场成功后调用 **`applyStatusAfterBattle("success")`**。
- **`actType` / `actArgs`**:在 **`performEnemyAction`**(或由 **`resolveEnemyActionsForSingleTick`** 内联)按怪物表字段分支执行移动/技能等行为。
---
## 3. 验收清单映射(对应 map-turn-spec §10
| §10 条目 | 依赖的待办小节 |
|----------|----------------|
| 状态技能不立即推进时间;战后层数 `-1` | 战斗胜利接线 + **`skillState`** 数据与 **`applyStatusAfterBattle`** 调用时机 |
| 远程/恢复立即推进;怪物按时间响应 | 道具 **`timeCost`**(已有 `useItem` 钩子)+ **`resolveEnemyActions` / `activeEnemies`** 实装 |
| 战斗耗时 **`max`** | **`settleBattleTimeCost`** 已在插件中;需战斗路径实际调用 |
| 多怪捕捉逐场扣层 | **`afterBattle`** 多场链路与每场成功分支 |
| Boss **`contactBattleOnly`** | 战斗触发层逻辑 + 怪物字段 |
| 死亡清理 | **`clearOnDeath`** 接入 **`lose` / 失败** |
| 存档读档连续 | **`flags`** 持久化已有方向;读档后 **`rebuildActiveEnemies`** |
| **`timeCost=0`** 不参与调度 | **`rebuildActiveEnemies`** 过滤 + **`resolveEnemyActions`** 只扫缓存 |
| **`consumeTime(3)`** 恰好 3 tick | **`consumeTime`** 已实现循环;需集成测试断言 |
| **`activeEnemies` 路径无每 tick 全图扫描** | **`rebuild` / `patch`** 实装 |
---
## 4. 相关路径速查
| 用途 | 路径 |
|------|------|
| 规范全文 | [docs/map-turn-spec.md](map-turn-spec.md) |
| 样板 API | [_docs/api.md](../_docs/api.md) |
| 地图回合插件 | [project/plugins.js](../project/plugins.js) 搜索 **`"mapTurn"`** |

View File

@ -1,5 +1,7 @@
# 地图回合与技能状态伪代码规范
实现进度与代码接线台账见 [map-turn-implementation-status.md](map-turn-implementation-status.md)。**角色分工**:本规范写冻结规则与实现契约(尤其 §6台账写「已接线文件 / API 清单 / 待办勾选项」,二者交叉引用,避免重复维护大段表格。
## 1. 目标与范围
本规范用于在当前项目中引入“地图时间Map Turn”和“四类技能状态”机制确保
@ -81,7 +83,7 @@ flags.mapTurnState = {
// 仅收录「怪物定义 timeCost > 0」的实例按楼层缓存供每 tick 遍历,避免全图 extractBlocks
activeEnemiesByFloor: {
// [floorId]: [
// { runtimeId, x, y, enemyId, blockIndex?, ... } // runtimeId 实现阶段约定,如 "floorId:x:y:enemyId"
// { runtimeId, x, y, enemyId, def, ... } // runtimeId 约定如 "floorId:x:y:enemyId"def 为 material.enemys[id] 引用,供 resolve 读 timeCost / actType
// ]
},
// 可选:每次 rebuild / patch 后自增,便于调试与断言缓存有效
@ -109,9 +111,10 @@ flags.skillState = {
- 实现 `core.plugin.mapTurn`(名称可调整,全文保持一致),至少包含:
- `consumeTime(deltaTime, reason)`
- `advanceMapTurnOne(reason)`
- `rebuildActiveEnemies(floorId)`、`patchActiveEnemiesForBlockChange(...)`(见 §6.6
- `rebuildActiveEnemies(floorId)`、`patchActiveEnemiesForBlockChange(floorId, hint)`(见 §6.3
- `resolveEnemyActionsForSingleTick(reason)`(仅遍历当前层 `activeEnemies`
- 可选:`__enable` 总开关,关闭时上述函数为空操作,便于分步接入游戏。
- `performEnemyAction(...)`、`settleBattleTimeCost()`、`applyStatusAfterBattle` / `clearOnDeath` 等与 §6 对齐的辅助入口
- **总开关**:持久化在 **`flags.mapTurnEnabled`**(随存档);由 `isEnabled` / `setEnabled` / `bootstrapPersistedState` 维护。开关为假时,`consumeTime` / `advanceMapTurnOne` 等推进与敌调度为空操作。勿再用闭包 `__enable` 与上述 flag 混用。
## 4. 行为触发矩阵(是否推进地图时间)
@ -192,10 +195,18 @@ function resolveEnemyActionsForSingleTick(reason) {
**注意**:列表中的怪物已保证 `enemyDef.timeCost > 0`(见 §6.3);此处 **禁止** 每 tick 调用 `extractBlocks` 全图扫描。
**`performEnemyAction`(当前契约)**:达阈时调用;`actType === "idle"`(或未配置)不产生额外地图效果。`chase` / `patrol` / `skill` 等在本阶段可为占位——若在单 tick 内同步调用会改写图块的引擎路径(如经 `removeBlock` 间接触发全图块整理),与上条性能约束冲突;后续应改为异步动作链或引擎侧无全表扫描的迁移 API 再接。
### 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`)。
2. **局部更新**:当 **地图上的怪物集合或坐标** 发生变化时(如 `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`** 的退化策略(规范允许)。
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` 内随存档走。
@ -246,6 +257,17 @@ function clearOnDeath() {
}
```
### 6.7 本仓库实现锚点(与当前代码一致)
以下与 [project/plugins.js](project/plugins.js) 及关联工程文件一致,便于对照 §8**文件级清单仍以** [map-turn-implementation-status.md](map-turn-implementation-status.md) **为准**
- **`core.plugin.mapTurn`** 定义于 [project/plugins.js](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](project/functions.js);开局 **`mapTurnEnabled`** 与 `bootstrapPersistedState` 见 [project/data.js](project/data.js)(台账 §1.6)。
- **移动与战后时间**:在 [project/plugins.js](project/plugins.js) 的 **`序章追击`** 等插件段内对 **`control.prototype.moveOneStep` / `moveDirectly`** 与 **`events.prototype.afterBattle`** 包装,调用 **`consumeTime`**(含战后 **`settleBattleTimeCost`** 与 **`applyStatusAfterBattle`****非** [project/functions.js](project/functions.js) 默认空壳上的直接编辑。
- **道具推进时间**`useItem` 内按 `timeCost` 调用 `consumeTime` 见 [libs/items.js](libs/items.js)(与台账 §1.3 一致)。
## 7. 关键业务分支
### 7.1 多怪捕捉导致多场战斗
@ -273,13 +295,12 @@ function activateBattleSkill(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/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`
- **地图变更挂钩(实现时择优)**:在会改变怪物位置/存亡的 API 之后调用 `patchActiveEnemiesForBlockChange`(若短期无法统一挂钩,可退化为每次变更后对该层 `rebuildActiveEnemies`
- **地图变更挂钩**:本仓库已通过 §6.7 对 `maps` / `events` 的包装触发 **`patchActiveEnemiesForBlockChange`** 或 **`rebuildActiveEnemies`**;若日后增加其它改图 API如未包装的 `swapBlock` 等),可再补挂钩或对该层退化为 **`rebuild`**
## 9. 存读档一致性要求
@ -299,5 +320,5 @@ function activateBattleSkill(skillId) {
- 存档读档后状态层数与地图时间连续。
- 设置怪物 `timeCost=0` 时,该怪不参与调度,性能无明显回退。
- **`consumeTime(3, ...)` 触发恰好 3 次 `advanceMapTurnOne`**`mapTurn` 连续 `+3`,且敌人行动槽/行为按 **3 个独立 tick** 结算(可用日志或计数器断言)。
- **`activeEnemies` 路径**:每 tick 不调用全图 `extractBlocks`;进楼 `rebuild` 后,增删怪`patch` 或单次 `rebuild``timeCost=0` 的怪不在列表中。
- **`activeEnemies` 路径**:每 tick 不调用全图 `extractBlocks`;进楼 `rebuild` 后,增删怪**`patch``removeCell` / `syncCell` / `migratePoint`)或单次 `rebuild`** 维护;`moveBlock` / `jumpBlock` 与深度计数规则见 §6.3`timeCost=0` 的怪不在列表中。

BIN
docs/暂定大纲.docx Normal file

Binary file not shown.

48
docs/暂定大纲.txt Normal file
View File

@ -0,0 +1,48 @@
简易大纲魔塔Like
1. 游戏概述
游戏名称:待定 (暂定)
游戏类型:固定地图探索+数值驱动+剧情导向+解谜
核心玩法:魔塔类玩法,攻防血等变量作为基础
核心体验:
固定地图探索:地图结构固定,鼓励探索、记忆和路线规划
线性成长:通过宝石、血瓶、战斗获得属性提升,逐步挑战更强敌人
状态继承:区域内状态会沿用上一场战斗剩余状态
非连续性:主角持有回档能力,在经过剧情后,对关键、信息道具继承,可以改变自身持有状态,改变地图通行,提前截获信息改变剧情走向到自己所需要的方向,剧情发生较大差异的变化可以视为不同周目
技能与队友:主角跟随剧情获得不同技能,同时,跟随主角的队友可以提供额外技能(被动+主动均有)提,更好的处理地图与敌人,按种类可以分为战斗伤害、地图伤害、敌我位移、状态变化、远程操作等技能
目标与敌对:在经过不同剧情后,主角的区域目标会跟随自己的想法而变动,不同周目所需面对的敌人是并不一致的,有些可以视为临时友军,并参与地图战斗(不可直接操作)
2. 设计目标
核心循环清晰:区域目标→探索战斗→达成目标→情况对比→更换思路→再设目标→敌我判断→重复战斗→再次对比→......→确认结果
信息透明:战场信息应当明确,玩家可以快速识别不同周目的差异
路线规划:周目间差异,之前剧情信息所限制的路线可以逐步对玩家放开
适度的挑战性:回档提供足够的下限保证玩家顺利度过区域战斗,同时,用严谨的技能方案、数值设计,提供足够的空间与深度给老玩家游玩。
3. 基础内容
生命值:归零则失败,触发回档。
攻防:回合制战斗逻辑,每回合按先后互相造成伤害
金币经验金币作采购武器、补给品经验自动分配给技能少量提升数值两者一般集中于boss身上占区域的30%
固定能力:区域提供能力为固定数值,玩家可以通过技能进行临时分配,不提供额外属性以降低复杂
战场时轴技能、战斗与地图联系的方式魔塔不可采用实际时间作为地图上的回合制使用达成敌我交互普通怪一般为站桩部分怪会主动攻击BOSS更进一步要求也仅要求BOSS层内的操作不同地图间一般独立互不影响。
地图通行:多为平面地图,地图间一般有多个联系方式,但初见只需到达目的地,无需重复移动,且由于回档带来的状态重置,通行性空间更大。
4. 章节流程
本塔预设为5个章节100-150层数受回档的多玩法影响一般玩家实际体验约为500-600层。
面向群众包含无基础玩家和老玩家,需对魔塔基本战斗方式、本塔引入机制等都进行介绍。
预设4个难度根据对现实修改能力强弱进行区分最高难度除剧情外无法修改任何内容。
预计在序章前半段介绍魔塔战斗方式,后半段启用并熟悉回档方式。
第一章,初期引入装备、技能(固定的临时队友加入,仅作为技能提供对象),讲述如何处理地图的难处理敌人,在后期引入时轴,介绍敌人在高强度战斗中技能造成的不同影响。
第二章,引入阵容和多队友选择,玩家自由选择作战人员(主角+2队友
第三章,接触针对性技能,玩家需要合理分配资源和反制这些技能。
第四章,引入复杂机制敌人,需要更好的分配作战,同时提供一定的地图战略技能支援,直接击杀这些敌人。
第五章玩家统合信息到达boss面前触发剧情被夺走回档技能主角为了队友选择放弃该能力回到最初。现在主角拥有一切的信息也保留所有技能但是无法使用资源差视为同个难度再次踏上旅程。
5. 任务分配
技术支持由于多种机制本塔录像要求较高且需要各种界面ui实现简化操作且战斗判断复杂。
地图、剧情:本塔剧情、地图结合紧密,需要剧情先行,且一定程度上互相配合妥协以降低实现难度。
玩家体验对平常地图和BOSS地图进行分割降低前者难度提高后者操作要求并对玩家持有资源量进行保障减少简易模式下卡关的出现。
设计思路
魔塔这类游戏的核心玩法,是通过对整体信息把控,用思路对路线做出规划,在这个过程中往往需要对路线可行性进行验证、修改局部,背板时间较多。玩家体验上看,区域规划>细节布局>整塔逻辑(意味着前面全部需要重打,成本大),针对这些问题,历来的各种功能、逻辑都给出了自己的答案,依次有浏览地图、快速存档、整塔重开,等等。
如何去玩法解决魔塔类游戏中的卡关问题这是本作品探讨的核心内容玩家往往会遇到以下几种问题资源消耗完之后无法再获取卡死在道中或者boss前特别是boss战前已知胜败结果需要通过大回档来重新规划甚至在规划后仍旧卡关。选择了采用部分继承机制——回档融入存档系统与剧情内也是正在讨论的内容。回档倾向于作为辅助功能和剧情功能。
而这自然衍生出另一个问题重复游玩不可行必然需要引入其他玩法这个内容可参考内容较多选择了较为可视且与地图拓扑相关的队友与阵容。将不同剧情下的地图区分开作为提供给各种玩法的空白画布。队友系统是作为主要系统让玩家进行决策且可参考内容多预计采用地图变化和添加特效等方式让效果更为直观例如怪物坑入它们设下的陷阱、消耗其技能为无意义释放或者操纵他们去内斗等多种方式可以参考战旗和ARPG等多种游戏。
卡关的本质是资源积累问题或者说对不优秀路线的惩罚。需要弱化道中决策成本而这会可能导致游玩节奏缺乏张力自然要提高boss战的难度作为张弛。在不考虑添加玩法的情况下如果只是模仿将boss拆分为多只怪这不过一堆怪而已而且与平常区域的玩法分割没能考虑拓扑性且boss单纯独立机制制作成本大玩家理解成本大考虑采用时轴辅助将机制与地图进行联系。按讨论所述系统不应占用太大空间但是会成为技能设计的重要方向。
多个系统同时引入一个塔理解成本大,需对流程进行把控各种元素的出场时机。需要尽早与玩家达成共识,将常规层游玩成本降低到多数玩家可接受,将重要怪物也添增足够的笔墨让玩家愿意去战胜并记住。
并且,由于篇幅带来的资源和状态差异问题问题,需注意到怪物技能设置问题和角色可用技能组的问题。同时需要避免两个问题,怪物技能着力过度导致角色只能推资源硬过,怪物技能无趣闭锁不与角色发生交互。

View File

@ -1604,6 +1604,64 @@ events.prototype._action_moveHero = function (data, x, y, prefix) {
this.__action_doAsyncFunc(data.async, core.eventMoveHero, data.steps, data.time);
}
/**
* moveHero steps 拆成每格一段 speed 段原样保留 moveHeroMapTurn 逐格 consumeTime
* 非样板扩展 core.eventMoveHero 语义对齐避免与 project/plugins moveOneStep 扣时重复
*/
events.prototype._expandMoveHeroMapTurnSteps = function (steps) {
var cardinal = ['up', 'down', 'left', 'right', 'forward', 'backward', 'leftup', 'leftdown', 'rightup', 'rightdown'];
var out = [];
(steps || []).forEach(function (t) {
if (typeof t !== 'string') return;
var v = t.split(':');
var dir = v[0];
if (dir === 'speed') {
out.push(t);
return;
}
var num = parseInt(v[1], 10);
if (v.length === 1 || isNaN(num)) num = 1;
if (num <= 0) {
out.push(dir + ':0');
return;
}
if (cardinal.indexOf(dir) < 0) {
out.push(t);
return;
}
for (var i = 0; i < num; i++) out.push(dir + ':1');
});
return out;
}
events.prototype._action_moveHeroMapTurn = function (data, x, y, prefix) {
var time = data.time != null ? data.time : null;
var flat = this._expandMoveHeroMapTurnSteps(data.steps || []);
if (flat.length === 0) return core.doAction();
var todo = [];
flat.forEach(function (oneStep) {
var timeStr = time != null ? String(time) : 'null';
var stepJson = JSON.stringify([oneStep]);
var parts = oneStep.split(':');
var skipConsume = oneStep.indexOf('speed:') === 0 ||
(parts.length > 1 && parseInt(parts[1], 10) === 0);
var consumeJs = skipConsume ? '' :
('if (core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.consumeTime === "function") {\n' +
'\tcore.plugin.mapTurn.consumeTime(1, "event:moveHeroMapTurn");\n' +
'}\n');
todo.push({
"type": "function",
"async": true,
"function": 'function(){\ncore.eventMoveHero(' + stepJson + ', ' + timeStr + ', function(){\n' +
consumeJs +
'core.doAction();\n' +
'});\n}'
});
});
core.insertAction(todo, x, y);
core.doAction();
}
events.prototype._action_jump = function (data, x, y, prefix) {
var from = this.__action_getLoc(data.from, x, y, prefix), to;
if (data.dxy) {

View File

@ -561,6 +561,15 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d =
}
],
"startText": [
{
"type": "setValue",
"name": "flag:mapTurnEnabled",
"value": "1"
},
{
"type": "function",
"function": "function(){\nif (core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.bootstrapPersistedState === \"function\") {\n\tcore.plugin.mapTurn.bootstrapPersistedState();\n}\n}"
},
{
"type": "comment",
"text": "初始剧情"

View File

@ -32,6 +32,12 @@ main.floors.C0_T01=
"type": "setValue",
"name": "flag:序追",
"value": "1"
},
{
"type": "moveHeroMapTurn",
"steps": [
"right:1"
]
}
],
"3,7": [

View File

@ -1076,6 +1076,10 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a =
}
core.setFlag('__fromLoad__', true);
if (core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.bootstrapPersistedState === "function") {
core.plugin.mapTurn.bootstrapPersistedState();
}
// TODO增加自己的一些读档处理
// 切换到对应的楼层

View File

@ -465,7 +465,7 @@ var items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a =
},
"bigKey": {
"cls": "tools",
"timeCost": null,
"timeCost": 3,
"name": "大黄门钥匙",
"text": "可以开启当前层所有黄门",
"itemEffect": "core.addItem('yellowKey', 1);\ncore.addItem('blueKey', 1);\ncore.addItem('redKey', 1);",
@ -642,7 +642,7 @@ var items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a =
},
"skill1": {
"cls": "constants",
"timeCost": null,
"timeCost": 3,
"name": "技能:二倍斩",
"text": "可以打开或关闭主动技能二倍斩",
"hideInReplay": true,

View File

@ -2334,6 +2334,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
}
var old_moveOneStep = control.prototype.moveOneStep;
var old_moveDirectly = control.prototype.moveDirectly;
var old_changeFloor = events.prototype.changeFloor;
var old_afterChangeFloor = events.prototype.afterChangeFloor;
var old_afterGetItem = events.prototype.afterGetItem;
@ -2349,10 +2350,30 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
enqueuePursuitTurn();
}
old_moveOneStep.call(this, callback);
if (core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.consumeTime === "function") {
core.plugin.mapTurn.consumeTime(1, "move");
}
};
control.prototype.moveDirectly = function (x, y, ignoreSteps) {
var effectiveIgnoreSteps = ignoreSteps;
if (effectiveIgnoreSteps == null && typeof core.canMoveDirectly === "function") {
effectiveIgnoreSteps = core.canMoveDirectly(x, y);
}
var ok = old_moveDirectly.call(this, x, y, ignoreSteps);
if (ok && typeof effectiveIgnoreSteps === "number" && effectiveIgnoreSteps > 0 &&
core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.consumeTime === "function") {
core.plugin.mapTurn.consumeTime(effectiveIgnoreSteps, "moveDirectly");
}
return ok;
};
events.prototype.afterGetItem = function (id, x, y, isGentleClick) {
old_afterGetItem.call(this, id, x, y, isGentleClick);
// 走路踩到 getItem 时引擎传 isGentleClick=false时轴已在 moveOneStep 扣过,此处不再扣以免双倍
if (isGentleClick && core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.consumeTime === "function") {
core.plugin.mapTurn.consumeTime(1, "getItem:gentleClick");
}
if (isPursuitEnabled() && core.getFlag("1fBoss_loc")) {
core.setFlag("序追_lock", { x: core.getHeroLoc("x"), y: core.getHeroLoc("y"), floorId: core.status.floorId });
enqueuePursuitTurn();
@ -2361,6 +2382,15 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
events.prototype.afterBattle = function (enemyId, x, y) {
old_afterBattle.call(this, enemyId, x, y);
if (core.status.hero && core.status.hero.hp > 0 && core.plugin && core.plugin.mapTurn) {
var mt = core.plugin.mapTurn;
if (typeof mt.settleBattleTimeCost === "function" && typeof mt.consumeTime === "function") {
mt.consumeTime(mt.settleBattleTimeCost(), "battle");
}
if (typeof mt.applyStatusAfterBattle === "function") {
mt.applyStatusAfterBattle("success");
}
}
if (enemyId === "redSwordsman") {
core.removeFlag("1fBoss_loc");
core.setFlag("序追", 0);
@ -2388,6 +2418,16 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
let in_point = core.getFlag("in_point", {});
old_afterChangeFloor.call(this, floorId);
// 读档换层时保留存档中的 mapTurn普通换层仍按层相对回合归零见 docs/map-turn-implementation-status.md
if (!fromLoad) {
var mapTurnState = core.getFlag("mapTurnState");
if (mapTurnState && typeof mapTurnState === "object") {
mapTurnState.mapTurn = 0;
}
}
if (core.plugin && core.plugin.mapTurn && typeof core.plugin.mapTurn.rebuildActiveEnemies === "function") {
core.plugin.mapTurn.rebuildActiveEnemies(core.status.floorId || floorId);
}
if (!fromLoad && core.status.floorId) in_point[core.status.floorId] = {
x: core.status.hero.loc.x,
y: core.status.hero.loc.y,
@ -2658,7 +2698,10 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
},
"mapTurn": function () {
var __enable = false;
// 总开关持久化在 flags.mapTurnEnabled随存档/读档);勿再用闭包 __enable
function mapTurnEnabled() {
return !!core.getFlag("mapTurnEnabled");
}
function ensureFlags() {
var m = core.getFlag("mapTurnState");
@ -2689,13 +2732,18 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
this.mapTurn = {
isEnabled: function () {
return __enable;
return mapTurnEnabled();
},
setEnabled: function (v) {
__enable = !!v;
core.setFlag("mapTurnEnabled", !!v);
if (v) ensureFlags();
},
/** 读档或 setValue 写入 flag 后调用,在 flag 为真时补齐 mapTurnState / skillState */
bootstrapPersistedState: function () {
if (mapTurnEnabled()) ensureFlags();
},
consumeTime: function (deltaTime, reason) {
if (!__enable || !deltaTime || deltaTime <= 0) return;
if (!mapTurnEnabled() || !deltaTime || deltaTime <= 0) return;
ensureFlags();
var n = Math.floor(deltaTime);
var s = core.getFlag("mapTurnState");
@ -2704,26 +2752,177 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
this.advanceMapTurnOne(reason);
},
advanceMapTurnOne: function (reason) {
if (!__enable) return;
if (!mapTurnEnabled()) return;
ensureFlags();
var s = core.getFlag("mapTurnState");
s.mapTurn = (s.mapTurn || 0) + 1;
this.resolveEnemyActionsForSingleTick(reason);
},
/** 单 tick 怪物调度:仅扫 activeEnemiesByFloor不调用 extractBlocksmap-turn-spec §6.2 */
resolveEnemyActionsForSingleTick: function (reason) {
/* 占位:按 docs/map-turn-spec.md §6.2 接入 activeEnemies 与怪物行动 */
if (!mapTurnEnabled()) return;
ensureFlags();
var floorId = core.status.floorId;
if (!floorId) return;
var s = core.getFlag("mapTurnState");
var list = s.activeEnemiesByFloor[floorId] || [];
if (list.length === 0) return;
var eg = s.enemyActionGauge;
if (!eg[floorId] || typeof eg[floorId] !== "object") eg[floorId] = {};
var gmap = eg[floorId];
var snapshot = list.slice();
var self = this;
for (var i = 0; i < snapshot.length; i++) {
var enemyRef = snapshot[i];
if (!enemyRef || !enemyRef.runtimeId) continue;
var def = enemyRef.def || core.material.enemys[enemyRef.enemyId];
if (!def || typeof def.timeCost !== "number" || def.timeCost <= 0) continue;
var rid = enemyRef.runtimeId;
gmap[rid] = (gmap[rid] || 0) + 1;
while (gmap[rid] >= def.timeCost) {
gmap[rid] -= def.timeCost;
self.performEnemyAction(enemyRef, def, floorId, reason);
rid = enemyRef.runtimeId;
}
}
},
/**
* 达阈行动初版仅实现 actType === idle无操作
* chase/patrol/skill 等若在此同步改图块会经引擎 removeBlock 触发 extractBlocks违反单 tick 约束后续用 insertAction 链或专用无扫描迁移 API 再接
*/
performEnemyAction: function (enemyRef, def, floorId, reason) {
var actType = def.actType || "idle";
if (actType === "idle") return;
/* chase / patrol / skill占位不在此 tick 内改图块 */
},
/**
* 全量重建当前层 activeEnemiesmap-turn-spec §6.3仅此路径内调用 extractBlocks
* 条目含 runtimeId / x / y / enemyId / defgauge 仅保留仍存在于图上的 runtimeId
*/
rebuildActiveEnemies: function (floorId) {
ensureFlags();
var s = core.getFlag("mapTurnState");
floorId = floorId || core.status.floorId;
if (!floorId) return;
if (!core.floors || !core.floors[floorId]) return;
core.extractBlocks(floorId);
var map = core.status.maps && core.status.maps[floorId];
var blocks = (map && map.blocks) || [];
var list = [];
for (var bi = 0; bi < blocks.length; bi++) {
var block = blocks[bi];
if (!block || block.disable) continue;
var ev = block.event;
if (!ev || !ev.cls) continue;
if (String(ev.cls).indexOf("enemy") !== 0) continue;
var enemyId = ev.id;
var def = core.material.enemys[enemyId];
if (!def || typeof def.timeCost !== "number" || def.timeCost <= 0) continue;
var runtimeId = floorId + ":" + block.x + ":" + block.y + ":" + enemyId;
list.push({
runtimeId: runtimeId,
x: block.x,
y: block.y,
enemyId: enemyId,
def: def
});
}
if (!s.activeEnemiesByFloor) s.activeEnemiesByFloor = {};
s.activeEnemiesByFloor[floorId] = [];
s.activeEnemiesByFloor[floorId] = list;
s.activeEnemiesVersion = (s.activeEnemiesVersion || 0) + 1;
var eg = s.enemyActionGauge;
var prev = eg[floorId];
var nextG = {};
if (prev && typeof prev === "object") {
for (var li = 0; li < list.length; li++) {
var rid = list[li].runtimeId;
if (typeof prev[rid] === "number") nextG[rid] = prev[rid];
}
}
eg[floorId] = nextG;
},
/**
* 局部维护 activeEnemies / enemyActionGaugemap-turn-spec §6.3
* hint.op: removeCell | syncCell | migratePoint | rebuild默认 rebuild
*/
patchActiveEnemiesForBlockChange: function (floorId, hint) {
if (!mapTurnEnabled()) return;
ensureFlags();
hint = hint || {};
floorId = floorId || core.status.floorId;
if (!floorId || !core.floors[floorId]) return;
var s = core.getFlag("mapTurnState");
if (!s.activeEnemiesByFloor) s.activeEnemiesByFloor = {};
var op = hint.op || "rebuild";
if (op === "rebuild") {
this.rebuildActiveEnemies(floorId);
return;
}
var eg = s.enemyActionGauge;
if (!eg[floorId] || typeof eg[floorId] !== "object") eg[floorId] = {};
var gmap = eg[floorId];
if (op === "removeCell") {
var rx = hint.x,
ry = hint.y;
var list0 = s.activeEnemiesByFloor[floorId] || [];
var out0 = [];
for (var a = 0; a < list0.length; a++) {
var e0 = list0[a];
if (e0.x === rx && e0.y === ry) {
delete gmap[e0.runtimeId];
continue;
}
out0.push(e0);
}
s.activeEnemiesByFloor[floorId] = out0;
s.activeEnemiesVersion = (s.activeEnemiesVersion || 0) + 1;
return;
}
if (op === "syncCell") {
var cx = hint.x,
cy = hint.y;
var list1 = s.activeEnemiesByFloor[floorId] || [];
var out1 = [];
for (var b = 0; b < list1.length; b++) {
var e1 = list1[b];
if (e1.x === cx && e1.y === cy) delete gmap[e1.runtimeId];
else out1.push(e1);
}
var block = core.getBlock(cx, cy, floorId, false);
if (block && !block.disable && block.event && block.event.cls && String(block.event.cls).indexOf("enemy") === 0) {
var eid = block.event.id;
var def = core.material.enemys[eid];
if (def && typeof def.timeCost === "number" && def.timeCost > 0) {
var rid = floorId + ":" + cx + ":" + cy + ":" + eid;
out1.push({ runtimeId: rid, x: cx, y: cy, enemyId: eid, def: def });
}
}
s.activeEnemiesByFloor[floorId] = out1;
s.activeEnemiesVersion = (s.activeEnemiesVersion || 0) + 1;
return;
}
if (op === "migratePoint") {
var list2 = s.activeEnemiesByFloor[floorId] || [];
for (var c = 0; c < list2.length; c++) {
var e2 = list2[c];
if (e2.x === hint.fromX && e2.y === hint.fromY) {
var oldR = e2.runtimeId;
var newR = floorId + ":" + hint.toX + ":" + hint.toY + ":" + e2.enemyId;
e2.x = hint.toX;
e2.y = hint.toY;
e2.runtimeId = newR;
if (typeof gmap[oldR] === "number") {
gmap[newR] = gmap[oldR];
delete gmap[oldR];
}
s.activeEnemiesVersion = (s.activeEnemiesVersion || 0) + 1;
return;
}
}
}
},
settleBattleTimeCost: function () {
ensureFlags();
@ -2757,6 +2956,95 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
ss.activeStatusSkills = {};
}
};
var mapTurnImpl = this.mapTurn;
(function installMapTurnActiveEnemyHooks() {
if (!core || !core.maps || !core.events) return;
var mapsProto = Object.getPrototypeOf(core.maps);
if (mapsProto.__mapTurnActiveEnemyHooks) return;
mapsProto.__mapTurnActiveEnemyHooks = true;
function blockAnimDepth() {
return core.__mapTurnBlockAnimDepth || 0;
}
function incBlockAnim() {
core.__mapTurnBlockAnimDepth = blockAnimDepth() + 1;
}
function decBlockAnim() {
core.__mapTurnBlockAnimDepth = Math.max(0, blockAnimDepth() - 1);
}
function mtCall(hint, floorId) {
if (!mapTurnEnabled() || !mapTurnImpl || typeof mapTurnImpl.patchActiveEnemiesForBlockChange !== "function") return;
mapTurnImpl.patchActiveEnemiesForBlockChange(floorId, hint);
}
function mtRebuild(floorId) {
if (!mapTurnEnabled() || !mapTurnImpl || typeof mapTurnImpl.rebuildActiveEnemies !== "function") return;
mapTurnImpl.rebuildActiveEnemies(floorId);
}
var oldRemoveBlock = mapsProto.removeBlock;
mapsProto.removeBlock = function (x, y, floorId) {
var ret = oldRemoveBlock.call(this, x, y, floorId);
if (ret && blockAnimDepth() <= 0) mtCall({ op: "removeCell", x: x, y: y }, floorId || core.status.floorId);
return ret;
};
var oldSetBlock = mapsProto.setBlock;
mapsProto.setBlock = function (number, x, y, floorId) {
var ret = oldSetBlock.call(this, number, x, y, floorId);
if (blockAnimDepth() <= 0) mtCall({ op: "syncCell", x: x, y: y }, floorId || core.status.floorId);
return ret;
};
var oldRemoveBlockByIndexes = mapsProto.removeBlockByIndexes;
mapsProto.removeBlockByIndexes = function (indexes, floorId) {
oldRemoveBlockByIndexes.call(this, indexes, floorId);
mtRebuild(floorId || core.status.floorId);
};
var oldMoveBlock = mapsProto.moveBlock;
mapsProto.moveBlock = function (x, y, steps, time, keep, callback) {
incBlockAnim();
return oldMoveBlock.call(this, x, y, steps, time, keep, function () {
decBlockAnim();
if (!keep && mapTurnEnabled() && mapTurnImpl)
mapTurnImpl.patchActiveEnemiesForBlockChange(core.status.floorId, { op: "removeCell", x: x, y: y });
if (callback) callback();
});
};
var oldJumpBlock = mapsProto.jumpBlock;
mapsProto.jumpBlock = function (sx, sy, ex, ey, time, keep, callback) {
incBlockAnim();
return oldJumpBlock.call(this, sx, sy, ex, ey, time, keep, function () {
decBlockAnim();
if (!keep && mapTurnEnabled() && mapTurnImpl)
mapTurnImpl.patchActiveEnemiesForBlockChange(core.status.floorId, { op: "removeCell", x: sx, y: sy });
if (callback) callback();
});
};
var oldHideBlock = mapsProto.hideBlock;
mapsProto.hideBlock = function (x, y, floorId) {
var ret = oldHideBlock.call(this, x, y, floorId);
if (blockAnimDepth() <= 0) mtCall({ op: "removeCell", x: x, y: y }, floorId || core.status.floorId);
return ret;
};
var oldShowBlock = mapsProto.showBlock;
mapsProto.showBlock = function (x, y, floorId) {
var ret = oldShowBlock.call(this, x, y, floorId);
if (blockAnimDepth() <= 0) mtCall({ op: "syncCell", x: x, y: y }, floorId || core.status.floorId);
return ret;
};
var evProto = Object.getPrototypeOf(core.events);
var oldMoveEnemyOnPoint = evProto.moveEnemyOnPoint;
evProto.moveEnemyOnPoint = function (fromX, fromY, toX, toY, floorId, norefresh) {
oldMoveEnemyOnPoint.call(this, fromX, fromY, toX, toY, floorId, norefresh);
mtCall({ op: "migratePoint", fromX: fromX, fromY: fromY, toX: toX, toY: toY }, floorId || core.status.floorId);
};
})();
}
}