0.14地图回合构筑-各行动接口插入
This commit is contained in:
parent
b30cd88fd6
commit
ba74ab06d9
@ -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)"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
@ -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
77
claude.md
Normal 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 只给方案,不做实现。
|
||||
106
docs/map-turn-implementation-status.md
Normal file
106
docs/map-turn-implementation-status.md
Normal 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.5–6.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 该条所列 1–4 步(含 **`performEnemyAction`**,`idle` 已实现,其余 `actType` 占位)。
|
||||
|
||||
- [x] **`rebuildActiveEnemies`**:已在 **`mapTurn.rebuildActiveEnemies`** 实现 §2 该条 1–4 步;**`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"`** |
|
||||
@ -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
BIN
docs/暂定大纲.docx
Normal file
Binary file not shown.
48
docs/暂定大纲.txt
Normal file
48
docs/暂定大纲.txt
Normal 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单纯独立机制制作成本大,玩家理解成本大),考虑采用时轴辅助,将机制与地图进行联系。按讨论所述,系统不应占用太大空间,但是会成为技能设计的重要方向。
|
||||
多个系统同时引入一个塔理解成本大,需对流程进行把控各种元素的出场时机。需要尽早与玩家达成共识,将常规层游玩成本降低到多数玩家可接受,将重要怪物也添增足够的笔墨让玩家愿意去战胜并记住。
|
||||
并且,由于篇幅带来的资源和状态差异问题问题,需注意到怪物技能设置问题和角色可用技能组的问题。同时需要避免两个问题,怪物技能着力过度导致角色只能推资源硬过,怪物技能无趣闭锁不与角色发生交互。
|
||||
@ -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) {
|
||||
|
||||
@ -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": "初始剧情"
|
||||
|
||||
@ -32,6 +32,12 @@ main.floors.C0_T01=
|
||||
"type": "setValue",
|
||||
"name": "flag:序追",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "moveHeroMapTurn",
|
||||
"steps": [
|
||||
"right:1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"3,7": [
|
||||
|
||||
@ -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:增加自己的一些读档处理
|
||||
|
||||
// 切换到对应的楼层
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;不调用 extractBlocks(map-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 内改图块 */
|
||||
},
|
||||
/**
|
||||
* 全量重建当前层 activeEnemies(map-turn-spec §6.3);仅此路径内调用 extractBlocks。
|
||||
* 条目含 runtimeId / x / y / enemyId / def;gauge 仅保留仍存在于图上的 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 / enemyActionGauge(map-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);
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user