feat(fx): 迭代20 T1 效果引擎词表扩签+发射点施加面(专长实装批 A段一)

词表:EVENTS 16→18(+dig_stop/battle_won)、FACTS 9→17(8 新事实含 hp_ratio 生命比例前置兑付)、
EFFECT_KINDS +7 新类型(heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/
target_attack_bonus)、OPS +mod_eq;DO_KEYS +4 键(value_event/value_counter/domain/axis)。
rewrite_board 暂留至 T3 删行同批除名(purge_sapper 仍在表),消费分支已摘除。

施加面:battle_orchestrator(dig_reveal 三事实/mine_hit 五新施加/terrain 减伤前置/flag_hit 四新/
round_end GP+AP 队列/battle_won 发射/bonus_delta 战末汇入/ac_bonus 累积器/揭示队列_.flush_pending_reveals)
+damage_controller(attack value_counter rider/敌技 damage_reduce/hp_ratio fact/kill 揭示回调)
+action_economy(dig_paid 返还/dig_stop 发射/上轮耗尽旗标)+minesweeper fx_reveal 效果通道
(只揭真格信息红线)+rng fx_reveal 分签。

T6 承载:EffectEngine adventure 域计数器(domain 键/counters_export/counters_seed)+
AdventureRunner.adventure_counters 共享引用(attune_charges 先例)+断点随档。

测试:effect_words_t1_test 新套(V1:新词在册/出域四路被拒/seed-export 往返/mod_eq 预评估口径)
入 run_all 19 套全绿;冒烟 846/0 持平基线零破坏。
This commit is contained in:
DKChan committed 2026-10-11 22:42:51 +08:00
1 parent 5dbcae227e
commit a57489e970
10 files changed
+506 -69

No files matched your search

