22 KiB
Map turn integration in tower game
Exported on 2026/4/22 at GMT+8 21:53:10 from Cursor (3.1.17)
User
目标是做一个魔塔游戏的变体,魔塔类型游戏其特征为固定地图、仅玩家进行行动。为了令操作更有交互,考虑引入地图回合(行动)这个概念。将角色每移动1格,或者进行单场战斗(或者其他的,待追加)视为1次行动。包括玩家操作的角色、敌方每只怪物共用地图回合这个概念。例如玩家使用某技能(其消耗了3时间),3时间对应3行动,那么随这段时间的推进中,只消耗1时间就可以1行动的怪物会行动3次。现在该如何在这个项目框架中进行插入地图回合(行动)这个概念?
Cursor
我先快速梳理你这个工程里“移动、战斗、技能耗时”分别是在哪些钩子里触发的,然后基于现有插件机制给你一个可插入的“地图回合系统”设计方案,尽量做到少改核心、以插件为主。
我看到 plugins.js 里已经有自定义角色系统和 heroMoving 帧回调,这很适合挂“每步计行动点”。我继续读这一段和战斗相关函数,确认最稳妥的接入点。
我已经定位到引擎层 libs/events.js,下一步会确认“每走1格”真正完成的时机函数,以及战斗触发链路,这样能给你一个不破坏现有事件系统的接入点。
我已经把关键接入点确认完了:这个框架里“每走1格”就在 moveOneStep,战斗结算在 afterBattle,道具/技能使用走 core.items.useItem。下面我给你一个可直接落地的“地图回合调度器”方案(先最小可用,再扩展怪物 AI)。可以,而且你这个项目结构非常适合做“地图回合(行动)”。
你要的核心是:把“玩家行动消耗时间”转换为“全体怪物获得行动机会”。
建议做成一个独立的 MapTurnScheduler(地图回合调度器),挂在 core.plugin 下,不改引擎底层逻辑。
推荐接入点(你这个工程里已存在)
每走1格 的钩子就在 project/functions.js 的 moveOneStep:
"moveOneStep": function (callback) {
// ... 省略 ...
// 增加步数
core.status.hero.steps++;
// ... 省略 ...
// 执行目标点的阻激夹域事件
core.checkBlock();
// 执行目标点的script和事件
if (!hasTrigger)
core.trigger(nowx, nowy, callback);
}
单场战斗 的结算点在 project/functions.js 的 afterBattle:
"afterBattle": function (enemyId, x, y) {
// 战斗结束后触发的事件
var enemy = core.material.enemys[enemyId];
var special = enemy.special;
// ...
}
道具/技能使用 的统一入口在 libs/items.js:
items.prototype.useItem = function (itemId, noRoute, callback) {
if (!this.canUseItem(itemId)) {
if (callback) callback();
return;
}
this._useItemEffect(itemId);
this._afterUseItem(itemId);
if (!noRoute) core.status.route.push("item:" + itemId);
if (callback) callback();
}
架构方案(建议)
1) 定义统一“时间消耗 -> 推进地图回合”
在插件里做一个接口(示意):
core.plugin.mapTurn.consume(cost, source)
cost: 本次行动消耗时间(比如移动=1,普通战斗=1,技能=3)source: 可选,记录来源(move / battle / skill)
2) 调度器内部做两层循环
- 外层:消费
cost次“时间单位” - 内层:每个时间单位让所有怪物累积行动值(AP),够了就行动
示意规则:
- 每只怪有
actSpeed(每时间单位获得多少AP),默认 1 - 每次行动门槛
actCost,默认 1 - 若玩家一次消耗 3 时间,
actSpeed=1的怪可行动 3 次,actSpeed=0.5的怪行动 1 次(余 0.5)
怪物数据扩展(最稳)
在 project/enemys.js 给需要行动的怪加自定义字段(不会破坏原系统):
actSpeed:速度(默认1)actCost:每次行动消耗(默认1)aiType:行为类型(chase/patrol/caster等)aiArgs:行为参数
这套字段引擎不会管,但你的调度器能读取。
如何在你的工程里“插入”而不炸
A. 玩家移动后推进时间
在 moveOneStep 末尾追加:
core.plugin.mapTurn.consume(1, { type: 'move' })
B. 战斗结算后推进时间
在 afterBattle 末尾追加:
core.plugin.mapTurn.consume(1, { type: 'battle', enemyId, x, y })
C. 技能/道具推进时间
两种做法(二选一):
- 精细控制:在具体技能
useItemEffect里手动调用(例如skill1用consume(3)) - 统一控制:包裹
core.items.useItem,按itemId -> timeCost表自动扣时间
你提到“技能消耗3时间”,建议先对技能单独写,最清晰。
怪物行动执行建议(从简单到复杂)
-
第一阶段(最小可用)
怪物行动只做“释放地图效果/远程伤害/召唤”,不移动坐标。
优点:几乎不碰路径和碰撞,稳定。 -
第二阶段(移动AI)
使用core.moveBlock(x,y,steps,time,true,cb)进行怪物移动(引擎已支持)。
- 靠近勇士时攻击(自定义伤害/触发战斗)
- 否则按
chase/patrol移动一格
必须注意的坑
- 事件重入:
consume()内可能触发insertAction,要有isResolving锁,避免递归调用。 - 自动寻路:怪物行动时可能打断自动路由,建议敌方行动期间临时停自动寻路。
- 楼层切换:建议按楼层存AP(
flags.enemyAP[floorId][x,y,id]),换层后恢复。 - 存读档一致性:把时间轴状态都放
flags,这样天然随存档走。 - 录像一致性:若有随机AI,需把随机结果写入route(你项目已支持自定义route项)。
你现在最适合的落地顺序
- 先做
mapTurn.consume()+moveOneStep/afterBattle两个接入 - 先实现 1 个怪物
aiType(例如只会每回合对勇士造成固定伤害) - 再接入技能时间(
skill1=3) - 最后再上移动AI(
moveBlock)
如果你愿意,我下一步可以直接给你一版“贴合你当前文件结构”的完整伪代码模板(按 project/plugins.js + project/functions.js + project/items.js 分段),你照抄改就能跑。
User
技能不一定非得消耗3回合,仅举例。考虑,在item-图块属性-canUseItemEffect的下方注册一个地图回合,默认null,以skill1二倍斩为例,其暂时设置为3。而怪物方面,考虑enemys和enemy48这两块,在point加点下方也注册一个地图回合,如果为0,在时间中不考虑其变化,以减少检索当前地图所有怪潜在可能导致的卡顿。如果怪物有技能,假设为净化,花费时间为2,那么描述为净化(2):...(具体效果)这样。这样如何?
Cursor
这个思路整体是对的,而且很适合你现在这套工程。
结论先说:可以这样做,但建议不要把“地图回合”直接挂在 canUseItemEffect 下面复用其语义,最好新增独立字段,避免后续维护混乱。
你这套设计的可行性判断
- 道具/技能侧:在
item里加一个“耗时字段”很合理(例如skill1= 3)。 - 怪物侧:在敌人定义里加“行动耗时/速度字段”也合理;
0表示静态怪、忽略地图回合,能明显减少计算。 - 描述展示:像“净化(2): ...”这种写法非常好,玩家可读性强。
建议的字段命名(比“放在 canUseItemEffect 下方”更稳)
道具(project/items.js)
建议新增:
timeCost: 使用该道具/技能消耗多少地图时间(默认null或0)
原因:canUseItemEffect 在引擎里是“可否使用”的布尔脚本,不是配置容器。
你可以看到它只被当条件执行:
items.prototype.useItem = function (itemId, noRoute, callback) {
if (!this.canUseItem(itemId)) {
if (callback) callback();
return;
}
this._useItemEffect(itemId);
this._afterUseItem(itemId);
if (!noRoute) core.status.route.push("item:" + itemId);
if (callback) callback();
}
items.prototype.canUseItem = function (itemId) {
if (!core.hasItem(itemId)) return false;
var canUseItemEffect = core.material.items[itemId].canUseItemEffect;
if (canUseItemEffect) {
try {
return eval(canUseItemEffect);
怪物(project/enemys.js)
建议新增:
timeCost或actCost(每次行动所需时间,默认0表示不参与)- 可选
actType(追击/施法/巡逻) - 可选
actValue(技能强度)
你提的“point 下方加字段”完全可以,兼容性没问题。
关于性能:你的“0 不参与”是关键优化点
你担心每回合扫全图卡顿,这个判断非常专业。
建议实现时再加一层优化:
- 进入楼层时,建立一次
activeEnemies列表(只收timeCost > 0的怪) - 地图变化(怪死亡/召唤/移动)时局部更新这份列表
- 每次时间推进只遍历
activeEnemies,不扫全图
这样比“每次都 extractBlocks + filter”快很多。
“净化(2): ...”显示位置建议
你现在怪物特殊属性文案在 getSpecials 里定义,可直接拼接耗时展示:
return [
[1, "先攻", "怪物首先攻击", "#ffcc33"],
...
[9, "净化", function (enemy) { return "战斗前,怪物附加角色护盾的" + (enemy.purify || core.values.purify) + "倍作为伤害"; }, "#80eed6"],
可以改成类似:
- 名称:
净化(2)(静态) - 或描述函数里动态拼:
净化(${enemy.timeCost||0}):...
你的方案方向是正确的;我只建议做一个小修正:
“地图回合耗时”单独字段化(timeCost),不要借 canUseItemEffect 的语义。
这样你后面做 UI 展示、战斗日志、AI 调度都会更顺。
如果你要,我下一条可以直接给你一份“字段规范 + 调度规则表”(含默认值和边界行为),你可以直接当项目约定文档用。
User
统一使用timecost。追击、巡逻、等怪物行动,统一为玩家行动消耗这个后,相应进行行动。对消耗时间的操作进一步确认:玩家移动一格(含事件中强制移动)、拾取(将面前道具获得,不视为移动)、使用tools类道具例如bigKey、单场战斗。特别的,部分boss需要特别设置为,单次接触就是一次战斗回合,不连续推进回合,也视为1地图回合,来玩家有更多操作空间。而玩家的技能根据类型(可否取消)来推进时间,例如远程技能扔出去就打在怪身上无法撤回的,直接消耗地图回合,而战斗技能或者状态技能由于战斗前玩家需要考虑和模拟是否打开会更好,则不直接消耗时间,直到进行战斗才结算,并且,如果是二倍斩花费3时间,跟战斗时间默认1之间,不累加为4,而是取最大值3。这样如何?
Cursor
这版规则很成熟了,已经接近“可直接实现”的设计稿。
结论:整体可行,逻辑自洽,且可玩性会明显提升。下面我帮你把关键点“定死”,避免实现时歧义。
核心结论
你这套应统一成一句话:
- 地图时间只由“玩家消耗 timeCost 的行为”推进;怪物按这段时间统一行动。
你列的行为里,大部分都应该推进时间,且用 timeCost 统一配置完全正确。
建议定版的时间结算规则
1) 玩家行为触发地图时间(建议)
- 移动1格(含事件强制移动)→
timeCost = 1 - 轻按拾取(面前道具,不移动)→ 默认
timeCost = 1(建议可配) - 使用
tools类道具(如bigKey)→ 道具自身timeCost(默认 1 或 0,看你设计) - 单场战斗 → 默认
timeCost = 1,可被技能覆盖(见第4条)
这部分定义非常清晰,建议再补一句:
纯UI操作(开菜单、开手册、查看模拟)不推进时间。
2) 怪物行动规则(追击/巡逻统一)
你说的“玩家消耗多少,怪物就相应行动”完全OK。
推荐实现成:
- 怪物字段
timeCost:0:静态,不参与地图回合(性能优化)>0:参与地图回合,每累计到门槛可行动一次
- 怪物字段
actType:chase、patrol、skill等
并且保留你说的描述显示:净化(2):...。
3) Boss“单次接触只打一回合”机制
这个需求很好,能显著提升策略空间。
建议加怪物字段:
contactBattleOnly: true
语义定为:
- 每次接触该 Boss,仅结算“一次战斗接触”
- 本次接触后立即结束,不触发连续回合推进
- 该次接触仍记为
1地图回合(或技能覆盖值)
这样你就能实现“打一下、走位、再打一下”的 Boss 节奏。
4) 技能耗时与战斗耗时合并(你的 max 规则)
你提出的规则非常好,建议正式定为:
finalBattleTimeCost = max(baseBattleTimeCost, pendingSkillTimeCost)
也就是你说的:
- 二倍斩
timeCost=3 - 战斗默认
1 - 最终取
3,不叠加成4
这个比相加更平衡,也避免“状态技能 + 战斗”被双重惩罚。
5) 可取消/不可取消技能的时间点(关键)
你这条很专业,建议直接固化:
- 不可取消技能(如抛射物已发出)
- 释放瞬间立即推进地图时间(消耗
timeCost)
- 释放瞬间立即推进地图时间(消耗
- 可取消技能(战斗前开关类/状态类)
- 开启时不推进时间,只记录
pendingSkillTimeCost - 真正触发战斗时按第4条结算
- 取消则清空
pendingSkillTimeCost
- 开启时不推进时间,只记录
这会让“预判、模拟、开关”体验很好,不会被操作惩罚。
你现在还需要补的两个小约束(防Bug)
- 多技能并存时的规则:
建议pendingSkillTimeCost取最大,不叠加(和战斗规则一致,最稳)。 - 战斗未发生时清理时机:
可取消技能若切楼/剧情中断/死亡,记得清空 pending,避免脏状态。
最终评价
这套方案可以直接作为你的“地图回合制规范 v1”:
- 字段统一:
timeCost - 行为统一:玩家消耗时间 -> 怪物响应行动
- 性能可控:
timeCost=0怪不参与 - Boss可特判:单次接触一回合
- 技能结算合理:可取消延后结算,且与战斗取
max
如果你愿意,我下一步可以给你一份“精确到变量名和状态机的伪代码规范”(例如 pendingSkillTimeCost, consumeTime(cost, reason), resolveEnemyActions(cost) 的完整流程图式文本),你可以直接照着实现。
User
补充:分为四种技能,战斗技能(战斗中)、状态技能(战斗前例如加攻击)、恢复技能(非战斗,例如回血回蓝)、远程技能(非战斗,对敌人)。多技能共存而言,战斗中技能不允许同时启用多个战斗技能。状态类技能,默认不消耗时间。恢复技能视为远程技能,均消耗时间。考虑对敌人的技能也准备按这样后续修改。这样的话,应该也不存在脏状态问题了。
Cursor
这个补充很到位,规则已经很完整了。
结论:按你这四类技能划分后,系统会更稳定,也更容易避免脏状态。你现在这版可以作为正式规范了。
建议你把规则最终固定成下面这份(可直接当实现约束):
战斗技能:仅战斗中生效;同一时刻最多启用1个(互斥)。状态技能:战斗前开关类;默认不推进地图时间;在战斗结算时参与max(战斗耗时, 状态技能耗时)。恢复技能:非战斗;按你定义归入“即时消耗时间”类(和远程一致)。远程技能:非战斗对敌;释放即生效,不可撤回,立即消耗timeCost。
你说“这样基本不存在脏状态”——基本成立,但我建议再加两条保险,彻底封死边角:
- 在以下时机统一清理临时态:
战斗结束、切楼、死亡、读档、中断事件链。
(比如pendingSkillTimeCost、pendingSkillId、inBattleSkillId) - 若状态技能是“可开关”,关闭时立即清空挂起耗时,避免下一场战斗误继承。
再加一个小建议(体验向):
- 给技能增加统一字段:
skillType: battle|status|recover|ranged,timeCost,exclusiveGroup(战斗技能用)。
后续你改“对敌技能”时就不需要额外写分支了,直接走同一调度器。
整体上,这个设计已经可以进实现阶段了。
User
状态技能可以多回合,例如强化3次战斗这样,每次战斗后-1。所以要考虑战斗结束,切楼、读档则不影响,死亡则会引入后续游戏机制,直接清除所有状态即可,事件不打断当前状态。其他还有问题吗?
Cursor
这版已经很接近可落地了。你这个补充下,主要再补 6 个边界定义就够了:
- 战斗计数何时扣减:建议统一在
afterBattle成功结算后-1;如果战斗前判定不可战斗、取消、或角色死亡前未完成结算,不扣。 - 哪些战斗算“1次”:要明确包含
普通接触战斗、强制战斗、伏击/捕捉触发战斗、多怪支援战是否算 1 次(建议支援仍算 1 次“单场”)。 - 多状态技能并存规则:同类是否叠加?建议先定为“同 ID 刷新层数/回合,不叠加多个实例”,避免状态爆炸。
- 状态与时间的关系:状态技能本身不耗时,但若它提供
battleTimeCost,战斗时用你之前定的max(1, 所有生效状态技能耗时),不要相加。 - 读档一致性:你说切楼/读档不影响,这没问题;前提是把状态剩余次数、来源技能、附加参数都存进
flags,否则会“看起来不影响、实际丢状态”。 - 死亡清空范围:你说“清除所有状态”很好,建议明确只清“临时战斗状态与挂起状态”,不清永久被动/装备效果。
你当前设定里“事件不打断当前状态”也可行,但再加一句更稳:
事件里如果显式调用了清状态事件,优先按事件脚本执行。
整体判断:没有大问题,剩下就是把“什么算一场战斗、何时扣层数、是否叠加”这三件事写死成规则文档即可。
User
按序进行:1、战斗成功结算后,如果失败按死亡计算。2、这些都视为1次战斗,非boss战都是如此,如果被不同位置的多怪捕捉,进行了多次战斗则逐一结算。3、部分技能可以,部分不行,后续会另行设计。4、状态技能恒定为timecost为0。5、就按读档一致性来。6、可以。
Cursor
可以,这 6 条现在已经是可实现且不冲突的一版规范了。按你给的顺序,我帮你固化成“最终判定口径”:
1只在战斗成功结算后扣状态次数;战斗失败直接走死亡流程,不做后续扣减流程。2普通战统一按“1次战斗”计;若由不同位置多怪捕捉触发了多场战斗,就逐场独立结算(每场都可触发状态-1)。3技能是否可叠加/共存留作后续分技能设计,当前框架兼容。4状态技能timeCost恒为0(开启/维持不推进地图时间)。5状态完整进入存档体系,读档后保持一致。6死亡时允许清状态(按你后续机制扩展)。
你这版下,系统核心就很清楚了:
地图时间推进只由非状态技能/移动/拾取/道具/战斗触发,状态技能只影响战斗效果与战后计数,不影响时间轴。
如果你愿意,下一步我可以给你一份“实现时的最小数据结构清单”(字段名级别,10行内),方便你直接落到 items/enemys/flags。
User
之前提到一个给我一份“精确到变量名和状态机的伪代码规范”,生成相应的计划和文本。
Cursor
我会给你一份可直接执行的计划:先把“地图时间/技能状态”的状态机与字段名规范化,再产出一份伪代码文本(按你已确认的6条规则固化),并附上实现落点清单。接下来我先生成计划供你确认。
User
Implement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself.
To-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos.
Cursor
我将按计划逐项落地,先完成规则冻结与优先级表,然后依次写数据模型、状态机伪代码、接入点映射和验收清单,最终产出 docs/map-turn-spec.md。先把第一个待办设为进行中并开始写文档。
我开始写规范文档正文,先固化规则优先级并连同变量名、状态机伪代码一次性写入,随后回填待办状态。
已按计划完整实现,并且所有待办都已依序完成。
已新增规范文档:
docs/map-turn-spec.md
文档内容已覆盖你要求的全部部分:
- 规则优先级冻结(按你确认的 1-6 条与补充口径固化)
- 统一字段命名(全部使用
timeCost)与运行时状态容器变量名 - 地图时间推进 + 技能状态的状态机(含 mermaid)
- 精确到函数名的伪代码(
consumeTime、resolveEnemyActions、settleBattleTimeCost、applyStatusAfterBattle、clearOnDeath) - 多怪捕捉逐场结算、Boss 单次接触特判
- 与现有工程函数的接入点映射(
moveOneStep/afterBattle/useItem/plugins) - 存读档一致性要求
- 最小验收清单(8 条)
如果你希望,我下一步可以基于这份规范再给一版“实施顺序最小改动清单”(先改哪 3 个函数最稳)。