diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 00f2655e..89c2a73e 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -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) diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 318bb3bc..1dc9bc57 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -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) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 8ce25a29..b393601c 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -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 随此值上抛即弃) diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index f6fba762..760df266 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -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) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 8bb37c71..cb497b3c 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -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" # 默认即时执行(每条命中各自执行一次) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 4ab2b4a1..ed5929b7 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -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: diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index b1af1103..840273e5 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -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} diff --git a/game/core/rng_service.gd b/game/core/rng_service.gd index 7c35870b..df17b392 100644 --- a/game/core/rng_service.gd +++ b/game/core/rng_service.gd @@ -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 diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 3056ff35..bae1b0fe 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -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 预评估口径) ] diff --git a/game/tests/unit/core/effect_words_t1_test.gd b/game/tests/unit/core/effect_words_t1_test.gd new file mode 100644 index 00000000..f0ce4845 --- /dev/null +++ b/game/tests/unit/core/effect_words_t1_test.gd @@ -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"))