+32 -7
View File
@@ -44,6 +44,7 @@ var charge_layers: int:
# 效果引擎(内容数据驱动扩展批 T1——dig_paid/charge_full 发射面;round_end 转存决策面)
var _fx: EffectEngine = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策移至 round_end 发射点——grant_ap(transfer) 订阅定夺)
var _ap_exhausted_last := false # 上轮 AP 耗尽旗(迭代 20 T1——一滴不剩 round_start when 面)
func set_fx(fx: EffectEngine) -> void:
@@ -97,18 +98,27 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
return false
var cost := _cost("dig")
# dig_paid 发射+施加(内容数据驱动扩展批 T3——付费开掘发起、动作经济闸收对价前:
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价)
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
# 迭代 20 T1——counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免,
# mod_eq 闸在行 when〕同面施加)
var dig_refund := 0
if _fx != null:
for h in _fx.dispatch("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
if String(h.get("kind", "")) != "counter_spend":
continue
var extra := int(h.get("value", 1))
if _ap >= cost + extra:
cost += extra
_fx.counter_add(String(h.get("counter_id", "")), 1)
if String(h.get("kind", "")) == "counter_spend":
var extra := int(h.get("value", 1))
if _ap >= cost + extra:
cost += extra
_fx.counter_add(String(h.get("counter_id", "")), 1)
elif String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
elif String(h.get("kind", "")) == "grant_ap":
dig_refund += int(h.get("value", 0))
if _ap < cost:
_reject("resource_face")
return false
if dig_refund > 0:
cost = maxi(0, cost - dig_refund) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
return _spend_cost_and_arbitrate(cost, clears_map)
@@ -153,6 +163,15 @@ func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
# dig_stop 发射+施加(迭代 20 T1——停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
if _fx != null:
for h in _fx.dispatch("dig_stop", {"ap_left": _ap}):
if String(h.get("kind", "")) == "counter_add":
var val := int(h.get("value", 0))
if String(h.get("value_event", "")) == "ap_left":
val = _ap
_fx.counter_add(String(h.get("counter_id", "")), val, h.has("set"))
_last_stop_ap = _ap
_ap = 0
_finish_round("stopped", "same_board")
@@ -249,9 +268,15 @@ func _finish_round(path_tag: String, continuation_hint: String) -> void:
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
_ap_exhausted_last = path_tag == "ap_exhausted" # 迭代 20 T1——一滴不剩 when 面(轮始发射读数)
_phase = Phase.WAITING
# 上轮 AP 是否耗尽(迭代 20 T1——round_start 发射 payload 读数面;轮始兑现后保持到本轮末)
func ap_exhausted_last_round() -> bool:
return _ap_exhausted_last
func _reject(source: String) -> void:
if source == "phase":
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source)
+10 -1
View File
@@ -77,6 +77,7 @@ var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档)
var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档)
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
# ---- 三选一停点态(迭代 10 奖励重构批)----
@@ -217,6 +218,10 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary
_feat_ticket_pity = int(bp.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
_feat_ticket_sold = int(bp.get("feat_ticket_sold", 0))
# 迭代 20 T6/T7——adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先)
var bp_counters = bp.get("adventure_counters", null)
if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true)
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty():
_pending_next_level = true
@@ -263,7 +268,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
return _start_current_level()
# 三选一停点态归零(冒险开始/结算收束共用;令牌池按表配额重建——池消耗即上限,账本随池重生)
# 三选一停点态归零(冒险开始/结算收束共用;令牌池按表配额重建——池消耗即上限,账本随池重生。
# 迭代 20 T6——adventure 域计数器账本随冒险开始清零;resume 路径调用后由断点附随件回填)
func _reset_curio_state() -> void:
_curio_taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
_curio_deck = _build_curio_deck()
@@ -273,6 +279,7 @@ func _reset_curio_state() -> void:
_bonus_treasure_carry = []
_keystone_drops = []
_node_curio_pending = {}
adventure_counters = {}
# 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = CURIO_KINDS×TIERS 逐层,
@@ -939,6 +946,7 @@ func _start_current_level() -> bool:
if _shop_mode == "pool":
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动)
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"))
_runner.set_adventure_counters(adventure_counters) # 迭代 20 T6——adventure 域计数器共享引用下传
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
@@ -1087,6 +1095,7 @@ func _snapshot_breakpoint() -> void:
"bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
"feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0)
"feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上)
"adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢)
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"bonus_aux_carry": _bonus_treasure_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失)
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
+230 -41
View File
@@ -76,6 +76,16 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键
var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本)
# ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)----
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零)
var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零)
var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写)
var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账)
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末并入 _fx_bonus_ref 后清零)
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
var _fx_pending_reveals: Array[Dictionary] = [] # 揭示效果待兑队列(见血开窍/斩首学者/地质学家/跳格采掘——授予段末统一兑付,防轮 facts 迭代中追加重入)
var _fx_revealing := false # fx 揭示兑付中标记(_on_cell_revealed 缓冲旁路——fx 揭示的奖励由兑付点直接入账,防二次缓冲二次兑付)
# 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
var _feat_first_dig_used := false
@@ -84,7 +94,7 @@ var _feat_first_dig_used := false
var _fx: EffectEngine
# 事件发射薄面(16 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
# 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
func _fx_emit(event: String, payload: Dictionary) -> Array:
if _fx == null:
return []
@@ -92,13 +102,73 @@ func _fx_emit(event: String, payload: Dictionary) -> Array:
# round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换
# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮)
# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮。
# 迭代 20 T1——payload 扩 ap_exhausted_last〔一滴不剩 when 面〕+ac_bonus 本轮累积器清零)
func _emit_round_start() -> void:
for h in _fx_emit("round_start", {"round": round_index}):
_fx_ac_round = 0
_sync_hero_ac()
for h in _fx_emit("round_start", {"round": round_index,
"ap_exhausted_last": action_economy.ap_exhausted_last_round()}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
# ac_bonus 累积后同步主角 AC(石肤/封印桩消费面——defender_ac 经 set_hero_ac_override 单点;
# 基线 = enc 口径,-1(表值兜底态)时解析为表值后叠加,守恒口径与 begin_battle 注入一致)
func _sync_hero_ac() -> void:
if _fx_hero_ac_base < 0:
_fx_hero_ac_base = int(db.hero.armor_class)
_damage.set_hero_ac_override(_fx_hero_ac_base + _fx_ac_round + _fx_ac_battle)
# ac_bonus 施加(迭代 20 T1——mode=round 本轮累积器〔封印桩,round_start 清零〕/
# mode=battle 本战累积器〔石肤,cap 封顶;缺省 battle〕;cap=0/缺省=无上限)
func _apply_ac_bonus(h: Dictionary) -> void:
var mode := String(h.get("mode", "battle"))
var cap := int(h.get("cap", 0))
if mode == "round":
_fx_ac_round += int(h.get("value", 0))
else:
_fx_ac_battle = _fx_ac_battle + int(h.get("value", 0)) if cap <= 0 else mini(cap, _fx_ac_battle + int(h.get("value", 0)))
_sync_hero_ac()
# 揭示效果入队(fx_reveal 掷格兑付延后到授予段末——防轮 facts 迭代中追加重入;见血开窍/
# 斩首学者〔经伤害控制器 fx_reveal_request 回调〕/地质学家/跳格采掘共用)
func _fx_enqueue_reveal() -> void:
_fx_pending_reveals.append({"round": round_index})
# 揭示效果兑付(授予段末统一——fx_reveal 封闭流从未揭示安全格掷 1 格全揭示;
# 信息红线=只揭真格〔雷格不入候选〕;空候选=零兑付留归因行;揭示奖励经 _grant_routed
# 正向路由直接入账——_fx_revealing 旁路 _on_cell_revealed 缓冲防二次兑付)
func _flush_pending_reveals(ri: int) -> void:
if _fx_pending_reveals.is_empty():
return
var board: BoardData = minesweeper.current_board()
while not _fx_pending_reveals.is_empty():
_fx_pending_reveals.pop_back()
var cands: Array = []
for c in board.all_cells():
if not minesweeper.is_revealed(c) and not board.is_mine(c):
cands.append(c)
if cands.is_empty():
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "")
continue
var rg: RandomNumberGenerator = rng.open_stream("fx_reveal")
var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)]
_fx_revealing = true
var ok: bool = minesweeper.fx_reveal(cell)
_fx_revealing = false
if not ok:
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "")
continue
var snap = _reward_economy.query_full_snapshot(cell)
if snap is Dictionary:
_grant_routed(snap, "positive", "fx_reveal", cell, ri, "fx_reveal")
_emit_row("fx_reveal", "", cell, ri, "专长揭示", 0, {}, "fx_reveal")
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
func _fx_fact(fact: String):
match fact:
@@ -118,6 +188,10 @@ func _fx_fact(fact: String):
if bool(fevt.get("hit", false)):
hits_n += 1
return hits_n
"hp_ratio":
var hu: Dictionary = _enemy_system.hero_unit()
var hmax := maxi(1, int(hu.get("max_hp", 1)))
return float(int(hu.get("hp", 0))) / float(hmax)
return null
@@ -238,6 +312,19 @@ func begin_battle(enc: Dictionary) -> bool:
# modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入)
# 特性运行态(内容数据驱动扩展批 T3——enc.feats 携带持有集:效果行为=数据行 triggers 经效果
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
# 迭代 20 T1——扩签运行态就位:AC 基线快照、累积器清零、adventure 计数器/加算区共享引用接收
_fx_hero_ac_base = int(enc.get("hero_armor_class", -1))
_fx_ac_round = 0
_fx_ac_battle = 0
_fx_new_resource_types = {}
_fx_pending_reveals = []
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
var bonus_ref = enc.get("bonus_ref", null)
_fx_bonus_ref = bonus_ref if bonus_ref is Dictionary else {}
var counters_ref = enc.get("adventure_counters", null)
_fx_adventure_counters_ref = counters_ref if counters_ref is Dictionary else {}
_damage.fx_reveal_request = Callable(self, "_fx_enqueue_reveal")
_damage.fx_ac_request = Callable(self, "_apply_ac_bonus")
var feat_ids: Array = enc.get("feats", []) as Array
if not feat_ids.is_empty() and db.feats != null:
var tiers: Dictionary = FeatsEngine.tiers_for(feat_ids, db.feats)
@@ -253,8 +340,10 @@ func begin_battle(enc: Dictionary) -> bool:
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
var tree_rules: Array = enc.get("tree_rules", []) as Array
# 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效;
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放)
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。
# 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕)
_fx.rebuild(feat_ids, attunement, tree_rules)
_fx.counters_seed(_fx_adventure_counters_ref)
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
@@ -413,7 +502,9 @@ func _on_cell_revealed(evt: Dictionary) -> void:
# 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序:
# cell_revealed 批 → flag_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实;
# initial 通道事件(install 初始展开——封锁格)resource=false,进入授予段被资格检查自然跳过。
if battle_result == "":
# fx_reveal 通道事件旁路缓冲(迭代 20 T1——专长揭示的奖励由 _flush_pending_reveals 兑付点
# 直接入账,此处缓冲会导致二次兑付)。
if battle_result == "" and not _fx_revealing:
if bool(evt.get("resource", false)):
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
# 防旧图已挖格在轮末错拿新图同坐标身份)。雷格同样携带(雷安同池——负向兑付的来源格)
@@ -463,10 +554,22 @@ func _settle() -> void:
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
# dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢)
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。
# 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
var board_b: BoardData = minesweeper.current_board()
var snap_pre = evt.get("reward_snapshot")
var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else ""
var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype)
if is_new_type:
_fx_new_resource_types[ctype] = true
var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not _feat_first_dig_used,
"terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array)})
"terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
"is_blank_zero": board_b.digit_of(evt.cell) == 0,
"new_resource_type": is_new_type})
var pos_m := 0
var neg_m := 0
var combo_cap := -1
@@ -483,6 +586,10 @@ func _settle() -> void:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末
# _flush_pending_reveals 统一掷格兑付)
if not _hits_of(dr_hits, "reveal_tiles").is_empty():
_fx_enqueue_reveal()
# 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行)
for h in dr_hits:
if String(h.get("kind", "")) == "grant_material":
@@ -510,17 +617,45 @@ func _settle() -> void:
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
# mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器)
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。
# 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面:
# damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/
# blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕)
var mine_power := int((snap as Dictionary).get("power", 0))
var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": int((snap as Dictionary).get("power", 0))})
"damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)),
"hp_ratio": float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))})
var halve_pct := 0
var mine_reduce := 0
for h in mh_hits:
if String(h.get("kind", "")) == "damage_mult":
halve_pct += int(h.get("value", 0))
match String(h.get("kind", "")):
"damage_mult":
halve_pct += int(h.get("value", 0))
"damage_reduce":
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免
else:
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
"heal":
var hu_heal: Dictionary = _enemy_system.hero_unit()
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
"bonus_delta":
var axis := String(h.get("axis", ""))
if _fx_bonus_delta.has(axis):
_fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1))))
var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board",
blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
_submit(bs_t, false)
if mine_reduce > 0:
snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
for h in mh_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
if not _hits_of(mh_hits, "reveal_tiles").is_empty():
_fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
_grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct)
for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
@@ -538,22 +673,30 @@ func _settle() -> void:
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内
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(kind=terrain) 发射+施加(内容数据驱动扩展批 T3——荆棘/地脉 grant_material
# 即时执行 + 熔火积层 counter_add;行序随订阅声明序〔ley 表序在 thorn 前〕,兑付值逐位一致)
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": int(vrow.get("effect_value", 0)),
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid)})
# 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
# grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化)
var dt_amount := int(vrow.get("effect_value", 0))
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
"hp_ratio": float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))})
for h in dt_hits:
match String(h.get("kind", "")):
"grant_material":
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
"damage_reduce":
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
"ac_bonus":
_apply_ac_bonus(h)
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
_submit(vt, false)
for h in dt_hits:
if String(h.get("kind", "")) == "grant_material":
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
for evt in _facts_flags:
_judge_flag(evt, ri)
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
@@ -607,9 +750,18 @@ func _settle() -> void:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/
# grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕
for h in re_fx:
if String(h.get("kind", "")) == "grant_gp":
escrow += int(h.get("value", 0))
_emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "")
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"))
if _facts_path == "cleared":
_grant_clear_reward(ri)
_flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
_damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
_emit_composition_effects("hero", ri)
@@ -912,29 +1064,43 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri,
"terrain_kind": _harmful_adjacent(cell)})
# 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道)
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。
# 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast)
for h in _hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast":
continue
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
_submit(fbt, false)
# 净化工兵(迭代 12 T6 吞噬流——标中旗中和相邻伤害型地形格):🔴 R4 甲永不对冲边界=
# **仅限伤害型**(thorn/magma/rockfall——variation 表 effect=flat_damage 判据),信息型
# (hide_number 族)永不清除(红线①看门税不可对冲);中和=变普通格(BoardData.clear_variants)。
if not _hits_of(fh_hits, "rewrite_board").is_empty():
var board: BoardData = minesweeper.current_board()
for n in board.neighbors8(cell):
var vids_n: Array = board.variant_ids(n)
var harmful := false
for vid_n in vids_n:
var vrow_n: Dictionary = db.variation.row_by_id(String(vid_n))
if String(vrow_n.get("effect", "")) == "flat_damage":
harmful = true
break
if harmful:
board.clear_variants(n)
_emit_row("terrain_neutralized", "feat_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
# 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/
# ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/
# remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。
# (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕)
for h in fh_hits:
match String(h.get("kind", "")):
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
"ac_bonus":
_apply_ac_bonus(h)
"bonus_delta":
var axis_f := String(h.get("axis", ""))
if _fx_bonus_delta.has(axis_f):
_fx_bonus_delta[axis_f] += int(h.get("delta", 0))
"remove_mines":
var removed: Array = minesweeper.remove_mines_in([cell])
if not removed.is_empty():
_emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "")
"preview_area":
var board_p: BoardData = minesweeper.current_board()
var types: Array = []
for n in board_p.neighbors8(cell):
var n_snap = _reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if treasure_inventory != null and not types.is_empty():
treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
_emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
for h in fh_hits:
@@ -1023,8 +1189,13 @@ func _roll_round_actions() -> void:
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,
# 迭代 20 T1——target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := _fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(encounter.get("node_type", "")) == "boss"})
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array = _enemy_system.rollable_skills(u)
if _enemy_system.basic_streak(u) >= pity_m:
var specials: Array = _enemy_system.special_skills(u)
@@ -1243,6 +1414,24 @@ func player_request_refresh() -> bool:
func _battle_over(result: String) -> void:
battle_result = result
# battle_won 发射+施加(迭代 20 T1——战地包扎 heal 订阅面;败北不发射。heal=英雄单位回血
# 〔封顶单位上限〕,战后生命经 hero_hp_left 回写冒险账本——时点在捕获前=回血计入带入下一层)
if result == "victory":
for h in _fx_emit("battle_won", {"round": round_index}):
if String(h.get("kind", "")) == "heal":
var hu_w: Dictionary = _enemy_system.hero_unit()
hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1))))
# bonus_delta 战末汇入(迭代 20 T1——血税双向账:本战累积并入冒险加算区共享引用后清零)+
# adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例)
if _fx_bonus_ref != null and not _fx_bonus_ref.is_empty():
for axis in _fx_bonus_delta.keys():
if _fx_bonus_ref.has(axis):
_fx_bonus_ref[axis] = int(_fx_bonus_ref.get(axis, 0)) + int(_fx_bonus_delta[axis])
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
if _fx != null and not _fx_adventure_counters_ref.is_empty():
var exported: Dictionary = _fx.counters_export()
for cid in exported.keys():
_fx_adventure_counters_ref[cid] = int(exported[cid])
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
var gp_dropped := _roll_gp_drop() # 战斗掉落(GP+商店房实施批——escrow 随此值上抛即弃)
+41 -6
View File
@@ -102,16 +102,25 @@ func _fx_dispatch(event: String, payload: Dictionary) -> void:
_fx_hits(event, payload)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
# 迭代 20 T1——hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
# fx_reveal_request=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
func _fx_fact(fact: String):
match fact:
"charge_layers":
return charge_layers
"dig_streak":
return dig_streak
"hp_ratio":
var hu: Dictionary = units.hero_unit()
return float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))
return null
var fx_reveal_request: Callable = Callable() # 揭示效果回调(编排器注入——kill 发射点 reveal_tiles 订阅面)
var fx_ac_request: Callable = Callable() # ac_bonus 回调(编排器注入——石肤 AC 累积单点同步)
# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/
# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶)
func _per_value(name: String, payload: Dictionary) -> int:
@@ -317,14 +326,27 @@ func _resolve_hero_attack(t: Dictionary) -> void:
# modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享,
# 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致)
var base_value := float(t.base_value)
# 单位标记加伤(迭代 20 T1——处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键;
# 阈值沿发射点已写入,本处通用消费——对该敌伤害加算)
base_value += float(int(target.get("fx_atk_bonus", 0)))
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0:
base_value *= 1.0 + float(int(h.trade_atk)) / 100.0
# 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers
# 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers。
# 迭代 20 T1——value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面,
# 值读于施加时点=命中序先行 rider 后清零行〕
for h in ac_hits:
if String(h.get("kind", "")) == "attack_bonus":
base_value += float(int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload))
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
contrib = _fx.counter(String(h.get("value_counter", "")))
base_value += float(contrib)
# 计数器施加(迭代 20 T1——attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
for h in ac_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
if spike_ready:
for h in ac_hits:
@@ -362,6 +384,9 @@ func _resolve_hero_attack(t: Dictionary) -> void:
# 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve)
if bool(payload.get("killed", false)):
for h in _fx_hits("kill", {"target": String(target.display_name), "overflow": landed}):
if String(h.get("kind", "")) == "reveal_tiles" and fx_reveal_request.is_valid():
fx_reveal_request.call() # 斩首学者——击杀揭示 1 格(编排器入队,授予段末兑付)
continue
if String(h.get("kind", "")) != "overflow":
continue
var next_target = units.default_target()
@@ -399,12 +424,22 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
# damage_taken 发射+施加(内容数据驱动扩展批 T3——kind=enemy_skill;铁壁整备减伤订阅面:
# shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅);
# 减后过下限保底)
# 减后过下限保底。迭代 20 T1——damage_reduce〔背水 hp_ratio when〕/counter_add〔保险丝/
# 石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
var shield := 0
for h in _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total,
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)}):
if String(h.get("kind", "")) == "shield":
shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {})
match String(h.get("kind", "")):
"shield":
shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {})
"damage_reduce":
total = maxi(0, total - int(h.get("value", 0)))
"ac_bonus":
if fx_ac_request.is_valid():
fx_ac_request.call(h) # 石肤 AC 累积归编排器单点(set_hero_ac_override 消费面)
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
if shield > 0:
total = maxi(int(db.settlement.damage_floor), total - shield)
units.apply_damage(hero, total)
+63 -13
View File
@@ -16,10 +16,11 @@
class_name EffectEngine
extends RefCounted
## 事件词汇(16 个发射点——需求卡片 §3.1 定稿;4 个登记暂不做事件不入本表)
## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕
## +battle_won 战斗胜利〔战地包扎〕;原 16 发射点不动)
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"]
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
@@ -32,26 +33,48 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
"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"]
"bias_rewards", "preview",
# 迭代 20 专长实装批 T1 扩签六型(奇物.md 基础件表 27 件声明清单——落法映射 commit 留痕):
# heal=回复生命〔血蜜/战地包扎〕;ac_bonus=护甲加值累积器〔封印桩本轮/石肤本场 cap〕;
# damage_reduce=受伤减免〔背水/安全帽〕;bonus_delta=冒险加算区增量〔血税 双向——battle 末汇入
# runner.bonus〕;blast_scaled=雷罚标定群伤〔陷阱爆发——值=事件伤害×mult〕;
# preview_area=资源类型预告〔示警烟火——3×3 邻域资源类型,不揭安危位置〕;
# target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕
"heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area",
"target_attack_bonus"]
# 注:rewrite_board 保留至本批 T3——老 27 行 purge_sapper(净化工兵)仍在表,删行同批除名
# (迭代 20 卡 V14 连带清理;编排器消费分支已先行摘除=命中零消费 no-op 过渡)
## fact 条件词汇(封闭枚举 9 项——需求卡片 §四定稿)
## fact 条件词汇(封闭枚举——迭代 20 T1 扩签 9→17:payload 同名键直读面〔fact 取值序 payload
## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕;
## is_edge_cell=边缘格〔边陲矿工〕;is_blank_zero=空白连锁〔跳格采掘〕;dug_under_flag=被挖格
## 当时有旗〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水
## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕)
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter"]
"flags_count", "charge_layers", "nest_count", "round_index", "counter",
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_flag",
"ap_exhausted_last", "hp_ratio", "amount", "ap_left"]
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法,
## 代拍板白名单覆盖,commit 留痕)
## ends_with=后缀匹配〔对某甲族的泛条件〕、mod_eq=取模余数判定〔双管镐每第 2 次——value=模数
## ref=余数;dispatch 预评估口径〔when 于命中前读计数器现值〕下「每第 N 次」系用 ref=N-1 表达〕。
## 均系映射表现状条件装进 fact 的查询面落法,代拍板白名单覆盖,commit 留痕)
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
"advantageous", "gte_ref", "lt_ref"]
"advantageous", "gte_ref", "lt_ref", "mod_eq"]
## when 条件键白名单(trigger 行 when 数组元素键)
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面)
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面。
## 迭代 20 T1 扩签四键:value_event=事件载荷取值键名〔爆破会计——counter_add 值=本轮挖格花费
## payload.ap_cost〕;value_counter=计数器取值键〔疼痛记忆/弃图兑换——attack rider 值=账面层值〕;
## domain=计数器寿命域〔battle=战斗域重建清零〔缺省〕/adventure=跨战持久随 battle 末导出〕;
## axis=bonus_delta 加算区轴向)
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
"cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id",
"reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit"]
"reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit",
"value_event", "value_counter", "domain", "axis"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
@@ -60,6 +83,8 @@ const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "r
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 _adventure_counter_ids: Array[String] = [] # domain=adventure 计数器名单(T6——重建时收集、不清零;
# battle 末 export → 冒险账本、begin_battle seed 回灌)
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
@@ -72,8 +97,8 @@ func _init(db_ref: ConfigDb = null) -> void:
## 声明收集重置点(round_start)。
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
_triggers.clear()
_counters.clear()
_reset_on_round_start.clear()
_adventure_counter_ids.clear()
if db != null and db.feats != null:
for row in db.feats.rows:
var fid := String(row.get("feat_id", ""))
@@ -99,6 +124,8 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
"posture_bulwark":
_push_trigger("tree", rk, {"on": "damage_taken",
"do": {"kind": "shield", "value": 1}}, {})
# 计数器账本重建(T6——battle 域清零照旧;adventure 域由 counters_seed 回灌〔rebuild 后调用〕)
_counters = {}
func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void:
@@ -127,6 +154,11 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary)
var cid := String(do_d.counter_id)
if not _reset_on_round_start.has(cid):
_reset_on_round_start.append(cid)
# 寿命域收集(T6——domain=adventure 计数器跨战持久:重建不清零、battle 末导出回冒险账本)
if do_d.has("counter_id") and String(do_d.get("domain", "")) == "adventure":
var aid := String(do_d.counter_id)
if not _adventure_counter_ids.has(aid):
_adventure_counter_ids.append(aid)
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存)
@@ -181,6 +213,21 @@ func counter_spend(counter_id: String, value: int) -> bool:
return true
## adventure 域计数器导出(T6——battle 末调用,快照回冒险账本;仅 domain=adventure 名单内键)
func counters_export() -> Dictionary:
var out := {}
for cid in _adventure_counter_ids:
out[cid] = int(_counters.get(cid, 0))
return out
## adventure 域计数器回灌(T6——rebuild 后由发射宿主调用:冒险账本值回填名单内键,缺省 0;
## 名单=本次 rebuild 装载行声明 domain=adventure 的 counter_id 集)
func counters_seed(seed: Dictionary) -> void:
for cid in _adventure_counter_ids:
_counters[cid] = maxi(0, int(seed.get(cid, 0)))
# ---- when 判定(条件谓词——零随机全确定) ----
func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool:
@@ -229,6 +276,9 @@ func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
"mod_zero":
var m := int(val)
return m > 0 and int(actual) % m == 0
"mod_eq":
var me_m := int(val)
return me_m > 0 and int(actual) % me_m == int(c.get("ref", 0))
"advantageous":
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
return _counter_delta_of(String(actual)) > 0
@@ -326,7 +376,7 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array,
## rewrite_board、apply_status——发射点逐条执行。
static func combo_of(kind: String) -> String:
match kind:
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield":
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield", "heal", "ac_bonus", "damage_reduce", "bonus_delta":
return "additive"
"grant_ap":
return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法)
@@ -338,6 +388,6 @@ static func combo_of(kind: String) -> String:
return "ordered_write"
"counter_spend":
return "ordered_spend"
"open_ticket", "rewrite_board", "apply_status":
"open_ticket", "apply_status", "blast_scaled":
return "immediate"
return "immediate" # 默认即时执行(每条命中各自执行一次)
+12
View File
@@ -482,6 +482,8 @@ func _start_battle(node: Dictionary) -> bool:
"attune_charges": _attune_charges,
"feats": _feats,
"tree_rules": _tree_rules,
"bonus_ref": bonus,
"adventure_counters": _adventure_counters_ref,
"node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
return false
@@ -614,6 +616,16 @@ func set_tree_rules(rules: Array) -> void:
_tree_rules = rules.duplicate()
# adventure 域计数器共享引用注入(迭代 20 T6——AdventureRunner 冒险级账本引用下传;
# begin_battle enc.adventure_counters 携带进效果引擎 rebuild 回灌与战末 counters_export 回写——
# **共享引用不复制**,attune_charges 同款;缺省空 dict=零扰动)
var _adventure_counters_ref: Dictionary = {}
func set_adventure_counters(ref: Dictionary) -> void:
_adventure_counters_ref = ref
# 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入:
# 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录)
func set_attune_charges(charges: Dictionary) -> void:
+20
View File
@@ -332,6 +332,26 @@ func _board_face_dig(c: Vector2i) -> String:
return ""
# fx_reveal 效果通道揭示(迭代 20 专长实装批 T1——订阅类「揭示 1 格」效果的棋盘面执行口:
# 不经行动经济〔零 AP 消耗零相位要求〕、不要求邻接〔任意揭示〕、只受理安全格〔信息红线=只揭真格,
# 雷格/已开/出界/移除格=拒绝零副作用〕。连锁与揭示事件与 _do_dig 同构(chain 通道复用),资源兑付归
# 战斗结算 _flush_pending_reveals 直接入账——channel=fx_reveal 供归因)
func fx_reveal(c: Vector2i) -> bool:
if _board == null or not _board.in_bounds(c) or is_revealed(c) or _board.is_mine(c) or _removed.has(c):
return false
var chain := _chain_closure(c, _flags)
for rc in _chain_reveal_order(c, _flags):
if _revealed.has(rc):
continue
_revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)}
cell_revealed.emit({
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
"variant_ids": _board.variant_ids(rc), "channel": "fx_reveal", "resource": true,
"dug_under_flag": false,
})
return true
# 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越旗
func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary:
var chain := {start: true}
+1 -1
View File
@@ -15,7 +15,7 @@ class_name RngServiceCore
extends Node
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(M-4 迭代 11 第②批 T1——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动)
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw", "fx_reveal"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(M-4 迭代 11 第②批 T1——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动);fx_reveal = 专长订阅类揭示效果掷格(迭代 20 T1——见血开窍/斩首学者等「揭示 1 格」,与 reward_pick 分签防取数序互扰;只揭真格=安全格信息红线承载)
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
+1
View File
@@ -44,6 +44,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
{"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0}, # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
]
@@ -0,0 +1,96 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 效果引擎词表扩签封闭性单测(迭代 20 专长实装批 T1——需求卡片 V1 验收条目)。
# 覆盖:①扩签后四词表仍封闭(新事件/新效果类型/新事实/新算子在册+出域值全部被拒——
# validate_trigger_row 加载期报错 / dispatch 运行期 fail-fast);②合法新词行零违例;
# ③adventure 域计数器 export/seed 往返(T6 承载面);④mod_eq 取模余数算子(双管镐落法)。
# 运行:godot --headless --path game --script res://tests/unit/core/effect_words_t1_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const EffectEngineScript = preload("res://core/effect_engine.gd")
func _init() -> void:
# ① 词表在册断言(扩签面:2 事件+8 事实+7 效果类型+1 算子)
Kit.check_quiet(EffectEngineScript.EVENTS.has("dig_stop") and EffectEngineScript.EVENTS.has("battle_won"),
"新事件在册:dig_stop/battle_won〔专长批 V1〕")
Kit.check_quiet(EffectEngineScript.FACTS.has("hp_ratio") and EffectEngineScript.FACTS.has("is_edge_cell")
and EffectEngineScript.FACTS.has("new_resource_type") and EffectEngineScript.FACTS.has("dug_under_flag")
and EffectEngineScript.FACTS.has("ap_exhausted_last") and EffectEngineScript.FACTS.has("ap_left")
and EffectEngineScript.FACTS.has("is_blank_zero") and EffectEngineScript.FACTS.has("amount"),
"新事实在册:hp_ratio/is_edge_cell/new_resource_type/dug_under_flag/ap_exhausted_last/ap_left/is_blank_zero/amount〔专长批 V1〕")
Kit.check_quiet(EffectEngineScript.EFFECT_KINDS.has("heal") and EffectEngineScript.EFFECT_KINDS.has("ac_bonus")
and EffectEngineScript.EFFECT_KINDS.has("damage_reduce") and EffectEngineScript.EFFECT_KINDS.has("bonus_delta")
and EffectEngineScript.EFFECT_KINDS.has("blast_scaled") and EffectEngineScript.EFFECT_KINDS.has("preview_area")
and EffectEngineScript.EFFECT_KINDS.has("target_attack_bonus"),
"新效果类型在册:heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/target_attack_bonus〔专长批 V1〕")
Kit.check_quiet(EffectEngineScript.OPS.has("mod_eq"), "新算子在册:mod_eq〔专长批 V1〕")
# ② 合法新词行零违例(validate_trigger_row 正例)
var errs: Array = []
var ok_row := {"on": "dig_stop", "when": [{"fact": "ap_left", "op": "gte", "value": 3}],
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left", "domain": "adventure"}}
var mine_covered: bool = EffectEngineScript.validate_trigger_row("V1正例", {}, [ok_row], errs)
Kit.check_quiet(errs.is_empty() and not mine_covered, "合法新词行 validate 零违例(dig_stop/ap_left/counter_add+domain)〔专长批 V1〕")
errs = []
EffectEngineScript.validate_trigger_row("V1正例2", {}, [{"on": "battle_won",
"when": [{"fact": "hp_ratio", "op": "lte", "value": 1}],
"do": {"kind": "heal", "value": 3}}], errs)
Kit.check_quiet(errs.is_empty(), "合法 battle_won+heal 行 validate 零违例〔专长批 V1〕")
# ③ 出域值全部被拒(on/fact/op/kind 各一行——enum category)
errs = []
EffectEngineScript.validate_trigger_row("出域on", {}, [{"on": "dig_stop_x", "do": {"kind": "heal", "value": 1}}], errs)
EffectEngineScript.validate_trigger_row("出域fact", {}, [{"on": "dig_stop", "when": [{"fact": "nope", "op": "eq", "value": 1}], "do": {"kind": "heal", "value": 1}}], errs)
EffectEngineScript.validate_trigger_row("出域op", {}, [{"on": "dig_stop", "when": [{"fact": "ap_left", "op": "mod_odd", "value": 2}], "do": {"kind": "heal", "value": 1}}], errs)
EffectEngineScript.validate_trigger_row("出域kind", {}, [{"on": "dig_stop", "do": {"kind": "nope_kind", "value": 1}}], errs)
Kit.check_quiet(errs.size() == 4, "出域 on/fact/op/kind 各一行全部被拒(4 违例)〔专长批 V1〕")
# ④ 运行期命中序(合法新事件;dispatch 出域 fail-fast=debug assert〔effect_engine dispatch 首行
# assert(EVENTS.has)——release 剥离系引擎语义,自动化覆盖走 ③ 加载期 validate 致命通道〕)
var fx = EffectEngineScript.new(null)
var hits: Array = []
fx.rebuild([], [], [])
fx._push_trigger("test", "v1_row", {"on": "dig_stop",
"when": [{"fact": "ap_left", "op": "gte", "value": 3}],
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}, {})
hits = fx.dispatch("dig_stop", {"ap_left": 5})
Kit.check_quiet(hits.size() == 1, "dig_stop 合法命中(ap_left 5≥3)〔专长批 V1〕")
hits = fx.dispatch("dig_stop", {"ap_left": 1})
Kit.check_quiet(hits.is_empty(), "dig_stop 反例不命中(ap_left 1<3)〔专长批 V1〕")
Kit.check_quiet(not EffectEngineScript.EVENTS.has("dig_stop_x"),
"词表封闭:出域事件词不在 EVENTS(dispatch 首行 assert 消费同一词表)〔专长批 V1〕")
# ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定)
var fx2 = EffectEngineScript.new(null)
fx2.rebuild([], [], [])
fx2._push_trigger("test", "mod_row", {"on": "dig_paid",
"when": [{"fact": "counter", "id": "dbl_digs", "op": "mod_eq", "value": 2, "ref": 1}],
"do": {"kind": "grant_ap", "value": 1}}, {})
var refunds := 0
for i in 4:
if not fx2.dispatch("dig_paid", {"ap_cost": 1}).is_empty():
refunds += 1
fx2.counter_add("dbl_digs", 1)
Kit.check_quiet(refunds == 2, "mod_eq 预评估口径:4 次付费挖格恰返 2 次(第 2/4 次)〔专长批 V1〕")
# ⑥ adventure 域计数器 export/seed 往返(T6)
var fx3 = EffectEngineScript.new(null)
fx3._push_trigger("test", "adv_row", {"on": "mine_hit",
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
fx3._push_trigger("test", "adv_row2", {"on": "mine_hit",
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1, "reset_at": "round_start"}}, {})
for _i in 2:
for h in fx3.dispatch("mine_hit", {}): # 施加面同构(编排侧 counter_add 逐条落账)
if String(h.get("kind", "")) == "counter_add":
fx3.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
var exported: Dictionary = fx3.counters_export()
Kit.check_quiet(exported.get("pain_layers", -1) == 2 and not exported.has("round_mines"),
"export 只含 adventure 域(pain_layers=2、round_mines battle 域不入)〔专长批 V1〕")
var fx4 = EffectEngineScript.new(null)
fx4._push_trigger("test", "adv_row_seed", {"on": "mine_hit",
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
fx4.counters_seed({"pain_layers": 7})
Kit.check_quiet(fx4.counter("pain_layers") == 7, "seed 回灌:counters_seed 后 adventure 域计数器=7〔专长批 V1〕")
quit(Kit.finish("effect_words_t1"))