效果引擎框架落地:事件发布订阅解释器+16 发射点埋点(T1——内容数据驱动扩展批)
把「内容的效果行为」从结算代码硬编码分支搬进数据行的第一步:通用解释器 EffectEngine 落地。 本提交=框架与埋点,双轨过渡期——发射点仅观测命中、施加面仍走旧消费点,行为与迁移前逐位一致 (冒烟 783 项 0 失败实证、单元 17 套全绿)。 新增 game/core/effect_engine.gd: - 事件词汇 16 个(任务书 §3.1 定稿)+扫雷侧事件集(流派扫雷触发铁律新形态) - 效果类型词汇 24 型(任务书 §3.3 定稿 19 行;标题计 23 系笔误按行内容全收,代拍板留痕) - fact 条件词汇 9 项+when 算子词汇封闭(mod_zero/advantageous/gte_ref/lt_ref/ends_with 系现状条件装进 9 项 fact 的查询面落法——代拍板白名单覆盖:掘进蓄能取模、淬毒克制正面、 深挖配额计数器比较、对某甲族泛条件) - 订阅装载 rebuild(每战 begin_battle 重建——中途获得下一战生效,同战内不热载; 装载序=feats 表序→treasure 表序→树规则合成行,写死保种子复放) - 计数器账本(counter_add/counter_spend/reset_at=round_start 重置点声明) - 组合规则六族(加法/乘法/任意真/按序改写/按序消耗/即时执行——发射点施加面引用) 16 发射点埋点(订阅空=零循环零扰动): - 战斗编排器:dig_reveal/mine_hit/flag_hit/flag_settle/round_end/enemy_hp_threshold/ treasure_used/damage_taken(kind=terrain)/round_start×2 - 伤害控制器:attack_commit(hero_attack 单算术前)/clamp_check×2(主角攻击+棋盘克制面)/ armor_roll/kill/damage_taken(kind=enemy_skill) - 行动经济:dig_paid(对价闸前)/charge_full(转蓄力满层沿——R6 框架先行,现无订阅者) - 地图战斗资源:reward_roll(摇号前权重合成层) 行为等价细节(本批预演、T3 落地): - 停手转存改记账制:request_stop 记剩余 AP 后清零,转存决策移至 round_end 发射点 finish_round_stop(grant_ap(mode:transfer) 订阅定夺;双轨期旧旗并行,缺省 RAP 转存不变) - attack_commit 发射点=管线算术前(载荷含 matchup/dig_streak/nest_count——连珠矢/执拗/ 猎巢人/淬毒订阅事实源) 设计权威:design/research/研究-内容数据驱动扩展架构-2026-10-06.md(案乙)。 任务书:design/research/任务书-内容数据驱动扩展批-2026-10-06.md §六 T1。
This commit is contained in:
1 parent
fc4dbde4f6
commit
7d157b3c4c
6 files changed
+442
-6
No files matched your search
@@ -48,6 +48,13 @@ var charge_layers: int:
|
||||
var _deep_paid_count := 0
|
||||
var deep_paid_count: int:
|
||||
get: return _deep_paid_count
|
||||
# 效果引擎(内容数据驱动扩展批 T1——dig_paid/charge_full 发射面;round_end 转存决策面)
|
||||
var _fx: EffectEngine = null
|
||||
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策移至 round_end 发射点——grant_ap(transfer) 订阅定夺)
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
_fx = fx
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
@@ -98,6 +105,10 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
if feat_dig_extra > 0 and _ap >= cost + feat_dig_extra:
|
||||
cost += feat_dig_extra
|
||||
_deep_paid_count += 1
|
||||
# dig_paid 发射点(内容数据驱动扩展批 T1——付费开掘发起、动作经济闸收对价前;
|
||||
# 深挖钻头 counter_spend 对价订阅面。双轨期对价仍走 feat_dig_extra 旧面)
|
||||
if _fx != null:
|
||||
_fx.dispatch("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost})
|
||||
if _ap < cost:
|
||||
_reject("resource_face")
|
||||
return false
|
||||
@@ -137,21 +148,35 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
|
||||
|
||||
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
|
||||
# 整备军备改写(迭代 12 T5——feat_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
|
||||
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)
|
||||
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)。
|
||||
# 内容数据驱动扩展批 T1:本方法改记账制——剩余 AP 存 _last_stop_ap 后清零,转存决策移至
|
||||
# round_end 发射点 finish_round_stop(grant_ap(mode:transfer) 订阅定夺;双轨期旧旗 feat_ap_carry
|
||||
# 并行生效,行为与迁移前逐位一致)。
|
||||
func request_stop() -> bool:
|
||||
if _phase != Phase.ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
if feat_ap_carry:
|
||||
_charge_layers = mini(3, _charge_layers + _ap)
|
||||
else:
|
||||
_rap += _ap
|
||||
_last_stop_ap = _ap
|
||||
_ap = 0
|
||||
_finish_round("stopped", "same_board")
|
||||
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
|
||||
return true
|
||||
|
||||
|
||||
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
|
||||
# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0
|
||||
# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
|
||||
func finish_round_stop(transfer: bool) -> void:
|
||||
if transfer:
|
||||
var prev_layers := _charge_layers
|
||||
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
|
||||
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
|
||||
_fx.dispatch("charge_full", {"layers": _charge_layers})
|
||||
else:
|
||||
_rap += _last_stop_ap
|
||||
_last_stop_ap = 0
|
||||
|
||||
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 =
|
||||
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
|
||||
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
|
||||
|
||||
@@ -84,6 +84,38 @@ var _feat_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1
|
||||
var _feat_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限)
|
||||
var _feat_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照)
|
||||
var _feat_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
|
||||
# 效果引擎(内容数据驱动扩展批 T1——发布订阅解释器;begin_battle 从持有集/装备集重建订阅。
|
||||
# 双轨过渡期:发射点 dispatch 仅观测命中(施加面仍走 feat_flags 旧消费点),T3 起逐点切换)
|
||||
var _fx: EffectEngine
|
||||
|
||||
|
||||
# 事件发射薄面(16 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
|
||||
func _fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
return []
|
||||
return _fx.dispatch(event, payload, Callable(self, "_fx_fact"))
|
||||
|
||||
|
||||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
|
||||
func _fx_fact(fact: String):
|
||||
match fact:
|
||||
"charge_layers":
|
||||
return action_economy.charge_layers
|
||||
"nest_count":
|
||||
var n := 0
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
n += 1
|
||||
return n
|
||||
"round_index":
|
||||
return round_index
|
||||
"flags_count":
|
||||
var hits_n := 0
|
||||
for fevt in _facts_flags:
|
||||
if bool(fevt.get("hit", false)):
|
||||
hits_n += 1
|
||||
return hits_n
|
||||
return null
|
||||
|
||||
|
||||
func _attune_has(item_id: String) -> bool:
|
||||
@@ -191,6 +223,7 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_fx = EffectEngine.new(db_ref)
|
||||
_board_generator = BoardGenerator.new()
|
||||
action_economy = ActionEconomy.new(db)
|
||||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||||
@@ -199,6 +232,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
_reward_economy = RewardEconomy.new(db, rng)
|
||||
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
|
||||
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
|
||||
_damage.set_fx(_fx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
|
||||
action_economy.set_fx(_fx) # dig_paid/charge_full 发射面
|
||||
_reward_economy.set_fx(_fx) # reward_roll 发射面
|
||||
minesweeper.cell_revealed.connect(_on_cell_revealed)
|
||||
minesweeper.flag_judged.connect(_on_flag_judged)
|
||||
minesweeper.round_end_board.connect(_on_round_end)
|
||||
@@ -267,6 +303,9 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
if tree_rules.has("posture_first_strike"):
|
||||
action_economy.queue_ap_delta(1)
|
||||
_damage.feat_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0
|
||||
# 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效;
|
||||
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放)
|
||||
_fx.rebuild((enc.get("feats", []) as Array), attunement, tree_rules)
|
||||
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
|
||||
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
|
||||
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||||
@@ -281,6 +320,7 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
return false
|
||||
round_index = 1
|
||||
_roll_round_actions() # 开轮摇号(敌人技能池批——首轮同样摇;先于 round_started,挖掘期动画位可读)
|
||||
_fx_emit("round_start", {"round": round_index})
|
||||
round_started.emit(round_index)
|
||||
return true
|
||||
|
||||
@@ -471,6 +511,10 @@ func _settle() -> void:
|
||||
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
|
||||
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
|
||||
continue
|
||||
# dig_reveal 发射点(内容数据驱动扩展批 T1——安全开格结算完成;载荷含首格事实与变异类型)
|
||||
_fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
|
||||
"is_first_dig_of_battle": not _feat_first_dig_used,
|
||||
"terrain_kind": (evt.get("variant_ids", []) as Array)})
|
||||
_grant_routed(_feat_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
|
||||
# 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支);
|
||||
# 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价)
|
||||
@@ -505,6 +549,9 @@ func _settle() -> void:
|
||||
elif evt.kind == "mine":
|
||||
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
|
||||
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
|
||||
# mine_hit 发射点(内容数据驱动扩展批 T1——踩雷时;减半/反打/连击清零订阅面)
|
||||
_fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
|
||||
"damage": int((snap as Dictionary).get("power", 0))})
|
||||
_grant_routed(_feat_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
|
||||
_feat_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
|
||||
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到
|
||||
@@ -521,6 +568,9 @@ func _settle() -> void:
|
||||
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
|
||||
int(vrow.get("effect_value", 0)), DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
|
||||
_submit(vt, false)
|
||||
# damage_taken 发射点(内容数据驱动扩展批 T1——kind=terrain;荆棘/地脉/熔火订阅面)
|
||||
_fx_emit("damage_taken", {"kind": "terrain", "amount": int(vrow.get("effect_value", 0)),
|
||||
"source": String(vid), "cell": evt.cell, "round": ri})
|
||||
# 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载):
|
||||
# ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源)
|
||||
if int(_damage.feat_flags.get("terrain_dig_double", 0)) > 0:
|
||||
@@ -541,6 +591,16 @@ func _settle() -> void:
|
||||
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
|
||||
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
|
||||
# 扫雷结算触发点=flag_strike/flag_all_hit/nest_aura 挂点承载红线①;伤害单据案C 暂存战斗段末兑付。
|
||||
# flag_settle 发射点(内容数据驱动扩展批 T1——轮末旗伤结算;战旗开单/完美布防订阅面)
|
||||
var fs_hits := 0
|
||||
var fs_misses := 0
|
||||
for fevt in _facts_flags:
|
||||
if bool(fevt.get("hit", false)):
|
||||
fs_hits += 1
|
||||
else:
|
||||
fs_misses += 1
|
||||
_fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0,
|
||||
"flags_count": fs_hits, "round": ri})
|
||||
if int(_damage.feat_flags.get("flag_strike_value", 0)) > 0:
|
||||
var hits := 0
|
||||
var misses := 0
|
||||
@@ -570,6 +630,13 @@ func _settle() -> void:
|
||||
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
|
||||
_submit(mg_t, false)
|
||||
_feat_magma_layers = 0
|
||||
# round_end 发射点(内容数据驱动扩展批 T1——每轮结算收口;熔火反打/整备转蓄力订阅面。
|
||||
# 转蓄力等价说明:停手侧 AP 已在 request_stop 记账(last_stop_ap),转存决策在本点由
|
||||
# grant_ap(mode:transfer) 订阅定夺——双轨期旧旗 feat_ap_carry 并行生效,无订阅走 RAP
|
||||
# 转存缺省,行为与迁移前逐位一致)
|
||||
_fx_emit("round_end", {"round": ri})
|
||||
action_economy.finish_round_stop(action_economy.feat_ap_carry
|
||||
or (_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer")))
|
||||
if _facts_path == "cleared":
|
||||
_grant_clear_reward(ri)
|
||||
_damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
|
||||
@@ -646,6 +713,8 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||||
func(c: Vector2i): return not board.is_revealed(c))
|
||||
if not bool(v.ok):
|
||||
return v # ② 拒绝零消耗
|
||||
# treasure_used 发射点(内容数据驱动扩展批 T1——使用主动秘宝受理面;掘进蓄能 counter_spend 订阅面)
|
||||
_fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target})
|
||||
# 掘进蓄能受理闸(迭代 11 同调批 T6——bank>0 才可发起,一行一点;积点不足拒绝零消耗)
|
||||
if String(v.item_id) == "dig_charge" and _attune_has("dig_charge"):
|
||||
if _attune_bank <= 0:
|
||||
@@ -840,6 +909,9 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
|
||||
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
|
||||
_flag_consumed[cell] = true
|
||||
_perfect_hit_true += 1
|
||||
# flag_hit 发射点(内容数据驱动扩展批 T1——标中雷兑付时;殉爆/中和/图谱订阅面)
|
||||
_fx_emit("flag_hit", {"cell": cell, "round": ri,
|
||||
"adjacent_terrain": (minesweeper.current_board().variant_ids(cell) if minesweeper.current_board() != null else [])})
|
||||
# 标中殉爆(迭代 11 同调批 T5——底稿 §3:每标中一面旗,全体存活敌吃 N 群伤;无类型走 blast 通道)
|
||||
if _attune_has("flag_burst"):
|
||||
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
|
||||
@@ -941,6 +1013,15 @@ func _roll_round_actions() -> void:
|
||||
_emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index,
|
||||
"%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
|
||||
0, {"skill_id": sid}, "skill_unlock")
|
||||
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
|
||||
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值)
|
||||
var pct_v := 0
|
||||
for pe in (u.get("pool", []) as Array):
|
||||
if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"):
|
||||
pct_v = int((pe as Dictionary).unlock_hp_pct)
|
||||
break
|
||||
_fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
|
||||
"is_boss": String(encounter.get("node_type", "")) == "boss"})
|
||||
var entries: Array = _enemy_system.rollable_skills(u)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array = _enemy_system.special_skills(u)
|
||||
@@ -1132,6 +1213,7 @@ func _exit_distribution() -> void:
|
||||
_refresh_used_this_round = 0
|
||||
round_index += 1
|
||||
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
|
||||
_fx_emit("round_start", {"round": round_index})
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(
|
||||
var _seq := 0 # 流水号
|
||||
var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废)
|
||||
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
|
||||
# 效果引擎(内容数据驱动扩展批 T1——attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面;
|
||||
# 双轨过渡期仅观测命中,施加面仍走 feat_flags 旧消费点)
|
||||
var _fx: EffectEngine = null
|
||||
# 特性求值旗(迭代 12 T2——begin_battle 编排器经 FeatsEngine 求值后注入;空字典=零扰动。
|
||||
# rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次
|
||||
# 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调)
|
||||
@@ -79,6 +82,26 @@ func set_hero_ac_override(v: int) -> void:
|
||||
_hero_ac_override = v
|
||||
|
||||
|
||||
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
_fx = fx
|
||||
|
||||
|
||||
# 事件发射薄面(发射点统一入口——订阅空=零循环零扰动)
|
||||
func _fx_dispatch(event: String, payload: Dictionary) -> void:
|
||||
if _fx != null:
|
||||
_fx.dispatch(event, payload)
|
||||
|
||||
|
||||
# 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面)
|
||||
func _nest_count() -> int:
|
||||
var n := 0
|
||||
for eff in effects.query_active("enemy"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# ---- 开单面(流程调用——编排器六调用点的唯一形态)----
|
||||
|
||||
# 单据构造(缺省填齐——调用点只写差异栏;context = kind 专用:enemy_skill 带 skill_id/baseline,
|
||||
@@ -196,8 +219,15 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
return
|
||||
var armor: String = String(target.get("armor_type", "light"))
|
||||
var atk_type: String = String(t.attack_type)
|
||||
var matchup := (atk_type + "_vs_" + armor) if atk_type != "" else ""
|
||||
# attack_commit 发射点(内容数据驱动扩展批 T1——我方伤害单据开立、管线算术前;载荷 atk/target/
|
||||
# matchup + 挖掘 streak 事实〔连珠矢/执拗订阅面〕+巢穴控制数〔猎巢人〕)
|
||||
_fx_dispatch("attack_commit", {"atk": int(t.base_value), "target": String(target.display_name),
|
||||
"matchup": matchup, "dig_streak": int(feat_flags.get("dig_streak", 0)), "nest_count": _nest_count()})
|
||||
var roll_read: Dictionary = _armor_roll()
|
||||
var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus)
|
||||
# armor_roll 发射点(内容数据驱动扩展批 T1——敌护甲掷骰判定;免掷恒折扣订阅面)
|
||||
_fx_dispatch("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
|
||||
# 矿工重锤·命中交换(迭代 12 T4 普攻流——正负双修原生形态:命中判定减成 trade_hit 点
|
||||
# 〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望)
|
||||
var trade_hit := int(feat_flags.get("hit_trade_hit", 0))
|
||||
@@ -219,6 +249,9 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
var shift := int(feat_flags.get("discount_shift", 0))
|
||||
if shift > 0 and armor == "light" and discount < 100:
|
||||
discount = _discount_shift_up(discount, shift)
|
||||
# clamp_check 发射点(内容数据驱动扩展批 T1——克制关系判定修正点;魔网/湛蓝/猎手订阅面)
|
||||
if atk_type != "":
|
||||
_fx_dispatch("clamp_check", {"matchup": matchup})
|
||||
var cdelta: int = _counter_delta(atk_type, armor) + _feat_clamp_bonus(atk_type, armor)
|
||||
# 淬毒刃纹(迭代 11 同调批 T5——底稿 §3:对被克制敌人的穿刺/魔法直伤 +N;条件 = 克制成立 ×
|
||||
# 攻击类型限定,主角攻击面专属——棋盘克制直伤走 _resolve_board_typed 不吃本加成)
|
||||
@@ -271,6 +304,9 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
payload["hero_rider"] = hero_rider
|
||||
units.apply_damage(target, landed)
|
||||
payload["killed"] = not target.alive # 击杀标记在扣血后取(原语义不变)
|
||||
# kill 发射点(内容数据驱动扩展批 T1——敌人死亡确认;贯穿射击 overflow 订阅面)
|
||||
if bool(payload.get("killed", false)):
|
||||
_fx_dispatch("kill", {"target": String(target.display_name), "overflow": landed})
|
||||
# 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent
|
||||
# +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。
|
||||
var crack := int(feat_flags.get("crack_value", 0))
|
||||
@@ -308,6 +344,9 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), 0)
|
||||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
|
||||
var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
|
||||
# damage_taken 发射点(内容数据驱动扩展批 T1——kind=enemy_skill;铁壁减伤订阅面)
|
||||
_fx_dispatch("damage_taken", {"kind": "enemy_skill", "amount": total,
|
||||
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)})
|
||||
# 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底)
|
||||
var shield := int(feat_flags.get("shield_per_layer", 0)) * int(feat_flags.get("charge_layers", 0))
|
||||
# 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」)
|
||||
@@ -337,7 +376,12 @@ func _resolve_board_typed(t: Dictionary) -> void:
|
||||
_ineffective_row(t, "无存活敌人")
|
||||
_outcomes[int(t.ticket_id)] = "zero"
|
||||
return
|
||||
var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light"))) + _feat_clamp_bonus(String(t.attack_type), String(target.get("armor_type", "light")))
|
||||
var armor_b: String = String(target.get("armor_type", "light"))
|
||||
var atk_b: String = String(t.attack_type)
|
||||
# clamp_check 发射点(内容数据驱动扩展批 T1——克制判定修正点棋盘面;与 hero_attack 同表同点)
|
||||
if atk_b != "":
|
||||
_fx_dispatch("clamp_check", {"matchup": atk_b + "_vs_" + armor_b})
|
||||
var delta: int = _counter_delta(atk_b, armor_b) + _feat_clamp_bonus(atk_b, armor_b)
|
||||
var dealt: int = DamageMath.counter_component(int(t.base_value), delta, int(db.settlement.damage_floor))
|
||||
units.apply_damage(target, dealt)
|
||||
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-10-06(迭代 17 卡⓪ 内容数据驱动扩展批 T1——效果引擎框架)
|
||||
# 内容数据驱动扩展(任务书 design/research/任务书-内容数据驱动扩展批-2026-10-06.md;设计权威
|
||||
# design/research/研究-内容数据驱动扩展架构-2026-10-06.md):把「内容的效果行为」从散布在结算
|
||||
# 代码里的硬编码分支搬进内容数据行三个字段——订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do)
|
||||
# ——由本解释器统一执行。完成后加一件「已有效果类型」的新专长/新秘宝=只改 .tres 数据行,零代码。
|
||||
# 架构模型(R1 裁决):发布订阅——结算管线同步 emit 事件(无队列无异步,确定性优先)→ 本引擎遍历
|
||||
# 持有订阅集匹配 → 返回按声明序排列的效果命中(do 列表),由发射点按组合规则施加。
|
||||
# 多订阅与冲突(R3 裁决):一事件多订阅=常态;冲突在效果类型层用组合规则封死(§3.4 六族);遍历序
|
||||
# =数据表声明序(feats 表序→treasure 表序→树规则合成行,装载时定死),写死保种子复放。
|
||||
# 重建时机(v1.1 补):每战 begin_battle 从持有集/装备集重建 trigger 列表——冒险中途获得/换取的
|
||||
# 专长下一战生效,同战内不热载,确定性优先。计数器账本随重建清零(battle 域)。
|
||||
# 确定性契约:全同步调用、零 RNG、零 signal 时序依赖;同输入同命中序。
|
||||
# 护栏保留:事件/效果/事实/算子四词表封闭(本文件常量),行内出域=加载期校验致命(schema 侧),
|
||||
# 运行期 dispatch 对未知事件/效果类型 fail-fast(push_error+断言)。
|
||||
class_name EffectEngine
|
||||
extends RefCounted
|
||||
|
||||
## 事件词汇(16 个发射点——任务书 §3.1 定稿;4 个登记暂不做事件不入本表)
|
||||
const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check",
|
||||
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle",
|
||||
"round_end", "reward_roll", "treasure_used", "charge_full"]
|
||||
|
||||
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
|
||||
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
|
||||
const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_settle", "round_end", "reward_roll"]
|
||||
|
||||
## 效果类型词汇(解释器 switch 闭集——任务书 §3.3 定稿 19 行;类型实数 24,含共享行拆型:
|
||||
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
|
||||
## bias_rewards/preview。任务书标题计 23 系笔误,落码按行内容全收——commit 留痕)
|
||||
const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket",
|
||||
"attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material",
|
||||
"grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add",
|
||||
"counter_spend", "rewrite_board", "reveal_tiles", "remove_mines", "refresh_pick",
|
||||
"bias_rewards", "preview"]
|
||||
|
||||
## fact 条件词汇(封闭枚举 9 项——任务书 §四定稿)
|
||||
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
|
||||
"flags_count", "charge_layers", "nest_count", "round_index", "counter"]
|
||||
|
||||
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
|
||||
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
|
||||
## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法,
|
||||
## 代拍板白名单覆盖,commit 留痕)
|
||||
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
|
||||
"advantageous", "gte_ref", "lt_ref"]
|
||||
|
||||
## when 条件键白名单(trigger 行 when 数组元素键)
|
||||
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
|
||||
|
||||
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面)
|
||||
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
|
||||
"cap", "cap_from", "per", "divisor", "dtype", "mode", "channel", "counter_id", "reset_at",
|
||||
"set", "status_id", "label", "fallback", "target_kinds"]
|
||||
|
||||
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
|
||||
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
|
||||
|
||||
# ---- 订阅集与计数器账本 ----
|
||||
var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死)
|
||||
var _counters: Dictionary = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明)
|
||||
var _reset_on_round_start: Array[String] = [] # round_start 重置点计数器名单(装载时收集)
|
||||
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb = null) -> void:
|
||||
db = db_ref
|
||||
|
||||
|
||||
## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备
|
||||
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器账本同步清零并按订阅行
|
||||
## 声明收集重置点(round_start)。
|
||||
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
|
||||
_triggers.clear()
|
||||
_counters.clear()
|
||||
_reset_on_round_start.clear()
|
||||
if db != null and db.feats != null:
|
||||
for row in db.feats.rows:
|
||||
var fid := String(row.get("feat_id", ""))
|
||||
if not feat_ids.has(fid):
|
||||
continue
|
||||
for tr in (row.get("triggers", []) as Array):
|
||||
_push_trigger("feat", fid, tr)
|
||||
if db != null:
|
||||
for row in db.treasure_attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if not item_ids.has(iid):
|
||||
continue
|
||||
for tr in (row.get("triggers", []) as Array):
|
||||
_push_trigger("attune", iid, tr)
|
||||
# 树规则合成订阅(T5——规则键效果走同面:非数据行,词表同闭集)
|
||||
for key_v in tree_rules:
|
||||
var rk := String(key_v)
|
||||
match rk:
|
||||
"posture_first_strike":
|
||||
_push_trigger("tree", rk, {"on": "round_start",
|
||||
"when": [{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}})
|
||||
"posture_bulwark":
|
||||
_push_trigger("tree", rk, {"on": "damage_taken",
|
||||
"do": {"kind": "shield", "value": 1}})
|
||||
|
||||
|
||||
func _push_trigger(source: String, id: String, tr: Dictionary) -> void:
|
||||
var on := String(tr.get("on", ""))
|
||||
assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id])
|
||||
var t := {"source": source, "id": id, "on": on,
|
||||
"when": (tr.get("when", []) as Array).duplicate(true),
|
||||
"do": (tr.get("do", {}) as Dictionary).duplicate(true)}
|
||||
_triggers.append(t)
|
||||
# 计数器重置点收集(counter_add/counter_spend 行声明 reset_at)
|
||||
var do_d: Dictionary = t.do
|
||||
if do_d.has("counter_id") and String(do_d.get("reset_at", "")) == "round_start":
|
||||
var cid := String(do_d.counter_id)
|
||||
if not _reset_on_round_start.has(cid):
|
||||
_reset_on_round_start.append(cid)
|
||||
|
||||
|
||||
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存)
|
||||
func has_effect_on(event: String, kind: String, mode: String = "") -> bool:
|
||||
for tr in _triggers:
|
||||
if String(tr.on) != event:
|
||||
continue
|
||||
var d: Dictionary = tr.do
|
||||
if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode):
|
||||
# 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。
|
||||
## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(charge_layers/nest_count 等
|
||||
## 运行态读数 Callable——payload 未带时回落)。
|
||||
func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) -> Array:
|
||||
assert(EVENTS.has(event), "[效果引擎] 未知事件: %s(发射点出域)" % event)
|
||||
if event == "round_start":
|
||||
for cid in _reset_on_round_start:
|
||||
_counters[cid] = 0
|
||||
var hits: Array = []
|
||||
for tr in _triggers:
|
||||
if String(tr.on) != event:
|
||||
continue
|
||||
if not _when_ok(tr.when, payload, facts):
|
||||
continue
|
||||
hits.append(tr.do)
|
||||
return hits
|
||||
|
||||
|
||||
## 计数器读数(fact counter 与发射点配额面共用)
|
||||
func counter(counter_id: String) -> int:
|
||||
return int(_counters.get(counter_id, 0))
|
||||
|
||||
|
||||
func counter_add(counter_id: String, value: int, set_mode: bool = false) -> void:
|
||||
if set_mode:
|
||||
_counters[counter_id] = value
|
||||
else:
|
||||
_counters[counter_id] = int(_counters.get(counter_id, 0)) + value
|
||||
|
||||
|
||||
## 计数器消耗(counter_spend 组合规则——按订阅序消耗共享资源,耗尽即止):
|
||||
## 返回 true=消耗成功;false=资源不足(调用方按 reason 通道拒绝)。
|
||||
func counter_spend(counter_id: String, value: int) -> bool:
|
||||
var cur := int(_counters.get(counter_id, 0))
|
||||
if cur < value:
|
||||
return false
|
||||
_counters[counter_id] = cur - value
|
||||
return true
|
||||
|
||||
|
||||
# ---- when 判定(条件谓词——零随机全确定) ----
|
||||
|
||||
func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool:
|
||||
for c_v in when:
|
||||
if not _cond_ok(c_v as Dictionary, payload, facts):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
|
||||
var fact := String(c.get("fact", ""))
|
||||
var op := String(c.get("op", "eq"))
|
||||
var val = c.get("value", null)
|
||||
var actual = _fact_value(fact, c, payload, facts)
|
||||
match op:
|
||||
"eq":
|
||||
return str(actual) == str(val)
|
||||
"neq":
|
||||
return str(actual) != str(val)
|
||||
"gt":
|
||||
return int(actual) > int(val)
|
||||
"gte":
|
||||
return int(actual) >= int(val)
|
||||
"lt":
|
||||
return int(actual) < int(val)
|
||||
"lte":
|
||||
return int(actual) <= int(val)
|
||||
"ends_with":
|
||||
if val is Array:
|
||||
for v in (val as Array):
|
||||
if String(actual).ends_with(String(v)):
|
||||
return true
|
||||
return false
|
||||
return String(actual).ends_with(String(val))
|
||||
"has":
|
||||
if actual is Array:
|
||||
return (actual as Array).has(val)
|
||||
return false
|
||||
"mod_zero":
|
||||
var m := int(val)
|
||||
return m > 0 and int(actual) % m == 0
|
||||
"advantageous":
|
||||
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
|
||||
return _counter_delta_of(String(actual)) > 0
|
||||
"gte_ref":
|
||||
return int(actual) >= counter(String(c.get("ref", "")))
|
||||
"lt_ref":
|
||||
return int(actual) < counter(String(c.get("ref", "")))
|
||||
assert(false, "[效果引擎] when 算子出域: %s" % op)
|
||||
return false
|
||||
|
||||
|
||||
func _fact_value(fact: String, c: Dictionary, payload: Dictionary, facts: Callable):
|
||||
match fact:
|
||||
"counter":
|
||||
return counter(String(c.get("id", "")))
|
||||
_:
|
||||
# fact 取值序:payload 同名字段优先 → 补充事实面(发射点注入的运行态读数)
|
||||
if payload.has(fact):
|
||||
return payload[fact]
|
||||
if facts.is_valid():
|
||||
return facts.call(fact)
|
||||
return null
|
||||
|
||||
|
||||
## 克制矩阵读值(advantageous 算子消费——与伤害控制器 _counter_delta 同源同表)
|
||||
func _counter_delta_of(matchup: String) -> int:
|
||||
if db == null or matchup == "" or not matchup.contains("_vs_"):
|
||||
return 0
|
||||
var parts := matchup.split("_vs_")
|
||||
if parts.size() != 2:
|
||||
return 0
|
||||
var tbl: Dictionary = db.settlement.armor_effectiveness
|
||||
var row: Dictionary = tbl.get(String(parts[0]), {})
|
||||
return int(row.get(String(parts[1]), 0))
|
||||
|
||||
|
||||
# ---- 组合规则(任务书 §3.4 六族——效果类型的必填属性,发射点施加面引用) ----
|
||||
|
||||
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、
|
||||
## counter_add、shield——发射点逐条累加收集器。
|
||||
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
|
||||
## 任意真族(任一命中即生效):spawn_spell、overflow、spike。
|
||||
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
|
||||
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
|
||||
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、
|
||||
## rewrite_board、apply_status——发射点逐条执行。
|
||||
static func combo_of(kind: String) -> String:
|
||||
match kind:
|
||||
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield":
|
||||
return "additive"
|
||||
"grant_ap":
|
||||
return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法)
|
||||
"yield_mult", "damage_mult":
|
||||
return "multiplicative"
|
||||
"spawn_spell", "overflow", "spike":
|
||||
return "any_true"
|
||||
"modify_ticket":
|
||||
return "ordered_write"
|
||||
"counter_spend":
|
||||
return "ordered_spend"
|
||||
"open_ticket", "rewrite_board", "apply_status":
|
||||
return "immediate"
|
||||
return "immediate" # 默认即时执行(每条命中各自执行一次)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgo4b5frip3e5
|
||||
@@ -23,6 +23,12 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返
|
||||
var _clear_identity := ""
|
||||
var _clear_granted := false
|
||||
var _clear_snapshot := {}
|
||||
# 效果引擎(内容数据驱动扩展批 T1——reward_roll 发射面;偏运硬币 bias_rewards 订阅面)
|
||||
var _fx: EffectEngine = null
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
_fx = fx
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -59,6 +65,10 @@ func roll_board(eligible: Array) -> bool:
|
||||
_clear_granted = false
|
||||
_clear_snapshot = {}
|
||||
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
|
||||
# reward_roll 发射点(内容数据驱动扩展批 T1——格面资源掷定、摇号前权重合成层;
|
||||
# 偏运硬币 bias_rewards 订阅面。双轨期权重投影仍走 luck_bias 旧面,发射仅观测)
|
||||
if _fx != null:
|
||||
_fx.dispatch("reward_roll", {"polarity": "positive", "cell_count": list.size()})
|
||||
var cell_rows: Array = _biased_cell_rows() # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
|
||||
var clear_rows: Array = _pool_rows("clear_default")
|
||||
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
|
||||
|
||||
Reference in new issue
Block a user