diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd index 984f19b3..5a33bb2d 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd @@ -1,32 +1,32 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 -# 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)· -# 15 类行协议可回放步骤流(零随机零 await;2026-09-15 窗口锚行批 13→15)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 · +# 战斗结算编排中枢:轮次生命周期唯一 owner · 五段结算(授予→增益/减益→战斗→衰减→判定——2026-09-15 +# 格面资源批,道具窗口/道具段随可选类删除下线)· 授予段极性路由(「动作 × 格型」矩阵:开安全格/标中旗 +# = 正向,开雷格/标错旗 = 负向;结算队列 + 抵消过滤——标旗后被撤旗以轮末在场旗为基线不判)· +# 10 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· # Waiting 相位结算触发(不依赖事件到达序)。 # 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体 -# 拆除(仅 Active 挖掘期受理;旧"每 Waiting 至多一次"简化登记随之作废)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。 -# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md) +# 拆除(仅 Active 挖掘期受理)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。 +# 实现 GDD:design/gdd/systems/战斗循环/战斗结算.md(v0.7 格面资源批——registry 追溯锚) class_name BattleOrchestrator extends RefCounted signal step_row(row: Dictionary) -signal window_opened(items: Array) -signal battle_finished(payload: Dictionary) # {result, hero_hp, remaining_items}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护) +signal battle_finished(payload: Dictionary) # {result, hero_hp}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护) signal round_started(round_index: int) signal board_changed() signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析) -# 明细流水行型协议(战斗结算 Core Rule 14;15 类——2026-09-15 窗口锚行批由 13 类扩为 15 类, +# 明细流水行型协议(战斗结算 Core Rule;10 类——2026-09-15 格面资源批由 15 类收拢, # 清单改动须同步 battle_log_view 的 _format_row 与本文档头注): -# 效果行 effect · 伤害行 damage · 治疗行 heal · 道具行 item · 溢出未生效行 overflow · -# 奖励未生效行 reward_ineffective · 已消耗未生效行 consumed_ineffective · 使用被拒行 use_rejected · -# 标旗未中行 flag_miss · 蓄力行 charge · 衰减行 decay · 判定行 verdict · 高光行 highlight · -# 窗口打开锚行 window_open · 窗口跳过锚行 window_skip(后两类 = 2026-09-15 补的窗口段锚行) -# 行来源断言豁免集合(行型层)——判定段 / 衰减段 / 窗口锚行本就无棋盘来源,勿一刀切误炸 -const SOURCE_EXEMPT_TYPES := ["decay", "verdict", "window_open", "window_skip"] -# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图 / 库存件而非棋盘格 -const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "item"] +# 效果行 effect · 伤害行 damage · 治疗行 heal · 溢出未生效行 overflow · +# 奖励未生效行 reward_ineffective · 标旗未中行 flag_miss · 蓄力行 charge · 衰减行 decay · +# 判定行 verdict · 高光行 highlight +# 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸 +const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] +# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格 +const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward"] var db var rng @@ -49,13 +49,11 @@ var _facts_reveals: Array = [] var _facts_flags: Array = [] var _facts_path := "" var _settling := false -var _awaiting_window := false -var _window_items: Array = [] var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_max_per_round) var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置) -var _flag_miss_seen := {} # 标旗未中行去重(每雷格实例一次) +var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次) var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置) var _perfect_hit_false := 0 var _enemy_cap := 0 # EnemyTable 行值快照(伏击 cap) @@ -64,7 +62,7 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte var _charge_counts: Dictionary = {} # 本轮充能计数 {奖励触发源: 计数}——投哪个攻击类型桶由 settlement.charge_grants(奖励定义)决定,本类不认识"开格=穿刺"这类绑定 -func _init(db_ref, rng_ref, shared_reward_economy = null) -> void: +func _init(db_ref, rng_ref) -> void: db = db_ref rng = rng_ref _board_generator = BoardGenerator.new() @@ -72,8 +70,7 @@ func _init(db_ref, rng_ref, shared_reward_economy = null) -> void: minesweeper = MinesweeperRules.new(db, action_economy) _enemy_system = UnitContainer.new(db) _status_effects = StatusContainer.new(db) - # 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 奖励道具经济 容器,库存跨战斗延续 - _reward_economy = shared_reward_economy if shared_reward_economy != null else RewardEconomy.new(db, rng) + _reward_economy = RewardEconomy.new(db, rng) minesweeper.cell_revealed.connect(_on_cell_revealed) minesweeper.flag_judged.connect(_on_flag_judged) minesweeper.round_end_board.connect(_on_round_end) @@ -91,7 +88,6 @@ func begin_battle(enc: Dictionary) -> bool: if not _status_effects.notify_battle_start(): _fatal("增益状态 开域被拒(编排违例)") return false - _reward_economy.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置) # 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰; # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); @@ -120,7 +116,7 @@ func begin_battle(enc: Dictionary) -> bool: _m_amb = int(db.status_effect_row("ambush").get("magnitude", 0)) if not _install_new_board(): return false - _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(reward_eligible 恒 false 的双保险) + _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false @@ -138,17 +134,19 @@ func _install_new_board(during_round := false) -> bool: if not res.accepted: _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) return false - # 奖励道具经济 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀ + # 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 雷格,雷安同池) var r0 := {} for c in board.compute_r0(): r0[c] = true var eligible: Array = [] for c in board.safe_cells(): - if r0.has(c): - continue - eligible.append({"cell": c}) + if not r0.has(c): + eligible.append({"cell": c}) + for c in board.mines.keys(): + if not r0.has(c): + eligible.append({"cell": c}) if not _reward_economy.roll_board(eligible): - _fatal("奖励道具经济 roll_board 被拒") + _fatal("地图战斗资源 roll_board 被拒") return false _flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) _flag_miss_seen.clear() @@ -165,11 +163,11 @@ func _install_new_board(during_round := false) -> bool: func _on_cell_revealed(evt: Dictionary) -> void: # 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序: # cell_revealed 批 → flag_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实; - # initial 通道事件(install 初始展开)reward_eligible=false,进入授予段被资格检查自然跳过。 + # initial 通道事件(install 初始展开——封锁格)resource=false,进入授予段被资格检查自然跳过。 if battle_result == "": - if evt.kind == "safe" and bool(evt.get("reward_eligible", false)): + if bool(evt.get("resource", false)): # 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份, - # 防旧图已挖格在轮末错拿新图同坐标身份) + # 防旧图已挖格在轮末错拿新图同坐标身份)。雷格同样携带(雷安同池——负向兑付的来源格) var snap = _reward_economy.query_full_snapshot(evt.cell) if snap != null: evt = evt.duplicate() @@ -194,91 +192,37 @@ func _maybe_settle() -> void: _settle() -# ---- 每轮结算序列(段序骨架——Core Rule 4)---- +# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)---- func _settle() -> void: _settling = true var ri := round_index - # ① 授予段(含 grant_clear 兑付) - var mine_cells: Array = [] + # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付。 + # 抵消过滤 = 旗判定批以轮末在场旗为基线(被拔不判——发射面已过滤,标旗后撤旗零兑付);开格事实不可撤。 for evt in _facts_reveals: - if evt.kind == "mine": - mine_cells.append(evt.cell) - elif evt.kind == "safe" and evt.reward_eligible: - _charge_counts["safe_cell"] = int(_charge_counts.get("safe_cell", 0)) + 1 # 充能计数(开格 = 奖励触发源;只计数,投哪个桶由 charge_grants 决定);初始展开格 reward_eligible=false 不计 - _grant_cell(evt.cell, ri, "safe_cell", evt.get("reward_snapshot")) - # reward_eligible=false(initial 通道)不入库——战斗结算 义务②(行动经济 EC5 委派闭环) + if not bool(evt.get("resource", false)): + continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) + var snap = evt.get("reward_snapshot") + if not (snap is Dictionary): + continue + if evt.kind == "safe": + _charge_counts["safe_cell"] = int(_charge_counts.get("safe_cell", 0)) + 1 # 充能计数(开安全格 = 奖励触发源;只计数,投哪个桶由 charge_grants 决定) + _grant_routed(snap, "positive", "safe_cell", evt.cell, ri, "") + elif evt.kind == "mine": + _grant_routed(snap, "negative", "mine_hit", evt.cell, ri, "") for evt in _facts_flags: _judge_flag(evt, ri) - if not mine_cells.is_empty(): - _convert_ambush(mine_cells, ri) if _facts_path == "cleared": _grant_clear_reward(ri) - # ② 道具使用窗口(授予段后、增益/减益段前——当轮拾取当轮可用) - _window_items = _assemble_window() - if not _window_items.is_empty(): - # 窗口打开锚行(战斗结算 验收:"窗口段恒在案")——零棋盘来源,行型豁免 - _emit_row("window_open", "", Vector2i(-1, -1), ri, "道具窗口(%d 件可选)" % _window_items.size(), _window_items.size(), {}) - _awaiting_window = true - window_opened.emit(_window_items) - _settling = false - return # 窗口选择序列 = 结算回放输入事件 - # 空库存不弹窗,但窗口段仍要在案("空库存记跳过")——锚行不可省 - _emit_row("window_skip", "", Vector2i(-1, -1), ri, "道具窗口跳过(空库存)", 0, {}) - _finish_settlement([]) - - -func _assemble_window() -> Array: - # 窗口数据组装 = 奖励道具经济 query_inventory 投影 + 战斗结算 侧守卫预判 usable_now(奖励道具经济 零跨系统读不破) - var out: Array = [] - for inst in _reward_economy.query_inventory(): - if inst.status != "InStock": - continue - var usable := true - if inst.category == "heal_pack": - usable = int(_enemy_system.hero_unit().hp) < int(_enemy_system.hero_unit().max_hp) # 满血治疗包禁选 - out.append({ - "index": inst.index, - "item_id": inst.item_id, - "display_name": inst.display_name, - "category": inst.category, - "power": inst.power, - "source_cell": inst.source_cell, - "round_index": inst.round_index, - "usable_now": usable, - }) - return out - - -func submit_window_selection(selection: Array) -> void: - if not _awaiting_window: - push_warning("[战斗结算] 无窗口期的选择提交(编排违例)") - return - _awaiting_window = false - _settling = true - if selection.size() > 1: - push_warning("[战斗结算] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略") - var picked: Array = selection.slice(0, 1) - if picked.is_empty(): - # 零选择跳过锚行(战斗结算 验收:"零选择记跳过")——窗口段在案的第二半 - _emit_row("window_skip", "", Vector2i(-1, -1), round_index, "道具窗口跳过(未选用)", 0, {}) - _finish_settlement(picked) - - -func _finish_settlement(selection: Array) -> void: - var ri := round_index - # ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦) + # ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦) _emit_composition_effects("hero", ri) _emit_composition_effects("enemy", ri) - # ④ 道具段(窗口选定序逐件生效;逐件动态守卫) - for idx in selection: - _apply_selected_item(int(idx), ri) - # ⑤ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路) + # ③ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路) _combat_segment(ri) - # ⑥ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) var k: int = _status_effects.decay_round() _emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") - # ⑦ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御) + # ⑤ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御) var verdict := "continue" if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: verdict = "defeat" @@ -298,52 +242,97 @@ func _finish_settlement(selection: Array) -> void: _settling = false -# ---- 授予段 helpers ---- +# ---- 授予段 helpers(极性路由)---- -# snap_override 非空 = 冻结快照授予(轮内刷新批——缓冲期身份,绕过现行 mapping/_granted) -func _grant_cell(cell: Vector2i, ri: int, source_class: String, snap_override = null) -> void: - var snap = snap_override if snap_override != null else _reward_economy.grant(cell) - if not (snap is Dictionary): - return - if snap.category == "buff": - var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri}) - if ok: - var row: Dictionary = db.status_effect_row(snap.effect_id) - var pool := _pool_effective(snap.effect_id) - _emit_row("effect", source_class, cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": pool}) - else: - _emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)", 0, {"batch_key": String(snap.effect_id)}) - elif _resolve_instant(snap, source_class, cell, ri) != "not_instant": - pass # 即时类:授予段当场兑付(兑付行/零兑付未生效行均由 _resolve_instant 自记——2026-09-11 两类奖励裁决) - else: - _reward_economy.stock(snap, cell, ri) - _emit_row("item", source_class, cell, ri, "%s 入库" % snap.display_name, int(snap.power), {}) +# 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份), +# polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative), +# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。 +func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: + var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) + if row.is_empty(): + _fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity]) + return false + if polarity == "positive": + row["power"] = int(snap.get("power", int(row.power))) # 区间行快照 power(身份批 roll 值)覆盖表值——授予期零随机口径(E14) + return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix) -# 即时类当场兑付(2026-09-11 两类奖励裁决):instant_aoe = 全体存活敌各 power 点; -# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。 -# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(奖励未生效行已记)/ "not_instant" = 非即时类(道具类走库存)。 -# 零兑付必须可区分:完美排雷高光行只记实际有效兑付,首份即零兑付则高光行不发(战斗结算 授予段规则)。 -func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> String: - if snap.category == "instant_aoe": +# 兑付形态分派:effect_id 非空 = percent 行(进增益状态容器);空 = flat 行(当场兑付) +func _dispatch_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: + var eff := String(row.get("effect_id", "")) + if eff != "": + return _apply_percent_row(row, source_class, cell, ri, highlight, label_suffix) + _apply_flat_row(row, source_class, cell, ri, highlight, label_suffix) + return false + + +# percent 行兑付:apply_effect 入容器;满层拒收 = "奖励未生效"行(零静默) +func _apply_percent_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: + var eff := String(row.effect_id) + var ok: bool = _status_effects.apply_effect(eff, {"source_class": source_class, "origin_cell": cell, "round_index": ri}) + if ok: + var srow: Dictionary = db.status_effect_row(eff) + _emit_row("effect", source_class, cell, ri, String(srow.display_name) + label_suffix, int(srow.magnitude), {"effect_id": eff, "pool_value": _pool_effective(eff)}, highlight) + return true + _emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)%s" % label_suffix, 0, {"batch_key": eff}, highlight) + return false + + +# flat 行兑付(负向恒即时,无库存语义): +# blast 正向 = 全体存活敌各 power 点(无敌 → 奖励未生效行);blast 负向 = 敌方伏击 +stacks 层(cap/溢出行协议不变); +# heal 正向 = 主角回复 clamp(满血 → 奖励未生效行);heal 负向 = 主角直伤 power 点(不经增益状态;归零 → 战斗段短路/判定段判负)。 +# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(行已自记)——清图高光只记实际有效兑付。 +func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> String: + var ctype := String(row.cell_type) + var power := int(row.power) + var display := String(row.display_name) + label_suffix + if ctype == "blast": + if String(row.get("polarity", "positive")) == "negative": + return _apply_ambush_grant(row, source_class, cell, ri, highlight, label_suffix) var alive: Array = _enemy_system.query_alive("enemy") if alive.is_empty(): - _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(无存活敌人)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_aoe"}, highlight) + _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(无存活敌人)" % display, 0, {"batch_key": String(row.reward_id)}, highlight) return "zero" for u in alive: - _enemy_system.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手 - _emit_row("damage", source_class, cell, ri, "%s → 全体敌人 ×%d%s" % [snap.display_name, alive.size(), suffix], int(snap.power), {"target": "all_enemies", "count": alive.size()}, highlight) + _enemy_system.apply_damage(u, power) # 恒定表值,逐个;受击即死——死亡即不动手 + _emit_row("damage", source_class, cell, ri, "%s → 全体敌人 ×%d" % [display, alive.size()], power, {"target": "all_enemies", "count": alive.size()}, highlight) return "redeemed" - if snap.category == "instant_heal": - if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp): - _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_heal"}, highlight) + if ctype == "heal": + if String(row.get("polarity", "positive")) == "negative": + _enemy_system.apply_damage(_enemy_system.hero_unit(), power) + _emit_row("damage", source_class, cell, ri, "%s → 主角" % display, power, {"target": "hero"}, highlight) + return "redeemed" + if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: + # 主角已倒下(同轮更早的负向直伤致死)——治疗不再合法作用,按"奖励未生效"留痕 + _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(主角已倒下)" % display, 0, {"batch_key": String(row.reward_id)}, highlight) return "zero" - var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), int(snap.power)) # clamp 归 战斗结算 - _emit_row("heal", source_class, cell, ri, "%s 回复%s" % [snap.display_name, suffix], healed, {"target": "hero"}, highlight) + if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp): + _emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)" % display, 0, {"batch_key": String(row.reward_id)}, highlight) + return "zero" + var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), power) # clamp 归 战斗结算 + _emit_row("heal", source_class, cell, ri, "%s 回复" % display, healed, {"target": "hero"}, highlight) return "redeemed" - return "not_instant" + _fatal("flat 行未知类型: %s" % ctype) + return "zero" +# blast 负向:敌方伏击 +stacks 层(伏击池 cap 与"溢出未生效"行协议不变——敌人系统 F3 逐层守恒, +# 多源共享同一池值;单结算内先到先占 cap,超出部分逐层记溢出行) +func _apply_ambush_grant(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> String: + var requested: int = maxi(1, int(row.get("stacks", 1))) + var remaining_cap: int = maxi(0, _enemy_cap - _status_effects.pool_layers("ambush")) + var plan: Dictionary = _enemy_system.ambush_grant_plan(requested, remaining_cap) + for i in range(int(plan.granted)): + _status_effects.apply_effect("ambush", {"source_class": source_class, "origin_cell": cell, "round_index": ri}) + if int(plan.granted) > 0: + _emit_row("effect", source_class, cell, ri, "敌方伏击 +%d 层%s" % [int(plan.granted), label_suffix], int(plan.granted), {"effect_id": "ambush", "pool_value": _status_effects.pool_layers("ambush") * _m_amb}, highlight) + for i in range(int(plan.overflow)): + _emit_row("overflow", source_class, cell, ri, "伏击溢出未生效", 0, {"batch_key": "ambush_overflow"}) + return "redeemed" if int(plan.granted) > 0 else "zero" + + +# 旗判定批:标中 = 该雷格资源的正向兑付(每雷实例一次;高光 = 强制检查点); +# 标空 = "标旗未中"归因行 + 该安全格资源的负向兑付(同键去重——每格实例一次)。 func _judge_flag(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell if evt.hit: @@ -351,69 +340,28 @@ func _judge_flag(evt: Dictionary, ri: int) -> void: return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 _flag_consumed[cell] = true _perfect_hit_true += 1 - _charge_counts["flag_hit"] = int(_charge_counts.get("flag_hit", 0)) + 1 # 充能计数(标中旗 = 奖励触发源;与 weaken 消费同键,每雷实例一次——忘拔旗不重复充能) - var ok: bool = _status_effects.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri}) - if ok: - var row: Dictionary = db.status_effect_row("weaken") - _emit_row("effect", "flag_hit", cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": "weaken", "pool_value": _pool_effective("weaken")}, "flag_hit") - else: - _emit_row("reward_ineffective", "flag_hit", cell, ri, "奖励未生效(满层)", 0, {"batch_key": "weaken"}) + _charge_counts["flag_hit"] = int(_charge_counts.get("flag_hit", 0)) + 1 # 充能计数(标中旗 = 奖励触发源;每雷实例一次——忘拔旗不重复充能) + var snap = _reward_economy.grant(cell) + if not (snap is Dictionary): + return + _grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") else: _perfect_hit_false += 1 if _flag_miss_seen.has(cell): - return # 每雷格实例一次去重(防逐轮惩罚轰炸) + return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) _flag_miss_seen[cell] = true var hl := "" if _perfect_hit_false == 1: hl = "perfect_broken" # 本板完美谓词已破的归因锚 _emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) - _apply_miss_backlash(cell, ri) - - -# 标错惩罚(2026-09-12 用户拍板:随机给敌方一个增伤/buff,或我方 debuff;时效 1 结算——用户拍板"短点,比如 1 回合")。 -# 随机载体 = 坐标×板序号确定性派生(零新流、同种子可回放;正式侧候选 = RngService 新增封闭流——待拍板, -# 见 design/research/lore-nest-metaphor-2026-09-12.md ③ 边界提醒)。三池项:enrage(敌增伤)/ ambush×2(敌 buff)/ off_guard(我方 debuff)。 -func _apply_miss_backlash(cell: Vector2i, ri: int) -> void: - var kind: int = (absi(cell.x) * 7 + absi(cell.y) * 13 + _board_seq * 5) % 3 - var src: Dictionary = {"source_class": "flag_miss", "origin_cell": cell, "round_index": ri} - match kind: - 0: - if _status_effects.apply_effect("enrage", src): - var row: Dictionary = db.status_effect_row("enrage") - _emit_row("effect", "flag_miss", cell, ri, "反噬·%s(敌方)" % String(row.display_name), int(row.magnitude), {"effect_id": "enrage", "pool_value": _pool_effective("enrage")}) - else: - _emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "enrage"}) - 1: - var granted := 0 - for i in range(2): - if _status_effects.apply_effect("ambush", src): - granted += 1 - if granted > 0: - _emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _status_effects.pool_layers("ambush") * _m_amb}) - else: - _emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "ambush"}) - 2: - if _status_effects.apply_effect("off_guard", src): - var row2: Dictionary = db.status_effect_row("off_guard") - _emit_row("effect", "flag_miss", cell, ri, "反噬·%s(我方)" % String(row2.display_name), int(row2.magnitude), {"effect_id": "off_guard", "pool_value": _pool_effective("off_guard")}) - else: - _emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "off_guard"}) - - -func _convert_ambush(mine_cells: Array, ri: int) -> void: - # 敌人系统 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次 - var plan: Dictionary = _enemy_system.ambush_grant_plan(mine_cells.size(), _enemy_cap) - for i in range(plan.granted): - var cell: Vector2i = mine_cells[i] - var ok: bool = _status_effects.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri}) - if ok: - var layers := _status_effects.pool_layers("ambush") - _emit_row("effect", "mine_hit", cell, ri, "伏击", _m_amb, {"effect_id": "ambush", "pool_value": layers * _m_amb}) - for i in range(plan.granted, plan.granted + plan.overflow): - var cell: Vector2i = mine_cells[i] - _emit_row("overflow", "mine_hit", cell, ri, "伏击溢出未生效", 0, {"batch_key": "ambush_overflow"}) + var snap = _reward_economy.grant(cell) + if not (snap is Dictionary): + return + _grant_routed(snap, "negative", "flag_miss", cell, ri, "") +# 清图批:清图奖励对象恰好发一次;完美排雷追加第二份同身份兑付(grant_clear 本身恰一次)。 +# 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。 func _grant_clear_reward(ri: int) -> void: if _board_from_midround_refresh: return # 轮内刷新换来的新图不发清图奖励(行动经济/战斗结算 明文——刷新图不是清图挣来的) @@ -421,99 +369,42 @@ func _grant_clear_reward(ri: int) -> void: if not (snap is Dictionary): return var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1 - # 首份兑付(first_ok = 实际有效兑付——零兑付/被拒均不算) - var first_ok := _grant_clear_identity(snap, ri) - # 完美排雷:真实双份交付(第二份按身份类型走真实兑付——即时类重走 _resolve_instant; - # 增益类重授容器;道具类入库。grant_clear 本身仍恰一次) + var first_ok := _grant_clear_identity(snap, ri, "(清图奖励)") var second_ok := false if perfect and first_ok: - if snap.category == "buff": - second_ok = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri}) - if second_ok: - var row: Dictionary = db.status_effect_row(snap.effect_id) - _emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, "%s(完美第二份)" % String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)}) - else: - _emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(满层)", 0, {"batch_key": String(snap.effect_id)}) - else: - var res2 := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(完美第二份)") - if res2 == "redeemed": - second_ok = true - elif res2 == "not_instant": - var s2 = _reward_economy.stock(snap, Vector2i(-1, -1), ri) - second_ok = s2 is Dictionary - if second_ok: - _emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {}) - else: - _emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)}) - # res2 == "zero":零兑付的奖励未生效行已由 _resolve_instant 自记 + second_ok = _grant_clear_identity(snap, ri, "(完美第二份)") if perfect and first_ok: - # 高光行 value = 实际有效兑付份数(禁记名义值;首份满层零兑付 → 降级不发) _emit_row("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美排雷!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep") -func _grant_clear_identity(snap: Dictionary, ri: int) -> bool: - if snap.category == "buff": - var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri}) - if ok: - var row: Dictionary = db.status_effect_row(snap.effect_id) - _emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)}, "clear_reward_h2") - return true - _emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(满层)", 0, {"batch_key": snap.effect_id}) - return false - var res := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2") - if res != "not_instant": - return res == "redeemed" # 即时类零兑付(无存活敌/满血)= 未生效——首份即零兑付则高光行不发 - var s = _reward_economy.stock(snap, Vector2i(-1, -1), ri) - if not (s is Dictionary): - _emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)}) - return false - _emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(清图奖励)" % snap.display_name, int(snap.power), {}, "clear_reward_h2") - return true - - -# ---- 窗口 / 道具段 ---- - -func _apply_selected_item(window_index: int, ri: int) -> void: - if window_index < 0 or window_index >= _window_items.size(): - return - var item: Dictionary = _window_items[window_index] - if item.category == "heal_pack": - # 逐件动态守卫:前件推满后本件拦截(不烧包——战斗结算 不调 use) - if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp): - _emit_row("use_rejected", "item", item.source_cell, ri, "%s 被拒(满血)" % item.display_name, 0, {}) - return - var receipt = _reward_economy.use(item.index) - if not (receipt is Dictionary): - return - var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), receipt.power) # clamp 归 战斗结算 - _emit_row("heal", "item", receipt.source_cell, ri, "%s 回复" % item.display_name, healed, {"target": "hero"}) - else: - # 伤害道具:恒定表值不吃修正;零效果判定读 敌人系统 存活集(照常消耗——E10) - if _enemy_system.query_alive("enemy").is_empty(): - var receipt0 = _reward_economy.use(item.index) - if not (receipt0 is Dictionary): - return - _emit_row("consumed_ineffective", "item", receipt0.source_cell, ri, "已消耗未生效(无存活敌人)", 0, {}) - return - var receipt = _reward_economy.use(item.index) - if not (receipt is Dictionary): - return - var target = _enemy_system.default_target() - var killed_before: bool = not target.alive - _enemy_system.apply_damage(target, receipt.power) - var killed: bool = not killed_before and not target.alive - _emit_row("damage", "item", receipt.source_cell, ri, "%s 命中" % item.display_name, receipt.power, {"target": target.display_name, "killed": killed}) +# 清图奖励 = 板级独立对象(不属于任何一格):兑付形态按其 cell_type 的正向字典形态走, +# 行值全部取快照(身份批 roll 值)。返回实际有效兑付。 +func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool: + var row := { + "reward_id": String(snap.get("reward_id", "")), + "cell_type": String(snap.get("cell_type", "")), + "polarity": "positive", + "display_name": String(snap.get("display_name", "")), + "power": int(snap.get("power", 0)), + "effect_id": String(snap.get("effect_id", "")), + "stacks": int(snap.get("stacks", 1)), + } + var eff := String(row.effect_id) + if eff != "": + return _apply_percent_row(row, "clear_reward", Vector2i(-1, -1), ri, "clear_reward_h2", suffix) + return _apply_flat_row(row, "clear_reward", Vector2i(-1, -1), ri, "clear_reward_h2", suffix) == "redeemed" # ---- 战斗段 ---- func _combat_segment(ri: int) -> void: var L := _status_effects.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗 - # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动)。 + # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。 # 三段伤害构成(2026-09-12 用户拍板;2026-09-15 命名统一批 + 攻击类型/奖励抽象分离批):普通(atk×(1+attack_up),不吃克制) # + 穿刺/魔法(由充能供给——充能按攻击类型分桶,谁能充/充哪个桶归奖励定义 charge_grants)。攻击类型只管查甲效矩阵哪一行,与来源无关。 + var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0 var hero_target = _enemy_system.default_target() - if hero_target != null: + if hero_alive and hero_target != null: var armor: String = String(hero_target.get("armor_type", "light")) var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi)) var points := _charge_points() # {攻击类型: 充能点数}——奖励定义投递的结果(本段不认识"开格/标雷") @@ -658,7 +549,6 @@ func _emit_settlement_snapshot(verdict: String) -> void: "enemies": enemies, "buffs": buffs, "ambush_layers": _status_effects.pool_layers("ambush"), - "inventory_in_stock": _reward_economy.in_stock_count(), "round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_counts": _charge_counts.duplicate(), "charge_points": _charge_points()}, }) @@ -704,15 +594,14 @@ func player_request_refresh() -> bool: if not action_economy.request_refresh_active(): return false _refresh_used_this_round += 1 - _reward_economy.retire_mapping() # 旧图批次退役(奖励道具经济 Ready→retired;E6 新批重置前提) + _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) minesweeper.retire_board(true) # 旧板退役(轮内通道) return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast func _battle_over(result: String) -> void: battle_result = result - # 库存关卡域(2026-09-11 关内延续):battle_over 不再清库存——跨战斗延续,关卡末经 level_clear() 清空; - # 映射随战斗结束退役(下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) + # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层") _reward_economy.retire_mapping() _enemy_system.despawn_all() @@ -720,7 +609,7 @@ func _battle_over(result: String) -> void: action_economy.notify_combat_end() # → Idle,RAP 清零 db.set_reload_gate(false) _reset_round_facts() - battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _reward_economy.in_stock_count()}) + battle_finished.emit({"result": result, "hero_hp": hero_hp_left}) # ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口—— @@ -728,7 +617,7 @@ func _battle_over(result: String) -> void: # 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1): # round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} / # enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] / -# ambush_layers / hero_effects[](增益状态 query_active 投影) / inventory[](奖励道具经济 query_inventory 投影) / in_stock +# ambush_layers / hero_effects[](增益状态 query_active 投影) func presentation_snapshot() -> Dictionary: var enemies: Array = [] var all: Array = _enemy_system.all_enemies() @@ -763,8 +652,6 @@ func presentation_snapshot() -> Dictionary: "enemies": enemies, "ambush_layers": ambush, "hero_effects": _status_effects.query_active("hero"), - "inventory": _reward_economy.query_inventory(), - "in_stock": _reward_economy.in_stock_count(), } @@ -773,12 +660,7 @@ func is_battle_live() -> bool: return battle_result == "" and not _enemy_system.hero_unit().is_empty() -# 库存余量(层间过渡文案 / 终局清库反馈) -func stock_count() -> int: - return _reward_economy.in_stock_count() - - -# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 奖励道具经济) +# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 地图战斗资源) func query_reward_identity(cell: Vector2i): return _reward_economy.query_identity(cell) @@ -789,7 +671,6 @@ func diagnostic_snapshot() -> Dictionary: return { "battle_result": battle_result, "round": round_index, "settling": _settling, - "awaiting_window": _awaiting_window, "refresh_used_round": _refresh_used_this_round, "board_seq": _board_seq, "facts_path": _facts_path, @@ -799,20 +680,12 @@ func diagnostic_snapshot() -> Dictionary: return { "minesweeper_board_installed": minesweeper.current_board() != null, "minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0, "minesweeper_flags": minesweeper.flag_count() if minesweeper.current_board() != null else 0, - "reward_economy_phase_stock": [_reward_economy.in_stock_count()], + "reward_economy_phase": _reward_economy.phase(), "enemies_alive": _enemy_system.query_alive("enemy").size(), "hero_hp": int(_enemy_system.hero_unit().get("hp", -1)) if not _enemy_system.hero_unit().is_empty() else -1, } -# 关卡结束编排(关卡节点地图/冒险结构 关卡结构落位前由上层显式调用——2026-09-11 关内延续): -# 清空库存并返回快照计数("剩余道具 ×N 随关卡结束清空"反馈行 / 折算结构位的数据面) -func level_clear() -> Dictionary: - var remaining := _reward_economy.in_stock_count() - _reward_economy.clear_inventory() - return {"remaining": remaining} - - func _reset_round_facts() -> void: _facts_reveals.clear() _facts_flags.clear() @@ -820,7 +693,7 @@ func _reset_round_facts() -> void: _charge_counts.clear() # 充能计数随轮 facts 重置(每轮重新攒——结算段按奖励定义折算当轮点数) -# ---- 行协议(Core Rule 14:公共字段 + 行类型特有字段)---- +# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)---- func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void: _assert_row_source(type, source_class, origin_cell) diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd index f4a491cf..1a744510 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd @@ -16,7 +16,6 @@ const TABLE_MANIFEST := { "enemy": "enemy.tres", "hero": "hero.tres", "reward": "reward.tres", - "reward_config": "reward_config.tres", "settlement": "settlement.tres", "level": "level.tres", "difficulty": "difficulty.tres", @@ -29,7 +28,6 @@ var board_gen: BoardGenTable var enemy: EnemyTable var hero: HeroTable var reward: RewardTable -var reward_config: RewardConfigTable var settlement: SettlementTable var level: LevelTable var difficulty: DifficultyTable @@ -99,7 +97,6 @@ func load_tables(root: String, collect_only: bool) -> bool: enemy = loaded.get("enemy") hero = loaded.get("hero") reward = loaded.get("reward") - reward_config = loaded.get("reward_config") settlement = loaded.get("settlement") level = loaded.get("level") difficulty = loaded.get("difficulty") @@ -110,82 +107,27 @@ func load_tables(root: String, collect_only: bool) -> bool: func _cross_validate(loaded: Dictionary) -> void: var st: StatusEffectTable = loaded.get("status_effects") var rt: RewardTable = loaded.get("reward") - var rc: RewardConfigTable = loaded.get("reward_config") var st_tab: SettlementTable = loaded.get("settlement") - if st == null or rt == null or rc == null or st_tab == null: + if st == null or rt == null or st_tab == null: return # 单表校验已报错,不连锁 var effect_ids := {} for row in st.rows: effect_ids[row.get("effect_id", "")] = true - # ① RewardTable effect_id 悬空(首个引用行已回填 → 激活) + # ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命) for row in rt.rows: var eff: String = row.get("effect_id", "") if eff != "" and not effect_ids.has(eff): push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff]) - # ② 池空护栏:safe_cell 池须 ≥1 活行;活 buff 行集非空 - var safe_rows: Array = [] - var clear_rows: Array = [] - for row in rt.rows: - if int(row.get("weight", 0)) <= 0: - continue - if row.get("pool_tag", "") == "safe_cell": - safe_rows.append(row) - elif row.get("pool_tag", "") == "clear_default": - clear_rows.append(row) - if safe_rows.is_empty(): - push_diag("error", "pool_empty_safe", "safe_cell 池无活行") - var has_buff := false - for row in safe_rows: - if row.get("category", "") == "buff": - has_buff = true - if safe_rows.size() > 0 and not has_buff: - push_diag("error", "no_live_buff_row", "safe_cell 池零活 buff 行——tier_min_buff 无定义") - # ③ clear_default 恒正当量(行动经济 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类) - var clear_value_ok := false - for row in clear_rows: - if row.get("category", "") == "buff" and String(row.get("effect_id", "")) != "": - clear_value_ok = true - elif row.get("category", "") in ["damage_item", "heal_pack", "instant_aoe", "instant_heal"] and int(row.get("power", 0)) >= 1: - clear_value_ok = true - if not clear_value_ok: - push_diag("error", "clear_no_value", "clear_default 池无正当量活行(清图奖励 > 0 违例)") - # ④ floor_supply_missing(F5 护栏 i)——k_dmg ≥ 1 时才检查(供给控制器批后 k_dmg=0 休眠) - if rc.k_dmg >= 1: - var supply_ok := false - for row in safe_rows: - if row.get("category", "") == "damage_item" and int(row.get("power", 0)) >= rc.p_min: - supply_ok = true - if not supply_ok: - push_diag("error", "floor_supply_missing", "K_dmg ≥ 1 但 safe_cell 池无 power ≥ P_min 活刀行") - # ⑤ no_damage_row(F5 护栏 ii——零伤害世界后闸;2026-09-11 供给控制器批:扫描域扩至全池活行, - # 可选类退出格子池后伤害来源在 active_inject 池) - if rc.k_dmg == 0: - var any_knife := false - for row in rt.rows: - if int(row.get("weight", 0)) > 0 and row.get("category", "") == "damage_item": - any_knife = true - if not any_knife: - push_diag("error", "no_damage_row", "K_dmg = 0 且全池零活刀行——零伤害世界后闸") - # ⑨ active_inject 池护栏(可选类供给控制器批):非可选类拒绝;池须有活行(否则战斗首件保证落空) - var inject_live := 0 - for row in rt.rows: - if row.get("pool_tag", "") == "active_inject": - if int(row.get("weight", 0)) > 0: - inject_live += 1 - if row.get("category", "") not in ["damage_item", "heal_pack"]: - push_diag("error", "inject_pool_foreign", "active_inject 池混入非可选类行: %s" % row.get("reward_id")) - if inject_live == 0: - push_diag("error", "inject_pool_empty", "active_inject 池无活行(战斗首件保证必落空)") - # ⑥ SettlementTable.effect_pool_map 悬空 + # ② SettlementTable.effect_pool_map 悬空 for eff in st_tab.effect_pool_map.keys(): if not effect_ids.has(eff): push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff) - # ⑦ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {item, enemy_action, flag_miss}) - var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "item", "hero_attack", "enemy_action", "flag_miss"] + # ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键) + var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"] for s in required_sources: if not st_tab.source_label_map.has(s): push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ⑧ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— + # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) var lv: LevelTable = loaded.get("level") var bg: BoardGenTable = loaded.get("board_gen") diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd b/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd index ef2912ef..5663263a 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd @@ -2,8 +2,10 @@ # Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证 # Date: 2026-09-11 # 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 关卡节点地图): -# 关卡域生命周期唯一编排者——共享 奖励道具经济 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、 -# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——数据配置 Rule 7)。 +# 关卡域生命周期唯一编排者——HP 跨战斗携带、恢复点节点兑付、关卡末收束。 +# 2026-09-15 格面资源批:可选类道具与库存整体删除——奖励点节点内容随功能道具渠道(P1,渠道未定)悬置为 +# 直通节点(零兑付);战斗各自持有 地图战斗资源 容器(映射随战斗退役,无跨战斗状态可延续)。 +# Core 层零 UI 依赖(headless 可跑完整关卡——数据配置 Rule 7)。 # 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归元进度,切片不落盘)。 # E4 收口(2026-09-13):覆盖值经 encounter 显式传入战斗(不再写 db.hero 表——表值运行期不可变, # 双基线同步/双写叠加 6→18→54 整类隐患随表写一并移除)。 @@ -11,21 +13,19 @@ class_name LevelRunner extends RefCounted -signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={snapshot} · rest={healed,hp,max_hp} +signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={}(内容待拍板) · rest={healed,hp,max_hp} signal step_row(row: Dictionary) # 转发当前 战斗结算 结算行 -signal window_opened(items: Array) # 转发当前 战斗结算 道具窗口 signal round_started(round_index: int) signal board_changed() -signal level_finished(payload: Dictionary) # {result, hp, node_index, node_type, remaining_items} +signal level_finished(payload: Dictionary) # {result, hp, node_index, node_type} var db var rng var level_row: Dictionary = {} var node_index := -1 var hero_hp := 0 -# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement/reward_economy 读取经 current_battle()/inventory() 成文出口 +# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口 var _settlement = null # 当前战斗编排器(关卡结束置 null) -var _reward_economy = null # 关卡域共享 奖励道具经济 容器(库存跨战斗延续) var hero_atk_pct := 0 # 测试面板覆盖值(切片专用) var hero_hp_pct := 0 @@ -70,7 +70,6 @@ func start_level(level_index: int, seed_val: int) -> bool: node_index = -1 level_result = "" hero_hp = effective_max_hp() - _reward_economy = RewardEconomy.new(db, rng) rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构) _running = true return _enter_next_node() @@ -105,11 +104,9 @@ func _enter_next_node() -> bool: "battle", "boss": return _start_battle(node) "reward": - var inst = _reward_economy.grant_node_active(Vector2i(-1, -1), 0) - if not (inst is Dictionary): - push_error("[关卡节点地图] 奖励点入库被拒") - return false - node_entered.emit(node, {"snapshot": inst.snapshot}) + # 奖励点内容待拍板(格面资源批 2026-09-15:可选类道具删除,功能道具渠道 P1 未定—— + # 关卡节点地图定稿批裁决;节点保留占位、零兑付直通) + node_entered.emit(node, {}) return true "rest": var healed := _apply_rest() @@ -120,11 +117,10 @@ func _enter_next_node() -> bool: func _start_battle(node: Dictionary) -> bool: - _settlement = BattleOrchestrator.new(db, rng, _reward_economy) # 共享 奖励道具经济——库存跨战斗延续(关内延续裁决) + _settlement = BattleOrchestrator.new(db, rng) # core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长 # RefCounted 生命周期乃至成环:settlement 的信号持捕获 runner 的 lambda,runner 又持 settlement) _settlement.step_row.connect(_forward_step_row) - _settlement.window_opened.connect(_forward_window_opened) _settlement.round_started.connect(_forward_round_started) _settlement.board_changed.connect(_forward_board_changed) _settlement.battle_finished.connect(_on_battle_finished) @@ -142,10 +138,6 @@ func _forward_step_row(row: Dictionary) -> void: step_row.emit(row) -func _forward_window_opened(items: Array) -> void: - window_opened.emit(items) - - func _forward_round_started(ri: int) -> void: round_started.emit(ri) @@ -161,16 +153,12 @@ func _on_battle_finished(payload: Dictionary) -> void: hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含) if String(node.get("type", "")) == "boss" or result == "defeat": # Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 冒险结构/元进度) - var left: int = _settlement.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③) - if _reward_economy.in_stock_count() != 0: - push_error("[关卡节点地图] level_clear 后库存非空(清库违例——fail-fast)") - return level_result = "victory" if result == "victory" else "defeat" var hp_left := hero_hp if result == "victory" else 0 _running = false _settlement = null level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index, - "node_type": String(node.get("type", "")), "remaining_items": left}) + "node_type": String(node.get("type", ""))}) return _enter_next_node() @@ -184,16 +172,12 @@ func _apply_rest() -> int: return actual -# ---- 只读出口(E1/E3 封装收口:呈现层/工具经此读当前战斗与关卡库存,禁直读协作对象)---- +# ---- 只读出口(E1/E3 封装收口:呈现层/工具经此读当前战斗,禁直读协作对象)---- func current_battle(): return _settlement -func inventory() -> Array: - return _reward_economy.query_inventory() if _reward_economy != null else [] - - # ---- 玩家输入转发(呈现层只与 runner 对话)---- # 奖励/恢复点确认继续(呈现层在覆盖层上点击"继续"后调用;自动模拟在 node_entered 回调内立即调用) @@ -204,11 +188,6 @@ func continue_after_node() -> bool: return _enter_next_node() -func submit_window_selection(selection: Array) -> void: - if _settlement != null: - _settlement.submit_window_selection(selection) - - func player_request_refresh() -> bool: if _settlement == null: return false diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/minesweeper_rules.gd b/prototypes/dungeon-sweeper-vertical-slice/core/minesweeper_rules.gd index d56f72e7..467cd687 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/minesweeper_rules.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/minesweeper_rules.gd @@ -94,12 +94,12 @@ func install_board(b: BoardData, during_round := false) -> Dictionary: _spawn = {} for c in b.spawn_cells(): _spawn[c] = true - # 初始展开:O₀ = R₀ 恰等(免费/计入清图/无奖励/永不触雷);板级一次跨轮不重放 + # 初始展开:O₀ = R₀ 恰等(免费/计入清图/无格面资源/永不触雷);板级一次跨轮不重放 for c in _initial_expansion_order(b): _revealed[c] = {"kind": "safe", "digit": b.digit_of(c)} cell_revealed.emit({ "cell": c, "kind": "safe", "digit": b.digit_of(c), - "variant_ids": [], "channel": "initial", "reward_eligible": false, + "variant_ids": [], "channel": "initial", "resource": false, }) _hero = b.spawn_center return _outcome(true, "executed", "", "") @@ -255,17 +255,19 @@ func _do_dig(c: Vector2i) -> Dictionary: continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|) var is_mine := _board.is_mine(rc) var channel := "dig" if rc == c else "chain" + # 格面资源资格(2026-09-15 格面资源批):chain/dig 恒 true——安全格与雷格一视同仁(雷格亦携带, + # 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。 if is_mine: _revealed[rc] = {"kind": "mine", "digit": -1} cell_revealed.emit({ "cell": rc, "kind": "mine", "digit": -1, - "variant_ids": [], "channel": channel, "reward_eligible": false, + "variant_ids": [], "channel": channel, "resource": true, }) else: _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": channel, "reward_eligible": true, + "variant_ids": _board.variant_ids(rc), "channel": channel, "resource": true, }) if action_economy.phase != action_economy.PHASE_ACTIVE: _emit_round_end_flags_and_payload() diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/reward_economy.gd b/prototypes/dungeon-sweeper-vertical-slice/core/reward_economy.gd index 789c811f..6d965b28 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/reward_economy.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/reward_economy.gd @@ -1,51 +1,42 @@ # VERTICAL SLICE - NOT FOR PRODUCTION -# Validation Question: 可选类供给能否按"格极稀 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制 -# Date: 2026-09-10 -# 奖励道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)· -# 授予期零随机 · 库存关卡域(battle_over 全清)· 道具伤害恒定表值 · Ready 后只读。 -# 2026-09-11 可选类供给控制器批(用户拍板):格子池只剩即时类;可选类经 RewardSupplyController 注入 -# (战斗首件保证 + p0×decay^(j-1) 指数递减);旧 F5 格池刀保底(k_dmg)休眠;清图槽改一次性伤害。 -# 2026-09-14 伤害量值区间批(用户拍板②):区间行(power_min/max 可选配对)在身份预分配批内追加取号 -# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,战斗结算/呈现零改动;授予期零随机不变。 -# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定)。 -# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md) +# Validation Question: 每个非封锁格是否都携带可兑的格面资源身份,且同种子同操作逐位回放 +# Date: 2026-09-10(2026-09-15 格面资源批结构性重写:库存/供给轴下线,格面资源身份面纯化) +# 地图战斗资源:格面资源的身份层——install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流 +# 恰一 scope 批:格批 + 清图批尾)· 授予期零随机 · Ready 后只读、无重掷 · 换图映射消亡。 +# 安全格与雷格一视同仁(同一类型池、同一权重分布——雷安同池);封锁格不入摇号清单。 +# 极性不在资源内:「动作 × 格型」矩阵的字典定义归 GDD、路由执行归 战斗结算 授予段 +# (本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面)。 +# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定); +# 区间行 power roll 在身份批内追加取号(伤害量值区间批——快照 power = roll 值,授予期零随机不变)。 +# 实现 GDD:design/gdd/systems/战斗循环/地图战斗资源.md(v0.4 格面资源批——registry 追溯锚) class_name RewardEconomy extends RefCounted var db var rng -var _supply: RewardSupplyController # 可选类供给状态机(组合件——战斗域计数) -var _phase := "unrolled" # unrolled → ready → retired;clear_inventory 后 _inventory_cleared -var _inventory_cleared := false +var _phase := "unrolled" # unrolled → ready → retired var _eligible: Array = [] # [{cell}] 行主序规范序 -var _mapping := {} # Vector2i → reward_id -var _power_roll := {} # Vector2i → 区间行 roll 值(伤害量值区间批——身份批内追加取号) +var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载) +var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号) var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行) var _granted := {} # Vector2i → 快照(重复受理幂等返回) var _clear_identity := "" var _clear_granted := false var _clear_snapshot := {} -var _inventory: Array = [] # 逐件实例 {item_id, snapshot, source_cell, round_index, status} func _init(db_ref, rng_ref) -> void: db = db_ref rng = rng_ref - _supply = RewardSupplyController.new(db.reward_config) -# ---- 战斗开始(战斗结算.begin_battle 编排调用——奖励道具经济 唯一调用方纪律不变)---- -# 供给控制器战斗域计数清零;库存(关卡域)不受影响——关内延续照旧 -func notify_battle_start() -> void: - _supply.notify_battle_start() - - -# ---- F1/F2/F5:install 期 roll(恰一次;Ready 相位再调拒绝)---- +# ---- install 期 roll(恰一次;Ready 相位再调拒绝)---- +# eligible = 全部非封锁格(安全格 + 雷格,战斗结算 install 编排供给;封锁格不入清单) func roll_board(eligible: Array) -> bool: # 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半) if _phase == "ready": - push_warning("[奖励道具经济] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)") + push_warning("[地图战斗资源] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)") return false if eligible.is_empty(): _fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)") @@ -56,7 +47,7 @@ func roll_board(eligible: Array) -> bool: for e in eligible: var c: Vector2i = e.cell if seen.has(c): - push_warning("[奖励道具经济] 资格清单重复格已去重: %s" % str(c)) + push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c)) continue seen[c] = true list.append({"cell": c}) @@ -67,22 +58,16 @@ func roll_board(eligible: Array) -> bool: _clear_power = -1 _clear_granted = false _clear_snapshot = {} - var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(身份批 + 清图槽 + 注入判定/选格 + 区间行 power roll——追加式取号,既有序零扰动) - var safe_rows: Array = _pool_rows("safe_cell") + var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) + var cell_rows: Array = _rollable_rows() # cell 池正向行 weight>0(类型频次) var clear_rows: Array = _pool_rows("clear_default") - var inject_rows: Array = _pool_rows("active_inject") - # ① 身份批:每资格格恰一即时类身份(可选类已退出格子池——供给控制器批) + # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) var draft := {} for e in list: - draft[e.cell] = weighted_pick(safe_rows, stream.randi_range(0, _pool_weight(safe_rows) - 1)) - # ② 清图槽:一次性高额伤害(当场兑付类——不入库、不占可选类供给;2026-09-11 用户拍板) + draft[e.cell] = weighted_pick(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)) + # ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份 var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)) - # ③ 可选类供给注入(替代旧 F5 刀保底):首件保证 / p0×decay^(j-1) 指数递减——判定恰一取号 - if _supply.should_inject(stream): - var target = list[stream.randi_range(0, list.size() - 1)] # 均匀随机(拍板③) - draft[target.cell] = weighted_pick(inject_rows, stream.randi_range(0, _pool_weight(inject_rows) - 1)) - # ④ 伤害量值区间批(2026-09-14):身份定稿后逐格 roll power(区间行恰一取号,定值行零取号); - # 授予期零随机纪律不变——快照 E14 口径,战斗结算/呈现零改动 + # ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号) for cell in draft.keys(): var row: Dictionary = _row_by_id(draft[cell]) if row.has("power_min"): @@ -96,31 +81,17 @@ func roll_board(eligible: Array) -> bool: return true -# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 奖励道具经济/关卡节点地图 修订)---- -# 奖励点 = 用户拍板的关卡节点类型:在本表全部可选类活行(damage_item / heal_pack,即"主动道具") -# 中 roll 一件并直接入库。消耗 reward_roll 恰一 scope(与 board install 批同流异 scope——流集合封闭不变)。 -# cell=(-1,-1)/round_index=0 = 非格子来源标记(关卡节点授予,区别于安全格拾取的呈现面)。 -func grant_node_active(cell: Vector2i, round_index: int) -> Dictionary: - var pool: Array = [] - for row in db.reward.rows: - if int(row.weight) > 0 and (String(row.category) == "damage_item" or String(row.category) == "heal_pack"): - pool.append(row) - if pool.is_empty(): - _fatal("节点奖励池为空(damage_item/heal_pack 零活行)") - return {} - var stream: RandomNumberGenerator = rng.open_stream("reward_roll") - var rid := weighted_pick(pool, stream.randi_range(0, _pool_weight(pool) - 1)) - var row: Dictionary = _row_by_id(rid) - var rolled := -1 - if row.has("power_min"): - rolled = stream.randi_range(int(row.power_min), int(row.power_max)) # 区间行恰一追加取号(伤害量值区间批) - var inst = stock(_snapshot(row, rolled), cell, round_index) - if inst is Dictionary: - _supply.notify_granted() # 节点奖励 = 可选类实际发放——与 grant() 同一入账点(playtest-2026-09-12 报告 Bug#3:绕过供给计数修正) - return inst +# ---- 查询面(Ready 只读;未开格恒可查——魔法道具 桥接结构前提)---- + +# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段) +func phase() -> String: + return _phase -# ---- 查询面(Ready 只读)---- +# 静默映射查询(批量工具用——零 E19 诊断;语义 = query_identity 非 null 的布尔投影) +func is_mapped(cell: Vector2i) -> bool: + return _phase == "ready" and _mapping.has(cell) + func query_identity(cell: Vector2i): if _phase != "ready": @@ -128,14 +99,15 @@ func query_identity(cell: Vector2i): return null var rid: String = _mapping.get(cell, "") if rid == "": - _diag("query_identity 清单外坐标(E19——未分配非错误态)") + _diag("query_identity 清单外坐标(E19——未分配非错误态;封锁格不入清单)") return null var row: Dictionary = _row_by_id(rid) - return {"reward_id": rid, "category": String(row.category), "tier": int(row.tier)} + return {"reward_id": rid, "cell_type": String(row.cell_type), "tier": int(row.tier)} # 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批): -# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 战斗结算 冻结授予直通 +# 返回与 grant 相同结构的快照(cell_type/display_name/power/effect_id/stacks 齐备), +# 供 战斗结算 冻结授予直通 func query_full_snapshot(cell: Vector2i): if _phase != "ready": _diag("query_full_snapshot 非 Ready 相位") @@ -146,7 +118,7 @@ func query_full_snapshot(cell: Vector2i): return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1))) -# ---- 受理面(战斗结算 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)---- +# ---- 受理面(战斗结算 编排期调用;E6 收口 2026-09-13:失败统一返回 null)---- func grant(cell: Vector2i): if _phase != "ready": @@ -158,10 +130,7 @@ func grant(cell: Vector2i): if _granted.has(cell): _diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 战斗结算)") return _granted[cell] - var row: Dictionary = _row_by_id(_mapping[cell]) - var snap := _snapshot(row, int(_power_roll.get(cell, -1))) - if String(snap.category) == "damage_item" or String(snap.category) == "heal_pack": - _supply.notify_granted() # 发放即获取——首件保证/递减计数的唯一入账点(发放侧语义,满血拒"使用"不影响) + var snap := _snapshot(_row_by_id(_mapping[cell]), int(_power_roll.get(cell, -1))) _granted[cell] = snap return snap @@ -179,86 +148,6 @@ func grant_clear(): return _clear_snapshot -func stock(snapshot: Dictionary, source_cell: Vector2i, round_index: int): - if _inventory_cleared: - _diag("stock 战斗已清库(E7)") - return null - var cat: String = String(snapshot.get("category", "")) - if cat != "damage_item" and cat != "heal_pack": - _diag("stock 仅受理道具类(E20): %s" % cat) - return null - var inst := { - "item_id": String(snapshot.get("reward_id", "")), - "snapshot": snapshot, - "source_cell": source_cell, - "round_index": round_index, - "status": "InStock", - } - _inventory.append(inst) # 数量无上限;枚举序 = stock 受理序(E21) - return inst - - -# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 战斗结算) -func use(index: int): - if _inventory_cleared: - _diag("use 战斗已清库") - return null - if index < 0 or index >= _inventory.size(): - _diag("use 句柄越界") - return null - var inst: Dictionary = _inventory[index] - if inst.status != "InStock": - _diag("use 已消耗句柄(E9 Used 不可逆)") - return null - inst.status = "Used" - return { - "item_id": inst.item_id, - "category": String(inst.snapshot.get("category", "")), - "power": int(inst.snapshot.get("power", 0)), # 授予时快照值(E14——非表读值) - "source_cell": inst.source_cell, - "round_index": inst.round_index, - } - - -func query_inventory() -> Array: - var out: Array = [] - for i in _inventory.size(): - var inst: Dictionary = _inventory[i] - out.append({ - "index": i, - "item_id": inst.item_id, - "category": String(inst.snapshot.get("category", "")), - "display_name": String(inst.snapshot.get("display_name", "")), - "power": int(inst.snapshot.get("power", 0)), - "source_cell": inst.source_cell, - "round_index": inst.round_index, - "status": inst.status, - }) - return out - - -func in_stock_count() -> int: - var n := 0 - for inst in _inventory: - if inst.status == "InStock": - n += 1 - return n - - -# 观测面(冒烟/调参工具——本战斗已实际获取的可选类件数) -func supply_granted_count() -> int: - return _supply.granted_count() - - -func clear_inventory() -> bool: - if _inventory_cleared: - _diag("clear_inventory 重复调用(幂等守卫)") - return false - _inventory.clear() # battle_over 全清不跨战斗 - _inventory_cleared = true - return true - - func retire_mapping() -> bool: if _phase != "ready": _diag("retire_mapping 非 Ready 相位(幂等受理族)") @@ -271,7 +160,19 @@ func retire_mapping() -> bool: return true -# ---- F6 当量供给(战斗结算 条款①对照实物输入)---- +# ---- 极性路由字典面(战斗结算 授予段消费):(类型, 极性)→ 格面池字典行(拷贝出口)---- + +func polarity_row(cell_type: String, polarity: String) -> Dictionary: + for row in db.reward.rows: + if String(row.get("pool_tag", "")) == "cell" \ + and String(row.get("cell_type", "")) == cell_type \ + and String(row.get("polarity", "")) == polarity: + return row.duplicate() # E6:表行值拷贝出口——消费方禁改配置源 + _fatal("极性字典缺行(五型×正负齐备为加载前提): %s %s" % [cell_type, polarity]) + return {} + + +# ---- F6 当量供给(行动经济 条款①对照实物输入——全活行投影 + tier_min_buff)---- func equivalence_supply() -> Dictionary: var live: Array = [] @@ -280,8 +181,12 @@ func equivalence_supply() -> Dictionary: if int(row.weight) <= 0: continue live.append(row.duplicate()) # E6:表行值拷贝出口——消费方禁改配置源 - if row.pool_tag == "safe_cell" and row.category == "buff": - tier_min_buff = mini(tier_min_buff, int(row.tier)) + # tier_min_buff = 我侧增益(hero_offense/hero_defense 池的 percent 正向行——attack_up/defense_up)最低档 + if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" \ + and String(row.get("effect_id", "")) != "": + var eff_row: Dictionary = db.status_effect_row(String(row.effect_id)) + if not eff_row.is_empty() and String(eff_row.get("target_side", "")) == "hero": + tier_min_buff = mini(tier_min_buff, int(row.tier)) return {"live_rows": live, "tier_min_buff": tier_min_buff} @@ -299,6 +204,15 @@ func weighted_pick(rows: Array, r: int) -> String: return "" +func _rollable_rows() -> Array: + # 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪) + var out: Array = [] + for row in db.reward.rows: + if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0: + out.append(row) + return out + + func _pool_rows(pool_tag: String) -> Array: var out: Array = [] for row in db.reward.rows: @@ -324,10 +238,11 @@ func _row_by_id(rid: String) -> Dictionary: func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary: var snap := { "reward_id": String(row.reward_id), - "category": String(row.category), + "cell_type": String(row.cell_type), "display_name": String(row.display_name), "power": int(row.power) if rolled_power < 0 else rolled_power, # 区间行 = 身份批 roll 值(E14 授予时快照口径) "tier": int(row.tier), + "stacks": int(row.get("stacks", 1)), } if String(row.get("effect_id", "")) != "": snap["effect_id"] = String(row.effect_id) @@ -335,9 +250,9 @@ func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary: func _diag(msg: String) -> void: - push_warning("[奖励道具经济] " + msg) + push_warning("[地图战斗资源] " + msg) func _fatal(msg: String) -> void: - push_error("[奖励道具经济/fatal] " + msg) + push_error("[地图战斗资源/fatal] " + msg) assert(false, msg) diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd b/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd deleted file mode 100644 index de830173..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd +++ /dev/null @@ -1,43 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# Validation Question: 可选类供给能否按"格极稀 <0.01 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制 -# Date: 2026-09-11 -# 可选类供给控制器(2026-09-11 用户拍板批——奖励道具经济 私有组合件): -# 格子权重池不再产出可选类;本控制器在 install 期决定"本板是否注入 1 件可选类"—— -# 战斗内尚未获取首件 → 本板必注入(首件保证);已获取 j 件 → 概率 p0×decay^(j-1)(指数下降、永不绝育)。 -# 行值快照(p0/decay 构造期快照——抗热重载,敌人系统 同款);零自有随机数(判定取号经 奖励道具经济 传入的 reward_roll -# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 奖励道具经济.grant 挂钩上报)。 -# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md) -class_name RewardSupplyController -extends RefCounted - -var _granted := 0 # 本战斗内已实际获取的可选类件数(战斗域——notify_battle_start 清零) -var _p0 := 0.4 -var _decay := 0.5 - - -func _init(reward_config) -> void: - _p0 = clampf(float(reward_config.active_p0), 0.0, 1.0) - _decay = clampf(float(reward_config.active_decay), 0.001, 1.0) - - -# 战斗结算.begin_battle 编排调用(奖励道具经济 转发)——战斗域计数重置;跨战斗库存延续不受影响 -func notify_battle_start() -> void: - _granted = 0 - - -# install 期注入判定(奖励道具经济.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次) -func should_inject(stream: RandomNumberGenerator) -> bool: - if _granted == 0: - return true # 首件保证 - var p: float = _p0 * pow(_decay, float(_granted - 1)) - return stream.randf() < p - - -# 奖励道具经济.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义) -func notify_granted() -> void: - _granted += 1 - - -# 观测面(冒烟/调参工具) -func granted_count() -> int: - return _granted diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd.uid deleted file mode 100644 index fabba169..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/core/reward_supply_controller.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://de40p45nneaw1 diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd index 3f290277..e5001301 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd @@ -4,7 +4,7 @@ # RngService autoload(数据配置)——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。 # 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md) # R2 派生公式(黄金向量已预验证):derive = (SALT XOR wrap64(master + scope×GOLDEN)) AND 0x7FFF… -# 取号语义:全新实例首个 open_stream = scope 0(奖励道具经济 预推进术语的登记口径)。 +# 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。 extends Node const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步) diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd deleted file mode 100644 index 0d0c81ce..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd +++ /dev/null @@ -1,27 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) -# Date: 2026-09-10 -# 数据配置 RewardConfigTable schema(奖励道具经济 full 评审接口漂移新增;单行全局——奖励道具经济 标量 knob 的 Rule 1 载体)。 -class_name RewardConfigTable -extends Resource - -@export var k_dmg: int = 0 # [0,999] 旧 F5 格池刀保底——2026-09-11 可选类供给控制器批置 0(语义由"战斗首件保证"取代) -@export var n_draft: int = 32 # [1,999] 旧 F5 草稿重 roll 上限(随 k_dmg=0 休眠——字段保留) -@export var p_min: int = 4 # [1,999] K≥1 供给闸刀力下界(随 k_dmg=0 休眠——字段保留) -@export var active_p0: float = 0.4 # [0,1] 可选类供给:战斗首件之后,每张新板的注入基础概率(2026-09-11 用户拍板批) -@export var active_decay: float = 0.5 # (0,1] 可选类供给:战斗内每多获取 1 件,注入概率乘一次衰减(指数下降、永不绝育——用户拍板④) - - -func validate() -> Array: - var errs: Array = [] - if k_dmg < 0 or k_dmg > 999: - errs.append({"category": "domain", "message": "k_dmg 越域 [0,999]: %d" % k_dmg}) - if n_draft < 1 or n_draft > 999: - errs.append({"category": "domain", "message": "n_draft 越域 [1,999]: %d" % n_draft}) - if p_min < 1 or p_min > 999: - errs.append({"category": "domain", "message": "p_min 越域 [1,999]: %d" % p_min}) - if active_p0 < 0.0 or active_p0 > 1.0: - errs.append({"category": "domain", "message": "active_p0 越域 [0,1]: %f" % active_p0}) - if active_decay <= 0.0 or active_decay > 1.0: - errs.append({"category": "domain", "message": "active_decay 越域 (0,1]: %f" % active_decay}) - return errs diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd.uid deleted file mode 100644 index e78c4da6..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_config_table.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dc14jo26wutra diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_table.gd index 2b9ccd9c..17b1f66b 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/reward_table.gd @@ -1,16 +1,19 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) -# Date: 2026-09-10 -# 数据配置 RewardTable / RewardConfigTable schema(奖励道具经济 回填值)。 -# RewardTable 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。 +# Date: 2026-09-10(2026-09-15 格面资源批结构性重写:category/use_timing/库存/供给轴下线,五型 × 极性字典取代) +# 数据配置 RewardTable schema(地图战斗资源 回填值)。 +# 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。 +# 五型封闭枚举(attack/defense/heal/blast/weaken)× 极性(positive/negative): +# 正向行 = 摇号池(权重 = 类型频次);负向行 = 纯兑付字典(不消费权重)。 class_name RewardTable extends Resource @export var rows: Array = [] # Array[Dictionary] -const CATEGORIES := ["buff", "damage_item", "heal_pack", "instant_aoe", "instant_heal"] # 2026-09-11 两类奖励裁决五值:即时类 {buff, instant_aoe, instant_heal} / 可选类 {damage_item, heal_pack} -const POOL_TAGS := ["safe_cell", "clear_default", "active_inject"] # active_inject = 可选类注入池(2026-09-11 供给控制器批——可选类退出格子权重池);clear_ 命名空间预留拒绝 -const USE_TIMINGS := ["pre_settlement"] # MVP 单值承重 +const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken"] # 五型封闭——清单外加载拒绝 +const POLARITIES := ["positive", "negative"] +const POOL_TAGS := ["cell", "clear_default"] # cell = 格面资源单一池;clear_ 命名空间预留拒绝 +const CELL_TYPE_KEYS := ["attack", "defense", "heal", "blast", "weaken"] # 字典完整性校验轴(类型 × 极性 = 十格) func validate() -> Array: @@ -19,6 +22,9 @@ func validate() -> Array: errs.append({"category": "empty_table", "message": "RewardTable 为空"}) return errs var seen := {} + var cell_dict := {} # "type|polarity" → row(字典完整性) + var cell_positive_live := 0 # cell 池 weight>0 正向行计数(池空护栏) + var clear_value_ok := false # clear_default 恒正当量(行动经济 条款④) for row in rows: var rid: String = row.get("reward_id", "") if rid == "": @@ -27,17 +33,18 @@ func validate() -> Array: if seen.has(rid): errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid}) seen[rid] = true - var cat: String = row.get("category", "") - if not CATEGORIES.has(cat): - errs.append({"category": "enum", "message": "%s category 出域/预留值拒绝: %s" % [rid, cat]}) + var ctype: String = row.get("cell_type", "") + if not CELL_TYPES.has(ctype): + errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]}) + var pol: String = row.get("polarity", "") + if not POLARITIES.has(pol): + errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]}) var pool: String = row.get("pool_tag", "") var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default" if not POOL_TAGS.has(pool) and not pool_reserved: errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]}) elif pool_reserved: errs.append({"category": "enum", "message": "%s pool_tag clear_ 命名空间预留拒绝: %s" % [rid, pool]}) - if not USE_TIMINGS.has(row.get("use_timing", "")): - errs.append({"category": "enum", "message": "%s use_timing 出域: %s" % [rid, row.get("use_timing")]}) if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "%s display_name 空" % rid}) var power: int = int(row.get("power", 0)) @@ -47,15 +54,20 @@ func validate() -> Array: errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid}) if tier < 1 or tier > 8: errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid}) + var stacks: int = int(row.get("stacks", 1)) + if stacks < 1 or stacks > 16: + errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]}) + # 跨字段:effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。 + # flat 行 power ≥ 1(直付行:治疗/群伤/直伤)——例外 = blast 负向(伏击加层行,消费 stacks 不消费 power)。 var eff: String = row.get("effect_id", "") - # 跨字段:buff ⇒ effect_id 必填;非 buff ⇒ 恒空;item 类 power ≥ 1 - if cat == "buff" and eff == "": - errs.append({"category": "cross_field", "message": "%s buff 行 effect_id 必填" % rid}) - if cat != "buff" and eff != "": - errs.append({"category": "cross_field", "message": "%s 非 buff 行 effect_id 恒空" % rid}) - if cat != "buff" and power < 1: # 2026-09-11:即时类(aoe/heal)与道具类同 consumes power ≥ 1 - errs.append({"category": "cross_field", "message": "%s item 行 power ≥ 1" % rid}) - # 伤害量值区间批(2026-09-14):可选配对 power_min/power_max——缺省 = 定值行向后兼容(armor_type 先例) + var is_layer_row: bool = eff == "" and ctype == "blast" and pol == "negative" + if power < 0 or power > 999: + errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]}) + if eff == "" and power < 1 and not is_layer_row: + errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid}) + if is_layer_row and int(row.get("stacks", 1)) < 1: + errs.append({"category": "cross_field", "message": "%s 加层行 stacks ≥ 1" % rid}) + # 区间配对(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚 var has_min: bool = row.has("power_min") var has_max: bool = row.has("power_max") if has_min != has_max: @@ -65,9 +77,42 @@ func validate() -> Array: var pmax: int = int(row.get("power_max", 0)) if pmin < 1 or pmax > 999 or pmin > pmax: errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]}) - if cat == "buff": - errs.append({"category": "cross_field", "message": "%s buff 行禁区间字段(power 恒 0)" % rid}) + if eff != "": + errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid}) if power != int((pmin + pmax) / 2.0): # 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power) errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]}) + # 字典完整性 / 池护栏累计(cell 池行) + if pool == "cell" and CELL_TYPES.has(ctype) and POLARITIES.has(pol): + cell_dict["%s|%s" % [ctype, pol]] = true + if pol == "positive" and weight > 0: + cell_positive_live += 1 + if pool == "clear_default" and eff == "" and power >= 1: + clear_value_ok = true + # ① 字典完整性:五型 × 正负两行齐备(缺任一即致命) + for ctype in CELL_TYPE_KEYS: + for pol in POLARITIES: + if not cell_dict.has("%s|%s" % [ctype, pol]): + errs.append({"category": "cross_field", "message": "五型字典缺行: %s %s——(类型, 极性) 齐备为加载前提" % [ctype, pol]}) + # ② 池空护栏:cell 池 ≥1 正向行 weight>0(摇号池非空) + if cell_positive_live < 1: + errs.append({"category": "cross_field", "message": "cell 池零活正向行(weight>0)——摇号池空"}) + # ③ clear_default 恒正当量(行动经济 条款④——清图奖励 > 0 的行级兑现口径) + if not clear_value_ok: + errs.append({"category": "cross_field", "message": "clear_default 池无正当量活行(清图奖励 > 0 违例)"}) return errs + + +func rows_by_pool(pool_tag: String) -> Array: + var out: Array = [] + for row in rows: + if row.get("pool_tag", "") == pool_tag: + out.append(row) + return out + + +func row_by_id(rid: String) -> Dictionary: + for row in rows: + if row.get("reward_id", "") == rid: + return row + return {} diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd index 9662c583..1c10dcda 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/settlement_table.gd @@ -25,7 +25,7 @@ extends Resource const ARMOR_KEYS := ["light", "ward", "heavy"] const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"] -const FROZEN_SEGMENT_ORDER := ["grant", "item_window", "buff_debuff", "item", "combat", "decay", "verdict"] +const FROZEN_SEGMENT_ORDER := ["grant", "buff_debuff", "combat", "decay", "verdict"] const P_PERFECT_DOMAIN := [100, 200] diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres index a07038d1..662f57fd 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward.tres @@ -5,86 +5,112 @@ [resource] script = ExtResource("1") rows = [{ -"reward_id": "defense_up_basic", -"category": "buff", -"effect_id": "defense_up", -"display_name": "石肤术", -"power": 0, -"weight": 16, -"pool_tag": "safe_cell", -"tier": 2, -"use_timing": "pre_settlement" -}, { -"reward_id": "attack_up_basic", -"category": "buff", +"reward_id": "attack_up", +"cell_type": "attack", +"polarity": "positive", "effect_id": "attack_up", -"display_name": "增伤术", +"display_name": "增伤", "power": 0, "weight": 20, -"pool_tag": "safe_cell", -"tier": 2, -"use_timing": "pre_settlement" +"pool_tag": "cell", +"tier": 2 +}, { +"reward_id": "defense_up", +"cell_type": "defense", +"polarity": "positive", +"effect_id": "defense_up", +"display_name": "减伤", +"power": 0, +"weight": 16, +"pool_tag": "cell", +"tier": 2 }, { "reward_id": "spark_aoe", -"category": "instant_aoe", +"cell_type": "blast", +"polarity": "positive", "effect_id": "", "display_name": "爆裂粉尘", "power": 4, "weight": 16, -"pool_tag": "safe_cell", -"tier": 1, -"use_timing": "pre_settlement" +"pool_tag": "cell", +"tier": 1 }, { "reward_id": "heal_minor", -"category": "instant_heal", +"cell_type": "heal", +"polarity": "positive", "effect_id": "", "display_name": "急救敷料", "power": 4, "power_min": 2, "power_max": 6, "weight": 15, -"pool_tag": "safe_cell", -"tier": 1, -"use_timing": "pre_settlement" +"pool_tag": "cell", +"tier": 1 }, { -"reward_id": "knife_t1", -"category": "damage_item", -"effect_id": "", -"display_name": "小刀", -"power": 8, -"power_min": 5, -"power_max": 11, -"weight": 60, -"pool_tag": "active_inject", -"tier": 1, -"use_timing": "pre_settlement" +"reward_id": "weaken", +"cell_type": "weaken", +"polarity": "positive", +"effect_id": "weaken", +"display_name": "削弱", +"power": 0, +"weight": 12, +"pool_tag": "cell", +"tier": 1 }, { -"reward_id": "knife_adv", -"category": "damage_item", -"effect_id": "", -"display_name": "精工短刀", -"power": 16, -"power_min": 10, -"power_max": 22, -"weight": 30, -"pool_tag": "active_inject", -"tier": 2, -"use_timing": "pre_settlement" +"reward_id": "enrage", +"cell_type": "attack", +"polarity": "negative", +"effect_id": "enrage", +"display_name": "敌方暴走", +"power": 0, +"weight": 0, +"pool_tag": "cell", +"tier": 2 }, { -"reward_id": "heal_pack", -"category": "heal_pack", +"reward_id": "off_guard", +"cell_type": "defense", +"polarity": "negative", +"effect_id": "off_guard", +"display_name": "我方失衡", +"power": 0, +"weight": 0, +"pool_tag": "cell", +"tier": 2 +}, { +"reward_id": "heal_backlash", +"cell_type": "heal", +"polarity": "negative", "effect_id": "", -"display_name": "治疗包", -"power": 12, -"power_min": 6, -"power_max": 18, -"weight": 10, -"pool_tag": "active_inject", -"tier": 2, -"use_timing": "pre_settlement" +"display_name": "反噬震荡", +"power": 4, +"weight": 0, +"pool_tag": "cell", +"tier": 1 +}, { +"reward_id": "ambush_double", +"cell_type": "blast", +"polarity": "negative", +"effect_id": "", +"display_name": "敌方伏击", +"power": 0, +"stacks": 2, +"weight": 0, +"pool_tag": "cell", +"tier": 1 +}, { +"reward_id": "sapped", +"cell_type": "weaken", +"polarity": "negative", +"effect_id": "sapped", +"display_name": "我方乏力", +"power": 0, +"weight": 0, +"pool_tag": "cell", +"tier": 1 }, { "reward_id": "clear_blast", -"category": "instant_aoe", +"cell_type": "blast", +"polarity": "positive", "effect_id": "", "display_name": "清障炸药", "power": 40, @@ -92,21 +118,15 @@ rows = [{ "power_max": 55, "weight": 1, "pool_tag": "clear_default", -"tier": 2, -"use_timing": "pre_settlement" +"tier": 2 }] -; 2026-09-11 可选类供给控制器批(用户拍板): -; 可选类三行移出 safe_cell 权重池 → active_inject 注入池(小刀 60 / 精工短刀 30 / 治疗包 10——治疗资源宝贵), -; 由 RewardSupplyController 在 install 期注入(首件保证 + 指数递减),不再走格子权重 roll。 -; 清图槽 clear_knife(入库刀)→ clear_blast(instant_aoe 一次性高额伤害,当场兑付全体存活敌,不入库不占可选类供给)。 -; 格子池现仅即时类四行(attack_up 20 / defense_up 16 / spark_aoe 16 / heal_minor 15,合计 67)。 -; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发):defense_up 24 → 16—— -; 石肤自 32% 降至 23.9%,攻击成为池内最高行;与 settlement.tres pool_clamp_lo=-60 同批(免伤墙双杠杆)。 - -; 伤害量值区间批(2026-09-14 用户拍板②——"上限掺运气"第一步,奖励侧五行:上阶/下阶区间内随机; -; 中点恒等旧定值 = A3 条款①期望当量锚不漂移;宽度 provisional 归 C3 测量迭代。验收口径 = 完美策略档 -; 0 加成通关率 ≤40%〔OQ7 完美档基线 100%〕)。roll 时机 = 身份预分配批(reward_roll 恰一 scope 批内 -; 追加取号,既有取号序零扰动——追加式);快照 E14 口径不变,B1/呈现零改动。区间: -; heal_minor [2,6] · knife_t1 [5,11] · heal_pack [6,18] · knife_adv [10,22] · clear_blast [25,55]。 -; buff 两行(attack_up/defense_up)与 spark_aoe 不在本批(即时光效类按拍板面五行为准)。 +; 格面资源批(2026-09-15 用户拍板——registry reward_table 条目为现行 schema 事实源): +; 十行格面池(五型 × 正负各一行)+ 清图槽一行。安全格与雷格同池同分布(雷安同池), +; 装板期一次摇定;摇号只消费正向行权重(类型频次 = attack 20 / defense 16 / blast 16 / heal 15 / weaken 12,合计 79); +; 负向行 weight=0 不参与摇号,纯(类型, 极性)兑付字典——heal 负向 = 主角直伤 4(不经增益状态), +; blast 负向 = ambush +2 层(stacks=2,伏击池 cap 与"溢出未生效"行协议不变)。 +; 可选类三行(knife_t1/knife_adv/heal_pack)与注入池 active_inject 随可选类道具整体删除(供给控制器/保底/窗口同步下线)。 +; 区间行(伤害量值区间批 2026-09-14):heal_minor [2,6] · clear_blast [25,55]——中点锚 power 恒等期望当量, +; roll 时机 = 身份预分配批内追加取号(既有取号序零扰动);spark_aoe 定值行无区间字段。 +; 减伤墙调参批(2026-09-13):defense_up 权重 24→16——攻击成为池内最高行(历史注记)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward_config.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward_config.tres deleted file mode 100644 index c4218246..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/reward_config.tres +++ /dev/null @@ -1,15 +0,0 @@ -[gd_resource type="Resource" script_class="RewardConfigTable" load_steps=2 format=3] - -[ext_resource type="Script" path="res://data/schemas/reward_config_table.gd" id="1"] - -[resource] -script = ExtResource("1") -k_dmg = 0 -n_draft = 32 -p_min = 8 -active_p0 = 0.4 -active_decay = 0.5 - -; 2026-09-11 可选类供给控制器批(用户拍板):k_dmg 1→0(旧 F5 格池刀保底休眠,语义由"战斗首件保证"取代); -; active_p0 = 0.4 / active_decay = 0.5(首件之后每张新板注入概率 0.4 → 0.2 → 0.1 → …指数下降、永不绝育; -; provisional 起步值,标定目标 E[战斗获取] ≈ 1,level_monte_carlo 扫描验证后归 C3 定稿)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres index 7593f7f7..8527c810 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/settlement.tres @@ -6,13 +6,12 @@ script = ExtResource("1") equivalence_ref_damage = 12 p_perfect = 200 -segment_order = PackedStringArray("grant", "item_window", "buff_debuff", "item", "combat", "decay", "verdict") +segment_order = PackedStringArray("grant", "buff_debuff", "combat", "decay", "verdict") source_label_map = { "safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励", -"item": "道具", "enemy_action": "敌方行动", "hero_attack": "主角攻击", "flag_miss": "标旗未中" @@ -23,7 +22,8 @@ effect_pool_map = { "weaken": "enemy_output", "ambush": "flat_attach", "enrage": "enemy_output", -"off_guard": "hero_defense" +"off_guard": "hero_defense", +"sapped": "hero_offense" } pool_clamp_lo = -100 pool_clamp_hi = 100 @@ -61,15 +61,14 @@ armor_effectiveness = { ; 攻击类型 × 护甲克制批(2026-09-12 用户拍板结构;2026-09-15 命名统一批 + 抽象分离批,数值 provisional 待用户/C3 调参): ; 主角攻击 = 普通(atk×(1+attack_up),对全部护甲 100%)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加; ; 克制语义 = 克制 ×150 / 中性 ×100 / 被克 ×50(穿甲弹/高爆弹式对策三角)。 -; 【两条抽象分离,勿焊死】攻击类型 {normal, pierce, magic} = 普通 / 穿刺 / 魔法(原 {base, dmg_a, dmg_b})= +; 【两条抽象分离,勿焊死】攻击类型 {normal, pierce, magic} = 普通 / 穿刺 / 魔法 = ; 伤害的属性轴,只决定查矩阵哪一行,定义里不含任何棋盘来源;护甲类型 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲 -; (原 {none, a, b})是敌单位属性。充能 = 按攻击类型分桶的附加伤害储备:谁能充、充哪个桶 = 奖励定义。 -; charge_grants 现行供给(奖励侧定义,可整体替换——换桶/换量/换成增益或道具都不触动攻击类型与矩阵): -; safe_cell(本轮每开 1 资格格)→ 穿刺桶 1/格、单轮计数上限 8(0 = 不封顶); -; flag_hit(每颗首次标中雷,每雷实例一次去重——忘拔旗不重复充能)→ 魔法桶 2/雷,无上限。 -; 原位置字段 damage_a_per_cell / damage_a_cap / damage_b_per_flag("类型×来源"焊死的命名)已废除。 -; enrage(敌方暴走 +25% 进 enemy_output 池)/ off_guard(我方失衡 +25% 进 hero_defense 池)= -; 标错惩罚池两行(第三项 = 复用 ambush +2 层);时效均 1 结算(用户拍板"短点,比如 1 回合")。 -; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发):拟改 pool_clamp_lo=-60 被 schema -; 冻结域拦截([-100,+100] 结构冻结——B1 F2 乘区结构决策),杠杆改走 status_effects.tres -; defense_up magnitude -50 → -30(叠满两层 = -60 减伤帽,本表 clamp 不动);登记以 status_effects.tres 批注为准。 +; 是敌单位属性。充能 = 按攻击类型分桶的附加伤害储备:谁能充、充哪个桶 = 奖励定义。 +; charge_grants 现行供给(奖励侧定义,可整体替换):safe_cell(本轮每挖开 1 个安全格)→ 穿刺桶 1/格、 +; 单轮计数上限 8(0 = 不封顶);flag_hit(每颗首次标中雷,每雷实例一次去重)→ 魔法桶 2/雷,无上限。 +; 格面资源批(2026-09-15 用户拍板):段序七段 → 五段(item_window/item 两段随可选类道具删除下线); +; source_label_map 删 item 键;effect_pool_map 增 sapped→hero_offense(weaken 型负向——乏力)。 +; enrage(敌方暴走 +25%,enemy_output 池)/ off_guard(我方失衡 +25%,hero_defense 池)/ +; sapped(我方乏力 -25%,hero_offense 池)= attack/defense/weaken 三型负向字典行 +; (极性路由兑付:挖雷格 / 标错安全格按该格 cell_type 取负向行;heal 负向 = 主角直伤不经状态、blast 负向 = ambush+2 层)。 +; 减伤墙调参批(2026-09-13):pool_clamp 域 [-100,+100] 结构冻结不动;减伤杠杆走 status_effects.tres(批注在该表)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres index a74674d4..b1d78109 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/status_effects.tres @@ -58,9 +58,21 @@ rows = [{ "duration_settlements": 1, "stack_policy": "stack", "max_stack": 16 +}, { +"effect_id": "sapped", +"display_name": "乏力", +"target_side": "hero", +"magnitude": -25, +"unit": "percent", +"duration_settlements": 1, +"stack_policy": "stack", +"max_stack": 16 }] ; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发——defense_scan 实测墙轮 39%、 ; 真人局 12/12 轮零承伤满血通关):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽 ; (原拟 pool_clamp_lo=-60 被 schema 冻结域拦截,杠杆移至本表);石肤权重 24→16 随批(reward.tres)。 ; 承伤带对 C3 F4(≤20%/普通场、≤50%/Boss 场)复验责任随批;B3 GDD "-50" 文字同步随下次 B3 修订批。 +; 格面资源批(2026-09-15):新增 sapped(乏力——weaken 型负向,hero_offense 池,attack_up 镜像; +; 进 effect_pool_map 同批登记)——状态表六行→七行;enrage/off_guard 语义 = attack/defense 型负向 +; (标错安全格 / 踩到雷格经极性路由兑付,不再绑"标错反噬"单一来源)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd index 180cb92c..5802dda8 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd @@ -13,9 +13,8 @@ static func node_banner(runner, node: Dictionary, t: String) -> String: "Boss 房" if t == "boss" else "战斗", int(node.get("node_depth", 0))] -static func reward_line(snap: Dictionary) -> String: - return "[color=#d0c090]关卡奖励:%s(+%d)入库[/color]" % [ - String(snap.get("display_name", "")), int(snap.get("power", 0))] +static func reward_line() -> String: + return "[color=#d0c090]关卡奖励点(节点内容待定——直通)[/color]" static func rest_line(healed: int, hp: int, max_hp: int) -> String: @@ -31,34 +30,32 @@ static func level_open_line(level_display: String, seed_val: int, atk: int, max_ # 调试开场行 static func debug_open_line(seed_val: int, level: int, level_mode: bool) -> String: return "[color=#c8a860]种子 %d · 遭遇 %d 层%s[/color]" % [ - seed_val, level, "(关卡模式 · 库存延续)" if level_mode else ""] + seed_val, level, "(关卡模式 · HP 延续)" if level_mode else ""] # 关卡终局文案(result = victory/defeat;载荷 = runner level_finished 快照) static func level_end_text(result: String, payload: Dictionary, effective_max_hp: int) -> String: if result == "victory": - return "关卡通关!\n剩余道具 ×%d 随关卡结束清空 · 剩余 HP %d/%d" % [ - int(payload.get("remaining_items", 0)), int(payload.get("hp", 0)), effective_max_hp] + return "关卡通关!\n剩余 HP %d/%d" % [ + int(payload.get("hp", 0)), effective_max_hp] return "关卡失败……探险者倒在了节点 %d(%s)。" % [ int(payload.get("node_index", 0)) + 1, String(payload.get("node_type", ""))] -# 调试战斗终局文案(level_end = 关内两层模式的关卡末;left = 清库剩余件数) -static func battle_end_text(result: String, level_end: bool, left: int) -> String: +# 调试战斗终局文案(level_end = 关内两层模式的关卡末) +static func battle_end_text(result: String, level_end: bool) -> String: if level_end: - return "关卡通关!\n剩余道具 ×%d 随关卡结束清空" % left + return "关卡通关!" if result == "victory": return "战斗胜利!敌人全灭。" return "战斗失败……探险者倒下了。" # 相位指示文本(B 区轮次·阶段行) -static func phase_text(round_index: int, battle_result: String, window_open: bool, waiting: bool, boss_active: bool, boss_name: String) -> String: +static func phase_text(round_index: int, battle_result: String, waiting: bool, boss_active: bool, boss_name: String) -> String: var boss_tag := " · Boss:%s" % boss_name if boss_active else "" if battle_result != "": return "第 %d 轮 · 战斗结束%s" % [round_index, boss_tag] - if window_open: - return "第 %d 轮 · 道具窗口%s" % [round_index, boss_tag] if waiting: return "第 %d 轮 · 结算中%s" % [round_index, boss_tag] return "第 %d 轮 · 挖掘中%s" % [round_index, boss_tag] diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/debug_session.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/debug_session.gd index 81b005f1..35cf12c8 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/debug_session.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/debug_session.gd @@ -1,13 +1,14 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-13(封装审查 O1/O2 修复批——自 main.gd 拆出,规范 1.1 审视线 / 单一实现纪律) -# 调试模式编排控制器:单场遭遇(第 1/2 层)与连续两层(库存与 HP 关内延续)的模式状态机与战斗生命周期。 -# 职责边界:层序/携带 HP/共享 奖励道具经济 状态 + 战斗结算 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。 +# 调试模式编排控制器:单场遭遇(第 1/2 层)与连续两层(HP 关内延续)的模式状态机与战斗生命周期。 +# 职责边界:层序/携带 HP + 战斗结算 生命周期编排(创建 / begin_battle)。 +# 2026-09-15 格面资源批:库存与共享 地图战斗资源 随可选类道具删除下线——层间延续只剩 HP +# ("库存与 HP 关内延续"语义收窄为"HP 关内延续",O2 单一实现点随之收窄)。 # 授权先例:与 tune_sim_controller 同款(code-review 2026-09-12 批)——控制器承接模式编排, # 组合根仍持有全部信号接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根) -# 与终局 UI 决策;玩家意图写调用(settle_batch / 窗口提交 / 刷新)不经本类。 +# 与终局 UI 决策;玩家意图写调用(settle_batch / 刷新)不经本类。 # E4 收口(2026-09-13):加成生效值经注入 Callable 供给(组合根从面板百分比现算)→ encounter # 显式传入战斗——零表写,"双写叠加/双基线同步"整类隐患随表写移除。 -# O2 收口:调试两层的"库存与 HP 延续"语义唯一实现点(此前与 main.gd 调试分支双实现漂移)。 extends RefCounted signal battle_created(settlement, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001) @@ -22,7 +23,6 @@ var _level_mode := false var _level_battles: Array = [] var _level_idx := 0 var _carry_hp := 0 -var _shared_reward_economy = null var settlement = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手) @@ -38,21 +38,19 @@ func is_level_mode() -> bool: # 单场遭遇原语(2026-09-13 用户裁决:单场用户入口已删——本方法仅 ui_probe 单场段使用; -# 无库存延续、standalone 终局语义保留供探针战斗收尾) +# 无延续、standalone 终局语义保留供探针战斗收尾) func start_single(level: int) -> bool: _level_mode = false - _shared_reward_economy = null return _start_battle(level, -1) -# 连续两层(调试入口——共享 奖励道具经济 + HP 携带;首层满血开局) +# 连续两层(调试入口——HP 关内延续;首层满血开局) func start_debug_levels() -> bool: _level_mode = true _level_battles = [1, 2] _level_idx = 0 _carry_hp = 0 rng.new_adventure(_seed_value.call()) - _shared_reward_economy = RewardEconomy.new(db, rng) return _start_battle(_level_battles[0], -1) @@ -62,28 +60,21 @@ func next_layer() -> bool: # 终局处置(组合根 battle_finished 处理器调用)→ 决策载荷: -# {outcome: "next_layer"|"level_end"|"standalone_end", layer, carry_hp, items_left, remaining} +# {outcome: "next_layer"|"level_end"|"standalone_end", layer, carry_hp} func on_battle_finished(payload: Dictionary) -> Dictionary: var result: String = String(payload.get("result", "")) if result == "victory" and _level_mode and _level_idx < _level_battles.size() - 1: _carry_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值 _level_idx += 1 - return {"outcome": "next_layer", "layer": _level_idx, "carry_hp": _carry_hp, - "items_left": settlement.stock_count()} - var remaining := 0 + return {"outcome": "next_layer", "layer": _level_idx, "carry_hp": _carry_hp} var is_level_end := result == "victory" and _level_mode - if is_level_end: - remaining = int(settlement.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③) - elif _level_mode: - settlement.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规) return {"outcome": "level_end" if is_level_end else "standalone_end", - "layer": _level_idx, "carry_hp": _carry_hp, "items_left": 0, "remaining": remaining} + "layer": _level_idx, "carry_hp": _carry_hp} # 会话复位(组合根终局出口"返回开始界面"调用) func reset() -> void: _level_mode = false - _shared_reward_economy = null settlement = null @@ -91,7 +82,7 @@ func _start_battle(level: int, carry_hp: int) -> bool: if not _level_mode or _level_idx == 0: rng.new_adventure(_seed_value.call()) var ov: Dictionary = _hero_override.call() # E4:生效值经 encounter 传入——零表写 - settlement = BattleOrchestrator.new(db, rng, _shared_reward_economy if _level_mode else null) + settlement = BattleOrchestrator.new(db, rng) battle_created.emit(settlement, level, carry_hp) var hp: int = carry_hp if carry_hp > 0 else int(ov.max_hp) if not settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp, diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd index ce376b49..7590b3cf 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd @@ -8,7 +8,7 @@ # 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/beat/ring 字段)。 # 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(排雷玩法 发射面封闭)—— # 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查; -# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点;库存 奖励道具经济 查询面——写路径回执后重查。 +# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点。 # Date: 2026-09-12(五区改造批;基础 = 2026-09-11 HUD 装配器;2026-09-13 快照面改造) class_name HudPresenter extends RefCounted @@ -16,20 +16,19 @@ extends RefCounted # 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配) static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, - inv: InventoryPanelView, settlement, runner, level_active: bool, db, refresh_remaining: int) -> void: + settlement, runner, level_active: bool, db, refresh_remaining: int) -> void: if settlement == null: - _refresh_out_of_battle(stage, info, inv, runner, level_active, db) + _refresh_out_of_battle(stage, info, runner, level_active, db) return var s: Dictionary = settlement.presentation_snapshot() var hero: Dictionary = s.hero if hero.is_empty() or not bool(hero.get("alive", false)): - _refresh_out_of_battle(stage, info, inv, runner, level_active, db) + _refresh_out_of_battle(stage, info, runner, level_active, db) return _refresh_hero(stage, info, s) _refresh_economy(info, ap_bar, s, db, refresh_remaining) stage.set_enemies(s.enemies) info.set_status_effects(s.hero_effects) - _fill_inventory(inv, s.inventory) # ---- B 区英雄卡 + C 区数据框 ---- @@ -73,58 +72,8 @@ static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dict ap_bar.set_stop_enabled(bool(s.hero.get("alive", false)) and digging) -# ---- E 区库存常读列表(枚举序 = stock 受理序 E21;来源不呈现——2026-09-12 拍板) ---- - -static func _fill_inventory(inv: InventoryPanelView, inventory: Array) -> void: - # 窗口态占屏(D2 原位窗口激活)——常读刷新不得覆盖窗口条目(窗口关闭后由下一轮刷新恢复常读面) - if inv.is_window_open(): - return - var rows: Array = [] - for inst in inventory: - if inst.status != "InStock": - continue - rows.append(_inventory_row(inst.display_name, String(inst.category), int(inst.power), "可用")) - if rows.is_empty(): - inv.show_empty() - else: - inv.show_inventory(rows) - - -# 行值快照投影派生属性简述(伤害/回复/增益一句话)——来源 {cell, round} 不呈现 -static func _inventory_row(name_: String, category: String, power: int, status: String) -> Dictionary: - var desc := "" - var tooltip := "" - match category: - "damage_item": - desc = "伤害 %d · 一次性" % power - tooltip = "对最前敌造成 %d 点伤害(恒定表值,不吃修正)。触发:轮末道具窗口选用,道具段生效。" % power - "heal_pack": - desc = "回复 %d · 可用" % power - tooltip = "回复 %d 点 HP(超量截断)。触发:轮末道具窗口选用;满血时禁选(不烧包)。" % power - "buff": - desc = "增益 · 结算期生效" - tooltip = "状态效果增益。触发:轮末结算增益/减益段。" - _: - desc = "威力 %d" % power - tooltip = "触发:轮末结算。" - return {"display_name": name_, "category": category, "desc": desc, "tooltip": tooltip, "status": status} - - -# 窗口条目投影(战斗结算 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约) -static func window_rows(items: Array, hero_hp: int, hero_max_hp: int) -> Array: - var rows: Array = [] - for pos in items.size(): - var it: Dictionary = items[pos] - var row := _inventory_row(String(it.display_name), String(it.category), int(it.power), "") - row["pos"] = pos - row["usable_now"] = bool(it.usable_now) - row["reason"] = "HP 已满 · 禁选" if not bool(it.usable_now) else "" - rows.append(row) - return rows - - # 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值 -static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, inv: InventoryPanelView, +static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, runner, level_active: bool, db) -> void: if not level_active: return @@ -134,14 +83,6 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "") stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成) info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务) - var rows: Array = [] - for inst in runner.inventory(): - if inst.status == "InStock": - rows.append(_inventory_row(inst.display_name, String(inst.category), int(inst.power), "可用")) - if rows.is_empty(): - inv.show_empty() - else: - inv.show_inventory(rows) # 三源拒绝可感知(排雷玩法 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 4c45220f..ea1af09e 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -1,17 +1,18 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证 -# 实现 GDD:design/ux/battle.md(战斗屏五区 A/B/C/D/E + 全屏 States + 遥测)。 +# 实现 GDD:design/ux/battle.md(战斗屏 A/B/C/D 四区 + 全屏 States + 遥测;E 区随可选类删除下线 2026-09-15)。 # Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 组合根;code-review 修复批重构) # 2026-09-13(封装审查修复批):核心状态读取收口至 战斗结算/runner 成文只读面(presentation_snapshot / # current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离 # debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。 # 组合根(composition root):唯一的模式流转与 core↔view 接线处。 -# 职责边界:① 构建五区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线) +# 职责边界:① 构建四区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线) # ② 模式管理(完整关卡 关卡节点地图 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用) -# ③ 核心写调用只在此处发生(minesweeper.settle_batch / settlement.submit_window_selection / 刷新编排入口 / runner 推进; +# ③ 核心写调用只在此处发生(minesweeper.settle_batch / 刷新编排入口 / runner 推进; # E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable # 以 encounter 参数入战斗,db.hero 运行期只读) -# ④ 全屏 States 状态机(挖掘/结算/道具窗口/终局)与一次性提示去重。 +# ④ 全屏 States 状态机(挖掘/结算/终局)与一次性提示去重。 +# 2026-09-15 格面资源批:道具窗口与库存面板随可选类道具删除下线——E 区移除,窗口接线/选择序列删除。 # 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归 # ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归 # debug_session.gd——本类不拼呈现字符串。 @@ -38,7 +39,7 @@ var _inter_label: Label # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null -# 视图(views/)——battle.md 五区 +# 视图(views/)——battle.md 四区(E 区道具面板随可选类删除下线) var strip_view: LevelStripView # A 区 var stage: BattleStageView # B 区(含 log_view 明细带) var log_view: BattleLogView @@ -46,7 +47,6 @@ var info_panel: HeroInfoPanelView # C 区 var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) var board_view: BoardView # 探针兼容别名(= board_zone.board_view) var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) -var inventory_panel: InventoryPanelView # E 区(原位窗口态) # 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) var legend_overlay: LegendOverlay var review_overlay: SettlementReviewOverlay @@ -58,7 +58,6 @@ var _tune_sim = null # 调整板批量模拟控制器(tune_sim_c var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) -var _window_payload: Array = [] # 窗口投影缓存(ux_window_choice 遥测读 usable_now/item_id) func _ready() -> void: @@ -89,7 +88,6 @@ func _build_ui() -> void: board_zone = five.board_zone board_view = five.board_view # 探针兼容别名(= board_zone.board_view) ap_bar = five.ap_bar # 探针兼容别名(= board_zone.ap_bar) - inventory_panel = five.inventory_panel log_view.review_requested.connect(_open_review_from_band) board_zone.action_requested.connect(_on_board_action) board_zone.stop_pressed.connect(_on_stop_pressed) @@ -97,11 +95,8 @@ func _build_ui() -> void: board_zone.legend_toggled.connect(_on_legend_toggled) board_zone.hover_forbidden.connect(_on_hover_forbidden) board_zone.chord_attempted.connect(_on_chord_attempted) - inventory_panel.item_selected.connect(_on_window_item_selected) - inventory_panel.skipped.connect(_on_window_skipped) - inventory_panel.reject_reason.connect(_on_inventory_reject) - # 奖励徽章数据源:奖励道具经济 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) + # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) board_view.set_identity_resolver(_identity_lookup) logger = PlaytestLogger.new() @@ -184,7 +179,6 @@ func _start_full_level() -> void: runner = LevelRunner.new(db, get_node("/root/RngService")) runner.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) runner.step_row.connect(_on_step_row) - runner.window_opened.connect(_on_window_opened) runner.round_started.connect(_on_round_started) runner.board_changed.connect(_on_board_changed) runner.node_entered.connect(_on_runner_node) @@ -207,7 +201,6 @@ func _reset_run_scope() -> void: _hints_shown = {} _refresh_used_round = 0 _boss_active = false - inventory_panel.close_window() func _on_runner_node(node: Dictionary, info: Dictionary) -> void: @@ -219,10 +212,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: _show_board_fx_for_node(t) log_view.append_bb(Narrator.node_banner(runner, node, t)) elif t == "reward": - var snap: Dictionary = info.get("snapshot", {}) _open_modal(node_overlay) - node_overlay.show_reward(snap) - log_view.append_bb(Narrator.reward_line(snap)) + node_overlay.show_reward() + log_view.append_bb(Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) _open_modal(node_overlay) @@ -241,9 +233,7 @@ func _on_level_finished(payload: Dictionary) -> void: info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④) stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面) if result == "victory": - # 关卡结算:库存清空可见反馈(奖励道具经济 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央) - var remaining: int = int(payload.get("remaining_items", 0)) - inventory_panel.play_cleared_feedback(remaining) + # 胜利终局 overlay(金蜡封落于棋盘中央) _end_overlay.open_victory(Narrator.level_end_text("victory", payload, runner.effective_max_hp()), "返回冒险", board_view.get_global_rect()) else: @@ -319,7 +309,7 @@ func _scaled(base: int, pct: int) -> int: return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0))) -# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——奖励道具经济 Ready 域外零读、零诊断噪音) +# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音) func _identity_lookup(c: Vector2i): if settlement == null or settlement.minesweeper.current_board() == null or settlement.action_economy.phase != settlement.action_economy.PHASE_ACTIVE: return null @@ -342,21 +332,20 @@ func _on_battle_finished(payload: Dictionary) -> void: var decision: Dictionary = _debug.on_battle_finished(payload) match String(decision.outcome): "next_layer": - _inter_label.text = "第 %d 层通过!\n库存道具 ×%d · HP %d/%d 带入下一层" % [ - int(decision.layer), int(decision.items_left), int(decision.carry_hp), int(db.hero.max_hp)] + _inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ + int(decision.layer), int(decision.carry_hp), int(db.hero.max_hp)] _inter_overlay.visible = true _refresh_presentations() return "level_end": - inventory_panel.play_cleared_feedback(int(decision.remaining)) - _end_overlay.open_victory(Narrator.battle_end_text("victory", true, int(decision.remaining)), + _end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", board_view.get_global_rect()) "standalone_end": if result == "victory": - _end_overlay.open_victory(Narrator.battle_end_text("victory", false, 0), + _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", board_view.get_global_rect()) else: - _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false, 0), + _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) _open_modal(_end_overlay) _refresh_presentations() @@ -366,7 +355,6 @@ func _on_battle_finished(payload: Dictionary) -> void: func _wire_battle(settlement_ref) -> void: settlement_ref.step_row.connect(_on_step_row) - settlement_ref.window_opened.connect(_on_window_opened) settlement_ref.battle_finished.connect(_on_battle_finished) settlement_ref.round_started.connect(_on_round_started) settlement_ref.board_changed.connect(_on_board_changed) @@ -431,40 +419,7 @@ func _show_hint(text: String, hint_id: String) -> void: board_zone.toast.show_toast(text, "info") -# ---- 道具窗口(E 区原位激活——布局决策 D2 / pattern 2) ---- - -func _on_window_opened(items: Array) -> void: - _window_payload = items - var hero: Dictionary = settlement.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP - var hp: int = int(hero.get("hp", 0)) if not hero.is_empty() else 0 - var max_hp: int = int(hero.get("max_hp", 0)) if not hero.is_empty() else 1 - inventory_panel.open_window(HudPresenter.window_rows(items, hp, max_hp)) - _sync_settlement_visuals() # 阶段指示 → 道具窗口 - - -func _on_window_item_selected(pos: int) -> void: - # 选择语义 = 窗口条目位置(缺陷④契约——非库存句柄) - if pos >= 0 and pos < _window_payload.size(): - var it: Dictionary = _window_payload[pos] - logger.log_ux("ux_window_choice", {"selected_item_id": String(it.get("item_id", "")), - "usable_now": true, "skip": false}) - inventory_panel.close_window() - if settlement != null: - settlement.submit_window_selection([pos]) - _refresh_presentations() - - -func _on_window_skipped() -> void: - logger.log_ux("ux_window_choice", {"selected_item_id": "", "usable_now": false, "skip": true}) - inventory_panel.close_window() - if settlement != null: - settlement.submit_window_selection([]) - _refresh_presentations() - - -func _on_inventory_reject(text: String) -> void: - board_zone.toast.show_toast(text, "warn") - +# ---- 节点覆盖层"继续"(奖励/恢复点确认) ---- func _on_node_continue() -> void: _close_modal(node_overlay) @@ -475,8 +430,8 @@ func _on_node_continue() -> void: # ==================== 玩家意图 → 核心写调用(唯一出口)==================== func _on_board_action(cell: Vector2i, kind: String) -> void: - # 玩家动作 → 核心写调用的唯一通道(模态/窗口期/无战斗拒绝在此收口) - if _input_locked() or inventory_panel.is_window_open(): + # 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口) + if _input_locked(): return var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted) @@ -491,7 +446,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: func _on_stop_pressed() -> void: - if _input_locked() or inventory_panel.is_window_open(): + if _input_locked(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) @@ -506,7 +461,7 @@ func _on_stop_pressed() -> void: func _on_refresh_confirmed() -> void: # 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP−8 · 旧板旗作废,每轮上限 2 次) - if _input_locked() or inventory_panel.is_window_open(): + if _input_locked(): return var ok: bool = false if _level_active and runner != null: @@ -549,7 +504,7 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void: func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_max_per_round) - _refresh_used_round) - HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, settlement, runner, _level_active, db, refresh_remaining) + HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining) _sync_settlement_visuals() @@ -574,7 +529,7 @@ func _phase_text() -> String: if settlement == null: return "第 — 轮 · —" return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result), - inventory_panel.is_window_open(), settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING, + settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING, _boss_active, Narrator.boss_name(runner, db)) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/playtest_logger.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/playtest_logger.gd index ea93e103..24f235a4 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/playtest_logger.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/playtest_logger.gd @@ -7,7 +7,7 @@ # 文件 = user://logs/playtest__<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。 # ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、 # ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 战斗结算 的 ADR 纪律下唯一合法来源)、 -# ux_refresh_attempt / ux_window_choice / ux_reject / ux_hint_shown(组合根显式上报)。 +# ux_refresh_attempt / ux_reject / ux_hint_shown(组合根显式上报;ux_window_choice 随道具窗口删除下线 2026-09-15)。 # 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。 class_name PlaytestLogger extends RefCounted @@ -55,7 +55,7 @@ func attach_battle(settlement) -> void: settlement.settlement_snapshot.connect(_on_settlement_snapshot) settlement.battle_finished.connect(func(p): _append({ "kind": "battle_end", "result": String(p.get("result", "")), - "hero_hp": int(p.get("hero_hp", 0)), "remaining_items": int(p.get("remaining_items", 0))})) + "hero_hp": int(p.get("hero_hp", 0))})) # ---- ux 遥测(battle.md §Events Fired——呈现层观测行族,不改 core) ---- @@ -96,12 +96,9 @@ func log_node(node: Dictionary, info: Dictionary, node_index: int) -> void: var line := {"kind": "node", "type": t, "index": node_index} if t == "battle" or t == "boss": line["depth"] = int(node.get("node_depth", 0)) - elif t == "reward": - var snap: Dictionary = info.get("snapshot", {}) - line["reward_id"] = String(snap.get("reward_id", "")) - line["power"] = int(snap.get("power", 0)) elif t == "rest": line["healed"] = int(info.get("healed", 0)) + # reward 节点:内容待拍板(格面资源批 2026-09-15 道具删除)——零载荷 _append(line) @@ -115,13 +112,12 @@ func log_run(meta: Dictionary) -> void: func log_level_end(payload: Dictionary) -> void: _append({"kind": "level_end", "result": String(payload.get("result", "")), - "node_index": int(payload.get("node_index", -1)), "hp": int(payload.get("hp", 0)), - "remaining_items": int(payload.get("remaining_items", -1))}) + "node_index": int(payload.get("node_index", -1)), "hp": int(payload.get("hp", 0))}) func _on_step_row(row: Dictionary) -> void: _ring(_recent_steps, {"t": row.type, "src": row.source_class, "v": int(row.value), "r": int(row.round_index)}) - # 结算明细行逐行落盘(2026-09-12 用户要求——七段结算完整回放数据面) + # 结算明细行逐行落盘(2026-09-12 用户要求——五段结算完整回放数据面) _append({"kind": "step", "type": row.type, "source": row.source_class, "cell": row.origin_cell, "round": int(row.round_index), "text": String(row.display_name), "value": int(row.value), diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd index f6de17ef..5b80026e 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd @@ -7,8 +7,8 @@ extends RefCounted -# 战斗屏五区(A 进度条 / B 战斗画面 / C·D·E 三列)。返回句柄字典: -# {strip_view, stage, log_view, info_panel, board_zone, board_view, ap_bar, inventory_panel} +# 战斗屏四区(A 进度条 / B 战斗画面 / C·D 两列——E 区道具面板已随可选类删除下线,2026-09-15 格面资源批)。 +# 返回句柄字典:{strip_view, stage, log_view, info_panel, board_zone, board_view, ap_bar} static func build_five_zone(root: Control, db) -> Dictionary: # set_anchors_and_offsets_preset(非 set_anchors_preset):后者会把控件重置回最小内容尺寸 # (实测 927×613——五区缩顶、棋盘 45px 格 ×10 行超出 D 区 333px 高被裁,2026-09-13 用户报告回归) @@ -30,7 +30,7 @@ static func build_five_zone(root: Control, db) -> Dictionary: var stage := BattleStageView.new(db) root.add_child(stage) - # C/D/E 三列(520px) + # C/D 两列(520px)——E 区库存面板删除后 D 区横向加宽 var bottom := HBoxContainer.new() bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL bottom.add_theme_constant_override("separation", 4) @@ -42,9 +42,6 @@ static func build_five_zone(root: Control, db) -> Dictionary: var board_zone := BoardZoneView.new() bottom.add_child(board_zone) - var inventory_panel := InventoryPanelView.new() - bottom.add_child(inventory_panel) - return { "strip_view": strip_view, "stage": stage, @@ -53,7 +50,6 @@ static func build_five_zone(root: Control, db) -> Dictionary: "board_zone": board_zone, "board_view": board_zone.board_view, "ap_bar": board_zone.ap_bar, - "inventory_panel": inventory_panel, } @@ -106,14 +102,14 @@ static func build_start_overlay(host: Control) -> Dictionary: seed_edit.custom_minimum_size = Vector2(120, 0) seed_row.add_child(seed_edit) - var lvbtn := UiKit.button("▶ 第一关 · 苔藓回廊(战×2 → 道具 → 战×2 → 恢复 → Boss)") + var lvbtn := UiKit.button("▶ 第一关 · 苔藓回廊(战×2 → 奖励点 → 战×2 → 恢复 → Boss)") box.add_child(lvbtn) var tunebtn := UiKit.button("测试用 · 角色数值调整板(模拟天赋树属性提升)") box.add_child(tunebtn) - var dbgbtn := UiKit.button("调试 · 连续关卡两层(库存与 HP 关内延续)") + var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)") box.add_child(dbgbtn) - var help := UiKit.label("玩法:每轮 8 AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n安全格=拾取战利品(轮末结算生效)· 雷=敌方获得伏击 · 标中旗=削弱敌人。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−8 当场换图继续挖(每轮剩 2 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。", 13, false) + var help := UiKit.label("玩法:每轮 8 AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/减伤/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−8 当场换图继续挖(每轮剩 2 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。", 13, false) help.add_theme_color_override("font_color", Color8(178, 166, 146)) box.add_child(help) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd index 9d24531e..faa36b93 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_log_view.gd @@ -3,8 +3,9 @@ # 实时流 / 点击展开回看 overlay〔pattern 6 模态纪律〕;行模板「{行为} {值} → {目标}(来源)」、 # 聚合计数行、零值行可见、效果行 value≠pool_value 禁相加、未生效四类行 icon/划线形状冗余、 # highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。 -# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 窗口锚行批 13→15 类: -# 新增 window_open / window_skip 两类锚行格式化——战斗结算 行协议清单同步) +# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 格面资源批 15→10 类: +# 删 item / consumed_ineffective / use_rejected / window_open / window_skip 五类——可选类道具与窗口下线, +# 战斗结算 行协议清单同步) # 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。 class_name BattleLogView extends PanelContainer @@ -13,7 +14,7 @@ signal review_requested const BAND_H_DIG := 24 const BAND_H_SETTLE := 84 -const AGG_TYPES := ["reward_ineffective", "consumed_ineffective", "use_rejected", "overflow"] +const AGG_TYPES := ["reward_ineffective", "overflow"] const COL_GOLD := "#f0a828" const COL_INK := "#33261b" @@ -181,10 +182,8 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: return prefix + "[color=%s]%s[/color]" % [body_col, body] "heal": return prefix + "[color=#7cc47c]%s +%d%s%s[/color]" % [name_, v, tail, suffix] - "item": - return prefix + "[color=#d0c090]%s(+%d)%s[/color]" % [name_, v, suffix] - "reward_ineffective", "consumed_ineffective", "use_rejected", "overflow": - # 未生效四类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 + "reward_ineffective", "overflow": + # 未生效两类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 var n := (" ×%d" % agg_count) if agg_count > 1 else "" return prefix + "[s][color=#b08060]⚠ %s%s[/color][/s]%s" % [name_, n, suffix] "flag_miss": @@ -200,10 +199,6 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: return "[color=#e0b860][b]◆ %s[/b][/color]" % name_ "highlight": return prefix + _gold_frame("★ %s" % name_) - "window_open": - return prefix + "[color=#c8b070]▤ %s[/color]" % name_ - "window_skip": - return prefix + "[color=#9a8a72]▤ %s[/color]" % name_ return prefix + name_ + suffix diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd index ddb91792..20f09a5c 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd @@ -4,7 +4,7 @@ # Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 版) # 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、 # 绘制优先级 数字>小人>旗>奖励徽章>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。 -# 高内聚边界:只读 排雷玩法 查询面(+奖励道具经济 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口 +# 高内聚边界:只读 排雷玩法 查询面(+地图战斗资源 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口 # (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。 # 缺陷①回归约束:格子控件常驻、逐格重绘在 refresh 内完成,无布局回流(格控件/子标记定长,不增删)。 # ADR-0003 形态约束:GridContainer + 逐格常驻控件(探针耦合 _cells/click_cell——禁盲目迁移 _draw)。 @@ -24,8 +24,8 @@ const COL_GOLD := Color8(240, 168, 40) # 奖励金 #F0A828 const COL_PATH := Color8(46, 110, 142) # 路径预览点迹——小人蓝细线 const COL_DIG_EDGE := Color8(120, 190, 120) # 可挖域呼吸微光基色(绿相,明度贴格底) const COL_HL_TARGET := Color8(240, 168, 40) # 悬停目标格边框(金) -const REWARD_BADGE := { # 已开格奖励身份 icon(tier 档位另经字号/形状区分) - "instant_aoe": "爆", "instant_heal": "急", "damage_item": "刃", "heal_pack": "医", "buff": "纹", +const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 五型键;极性视觉归批 2) + "attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", } const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标) const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体 @@ -39,13 +39,13 @@ var _cell_sb := {} # Vector2i → StyleBoxFlat(呼吸微光就地改 var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "hl": Panel} var _board: BoardData = null var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗) -var _identity_cb := Callable() # 奖励道具经济 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null +var _identity_cb := Callable() # 地图战斗资源 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集 var _breath_t := 0.0 var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1) var _hover := Vector2i(-9, -9) var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000} -var _r0 := {} # 出生展开区 R₀(奖励道具经济 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音) +var _r0 := {} # 出生展开区 R₀(地图战斗资源 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音) var _desat_mat: ShaderMaterial var _desat_tween: Tween var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重算——battle.md 格尺寸公式) @@ -87,7 +87,7 @@ func set_desaturated(on: bool) -> void: (_cells[c] as Button).material = _desat_mat if on else null -# 组合根注入 奖励道具经济 身份解析只读回调(query_identity)——奖励徽章数据源 +# 组合根注入 地图战斗资源 身份解析只读回调(query_identity)——奖励徽章数据源 func set_identity_resolver(cb: Callable) -> void: _identity_cb = cb @@ -266,19 +266,19 @@ func _update_badge(c: Vector2i, btn: Button) -> void: if _minesweeper == null or not _minesweeper.is_revealed(c) or not _identity_cb.is_valid(): return if _r0.has(c): - return # R₀ 初始展开格无奖励身份(奖励道具经济 资格域 = S∖R₀) + return # R₀ 初始展开格无奖励身份(地图战斗资源 资格域 = S∖R₀) var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典) if rec.get("kind", "") != "safe": return var ident = _identity_cb.call(c) if not (ident is Dictionary): return - badge.text = _badge_glyph(String(ident.get("category", ""))) + badge.text = _badge_glyph(String(ident.get("cell_type", ""))) badge.add_theme_font_size_override("font_size", maxi(10, int(_cell * 0.28))) badge.visible = true -# 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 奖励道具经济 category 五值枚举) +# 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 格面资源 cell_type 五值枚举) func _badge_glyph(category: String) -> String: if REWARD_BADGE.has(category): return REWARD_BADGE[category] diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd deleted file mode 100644 index dd5d80c0..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd +++ /dev/null @@ -1,226 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# 实现 GDD:design/ux/battle.md(E 区道具 280×520——库存常读列表〔逐件 icon+名称+属性简述, -# 枚举序 = stock 受理序、固定槽高 + clip_contents(缺陷①教训)〕;窗口期原位激活为道具使用窗口 -# 〔布局决策 D2 / pattern 2 原位窗口激活〕:描边金光点亮、标题切换+时机指示、usable_now 禁选态 -# +原因〔pattern 8 禁用态即理由〕、跳过按钮、政策注记常驻)。 -# Date: 2026-09-12(改造自 item_window_overlay.gd——中央模态形态废弃,原位激活替代) -# 契约红线:选择语义 = 窗口条目位置(战斗结算 义务④"选定序"),库存句柄不出本类 -# (缺陷④契约约束的呈现侧收口点)。来源 {cell, round} 不呈现于列表与 tooltip(2026-09-12 拍板)。 -class_name InventoryPanelView -extends PanelContainer - -signal item_selected(pos: int) -signal skipped -signal reject_reason(text: String) # 禁选件点击 → pattern 8 原因 toast(组合根接 toast 槽) - -const COL_GOLD := Color8(240, 168, 40) -const ROW_H := 44 - -var _title: Label -var _timing: Label -var _list: VBoxContainer -var _skip_btn: Button -var _policy: Label -var _normal_title := "道包 · 关卡域" -var _window_title := "⏳ 轮末道具窗口 · 至多选 1 件" -var _panel_sb: StyleBoxFlat -var _window_mode := false -var _cleared_hold_until := 0 # 库存清空反馈持有期(ms 时间戳——期间常读刷新不覆盖提示) - - -func _init() -> void: - custom_minimum_size = Vector2(280, 0) - clip_contents = true - _panel_sb = StyleBoxFlat.new() - _panel_sb.bg_color = Color8(232, 220, 196) - _panel_sb.set_corner_radius_all(0) - _panel_sb.set_border_width_all(1) - _panel_sb.border_color = Color8(120, 106, 88) - _panel_sb.content_margin_left = 8.0 - _panel_sb.content_margin_right = 8.0 - _panel_sb.content_margin_top = 6.0 - _panel_sb.content_margin_bottom = 6.0 - add_theme_stylebox_override("panel", _panel_sb) - var col := VBoxContainer.new() - col.add_theme_constant_override("separation", 4) - add_child(col) - _title = UiKit.label(_normal_title, 16, true) - col.add_child(_title) - _timing = UiKit.label("", 12, false) - _timing.add_theme_color_override("font_color", Color8(140, 124, 100)) - col.add_child(_timing) - _list = VBoxContainer.new() - _list.add_theme_constant_override("separation", 2) - _list.size_flags_vertical = Control.SIZE_EXPAND_FILL - col.add_child(_list) - _skip_btn = UiKit.button("跳过") - _skip_btn.visible = false - _skip_btn.pressed.connect(func(): skipped.emit()) - col.add_child(_skip_btn) - _policy = UiKit.label("注:未选用件随关卡结束清空", 12, false) - _policy.add_theme_color_override("font_color", Color8(140, 124, 100)) - _policy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - col.add_child(_policy) - - -# ---- 常读态(挖掘期规划面——奖励道具经济 D1 义务①) ---- - -func show_inventory(rows: Array) -> void: - if Time.get_ticks_msec() < _cleared_hold_until: - return # 清空反馈展示中(「剩余道具 ×N 随关卡结束清空」) - _window_mode = false - _set_window_visual(false) - _title.text = _normal_title - _timing.text = "" - _skip_btn.visible = false - _rebuild_rows(rows, false) - - -# 空态(指引性,非报错——奖励道具经济 EC 空态) -func show_empty() -> void: - if Time.get_ticks_msec() < _cleared_hold_until: - return - show_inventory([]) - _add_empty_hint() - - -# ---- 窗口态(pattern 2 原位激活:面板描边金光点亮、标题切换 + 时机指示常驻) ---- - -# items = 战斗结算 组装窗口投影(usable_now 由 战斗结算 组装——呈现层只消费不推断) -func open_window(items: Array) -> void: - _window_mode = true - _set_window_visual(true) - _title.text = _window_title - _timing.text = "时机:授予段后 · 增益/减益段前" - _skip_btn.visible = true - _rebuild_rows(items, true) - - -func close_window() -> void: - _window_mode = false - _set_window_visual(false) - - -func is_window_open() -> bool: - return _window_mode - - -# 探针兼容面:窗口悬置判定 -func is_open() -> bool: - return _window_mode - - -# 库存清空反馈(奖励道具经济 D1×⑤):逐件化光点飞出 + 「剩余道具 ×N 随关卡结束清空」占位 -func play_cleared_feedback(remaining: int) -> void: - _cleared_hold_until = Time.get_ticks_msec() + 3000 - var rows := _list.get_children() - for r in rows: - var tw := (r as Control).create_tween() - tw.tween_property(r, "modulate:a", 0.0, 0.5) - tw.tween_property(r, "position:y", (r as Control).position.y - 40.0, 0.5) - _tw_after_clear(remaining) - - -func _tw_after_clear(remaining: int) -> void: - var tw := create_tween() - tw.tween_interval(0.6) - tw.tween_callback(func(): - _title.text = _normal_title - _cleared_hold_until = 0 - _rebuild_rows([], false) - _add_empty_hint("剩余道具 ×%d 随关卡结束清空" % remaining)) - - -# ---- 行构建(固定槽高 + clip——缺陷①回归约束) ---- - -func _rebuild_rows(rows: Array, window: bool) -> void: - for child in _list.get_children(): - child.queue_free() - for r in rows: - _list.add_child(_make_row(r, window)) - if rows.is_empty() and not window: - _add_empty_hint() - - -func _add_empty_hint(text := "道包空空——安全格里藏着补给") -> void: - var lab := UiKit.label(text, 13, false) - lab.add_theme_color_override("font_color", Color8(140, 124, 100)) - lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - lab.custom_minimum_size = Vector2(0, ROW_H) - lab.clip_contents = true - _list.add_child(lab) - - -func _make_row(r: Dictionary, window: bool) -> PanelContainer: - var row := PanelContainer.new() - row.custom_minimum_size = Vector2(0, ROW_H) - row.clip_contents = true - var sb := StyleBoxFlat.new() - sb.bg_color = Color8(224, 208, 180) - sb.set_corner_radius_all(0) - sb.set_border_width_all(1) - sb.border_color = Color8(150, 134, 112) - sb.content_margin_left = 6.0 - sb.content_margin_right = 6.0 - sb.content_margin_top = 3.0 - sb.content_margin_bottom = 3.0 - row.add_theme_stylebox_override("panel", sb) - var h := HBoxContainer.new() - h.add_theme_constant_override("separation", 6) - row.add_child(h) - var icon := UiKit.label(_icon_glyph(String(r.get("category", ""))), 16, true) - icon.custom_minimum_size = Vector2(20, 0) - h.add_child(icon) - var mid := VBoxContainer.new() - mid.size_flags_horizontal = Control.SIZE_EXPAND_FILL - mid.alignment = BoxContainer.ALIGNMENT_CENTER - h.add_child(mid) - var name_lab := UiKit.label(String(r.get("display_name", "")), 14, true) - mid.add_child(name_lab) - var desc := UiKit.label(String(r.get("desc", "")), 11, false) - desc.add_theme_color_override("font_color", Color8(120, 106, 88)) - desc.clip_text = true - desc.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - mid.add_child(desc) - row.tooltip_text = String(r.get("tooltip", "")) # 完整描述 + 触发注记(不含来源——拍板) - if not window: - var st := UiKit.label(String(r.get("status", "")), 12, false) - st.size_flags_vertical = Control.SIZE_SHRINK_CENTER - h.add_child(st) - return row - # 窗口态:usable_now 禁选 + 原因(pattern 8——灰置 + tooltip「HP 已满 · 禁选」+ 点击原因 toast) - var usable: bool = bool(r.get("usable_now", false)) - var use_btn := UiKit.button("选用") - use_btn.custom_minimum_size = Vector2(56, 0) - use_btn.size_flags_vertical = Control.SIZE_SHRINK_CENTER - use_btn.disabled = not usable - use_btn.pressed.connect(func(): item_selected.emit(int(r.get("pos", -1)))) - h.add_child(use_btn) - if not usable: - name_lab.add_theme_color_override("font_color", Color8(150, 140, 130)) - row.tooltip_text = String(r.get("reason", "暂不可用")) + "\n" + row.tooltip_text - row.gui_input.connect(func(ev: InputEvent): - if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: - reject_reason.emit(String(r.get("reason", "暂不可用")))) - return row - - -func _icon_glyph(category: String) -> String: - match category: - "damage_item": - return "刃" - "heal_pack": - return "医" - "buff": - return "纹" - return "◆" - - -func _set_window_visual(on: bool) -> void: - # 原位激活视觉(pattern 2):描边金光点亮(占位级——静态金描边) - if on: - _panel_sb.set_border_width_all(3) - _panel_sb.border_color = COL_GOLD - else: - _panel_sb.set_border_width_all(1) - _panel_sb.border_color = Color8(120, 106, 88) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd.uid deleted file mode 100644 index 6847c48f..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/inventory_panel_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cnd5vc3sonulf diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/node_overlay.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/node_overlay.gd index 75b9e69b..1b74301c 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/node_overlay.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/node_overlay.gd @@ -32,10 +32,10 @@ func _init() -> void: visible = false -func show_reward(snap: Dictionary) -> void: - _label.text = "关卡奖励点\n获得「%s」(威力 %d)\n已收入库存——跨战斗延续 · 关卡末清空" % [ - String(snap.get("display_name", "")), int(snap.get("power", 0))] - _btn.text = "收下,继续" +func show_reward() -> void: + # 格面资源批(2026-09-15):奖励点内容随可选类道具删除悬置(功能道具渠道 P1 待拍板)——直通节点确认 + _label.text = "关卡奖励点\n(节点内容待定——冒险结构/关卡节点地图 定稿批裁决)" + _btn.text = "继续" visible = true diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd index e6973cb9..4684bf46 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd @@ -10,9 +10,9 @@ extends HBoxContainer const MAX_SLOTS := 4 # 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name): -# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕(旗朱族)——形状+色相双通道,文字名常驻第三通道。 -# 状态表六行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard)——HUD框架 图标键契约 -# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 补暴走/失衡两键)。 +# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕/☾(旗朱族)——形状+色相双通道,文字名常驻第三通道。 +# 状态表七行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约 +# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键)。 const EFFECT_ICON := { "attack_up": ["▲", Color8(176, 122, 20), "益"], "defense_up": ["◆", Color8(46, 110, 142), "益"], @@ -20,14 +20,16 @@ const EFFECT_ICON := { "ambush": ["⚡", Color8(198, 61, 34), "害"], "enrage": ["⚔", Color8(198, 61, 34), "害"], "off_guard": ["✕", Color8(198, 61, 34), "害"], + "sapped": ["☾", Color8(198, 61, 34), "害"], } const EFFECT_SEMANTIC := { "attack_up": "主角攻击加成(hero_offense 池加算)", "defense_up": "受到伤害减免(hero_defense 池加算)", "weaken": "敌方输出削弱(enemy_output 池乘算)", - "ambush": "敌方全体攻击附加(flat_attach——踩雷转化,战斗段共享不消耗)", - "enrage": "敌方输出加成(enemy_output 池加算——标错反噬)", - "off_guard": "主角承伤加深(hero_defense 池加算——标错反噬)", + "ambush": "敌方全体攻击附加(flat_attach——blast 型负向,战斗段共享不消耗)", + "enrage": "敌方输出加成(enemy_output 池加算——attack 型负向)", + "off_guard": "主角承伤加深(hero_defense 池加算——defense 型负向)", + "sapped": "主角输出降低(hero_offense 池加算——weaken 型负向)", } const SOURCE_NAMES := {"safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励"} # 未知图标键的占位纹样(HUD框架:占位也要可见,禁静默空图标) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/auto_player.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/auto_player.gd index 06fbe0a1..24fec97c 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/auto_player.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/auto_player.gd @@ -1,8 +1,8 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-13(O4/W-3 收口——自动玩家策略四份复制单一实现) # 自动玩家策略集(headless 冒烟/扫描/转储共用的理性玩家近似): -# choose_dig = 挖最近可挖格(先移到邻接已开格);window_selection = 道具窗口确定性选择 -# (治疗包优先于刀;半血阈值/轮次门/刀力口径经参数表达——四调用点原语义逐一保留)。 +# choose_dig = 挖最近可挖格(先移到邻接已开格)。 +# 2026-09-15 格面资源批:window_selection(道具窗口确定性选择)随道具窗口删除下线。 # 读取面:战斗结算.presentation_snapshot() / 排雷玩法 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。 # 等价性证明:smoke_all 同种子回放断言(步骤流逐行相同)在重构后保持全绿。 extends RefCounted @@ -28,47 +28,6 @@ static func choose_dig(minesweeper) -> Dictionary: return {} -# 道具窗口确定性选择(s = settlement.presentation_snapshot();半血以下治疗包优先,否则刀): -# opts.heal_hp_div = 治疗包启用分母(hero_hp × div ≤ max_hp——原四实现 2/3 两档) -# opts.round_gate = 刀启用轮次门(round ≥ gate 才用刀;0 = 不设门——monte_carlo 口径) -# opts.knife_compare = "best"(最强单刀 ≥ 敌余 HP)| "sum"(刀力合计——dump/monte 口径) -static func window_selection(s: Dictionary, items: Array, opts: Dictionary = {}) -> Array: - var heal_div: int = int(opts.get("heal_hp_div", 3)) - var round_gate: int = int(opts.get("round_gate", 3)) - var compare: String = String(opts.get("knife_compare", "best")) - var enemy_hp := 0 - for u in s.enemies: - if bool(u.alive): - enemy_hp += int(u.hp) - var best_knife := -1 - var best_power := 0 - var sum_power := 0 - var best_heal := -1 - var best_heal_power := 0 - for i in items.size(): - var it: Dictionary = items[i] - if not bool(it.usable_now): - continue - if it.category == "damage_item": - sum_power += int(it.power) - if int(it.power) > best_power: - best_power = int(it.power) - best_knife = i - elif it.category == "heal_pack": - best_heal = i - best_heal_power = int(it.power) - var knife_power: int = best_power if compare == "best" else sum_power - var selection: Array = [] - # heal_no_waste(完美档):缺口 < 治疗量时不用——满血贴药 = 白费一次性资源(严格劣策略) - var heal_missing_ok: bool = not bool(opts.get("heal_no_waste", false)) \ - or int(s.hero.max_hp) - int(s.hero.hp) >= best_heal_power - if best_heal >= 0 and int(s.hero.hp) * heal_div <= int(s.hero.max_hp) and heal_missing_ok: - selection.append(best_heal) - elif best_knife >= 0 and knife_power >= enemy_hp and (round_gate <= 0 or int(s.round) >= round_gate): - selection.append(best_knife) - return selection - - # ==================== 完美策略档(2026-09-14 用户拍板:OQ7 技能轴上界测量仪——只作测量,非玩法数值) ==================== # 策略语义:零盲挖(只挖已证安全格 ⇒ 构造保证零踩雷)· 已证雷格全标旗(旗先于挖——旗 = 纯收益 # 〔轮末 weaken + 魔法 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新 @@ -77,7 +36,6 @@ static func window_selection(s: Dictionary, items: Array, opts: Dictionary = {}) # 口径差异 = unknown 集不含 棋盘生成 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸 # 保守,卡死率略高属口径诚实。输入只经 排雷玩法 查询面(reveal_record/is_revealed/has_flag/current_board # 的邻居与可达面),禁读棋盘雷区真值(测推理上界,非全知作弊)。 -# 道具窗口:轮次门解除(round_gate=0)+ 治疗无浪费(heal_no_waste)。 # 待拍板(未实现):非卡死主动刷新、囤刀倾泻 vs 即用即耗、完美板目标加权。 # 单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/flag/refresh/stop 五值。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd index c565631a..af8baaa9 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/damage_probe.gd @@ -88,9 +88,6 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary: settlement.step_row.connect(func(row): cap["rows"].append(row)) settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s)) settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", ""))) - settlement.window_opened.connect(func(items): - var s: Dictionary = settlement.presentation_snapshot() - settlement.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"}))) if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}): cap["result"] = "begin_failed" return cap @@ -301,7 +298,6 @@ func _run_level(db, seed_val: int) -> Dictionary: var runner = load("res://core/level_runner.gd").new(db, rng) var cap := {"battles": [], "rows": [], "result": "", "end_node": -1} runner.step_row.connect(func(row): cap["rows"].append(row)) - runner.window_opened.connect(_on_level_window.bind(runner)) runner.node_entered.connect(_on_level_node_entered.bind(runner, cap)) runner.round_started.connect(_on_level_round_started.bind(runner, cap)) runner.level_finished.connect(_on_level_finished.bind(cap)) @@ -331,15 +327,6 @@ func _battle_nodes_upto(db, level_index: int, node_idx: int) -> int: return n -func _on_level_window(items: Array, runner) -> void: - var settlement = runner.current_battle() - if settlement == null: - runner.submit_window_selection([]) - return - var s: Dictionary = settlement.presentation_snapshot() - runner.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"})) - - func _on_level_node_entered(node: Dictionary, _info: Dictionary, runner, cap: Dictionary) -> void: var t := String(node.get("type", "")) if t == "battle" or t == "boss": diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/dump_battle.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/dump_battle.gd index 82bcf022..ad352a5b 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/dump_battle.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/dump_battle.gd @@ -15,11 +15,6 @@ func _initialize() -> void: var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) var stream: Array = [] settlement.step_row.connect(func(row): stream.append(row)) - settlement.window_opened.connect(func(items): - var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况 - var selection: Array = AutoPlayer.window_selection(s, items, - {"heal_hp_div": 3, "round_gate": 3, "knife_compare": "sum"}) # O4:原 dump_battle 口径 - settlement.submit_window_selection(selection)) var level := 2 if OS.get_cmdline_user_args().size() > 0: level = int(OS.get_cmdline_user_args()[0]) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd index 7fd5d23e..eb7db809 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd @@ -4,7 +4,8 @@ # 批量调用方传共享 logger(log_run 局次标记切分)。 # 关卡自动通关策略(理性玩家近似)——呈现层数值调整板的批量模拟与 headless 期望扫描(level_monte_carlo) # 共用同一实现,保证"面板模拟读数"与"扫描表读数"同口径。策略与 smoke_all 战斗自动玩家同族: -# 挖最近可挖格 / 无可挖停手 / 半血以下用治疗包 / 击杀有把握且第 3 轮起用最强刀 / 奖励恢复点立即确认。 +# 挖最近可挖格 / 无可挖停手 / 奖励恢复点立即确认。 +# 2026-09-15 格面资源批:道具窗口选择与可选类获取计数随可选类道具删除下线。 # 2026-09-14 增完美策略档(policy="perfect",默认 "normal" 零行为变更——既有调用方签名兼容): # 零盲挖/全标旗/卡死刷新——上界测量仪,求解器与动作语义见 auto_player.gd 完美策略档节。 # 运行前提:db 已 load_tables;每个 run_level 全新 RngService(同种子 ⇒ 全程逐步可回放)。 @@ -26,13 +27,12 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: var runner = load("res://core/level_runner.gd").new(db, rng) var st := { "result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, - "reward_snapshot": {}, "rest_healed": 0, "hp_before_rest": -1, - "defeat_node_index": -1, "defeat_node_type": "", "remaining_items": -1, + "rest_healed": 0, "hp_before_rest": -1, + "defeat_node_index": -1, "defeat_node_type": "", "boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, - "actives_total": 0, "log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, + "log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, } runner.step_row.connect(_on_step_row.bind(runner, st, collect_rows)) - runner.window_opened.connect(_on_window_opened.bind(runner, st, logger)) runner.node_entered.connect(_on_node_entered.bind(runner, st, logger)) runner.level_finished.connect(_on_level_finished.bind(st, logger)) runner.set_hero_override(atk_pct, hp_pct) @@ -63,29 +63,11 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: if row.type == "damage" and row.source_class == "enemy_action": st.hero_dmg += int(row.value) if row.source_class == "mine_hit": - st.mine_hits += 1 # 踩雷证据行(effect/overflow——完美档断言恒 0;盲挖惩罚链埋点) - if row.type == "item": - st.actives_total += 1 # 可选类入库行 = 获取计数(供给控制器批度量口径) + st.mine_hits += 1 # 踩雷证据行(负向兑付——完美档断言恒 0;盲挖惩罚链埋点) if collect_rows: st.rows.append(row) -static func _on_window_opened(items: Array, runner, st: Dictionary, logger) -> void: - var settlement = runner.current_battle() - var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况 - var opts := {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"} # O4:原 autoplay 口径 - if String(st.get("policy", "normal")) == "perfect": - opts = {"heal_hp_div": 2, "round_gate": 0, "knife_compare": "best", "heal_no_waste": true} - var sel: Array = AutoPlayer.window_selection(s, items, opts) - if logger != null: - var chosen: Array = [] - for i in sel: - chosen.append(String(items[i].get("item_id", ""))) - logger.log_ux("ux_window_choice", {"selected_item_id": ",".join(chosen), - "usable_now": not sel.is_empty(), "skip": sel.is_empty()}) - runner.submit_window_selection(sel) - - static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dictionary, logger) -> void: var t := String(node.get("type", "")) if t == "battle" or t == "boss": @@ -96,8 +78,7 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic st.boss_depth = int(node.get("node_depth", -1)) st.hp_at_boss = runner.hero_hp elif t == "reward": - st.reward_snapshot = info.get("snapshot", {}) - runner.continue_after_node() + runner.continue_after_node() # 奖励点 = 内容待拍板直通节点(格面资源批 2026-09-15) elif t == "rest": st.rest_healed = int(info.get("healed", 0)) st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed @@ -111,7 +92,6 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void: st.hp_left = int(p.get("hp", 0)) st.defeat_node_index = int(p.get("node_index", -1)) st.defeat_node_type = String(p.get("node_type", "")) - st.remaining_items = int(p.get("remaining_items", -1)) if logger != null: logger.log_level_end(p) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd index 53c98428..ca7a3642 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd @@ -29,6 +29,6 @@ func _initialize() -> void: print("开始:seed=%d atk+%d%% hp+%d%%" % [seed_val, atk_pct, hp_pct]) var st = autoplay.run_level(db, seed_val, 1, atk_pct, hp_pct) print("结果: %s · 用时 %.2f 秒" % [st.result, float(Time.get_ticks_msec() - t0) / 1000.0]) - print(" 战斗节点=%d 轮数=%d 整关承伤=%d 剩余HP=%d 进BossHP=%d 可选类获取=%d" % [st.battles, st.rounds, st.hero_dmg, st.hp_left, st.hp_at_boss, st.actives_total]) - print(" 败北节点=%d(%s) 恢复点回复=%d(前HP %d) 奖励=%s" % [st.defeat_node_index, st.defeat_node_type, st.rest_healed, st.hp_before_rest, str(st.reward_snapshot)]) + print(" 战斗节点=%d 轮数=%d 整关承伤=%d 剩余HP=%d 进BossHP=%d 踩雷=%d" % [st.battles, st.rounds, st.hero_dmg, st.hp_left, st.hp_at_boss, st.mine_hits]) + print(" 败北节点=%d(%s) 恢复点回复=%d(前HP %d)" % [st.defeat_node_index, st.defeat_node_type, st.rest_healed, st.hp_before_rest]) quit(0) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd index cc5b3dc2..85ee6abf 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd @@ -41,7 +41,7 @@ func _initialize() -> void: var atk_axis: Array = [0, 50, 100, 150, 200] var hp_axis: Array = [0, 100] print("=== 关卡期望扫描:第一关(战×2→道具→战×2→恢复→Boss)· N=%d/格 · 种子 %d+%d · 策略=%s ===" % [n, seed0, n, policy_arg]) - print("%-6s %-10s %-8s %-14s %-14s %-14s %-10s %-12s %-12s %-10s %-10s %-8s %-8s" % ["策略", "atk%/hp%", "通关率", "整关承伤均/p90", "通关剩余HP均(胜)", "进BossHP均", "平均轮数", "可选类/战", "普通承伤/场%", "跳战率", "Boss承伤%", "踩雷/局", "刷新/局"]) + print("%-6s %-10s %-8s %-14s %-14s %-14s %-10s %-12s %-12s %-10s %-10s %-8s %-8s" % ["策略", "atk%/hp%", "通关率", "整关承伤均/p90", "通关剩余HP均(胜)", "进BossHP均", "平均轮数", "踩雷/战", "普通承伤/场%", "跳战率", "Boss承伤%", "踩雷/局", "刷新/局"]) var perfect_mine_total := 0 # 完美档构造保证 = 零踩雷(只挖已证安全格)——扫描末断言 for policy in policies: var tag := "基线" if policy == "normal" else "完美" @@ -52,7 +52,7 @@ func _initialize() -> void: var hp_left_win: Array = [] var hp_boss: Array = [] var rounds_all: Array = [] - var actives_per_battle: Array = [] + var mines_per_battle: Array = [] var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤20% 带) var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤50% 带) var wipes := 0 # 普通战斗 round-1 即胜数(首轮全灭跳战——拍板④观测面) @@ -66,7 +66,7 @@ func _initialize() -> void: rounds_all.append(st.rounds) mine_per_run.append(int(st.mine_hits)) refresh_per_run.append(int(st.refreshes)) - actives_per_battle.append(float(st.actives_total) / float(maxi(1, st.battles))) + mines_per_battle.append(float(st.mine_hits) / float(maxi(1, st.battles))) var eff_max := maxi(1, int(round(float(db.hero.max_hp) * (100.0 + float(hp_pct)) / 100.0))) # 逐战斗承伤切分(round_index 回卷 = 新战斗;末段 = Boss 战——当且仅当本局抵达 Boss 节点) var segs := _damage_segments(st.rows) @@ -97,9 +97,9 @@ func _initialize() -> void: perfect_mine_total += int(round(mine_mean * float(n))) print("%-6s atk%+3d%% hp%+3d%% %5.0f%% %6.1f / %-6.1f %6.1f %6.1f %6.1f %5.2f %6.1f %5.0f%% %6.1f %6.2f %6.2f %s" % [ tag, atk_pct, hp_pct, 100.0 * float(wins) / float(n), - _mean(dmg_all), _p90(dmg_all), _mean(hp_left_win), _mean(hp_boss), _mean(rounds_all), _mean(actives_per_battle), + _mean(dmg_all), _p90(dmg_all), _mean(hp_left_win), _mean(hp_boss), _mean(rounds_all), _mean(mines_per_battle), _mean(normal_dmg_pct), 100.0 * float(wipes) / float(maxi(1, normal_battles)), _mean(boss_dmg_pct), mine_mean, _mean(refresh_per_run), death_txt]) - print("(F4 带:承伤 ≤20%%/普通场 · ≤50%%/Boss 场,占生效 max_hp · F5 带:L1 基线通关率 [30,50]%%——2026-09-14 带锚定稿 · 跳战率 = 普通战斗 round-1 即胜(首轮全灭)占比 · 承伤含败者至死 · 败北节点 0-3=战斗节点序 4=Boss · 可选类/战=每战斗实际获取件数期望 · 完美档 = 技能轴上界测量(零盲挖/全标旗/卡死刷新——口径见 auto_player.gd)· 耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0]) + print("(F4 带:承伤 ≤20%%/普通场 · ≤50%%/Boss 场,占生效 max_hp · F5 带:L1 基线通关率 [30,50]%%——2026-09-14 带锚定稿 · 跳战率 = 普通战斗 round-1 即胜(首轮全灭)占比 · 承伤含败者至死 · 败北节点 0-3=战斗节点序 4=Boss · 踩雷/战=每战斗负向兑付触发期望(格面资源批口径) · 完美档 = 技能轴上界测量(零盲挖/全标旗/卡死刷新——口径见 auto_player.gd)· 耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0]) if "perfect" in policies and perfect_mine_total > 0: push_error("[level_monte_carlo] 完美策略档出现踩雷 %d 次——构造保证被破坏(只挖已证安全格),求解器/驱动有缺陷" % perfect_mine_total) quit(1) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd index d5416086..e4872a7d 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/monte_carlo.gd @@ -1,10 +1,12 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-11 # 战斗期望蒙特卡洛:当前参数下"胜轮数 / 每轮承伤 / 清图次数 / 开格数"的分布实测。 -# 政策 = 理性玩家近似:击杀有把握即用刀、半血以下用库存治疗包、挖最近可挖格。 +# 政策 = 理性玩家近似:挖最近可挖格(AutoPlayer 单一实现——O4 收口;道具有效期政策随可选类删除退役 2026-09-15)。 # 运行:Godot_console.exe --headless --path . --script res://smoke/monte_carlo.gd extends SceneTree +const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口(批注 2026-09-15:原文件缺失 preload——HEAD 起即解析失败,本批顺手修复) + const N := 30 const SEED0 := 42 @@ -64,11 +66,6 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary: settlement.minesweeper.cell_revealed.connect(func(evt): if evt.kind == "safe" and evt.channel != "initial": out.safe_opened += 1) - settlement.window_opened.connect(func(items): - var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况 - var selection: Array = AutoPlayer.window_selection(s, items, - {"heal_hp_div": 2, "round_gate": 0, "knife_compare": "sum"}) # O4:原 monte_carlo 口径 - settlement.submit_window_selection(selection)) if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}): return out var guard_rounds := 40 diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/reward_expectation.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/reward_expectation.gd index 66d3aead..9b57ae86 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/reward_expectation.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/reward_expectation.gd @@ -1,7 +1,8 @@ # VERTICAL SLICE - NOT FOR PRODUCTION -# Date: 2026-09-11 -# 奖励期望测量(供给控制器批口径):逐深度实测 |资格格|(safe ∖ R0)分布、每板注入件数(j=0 → 恒 1)、 -# 注入种类分布(60/30/10 拍板值)、格子池零可选类验证——走真实 奖励道具经济.roll_board 管线(含注入判定取号序)。 +# Date: 2026-09-11(2026-09-15 格面资源批重写:类型频次测量——供给控制器批口径随可选类删除退役) +# 奖励期望测量:逐深度实测 |非封锁格|(safe ∪ mines ∖ R₀)分布、格面资源类型频次 +# (应涌现自正向行权重 attack 20 / defense 16 / blast 16 / heal 15 / weaken 12,合计 79)、 +# 封锁格零泄漏(R₀ 格不在摇号清单)——走真实 地图战斗资源.roll_board 管线。 # 运行:Godot_console.exe --headless --path . --script res://smoke/reward_expectation.gd extends SceneTree @@ -17,14 +18,13 @@ func _initialize() -> void: var board_generator = load("res://core/board_generator.gd").new() var reward_economy_factory = load("res://core/reward_economy.gd") var rng_factory = load("res://core/rng_service.gd") - var active_ids := {"knife_t1": true, "knife_adv": true, "heal_pack": true} - print("=== 奖励期望测量(供给控制器批 · N=%d 板/深度 · 每板全新战斗态 j=0)===" % N) - print("depth | 资格格均/min/max | 注入件数/板 均 | 小刀%% | 精工%% | 治疗包%% | 格池可选类泄漏") + var types := ["attack", "defense", "heal", "blast", "weaken"] + print("=== 奖励期望测量(格面资源批 · N=%d 板/深度)===" % N) + print("depth | 非封锁格均/min/max | attack% | defense% | heal% | blast% | weaken% | 封锁格泄漏板") for depth in range(5): var elig: Array = [] - var act: Array = [] + var type_counts := {"attack": 0.0, "defense": 0.0, "heal": 0.0, "blast": 0.0, "weaken": 0.0} var leak := 0 - var species := {"knife_t1": 0.0, "knife_adv": 0.0, "heal_pack": 0.0} for i in range(N): var rng = rng_factory.new() rng.set_master_seed(90000 + depth * 1000 + i) @@ -38,6 +38,9 @@ func _initialize() -> void: for c in board.safe_cells(): if not r0.has(c): eligible.append(c) + for c in board.mines.keys(): + if not r0.has(c): + eligible.append(c) elig.append(eligible.size()) var reward_economy = reward_economy_factory.new(db, rng) var list: Array = [] @@ -45,24 +48,28 @@ func _initialize() -> void: list.append({"cell": c}) if not reward_economy.roll_board(list): continue - var n_active := 0 - for c in eligible: - var snap = reward_economy.query_identity(c) - if snap != null and active_ids.has(String(snap.reward_id)): - n_active += 1 - species[String(snap.reward_id)] += 1.0 - act.append(n_active) - if n_active > 1: + # 封锁格零泄漏:R₀ 格必不在映射(is_mapped 静默查询——零诊断噪音) + var r0_listed := false + for c in r0.keys(): + if reward_economy.is_mapped(c): + r0_listed = true + if r0_listed: leak += 1 - var total: float = species.knife_t1 + species.knife_adv + species.heal_pack - var kt: float = 100.0 * float(species.knife_t1) / maxf(1.0, total) - var ka: float = 100.0 * float(species.knife_adv) / maxf(1.0, total) - var hp: float = 100.0 * float(species.heal_pack) / maxf(1.0, total) - print("D%d %dx%d | %6.1f / %3d / %3d | %5.2f | %4.0f%% | %4.0f%% | %4.0f%% | 多件板 %d" % [ + for c in eligible: + var ident = reward_economy.query_identity(c) + if ident != null: + type_counts[String(ident.cell_type)] += 1.0 + var total := 0.0 + for t in types: + total += type_counts[t] + print("D%d %dx%d | %6.1f / %3d / %3d | %5.1f%% | %6.1f%% | %4.1f%% | %5.1f%% | %6.1f%% | %d" % [ depth, 10 if depth <= 1 else 12, 10 if depth <= 1 else 12, - _mean(elig), _min(elig), _max(elig), _mean(act), kt, ka, hp, leak]) - print("(口径:每板全新战斗态 j=0 → 首件保证恒注入 1 件;战斗内后续板的 0.78/板 期望 = 1 ÷ 每战板数折算,") - print(" 实际获取期望由 level_monte_carlo 的\"可选类/战\"列度量——获取以实际挖到入库为准,残余格未挖不计数)") + _mean(elig), _min(elig), _max(elig), + 100.0 * type_counts.attack / total, 100.0 * type_counts.defense / total, + 100.0 * type_counts.heal / total, 100.0 * type_counts.blast / total, + 100.0 * type_counts.weaken / total, leak]) + print("(口径:类型频次 = 正向行权重涌现——attack 25.3%% / defense 20.3%% / blast 20.3%% / heal 19.0%% / weaken 15.2%%;") + print(" 非封锁格 = safe ∪ mines ∖ R₀,雷安同池同分布;封锁格泄漏 = R₀ 格 query_identity 非 null 的板数,恒应为 0)") quit(0) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index 107a757c..7966d925 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -55,7 +55,6 @@ func _initialize() -> void: # ---- Core 集成冒烟(表加载成功才执行)---- var _db var _rng -var _last_settlement = null func _run_core_smoke(db, rng) -> void: @@ -68,7 +67,7 @@ func _run_core_smoke(db, rng) -> void: _smoke_full_battle(2, 0, "L2 · ambusher") _smoke_level() _smoke_flag_ap_exhausted() - _smoke_supply() + _smoke_reward_identity_unit() _smoke_power_interval() _smoke_active_refresh() _smoke_difficulty_table() @@ -78,86 +77,55 @@ func _run_core_smoke(db, rng) -> void: _smoke_attack_chain_fresh_pool() _smoke_clear_reward_chain() _smoke_refresh_provenance_guard() - _smoke_window_anchor_rows() + _smoke_polarity_routing() + _smoke_negative_dictionary() + _smoke_resource_surface() + _smoke_settlement_segments() -# ---- 可选类供给控制器冒烟(2026-09-11 供给控制器批:首件保证/指数递减/清图新语义/注入占比)---- +# ---- 地图战斗资源·身份面单元冒烟(格面资源批:roll 恰一次 / 雷安同池 / 幂等 / 清图批尾)---- -func _smoke_supply() -> void: - print("--- 奖励道具经济·可选类供给控制器 ---") +func _smoke_reward_identity_unit() -> void: + print("--- 地图战斗资源·格面资源身份面 ---") var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260911) var reward_economy_factory = load("res://core/reward_economy.gd") - # 合成资格清单:6 格 + var reward_economy = reward_economy_factory.new(_db, rng) var eligible: Array = [] for i in range(6): eligible.append({"cell": Vector2i(i, 0)}) - # ① 首件保证 + 每板恰 1 注入 + 注入占比(j=0 恒注入;种子固定 → 分布确定,宽容差断言) - var reward_economy = reward_economy_factory.new(_db, rng) - var ok_roll: bool = reward_economy.roll_board(eligible) - check(ok_roll, "roll_board 受理(供给控制器批)") - var species := {"knife_t1": 0, "knife_adv": 0, "heal_pack": 0} - var multi := 0 - for trial in range(400): - var reward_economy_trial = reward_economy_factory.new(_db, rng) - if not reward_economy_trial.roll_board(eligible): - continue - var actives := 0 - for e in eligible: - var snap = reward_economy_trial.query_identity(e.cell) - if snap != null and (snap.category == "damage_item" or snap.category == "heal_pack"): - actives += 1 - species[snap.reward_id] = float(species[snap.reward_id]) + 1.0 - if actives != 1: - multi += 1 - check(multi == 0, "j=0 时每板恰注入 1 件可选类(400 板,多件板=%d)" % multi) - var total: float = species.knife_t1 + species.knife_adv + species.heal_pack - check(total == 400.0, "注入总量 = 板数(%d)" % int(total)) - check(species.knife_t1 / total > 0.52 and species.knife_t1 / total < 0.68, "小刀占比 ≈60%%(实测 %.0f%%)" % [100.0 * species.knife_t1 / total]) - check(species.knife_adv / total > 0.23 and species.knife_adv / total < 0.37, "精工短刀占比 ≈30%%(实测 %.0f%%)" % [100.0 * species.knife_adv / total]) - check(species.heal_pack / total > 0.05 and species.heal_pack / total < 0.15, "治疗包占比 ≈10%%(实测 %.0f%%——稀缺)" % [100.0 * species.heal_pack / total]) - # ② 格子池零可选类:身份批不含任何可选类身份(j=0 板上唯一可选类 = 注入件——已由①覆盖计数 =1) - # ③ 清图槽新语义:一次性高额伤害(instant_aoe,当场兑付类) - check(reward_economy.grant_clear().category == "instant_aoe", "清图奖励 = 一次性伤害(instant_aoe,非入库道具)") - check(int(reward_economy.grant_clear().power) >= 1, "清图奖励正当量(power=%d)" % int(reward_economy.grant_clear().power)) - # ④ 递减判定:j=1 → ≈p0;j=2 → ≈p0×decay(固定种子统计,宽容差) - var ctrl = load("res://core/reward_supply_controller.gd").new(_db.reward_config) - var raw := RandomNumberGenerator.new() - raw.seed = 777 - ctrl.notify_granted() - var hits1 := 0 - for i in range(2000): - if ctrl.should_inject(raw): - hits1 += 1 - check(absf(float(hits1) / 2000.0 - float(_db.reward_config.active_p0)) < 0.03, - "j=1 注入率 ≈ p0=%.2f(实测 %.3f)" % [float(_db.reward_config.active_p0), float(hits1) / 2000.0]) - ctrl.notify_granted() - var hits2 := 0 - for i in range(2000): - if ctrl.should_inject(raw): - hits2 += 1 - var expect2: float = float(_db.reward_config.active_p0) * float(_db.reward_config.active_decay) - check(absf(float(hits2) / 2000.0 - expect2) < 0.03, - "j=2 注入率 ≈ p0×decay=%.2f(实测 %.3f——指数递减)" % [expect2, float(hits2) / 2000.0]) - # ⑤ 首件保证恒真:j=0 时 2000 次判定全注入 - ctrl.notify_battle_start() - var all_true := true - for i in range(2000): - if not ctrl.should_inject(raw): - all_true = false - check(all_true, "j=0 首件保证恒注入(2000 次判定)") - # ⑥ 发放计数 + 战斗重置 - ctrl.notify_granted() - ctrl.notify_granted() - check(ctrl.granted_count() == 2, "发放计数(%d)" % ctrl.granted_count()) - ctrl.notify_battle_start() - check(ctrl.granted_count() == 0, "战斗开始计数清零(每战重新保底)") - # ⑦ 节点奖励入账回归(playtest-2026-09-12 报告 Bug#3):grant_node_active 此前直调 stock 绕过 - # _supply.notify_granted——发放即入账与格子授予(grant)同一口径,绕过通道已封 - var reward_economy_node = reward_economy_factory.new(_db, rng) - var node_inst = reward_economy_node.grant_node_active(Vector2i(-1, -1), 0) - check(node_inst is Dictionary, "节点奖励入库受理(%s)" % str(node_inst.get("item_id", "无"))) - check(reward_economy_node.supply_granted_count() == 1, "节点奖励计入供给控制器(granted=%d——Bug#3 回归:发放即入账)" % reward_economy_node.supply_granted_count()) + check(reward_economy.roll_board(eligible), "roll_board 受理") + check(not reward_economy.roll_board(eligible), "重复 roll_board 被拒(恰一次——double-roll 守卫)") + # 身份查询投影 = {reward_id, cell_type, tier};五型封闭 + var types_ok := true + var types_seen := {} + for e in eligible: + var ident: Dictionary = reward_economy.query_identity(e.cell) + if not (ident.has("reward_id") and ident.has("cell_type") and ident.has("tier")): + types_ok = false + if not RewardTable.CELL_TYPES.has(String(ident.cell_type)): + types_ok = false + types_seen[String(ident.cell_type)] = true + check(types_ok, "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" % str(types_seen.keys())) + # 清单外坐标 = null(E19——封锁格不入清单的同款出口) + check(reward_economy.query_identity(Vector2i(99, 99)) == null, "清单外坐标查询 = null(封锁格无身份同口径)") + # 同格重复受理幂等:同一快照逐字段相同 + var snap1: Dictionary = reward_economy.grant(eligible[0].cell) + var snap2: Dictionary = reward_economy.grant(eligible[0].cell) + check(JSON.stringify(snap1) == JSON.stringify(snap2), "同格重复 grant 幂等(同一快照——受理恰一次)") + check(reward_economy.grant(Vector2i(99, 99)) == null, "清单外 grant 拒绝") + # 清图批尾身份:clear_default 唯一活行 clear_blast(blast 型正向);重调幂等 + var cs1: Dictionary = reward_economy.grant_clear() + var cs2: Dictionary = reward_economy.grant_clear() + check(String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast", "清图批尾身份 = clear_blast(clear_default 唯一活行)") + check(int(cs1.power) >= 25 and int(cs1.power) <= 55, "清图奖励 power ∈ [25,55](区间行 roll 值 %d)" % int(cs1.power)) + check(JSON.stringify(cs1) == JSON.stringify(cs2), "grant_clear 同批次重调幂等") + # 退役后受理拒绝 + reward_economy.retire_mapping() + check(reward_economy.grant(eligible[0].cell) == null, "映射退役后 grant 拒绝(生命周期闭合)") + + +# ---- 地图战斗资源·伤害量值区间批冒烟(2026-09-14:身份批 roll 域内 + 中点锚 + 变异实际流动 + 定值行不受扰)---- # ---- 排雷玩法 旗动作耗尽末点 AP 回归(playtest 缺陷②——v1 冒烟盲区:自动玩家不用旗)---- @@ -169,7 +137,6 @@ func _smoke_flag_ap_exhausted() -> void: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rounds: Array = [] settlement.round_started.connect(func(ri): rounds.append(ri)) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) # 结算窗口立即应答 check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载旗回归场景)") var minesweeper = settlement.minesweeper # 恰花 7 点 AP 挖掘,留末点给旗 @@ -206,11 +173,6 @@ func _smoke_level() -> void: check(win.result == "victory", "关卡·满配(atk+200/hp+100)通关(实测:%s @节点 %d)" % [win.result, win.defeat_node_index]) check(win.battles == 5, "战斗节点数 = 5(战×4 + Boss)") check(win.boss_depth == 4, "Boss 节点以 Dmax=4 解析(棋盘生成 第三轴登记约定)") - check(win.reward_snapshot.has("reward_id") and (win.reward_snapshot.category == "damage_item" or win.reward_snapshot.category == "heal_pack"), - "奖励点 rolled 可选类主动道具(%s)" % str(win.reward_snapshot.get("reward_id", "无"))) - check(win.remaining_items >= 0, "关卡结束 level_clear 快照计数返回(清库前 %d 件随关卡结束清空)" % win.remaining_items) - check(win.actives_total >= 1 and win.actives_total <= win.battles + 1, - "可选类获取 ∈ [1, 战斗数+奖励点](实测 %d 件/%d 战——供给控制器批口径)" % [win.actives_total, win.battles]) check(win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3, "进 Boss HP 携带在域内(%d)" % win.hp_at_boss) # ② 恢复点:回复 = min(floor(生效max_hp×pct/100), 缺口)——hp+100% 档生效 max = 基线×2 var rest = autoplay.run_level(_db, 4242, 1, 100, 100) @@ -338,43 +300,22 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void: check(has_verdict, "%s 步骤流含判定行" % label) check(hero_attacks >= 1, "%s 主角先手攻击行存在(2026-09-11 拍板①)×%d" % [label, hero_attacks]) check(hero_breakdown_ok, "%s 主角攻击行携带三分量 normal/pierce/magic(克制批)" % label) - if run1.terminated and run1.result == "victory": - var lc = _last_settlement.level_clear() # 拍板③:关卡结束清库路径(战斗域不再清) - check(lc.has("remaining"), "%s level_clear 返回快照计数(剩余 %d)" % [label, int(lc.get("remaining", -1))]) func _run_battle(L: int, D: int) -> Dictionary: - # 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(奖励道具经济 AC"两套全新 RngService+奖励道具经济"同款) + # 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(42) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false} - var waiting_probe := {"tested": false, "rejected": false} # 等待期刷新反向断言触达标记(通道 2026-09-15 整体移除) settlement.step_row.connect(func(row): out.stream.append(row)) settlement.battle_finished.connect(func(payload): out.result = payload.get("result", "")) - settlement.window_opened.connect(func(items): - # 窗口数据契约断言:UI 读取的全部字段必须齐备(防呈现层中途崩溃把游戏锁死) - for it in items: - for key in ["index", "item_id", "display_name", "category", "power", "source_cell", "round_index", "usable_now"]: - if not it.has(key): - print(" [FAIL] 窗口条目缺字段 %s" % key) - _failures += 1 - # 等待期刷新通道已于 2026-09-15 移除:结算窗口期(Waiting)发起刷新必须被拒且状态不变 - if settlement.round_index == 2: - waiting_probe.tested = true - waiting_probe.rejected = not settlement.player_request_refresh() - # 确定性策略(窗口至多一件——2026-09-11 用户拍板): - # 半血以下优先用治疗包;否则第 3 轮起且最强单刀 ≥ 敌剩余总 HP(击杀有把握)时用该刀;否则跳过 - var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况(禁直读 enemy_system) - var selection: Array = AutoPlayer.window_selection(s, items) # O4:策略单一实现(div=3/gate=3/best 默认口径) - settlement.submit_window_selection(selection)) settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt)) settlement.minesweeper.flag_judged.connect(func(evt): out.events.append(evt)) settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag})) settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag})) var minesweeper = settlement.minesweeper var action_economy = settlement.action_economy - _last_settlement = settlement if not settlement.begin_battle({"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}): out.stream.append({"type": "verdict", "value": "begin_failed"}) return out @@ -385,11 +326,6 @@ func _run_battle(L: int, D: int) -> Dictionary: if out.result == "": out.result = "timeout" out.terminated = out.result == "victory" or out.result == "defeat" - # 等待期刷新反向断言(等待期换图通道 2026-09-15 整体移除):Waiting 相位请求必须被拒 - if waiting_probe.tested: - check(waiting_probe.rejected, "L%d·D%d 结算等待期刷新被拒(等待期换图通道已移除)" % [L, D]) - else: - print(" (跳过)L%d·D%d 等待期刷新反向断言未触达(该场战斗无第 2 轮结算窗口)" % [L, D]) return out @@ -414,12 +350,12 @@ func _auto_play_round(settlement, out: Dictionary) -> void: -# ---- 奖励道具经济·伤害量值区间批冒烟(2026-09-14:身份批 roll 域内 + 中点锚 + 变异实际流动 + 定值行不受扰)---- +# ---- 地图战斗资源·伤害量值区间批冒烟(2026-09-14:身份批 roll 域内 + 中点锚 + 变异实际流动 + 定值行不受扰)---- func _smoke_power_interval() -> void: - print("--- 奖励道具经济·伤害量值区间(上阶/下阶随机)---") - # ① 表侧静态锚:五行配对齐全、power = 中点(行动经济 条款①期望当量锚) - var interval_rows := {"heal_minor": [2, 6], "knife_t1": [5, 11], "heal_pack": [6, 18], "knife_adv": [10, 22], "clear_blast": [25, 55]} + print("--- 地图战斗资源·伤害量值区间(上阶/下阶随机)---") + # ① 表侧静态锚:区间行配对齐全、power = 中点(行动经济 条款①期望当量锚)——现行区间行 = heal_minor/clear_blast 两行 + var interval_rows := {"heal_minor": [2, 6], "clear_blast": [25, 55]} var anchors_ok := true for row in _db.reward.rows: var rid := String(row.reward_id) @@ -429,16 +365,16 @@ func _smoke_power_interval() -> void: if lo != interval_rows[rid][0] or hi != interval_rows[rid][1] or int(row.power) != (lo + hi) / 2: anchors_ok = false elif row.has("power_min") or row.has("power_max"): - anchors_ok = false # 区间配对只许五行(buff/spark_aoe 定值) - check(anchors_ok, "五行区间域与中点锚在位(其余行零区间字段)") - # ② 运行侧:24 板 roll——区间行快照 power 全域内 + 至少上下阶都出现过 + 定值行不受扰 + anchors_ok = false # 区间配对只许两行(其余全定值行) + check(anchors_ok, "区间行域与中点锚在位(heal_minor/clear_blast——其余行零区间字段)") + # ② 运行侧:24 板 roll——区间行快照 power 全域内 + 上下阶实际流动 + 定值行不受扰 var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260914) var reward_economy_factory = load("res://core/reward_economy.gd") var eligible: Array = [] for i in range(6): eligible.append({"cell": Vector2i(i, 0)}) - var seen := {"heal_minor": {}, "knife_t1": {}, "heal_pack": {}, "knife_adv": {}, "clear_blast": {}} # rid → {roll 值集} + var seen := {"heal_minor": {}, "clear_blast": {}} # rid → {roll 值集} var in_domain := true var fixed_ok := true for trial in range(24): @@ -448,8 +384,6 @@ func _smoke_power_interval() -> void: for e in eligible: var snap: Dictionary = reward_economy.query_full_snapshot(e.cell) var rid := String(snap.reward_id) - if not seen.has(rid): - seen[rid] = {} if interval_rows.has(rid): var v := int(snap.power) seen[rid][v] = true @@ -463,22 +397,10 @@ func _smoke_power_interval() -> void: if cv < 25 or cv > 55: in_domain = false check(in_domain, "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)") - check(fixed_ok, "定值行(buff/spark_aoe)快照 power 恒等表值——区间批零外溢") - var variance_ok: bool = seen.has("knife_t1") and int(seen["knife_t1"].size()) >= 2 \ - and seen.has("clear_blast") and int(seen["clear_blast"].size()) >= 2 \ - and seen.has("heal_minor") and int(seen["heal_minor"].size()) >= 2 - check(variance_ok, "上阶/下阶实际流动(knife_t1/clear_blast/heal_minor 各 ≥2 个不同 roll 值)") - # ③ 节点奖励路径:grant_node_active 同样 roll(域内) - var reward_economy_node = reward_economy_factory.new(_db, rng) - var node_ok := true - for trial in range(8): - var inst = reward_economy_node.grant_node_active(Vector2i(-1, -1), 0) - var rid := String(inst.item_id) - if interval_rows.has(rid): - var v := int(inst.snapshot.power) - if v < interval_rows[rid][0] or v > interval_rows[rid][1]: - node_ok = false - check(node_ok, "grant_node_active 区间行 roll 域内(节点奖励路径)") + check(fixed_ok, "定值行快照 power 恒等表值——区间批零外溢") + var variance_ok: bool = int(seen["clear_blast"].size()) >= 2 \ + and int(seen["heal_minor"].size()) >= 2 + check(variance_ok, "上阶/下阶实际流动(clear_blast/heal_minor 各 ≥2 个不同 roll 值)") func _row_power(rid: String) -> int: @@ -498,15 +420,19 @@ func _smoke_active_refresh() -> void: rng.set_master_seed(20260912) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) # 计数器用容器(GDScript lambda 对局部 int 按值捕获——int 计数在 lambda 内加无效) - var ct := {"board_swaps": 0, "safe_events": 0, "safe_rows": 0} + var ct := {"board_swaps": 0, "safe_events": 0, "safe_rows": 0, "waiting_refresh_rejected": true, "waiting_probe_hit": false} settlement.board_changed.connect(func(): ct["board_swaps"] += 1) settlement.minesweeper.cell_revealed.connect(func(evt): if evt.kind == "safe" and evt.channel != "initial": ct["safe_events"] += 1) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) settlement.step_row.connect(func(row): if row.source_class == "safe_cell": - ct["safe_rows"] += 1) # 结算流中 safe_cell 来源行(含兑付/未生效——每格必有行) + ct["safe_rows"] += 1 # 结算流中 safe_cell 来源行(含兑付/未生效——每格必有行) + # 结算中(Waiting 相位——段内)发起刷新必须被拒(等待期换图通道 2026-09-15 整体移除的回归探针: + # 衰减行恒在每结算出现,借其在段内同步探测一次) + if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]): + ct["waiting_probe_hit"] = true + ct["waiting_refresh_rejected"] = not settlement.player_request_refresh()) check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)") var minesweeper = settlement.minesweeper # 轮 1–3:零挖掘停手攒 RAP 24(每轮 8 AP 全转存) @@ -553,6 +479,7 @@ func _smoke_active_refresh() -> void: check(settlement.round_index == 5 and settlement.battle_result == "", "轮 4 挖至 AP 耗尽自动结算(round=%d result='%s')" % [settlement.round_index, settlement.battle_result]) check(int(ct["safe_rows"]) == int(ct["safe_events"]), "跨图 facts 完整兑现(safe_cell 结算行 %d == safe 事件 %d——快照批次隔离)" % [int(ct["safe_rows"]), int(ct["safe_events"])]) check(int(ct["safe_events"]) >= 3, "跨图挖掘 safe 事件 ≥3(%d——断言非空转)" % int(ct["safe_events"])) + check(bool(ct["waiting_refresh_rejected"]) and bool(ct["waiting_probe_hit"]), "结算段内(Waiting 相位)刷新被拒(等待期换图通道已移除)") # 回放确定性:同种子两遍步骤流逐行相同(含两次轮内刷新取号序) var rep1 = _active_refresh_replay() var rep2 = _active_refresh_replay() @@ -566,7 +493,6 @@ func _active_refresh_replay() -> Array: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] settlement.step_row.connect(func(row): rows.append(row)) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}): return rows var minesweeper = settlement.minesweeper @@ -649,42 +575,45 @@ func _smoke_damage_types() -> void: check(base_magic >= 1, "充能落桶基线:标中旗奖励进魔法桶(魔法分量合计 %d)" % base_magic) check(swap_magic == 0, "改投穿刺桶后:主角攻击行魔法分量归零(×%d——改奖励即改道,攻击类型定义未动)" % swap_magic) check(JSON.stringify(run_base.rows) == JSON.stringify(_run_flag_backlash_round(20260912).rows), "还原奖励定义后步骤流逐行相同(改道只经奖励定义,矩阵零改动)") - # ③ 标错惩罚 + 标中充能(奖励定义现投魔法桶;Boss 行 150HP 多轮样本;板真相直读保证 hit/miss 确定性) + # ③ 标错(类型化负向兑付)+ 标中充能(奖励定义现投魔法桶;Boss 行 150HP 多轮样本;板真相直读保证 hit/miss 确定性) var run1 = _run_flag_backlash_round(20260912) var miss_rows := 0 - var backlash_rows := 0 + var negative_rows := 0 var magic_round := -1 var magic_seen := 0 for row in run1.rows: if row.get("type", "") == "flag_miss": miss_rows += 1 - if row.get("type", "") == "effect" and String(row.get("source_class", "")) == "flag_miss": - backlash_rows += 1 # 反噬效果行(暴走/伏击/失衡三选一) + if String(row.get("source_class", "")) == "flag_miss" and String(row.get("type", "")) in ["effect", "damage"]: + negative_rows += 1 # 标错的负向兑付行(该安全格资源的负向字典行——effect/直伤) if row.get("source_class", "") == "hero_attack" and int(row.get("magic", 0)) >= 1: magic_round = int(row.get("round_index", -1)) magic_seen = int(row.get("magic", 0)) - check(miss_rows >= 1, "标错旗产生 flag_miss 行(×%d)" % miss_rows) - check(backlash_rows >= 1, "标错旗触发随机反噬效果行(×%d——敌增伤/敌buff/我方debuff 三选一)" % backlash_rows) + check(miss_rows >= 1, "标错旗产生 flag_miss 归因行(×%d)" % miss_rows) + check(negative_rows >= 1, "标错旗兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" % negative_rows) check(magic_round >= 1, "标中雷充能进主角攻击(magic=%d @R%d——每雷实例一次)" % [magic_seen, magic_round]) - # ④ 同种子回放(确定性派生反噬——零新流同种子逐行相同) + # ④ 同种子回放(极性路由零随机——同种子逐行相同) var run2 = _run_flag_backlash_round(20260912) - check(JSON.stringify(run1.rows) == JSON.stringify(run2.rows), "标错惩罚轮回放断言:同种子步骤流逐行相同(反噬派生确定性)") + check(JSON.stringify(run1.rows) == JSON.stringify(run2.rows), "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)") -# 构造轮:标 1 面"安全格旗"(必 miss → 反噬)+ 标 1 面"雷旗"(必 hit → 充能)后停手收轮 +# 构造轮:标 1 面"安全格旗"(必 miss → 该格资源负向兑付)+ 标 1 面"雷旗"(必 hit → 正向兑付 + 充能)后停手收轮。 +# 返回 flagged safe 格与其 cell_type(Ready 域内查询——类型化断言的数据面)。 func _run_flag_backlash_round(seed_val: int) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] settlement.step_row.connect(func(row): rows.append(row)) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}): - return {"rows": rows} + return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""} var minesweeper = settlement.minesweeper var safe_t := _find_flag_target(minesweeper, false) var mine_t := _find_flag_target(minesweeper, true) + var safe_type := "" if not safe_t.is_empty(): + var ident = settlement.query_reward_identity(safe_t.flag_cell) + safe_type = String(ident.get("cell_type", "")) if ident != null else "" if safe_t.hero_cell != minesweeper.hero_pos(): minesweeper.settle_batch([{"action": "move", "cell": safe_t.hero_cell}]) minesweeper.settle_batch([{"action": "flag_place", "cell": safe_t.flag_cell}]) @@ -693,7 +622,7 @@ func _run_flag_backlash_round(seed_val: int) -> Dictionary: minesweeper.settle_batch([{"action": "move", "cell": mine_t.hero_cell}]) minesweeper.settle_batch([{"action": "flag_place", "cell": mine_t.flag_cell}]) minesweeper.settle_batch([{"action": "stop"}]) - return {"rows": rows} + return {"rows": rows, "safe_cell": safe_t.get("flag_cell", Vector2i(-1, -1)), "safe_type": safe_type} # 板真相直读找旗目标:未开 + 指定雷/安全属性 + 存在可达已开邻格(测试允许读板真相——排雷玩法 冒烟同口径) @@ -733,7 +662,7 @@ func _smoke_playtest_logger() -> void: minesweeper.settle_batch([{"action": "stop"}]) check(snaps.size() == 2, "每结算一快照(%d 次结算 → %d 快照)" % [2, snaps.size()]) var s1: Dictionary = snaps[0] - var required := ["kind", "level", "depth", "round", "verdict", "hero", "economy", "board", "enemies", "buffs", "ambush_layers", "inventory_in_stock", "round_facts", "spawn_count"] + var required := ["kind", "level", "depth", "round", "verdict", "hero", "economy", "board", "enemies", "buffs", "ambush_layers", "round_facts", "spawn_count"] var missing := [] for k in required: if not s1.has(k): @@ -848,7 +777,6 @@ func _smoke_attack_chain_fresh_pool() -> void: settlement.step_row.connect(func(row): if row.type == "damage" and row.source_class == "enemy_action": ticks.append({"raw": int(row.baseline) + int(row.attach), "tick": int(row.value)})) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)") # 停手轮:零挖掘零旗 → 零授予 → 池恒空;Boss pattern 前 4 拍 = attack/attack/charge/heavy for i in range(4): @@ -883,7 +811,7 @@ var _regular_clear_reward_rows := 0 # 常规(非刷新图)清图的 clear_ func _smoke_clear_reward_chain() -> void: print("--- 清图奖励·双份兑付链(完美排雷 / 零兑付 / 第二份被拒 / 高光记实)---") - # ① 真实流完美排雷:标中一面真雷旗 + 零误标 + 清全图 → grant_clear 身份(instant_aoe)双份真实兑付 + # ① 真实流完美排雷:标中一面真雷旗 + 零误标 + 清全图 → grant_clear 身份(clear_blast)双份真实兑付 var rows: Array = [] var run := _run_full_clear(20260915, false, rows) check(bool(run.cleared), "① 完美排雷清图达成(%d 轮)" % int(run.rounds)) @@ -893,7 +821,7 @@ func _smoke_clear_reward_chain() -> void: dmg.append(r) var hl := _rows_of_type(rows, "highlight") _regular_clear_reward_rows = dmg.size() - check(dmg.size() == 2, "① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——instant_aoe 身份两份各打一发)" % dmg.size()) + check(dmg.size() == 2, "① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" % dmg.size()) if dmg.size() == 2: check(String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份"), "① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)") @@ -913,26 +841,24 @@ func _smoke_clear_reward_zero_paths() -> void: var s = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] s.step_row.connect(func(row): rows.append(row)) - s.window_opened.connect(func(items): s.submit_window_selection([])) check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), "零兑付构造战斗启动") for u in s._enemy_system.all_enemies(): s._enemy_system.apply_damage(u, int(u.hp)) # 测试通道:构造"无存活敌"(弱形式——全灭而非 despawn) var snap = s._reward_economy.grant_clear() - check(snap != null and String(snap.category) == "instant_aoe", - "清图槽身份 = instant_aoe(clear_default 唯一活行——第二份同身份兑付)") + check(snap != null and String(snap.cell_type) == "blast", + "清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)") rows.clear() - var first_ok: bool = s._grant_clear_identity(snap, 1) + var first_ok: bool = s._grant_clear_identity(snap, 1, "(清图奖励)") check(not first_ok, "② 即时类首份零兑付(无存活敌)→ first_ok=false") - check(_has_source_row(rows, "reward_ineffective", "clear_reward", "instant_aoe"), - "② 零兑付产出 reward_ineffective 行(batch_key=instant_aoe)") + check(_has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast"), + "② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)") # ②-b 高光守卫:perfect 谓词成立但首份零兑付 → 高光行不发(不记名义值) var rng_b = load("res://core/rng_service.gd").new() rng_b.set_master_seed(20260915) var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) var rows_b: Array = [] s_b.step_row.connect(func(row): rows_b.append(row)) - s_b.window_opened.connect(func(items): s_b.submit_window_selection([])) check(s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), "高光守卫构造战斗启动") for u in s_b._enemy_system.all_enemies(): @@ -941,7 +867,7 @@ func _smoke_clear_reward_zero_paths() -> void: s_b._perfect_hit_false = 0 rows_b.clear() s_b._grant_clear_reward(1) - check(_has_source_row(rows_b, "reward_ineffective", "clear_reward", "instant_aoe"), + check(_has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast"), "② _grant_clear_reward 守卫路径重走首份 → 未生效行仍在案") check(_rows_of_type(rows_b, "highlight").is_empty(), "② 首份零兑付 → 高光行不发(不记名义值 2)") # ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行) @@ -950,7 +876,6 @@ func _smoke_clear_reward_zero_paths() -> void: var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2) var rows2: Array = [] s2.step_row.connect(func(row): rows2.append(row)) - s2.window_opened.connect(func(items): s2.submit_window_selection([])) check(s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), "第二份被拒构造战斗启动") var boss = s2._enemy_system.all_enemies()[0] @@ -960,7 +885,7 @@ func _smoke_clear_reward_zero_paths() -> void: rows2.clear() s2._grant_clear_reward(1) check(_has_source_row(rows2, "damage", "clear_reward"), "③ 首份 aoe 真实兑付(击杀唯一存活敌)") - check(_has_source_row(rows2, "reward_ineffective", "clear_reward", "instant_aoe"), + check(_has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast"), "③ 第二份被拒产出 reward_ineffective 行(零兑付)") var hl2 := _rows_of_type(rows2, "highlight") check(hl2.size() == 1 and int(hl2[0].value) == 1, "④ 高光行 value = 实际有效份数 1(不记名义值 2)") @@ -985,44 +910,306 @@ func _smoke_refresh_provenance_guard() -> void: check(_regular_clear_reward_rows > 0, "对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" % _regular_clear_reward_rows) -# 窗口段锚行(战斗结算 验收:"窗口段恒在案,零选择记跳过""空库存记跳过") -func _smoke_window_anchor_rows() -> void: - print("--- 道具窗口段锚行(打开 / 零选择跳过 / 空库存跳过)---") +# ==================== 格面资源批新增覆盖(极性路由 / 负向字典 / 资源面 / 五段全序) ==================== +# 极性路由断言(战斗结算 验收):挖安全格/标中旗只出正向、挖雷格/标错旗只出负向、兑付行指回来源格。 +# 正向字典效果集 = {attack_up, defense_up, weaken}(percent)+ spark_aoe/heal_minor(flat); +# 负向字典效果集 = {enrage, off_guard, sapped}(percent)+ 直伤(heal flat)/ ambush(blast flat)。 +const POSITIVE_EFFECTS := ["attack_up", "defense_up", "weaken"] +const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "ambush"] + + +func _smoke_polarity_routing() -> void: + print("--- 极性路由(挖安全格正向 / 挖雷格负向 / 标中正向 / 标空负向)---") + var probe := _run_polarity_probe(20260918) + check(bool(probe["flag_hit_done"]) and bool(probe["flag_miss_done"]), "构造触达:标中真雷 + 标空安全格均入队") + check(int(probe["mine_events"]) >= 1, "构造触达:挖到雷格 ≥1(%d)" % int(probe["mine_events"])) + check(int(probe["safe_events"]) >= 1, "构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"])) + # 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集 + var safe_all_pos := true + for e in probe["safe_cells"].keys(): + var cell_arr: Array = [e.x, e.y] + var rows := _rows_at(probe["rows"], "safe_cell", cell_arr) + if rows.is_empty(): + safe_all_pos = false # 每个资源格必有 ≥1 兑付行(每格必有货 → 每格必有行) + for r in rows: + var eff := String(r.get("effect_id", "")) + if eff != "" and not POSITIVE_EFFECTS.has(eff): + safe_all_pos = false + check(safe_all_pos, "挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" % probe["safe_cells"].size()) + # 挖到的雷格:只出负向——来源行必存在且 effect 行 ∈ 负向集(ambush = blast 负向;heal 负向 = damage 行) + var mine_all_neg := true + for e in probe["mine_cells"].keys(): + var cell_arr: Array = [e.x, e.y] + var rows := _rows_at(probe["rows"], "mine_hit", cell_arr) + if rows.is_empty(): + mine_all_neg = false + for r in rows: + var eff := String(r.get("effect_id", "")) + if eff != "" and not NEGATIVE_EFFECTS.has(eff): + mine_all_neg = false + check(mine_all_neg, "挖雷格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" % probe["mine_cells"].size()) + # 标中旗:该雷格正向兑付行(flag_hit 来源)+ 效果 ∈ 正向字典 + var hit_ok := false + for r in probe["rows"]: + if String(r.get("source_class", "")) == "flag_hit" and r.get("origin_cell", [0, 0]) == probe["hit_cell"]: + hit_ok = true + var eff := String(r.get("effect_id", "")) + if eff != "" and not POSITIVE_EFFECTS.has(eff): + hit_ok = false + check(hit_ok, "标中旗 → 该雷格资源的正向兑付行(origin 指回雷格,效果 ∈ 正向字典)") + # 标空旗:归因行 + 该安全格负向兑付行(flag_miss 来源) + var miss_rows := _rows_at(probe["rows"], "flag_miss", probe["miss_cell"]) + var miss_neg_ok := false + for r in miss_rows: + if String(r.get("type", "")) == "flag_miss": + miss_neg_ok = true + else: + var eff := String(r.get("effect_id", "")) + if eff != "" and not NEGATIVE_EFFECTS.has(eff): + miss_neg_ok = false + check(not miss_rows.is_empty() and miss_neg_ok, "标空旗 → 标旗未中归因行 + 该安全格资源负向兑付(同格同批)") + # 抵消过滤既有语义:标旗后被撤旗不判(发射面基线过滤)——由排雷玩法 冒烟与旗判定基线覆盖,此处核兑付行零泄漏 + var flag_srcs := {} + for r in probe["rows"]: + if String(r.get("source_class", "")) in ["flag_hit", "flag_miss"]: + flag_srcs[r.get("origin_cell")] = true + check(int(flag_srcs.size()) <= 2, "旗判定兑付仅旗判定批两旗在案(撤旗抵消过滤零泄漏——%d 格)" % flag_srcs.size()) + + +# 极性路由探针驱动:标 1 面真雷旗(标中)+ 1 面安全格旗(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。 +# 收集全部 cell_revealed / flag_judged 事件与结算行;返回格子集合与旗格坐标。 +func _run_polarity_probe(seed_val: int) -> Dictionary: + _db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断 var rng = load("res://core/rng_service.gd").new() - rng.set_master_seed(20260917) - var s = load("res://core/battle_orchestrator.gd").new(_db, rng) - var rows: Array = [] - var probe := {"opens": 0, "open_ok": 0, "skips": 0, "empty_skips": 0} - s.step_row.connect(func(row): - rows.append(row) - if String(row.get("type", "")) == "window_skip": - probe["skips"] = int(probe["skips"]) + 1 - if int(row.value) == 0 and String(row.display_name).contains("空库存"): - probe["empty_skips"] = int(probe["empty_skips"]) + 1) - s.window_opened.connect(func(items): - probe["opens"] = int(probe["opens"]) + 1 - if not rows.is_empty(): - var last: Dictionary = rows[rows.size() - 1] - if String(last.get("type", "")) == "window_open" and int(last.value) == items.size(): - probe["open_ok"] = int(probe["open_ok"]) + 1 - s.submit_window_selection([])) - check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}), - "窗口锚行战斗启动") - # 轮 1 零挖掘停手 → 库存空 → 记跳过锚行(空库存不弹窗) - s.minesweeper.settle_batch([{"action": "stop"}]) - check(int(probe["opens"]) == 0 and int(probe["empty_skips"]) == 1, - "空库存记跳过(window_skip ×%d,窗口未打开 ×%d)" % [int(probe["empty_skips"]), int(probe["opens"])]) - # 后续轮:挖到首件可选类入库 → 窗口打开锚行 + 零选择跳过锚行 - var guard := 10 + rng.set_master_seed(seed_val) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var probe := {"rows": [], "safe_cells": {}, "mine_cells": {}, "hit_cell": [], "miss_cell": [], + "mine_events": 0, "safe_events": 0, "flag_hit_done": false, "flag_miss_done": false} + settlement.step_row.connect(func(row): probe["rows"].append(row)) + settlement.minesweeper.cell_revealed.connect(func(evt): + if String(evt.get("channel", "")) == "initial": + return + if evt.kind == "mine": + probe["mine_cells"][evt.cell] = true + probe["mine_events"] = int(probe["mine_events"]) + 1 + elif evt.kind == "safe": + probe["safe_cells"][evt.cell] = true + probe["safe_events"] = int(probe["safe_events"]) + 1) + settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 300, "hero_max_hp": 300}) + var ms = settlement.minesweeper + # 旗 1:真雷(标中 → 正向);旗 2:安全格(标空 → 负向) + var mine_t := _find_flag_target(ms, true) + var safe_t := _find_flag_target(ms, false) + if not mine_t.is_empty(): + if mine_t.hero_cell != ms.hero_pos(): + ms.settle_batch([{"action": "move", "cell": mine_t.hero_cell}]) + ms.settle_batch([{"action": "flag_place", "cell": mine_t.flag_cell}]) + probe["hit_cell"] = [mine_t.flag_cell.x, mine_t.flag_cell.y] + probe["flag_hit_done"] = true + if not safe_t.is_empty(): + if safe_t.hero_cell != ms.hero_pos(): + ms.settle_batch([{"action": "move", "cell": safe_t.hero_cell}]) + ms.settle_batch([{"action": "flag_place", "cell": safe_t.flag_cell}]) + probe["miss_cell"] = [safe_t.flag_cell.x, safe_t.flag_cell.y] + probe["flag_miss_done"] = true + # 挖到终局(旗挡链不影响 AutoPlayer——choose_dig 跳过旗格) var out := {"result": "", "rounds": 0, "terminated": false, "stream": []} - while int(s.presentation_snapshot().in_stock) == 0 and guard > 0 and s.battle_result == "": + var guard := 40 + while settlement.battle_result == "" and guard > 0: guard -= 1 - _auto_play_round(s, out) - check(int(probe["opens"]) >= 1, "窗口打开锚行触达(window_opened ×%d)" % int(probe["opens"])) - check(int(probe["open_ok"]) == int(probe["opens"]), - "每个 window_opened 前恰有 window_open 锚行且值 = 可选件数(%d/%d)" % [int(probe["open_ok"]), int(probe["opens"])]) - check(int(probe["skips"]) >= int(probe["opens"]), - "零选择记跳过锚行(window_skip %d ≥ window_open %d)" % [int(probe["skips"]), int(probe["opens"])]) + _auto_play_round(settlement, out) + _db.difficulty.rows.clear() # 测试台账还原 + return probe + + +func _rows_at(rows: Array, source_class: String, cell) -> Array: + var out: Array = [] + for r in rows: + if String(r.get("source_class", "")) == source_class and r.get("origin_cell", [9, 9]) == cell: + out.append(r) + return out + + +# 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 _grant_routed 负向路由, +# 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast ambush+cap 溢出协议)。 +func _smoke_negative_dictionary() -> void: + print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 伏击+2 与 cap 溢出)---") + # 数据面:sapped 行在位(状态表七行 + effect_pool_map 收编) + var sapped: Dictionary = _db.status_effect_row("sapped") + check(not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \ + and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \ + and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack", + "sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)") + check(String(_db.settlement.effect_pool_map.get("sapped", "")) == "hero_offense", + "effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)") + # 行为面:逐型负向兑付形态(真实板 + 真实路由路径) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260919) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + check(settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 50, "hero_max_hp": 50}), + "负向字典战斗启动") + var board = settlement.minesweeper.current_board() + var r0 := {} + for c in board.compute_r0(): + r0[c] = true + var by_type := {} # cell_type → 首个非封锁格(query_identity 雷安同池——雷格可查) + for y in board.height: + for x in board.width: + var c := Vector2i(x, y) + if r0.has(c) or by_type.size() == 5: + continue + var ident = settlement.query_reward_identity(c) + if ident != null and not by_type.has(String(ident.cell_type)): + by_type[String(ident.cell_type)] = c + check(by_type.size() == 5, "板上五型齐备(%s)" % str(by_type.keys())) + # attack/defense/weaken:percent 负向行走 apply_effect + var percent_map := {"attack": "enrage", "defense": "off_guard", "weaken": "sapped"} + for ctype in percent_map.keys(): + if not by_type.has(ctype): + continue + rows.clear() + var snap: Dictionary = settlement._reward_economy.query_full_snapshot(by_type[ctype]) + settlement._grant_routed(snap, "negative", "mine_hit", by_type[ctype], 1) + var eff: String = percent_map[ctype] + check(_has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff), + "%s 型负向 → %s 效果行(mine_hit 来源指回该格)" % [ctype, eff]) + # heal:负向 = 主角直伤 4(不经增益状态) + if by_type.has("heal"): + rows.clear() + var hp_before: int = int(settlement.presentation_snapshot().hero.hp) + var snap_h: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["heal"]) + settlement._grant_routed(snap_h, "negative", "mine_hit", by_type["heal"], 1) + var hp_after: int = int(settlement.presentation_snapshot().hero.hp) + check(hp_after == hp_before - 4, "heal 型负向 → 主角直伤 4(%d → %d,不经增益状态)" % [hp_before, hp_after]) + # blast:负向 = 伏击 +2 层;cap 内生效、超 cap 逐层溢出(cap 读敌行值——零字面量) + if by_type.has("blast"): + var cap: int = int(_db.resolve_enemy_rowset(1, 0).get("advantage_per_round_cap", 0)) + rows.clear() + var snap_b: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["blast"]) + settlement._grant_routed(snap_b, "negative", "mine_hit", by_type["blast"], 1) + var layers_after_first: int = settlement._status_effects.pool_layers("ambush") + check(layers_after_first == mini(2, cap), "blast 型负向 → 伏击 +%d 层(cap=%d)" % [layers_after_first, cap]) + while settlement._status_effects.pool_layers("ambush") < cap: + settlement._grant_routed(snap_b, "negative", "mine_hit", by_type["blast"], 1) + rows.clear() + settlement._grant_routed(snap_b, "negative", "mine_hit", by_type["blast"], 1) + check(_rows_of_type(rows, "overflow").size() >= 1 and settlement._status_effects.pool_layers("ambush") == cap, + "blast 型负向超 cap → 溢出未生效行(伏击池守恒在 cap=%d)" % cap) + + +# 资源面断言:chain/dig 事件恒携带 resource=true(雷格同权);initial 恒 false(封锁格); +# 封锁格查询 = null、雷格未开恒可查。 +func _smoke_resource_surface() -> void: + print("--- 格面资源资格面(雷格携带 / 封锁格无资源)---") + var run1 = _run_battle(1, 0) + var run2 = _run_battle(2, 0) + var all_events: Array = run1.events + run2.events + var digchain_bad := 0 + var initial_bad := 0 + var mine_with_resource := 0 + var initial_count := 0 + for evt in all_events: + if not evt.has("resource"): + continue # 非揭示事件(round_end 标记等) + if String(evt.get("channel", "")) == "initial": + initial_count += 1 + if bool(evt.resource): + initial_bad += 1 + else: + if not bool(evt.resource): + digchain_bad += 1 + if evt.kind == "mine": + mine_with_resource += 1 + check(digchain_bad == 0, "chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" % digchain_bad) + check(initial_bad == 0 and initial_count >= 1, "initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" % initial_count) + check(mine_with_resource >= 1, "雷格揭示携带资源(mine ×resource=true %d——雷格不再恒无)" % mine_with_resource) + # 封锁格无身份 / 雷格未开恒可查(未开格恒可查——魔法道具 桥接结构前提) + var rng3 = load("res://core/rng_service.gd").new() + rng3.set_master_seed(20260920) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3) + settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 50}) + var board = settlement.minesweeper.current_board() + var r0_first: Variant = null + for c in board.compute_r0(): + r0_first = c + break + var mine_unrevealed = null + for c in board.mines.keys(): + if not settlement.minesweeper.is_revealed(c): + mine_unrevealed = c + break + check(settlement.query_reward_identity(r0_first) == null, "封锁格(R₀)身份查询 = null(不入摇号清单)") + check(mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null, + "未开雷格身份恒可查({reward_id, cell_type, tier}——魔法道具 桥接前提)") + + +# 五段全序断言(战斗结算 验收"五段全序"+ 行型封闭 10 类):逐轮行相位序 = 授予 → 增益/减益 → 战斗 → 衰减 → 判定。 +func _smoke_settlement_segments() -> void: + print("--- 五段全序 + 行型封闭 ---") + check(Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER + and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"], + "segment_order 五段冻结在位(表值 = 拍板序恒等)") + var run = _run_battle(1, 0) + var legacy := {"item": 0, "consumed_ineffective": 0, "use_rejected": 0, "window_open": 0, "window_skip": 0} + for row in run.stream: + if legacy.has(String(row.get("type", ""))): + legacy[String(row.get("type", ""))] += 1 + var legacy_total := 0 + for k in legacy: + legacy_total += int(legacy[k]) + check(legacy_total == 0, "行型封闭 10 类(删五类零残留:item/consumed_ineffective/use_rejected/window_open/window_skip)") + var order_ok := true + var decay_verdict_ok := true + var by_round := {} + for row in run.stream: + var ri: int = int(row.get("round_index", 0)) + if not by_round.has(ri): + by_round[ri] = [] + by_round[ri].append(row) + for ri in by_round.keys(): + var phases: Array = [] + for row in by_round[ri]: + phases.append(_row_phase(row, ri)) + var max_phase := 0 + for p in phases: + if int(p) < max_phase: + order_ok = false + max_phase = maxi(max_phase, int(p)) + var decays := 0 + var verdicts := 0 + for row in by_round[ri]: + if String(row.get("type", "")) == "decay": + decays += 1 + if String(row.get("type", "")) == "verdict": + verdicts += 1 + if decays != 1 or verdicts != 1: + decay_verdict_ok = false # 衰减/判定每结算恰一次 + check(order_ok, "每轮行相位单调(授予→增益/减益→战斗→衰减→判定)") + check(decay_verdict_ok, "衰减/判定段每结算恰一段(先兑付后衰减——含末轮)") + + +# 行相位归类(五段序断言用):0=授予(含高光/归因)1=增益减益(续存组合行)2=战斗 3=衰减 4=判定 +func _row_phase(row: Dictionary, ri: int) -> int: + match String(row.get("type", "")): + "decay": + return 3 + "verdict": + return 4 + "charge": + return 2 + "damage": + if String(row.get("source_class", "")) in ["hero_attack", "enemy_action"]: + return 2 + return 0 # 授予段 flat 兑付(爆裂/直伤) + match String(row.get("source_class", "")): + "safe_cell", "mine_hit", "flag_hit", "flag_miss", "clear_reward": + if int(row.get("round_index", ri)) < ri: + return 1 # 前轮授予实例的续存组合行(增益/减益段) + return 0 + _: + return 0 # 清图驱动(真实流):标一面真雷旗(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。 @@ -1033,7 +1220,6 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona rng.set_master_seed(seed_val) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) settlement.step_row.connect(func(row): rows.append(row)) - settlement.window_opened.connect(func(items): settlement.submit_window_selection([])) var res := {"cleared": false, "refreshed": false, "rounds": 0} if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}): @@ -1089,6 +1275,13 @@ func _has_source_row(rows: Array, t: String, source: String, batch_key: String = return true return false + +func _has_effect_id(rows: Array, effect_id: String) -> bool: + for r in rows: + if String(r.get("effect_id", "")) == effect_id: + return true + return false + func check_silent(cond: bool) -> void: _checks += 1 if not cond: diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd deleted file mode 100644 index 0c878d2f..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/supply_scan.gd +++ /dev/null @@ -1,80 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# Date: 2026-09-13(playtest-2026-09-12 Top1 复核——"seed 42 每局 ≥3 治疗包"是固定种子重放还是系统性偏置?) -# 跨种子可选类供给扫描:每局统计治疗包/小刀/精工短刀的实际获取数(step_row item 行 + 关卡奖励点补计), -# 输出构成占比(对 9:1 设计配比)与"≥3 治疗包"局数分布——单种子坏抽与系统性偏置由此分割。 -# 运行:Godot_console.exe --headless --path . --script res://smoke/supply_scan.gd -- [N] [seed0] -extends SceneTree - - -func _initialize() -> void: - var args := OS.get_cmdline_user_args() - var n := 16 - if args.size() >= 1 and args[0].is_valid_int(): - n = maxi(4, int(args[0])) - var seed0 := 100 - if args.size() >= 2 and args[1].is_valid_int(): - seed0 = int(args[1]) - var db = load("res://core/config_db.gd").new() - if not db.load_tables("res://data/tables", false): - print("表加载失败") - quit(1) - return - var autoplay = load("res://smoke/level_autoplay.gd") - print("=== 可选类供给跨种子扫描:第一关 · N=%d · 种子 %d+%d(基线数值)===" % [n, seed0, n]) - print("%-8s %-8s %-6s %-8s %-16s %-10s" % ["seed", "结果", "轮数", "承伤", "治疗包/小刀/短刀", "进BossHP"]) - var heals: Array = [] - var total_h := 0 - var total_k := 0 - var total_a := 0 - for i in range(n): - var seed_val: int = seed0 + i - var st = autoplay.run_level(db, seed_val, 1, 0, 0, true) - var h := 0 - var k := 0 - var a := 0 - for row in st.rows: - if String(row.get("type", "")) != "item": - continue - var txt := String(row.get("display_name", "")) - if txt.begins_with("治疗包"): - h += 1 - elif txt.begins_with("小刀"): - k += 1 - elif txt.begins_with("精工短刀"): - a += 1 - # 关卡奖励点(grant_node_active)不发 step_row——经 node 快照补计 - var nid := String(st.reward_snapshot.get("reward_id", "")) - if nid == "heal_pack": - h += 1 - elif nid == "knife_t1": - k += 1 - elif nid == "knife_adv": - a += 1 - heals.append(h) - total_h += h - total_k += k - total_a += a - print("%-8d %-8s %-6d %-8d %-16s %-10d" % [seed_val, st.result, st.rounds, st.hero_dmg, - "%d/%d/%d" % [h, k, a], st.hp_at_boss]) - var t := total_h + total_k + total_a - print("---- 合计 %d 件:治疗包 %d(%.0f%%)· 小刀 %d(%.0f%%)· 精工短刀 %d(%.0f%%)—— 伤害:治疗 = %.1f:1(设计 9:1)" % [ - t, total_h, 100.0 * total_h / maxi(1, t), total_k, 100.0 * total_k / maxi(1, t), - total_a, 100.0 * total_a / maxi(1, t), float(total_k + total_a) / maxi(1, total_h)]) - var ge3 := 0 - var zero := 0 - for c in heals: - if c >= 3: - ge3 += 1 - elif c == 0: - zero += 1 - print("每关治疗包:均值 %.2f · ≥3 包 %d/%d 局 · 0 包 %d/%d 局" % [_mean(heals), ge3, n, zero, n]) - quit(0) - - -func _mean(a: Array) -> float: - if a.is_empty(): - return 0.0 - var t := 0.0 - for v in a: - t += float(v) - return t / float(a.size()) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_click_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_click_probe.gd index c14b2493..4225fcfd 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_click_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_click_probe.gd @@ -3,7 +3,7 @@ # Date: 2026-09-12(五区改造批扩展;基础 = 2026-09-11 问题3 复现器 v2——既有断言全保留) # 合成鼠标事件走真实呈现层路径(BoardView.click_cell / 按钮信号 → 组合根动作通道), # 随机混合玩家行为——插旗/拔旗、提前停手、弃图刷新确认链、图例模态、明细回看模态、 -# tooltip 悬停、窗口选用/跳过、无效点击——扫描结算停滞与选用失效(缺陷②④回归载体) +# tooltip 悬停、无效点击——扫描结算停滞(缺陷②回归载体;缺陷④窗口契约随道具窗口删除退役 2026-09-15) # + 模态遮挡断言(缺陷⑤回归:图例/回看/终局 overlay 打开时上方遮挡层隐藏、输入锁定) # + 性能 AC:场景实例化到首格可点 ≤500ms 探针计时。 # 行为序列由 PROBE_SEED 决定(shell 传参);程序玩法数值不受影响(随机只用于探针策略)。 @@ -19,14 +19,11 @@ var _done := false var _rand := RandomNumberGenerator.new() var _flags_placed: Array = [] var _ap_spent_this_round := 0 -var _used_ok := 0 -var _used_bad := 0 var _problems: Array = [] # 新增覆盖的问题收集(退出码合并判定) var _tested_legend := false var _tested_review := false var _tested_refresh_chain := false var _tested_tooltip := false -var _tested_window_assert := false func _ready() -> void: @@ -61,14 +58,6 @@ func _ready() -> void: main.node_overlay.press_continue() _flags_placed.clear() continue - if main.inventory_panel.is_window_open(): - if not _tested_window_assert: - # 去饱和渐变(0.15s Tween)完成后断言——窗口悬置期无自动关闭,等待安全 - for _wait in 20: - await get_tree().process_frame - _assert_window_state_once() - _answer_window() - continue if main._end_overlay.is_open(): # 终局 overlay 模态断言(pattern 6:打开时输入锁定) if not main._input_locked(): @@ -82,8 +71,7 @@ func _ready() -> void: var st := { "node": main.runner.node_index, "round": main.settlement.round_index, "phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap, - "win": main.inventory_panel.is_window_open(), "settling": diag.settling, - "await": diag.awaiting_window, + "settling": diag.settling, } if JSON.stringify(st) == JSON.stringify(_last_state): _stall_frames += 1 @@ -97,14 +85,14 @@ func _ready() -> void: if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active": _run_modal_probes_once() _play_one_action() - print("[click_probe] 循环退出(done=%s budget=%s)选用生效=%s 选用失效=%s 问题=%d" % [ - str(_done), str(action_budget), str(_used_ok), str(_used_bad), _problems.size()]) + print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [ + str(_done), str(action_budget), _problems.size()]) for p in _problems: push_error("[click_probe] FAIL — " + p) var exit_code := 0 if not _done: exit_code = 1 - if _used_bad != 0 or not _problems.is_empty(): + if not _problems.is_empty(): exit_code = 1 get_tree().quit(exit_code) @@ -176,20 +164,6 @@ func _run_modal_probes_once() -> void: _problems.append("刷新轮内计数错误(%d)" % main._refresh_used_round) -func _assert_window_state_once() -> void: - if _tested_window_assert: - return - _tested_window_assert = true - # 原位窗口态(pattern 2):E 区描边金光点亮 + 标题切换 + D 区去饱和锁输入(明度不变) - if not main.inventory_panel._title.text.contains("轮末道具窗口"): - _problems.append("窗口态标题未切换(%s)" % main.inventory_panel._title.text) - if not main.inventory_panel._skip_btn.visible: - _problems.append("窗口态缺跳过按钮") - var strength: float = float(main.board_view._desat_mat.get_shader_parameter("strength")) - if strength < 0.9: - _problems.append("窗口期棋盘未去饱和(strength=%.2f)" % strength) - - func _play_one_action() -> void: var settlement = main.settlement if settlement == null: @@ -228,45 +202,6 @@ func _play_one_action() -> void: _click_stop() -func _answer_window() -> void: - # 行为⑥:选用首件可用道具 / 跳过(选择语义 = 窗口条目位置——与 UI 契约一致,缺陷④回归载体) - if _rand.randf() < 0.5 and main.settlement != null: - var settlement = main.settlement - var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读库存/英雄 - var pos := -1 - for inst in s.inventory: - if inst.status != "InStock": - continue - pos += 1 - var usable: bool = int(s.hero.hp) < int(s.hero.max_hp) or inst.category == "damage_item" - if usable: - var inv_index: int = int(inst.index) - settlement.submit_window_selection([pos]) - main.inventory_panel.close_window() - # 缺陷④回归断言:被选件必须真实消耗(Used),不得误耗邻件/留存 - var after: Array = settlement.presentation_snapshot().inventory - if not settlement.is_battle_live(): - # 终局结算窗口:选用后战斗立即结束、level_clear 合法清库——Used 断言不适用 - _used_ok += 1 - elif inv_index < after.size() and String(after[inv_index].status) == "Used": - _used_ok += 1 - else: - _used_bad += 1 - print("[dbg-use] FAIL pos=%d inv_index=%d cat=%s statuses=%s" % [ - pos, inv_index, inst.category, - str(after.map(func(x): return [int(x.index), String(x.status)]))]) - return - _skip_window() - return - _skip_window() - - -func _skip_window() -> void: - if main.settlement != null: - main.settlement.submit_window_selection([]) - main.inventory_panel.close_window() - - func _random_unknown_adjacent(minesweeper) -> Vector2i: var hero: Vector2i = minesweeper.hero_pos() var cands: Array = [] @@ -289,10 +224,8 @@ func _click_stop() -> void: func _report_stall(st: Dictionary) -> void: var why := "" - if st.phase == "Waiting" and not st.win and not st.await: - why = "结算停滞:Waiting 相位无窗口悬置" - elif st.phase == "Waiting" and st.await: - why = "窗口悬置未被消费(面板窗口态=%s)" % str(main.inventory_panel.is_window_open()) + if st.phase == "Waiting": + why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)" elif st.phase == "Active" and st.ap == 0: why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)" elif st.phase == "Idle" and main.settlement.battle_result == "": @@ -309,8 +242,7 @@ func _dump_state() -> void: print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [ str(main.runner.node_index), str(main.settlement.round_index), str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)]) - print(" battle_result='%s' settling=%s awaiting_window=%s window_visible=%s window_open=%s" % [ - str(diag.battle_result), str(diag.settling), str(diag.awaiting_window), - str(main.inventory_panel._window_mode), str(main.inventory_panel.is_window_open())]) + print(" battle_result='%s' settling=%s" % [ + str(diag.battle_result), str(diag.settling)]) print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [ str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)]) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_layout_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_layout_probe.gd index 4b54253d..c1521c2a 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_layout_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_layout_probe.gd @@ -22,7 +22,7 @@ func _ready() -> void: _dump_ctrl(c, 0) var zones := { "board_zone": main.board_zone, "board_view": main.board_view, - "info_panel": main.info_panel, "inventory_panel": main.inventory_panel, + "info_panel": main.info_panel, "stage": main.stage, } for k in zones: diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd index d1b047aa..4c95d150 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd @@ -2,7 +2,7 @@ # 实现 GDD:design/ux/battle.md(场景级 headless 探针——五区布局实例化断言;entry 17②③)。 # Date: 2026-09-12(五区改造批扩展;基础 = 2026-09-11 首战棋盘探针——既有断言全保留) # 项目运行模式(autoload 在位)实例化 main.tscn,直接驱动 _start_full_level(), -# 断言五区真实构建(棋盘/进度条/英雄卡/敌人卡/AP 点阵/明细带/数据框/库存面板)。 +# 断言四区真实构建(棋盘/进度条/英雄卡/敌人卡/AP 点阵/明细带/数据框;E 区道具面板已删除)。 # 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn extends Node @@ -44,8 +44,7 @@ func _ready() -> void: problems.append("C 区状态效果槽缺失") if main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0: problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)]) - if main.inventory_panel._title.text != "道包 · 关卡域": - problems.append("E 区库存常读标题错误(%s)" % main.inventory_panel._title.text) + # E 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖 # ---- 敌人卡投影(顺位/意图徽标常驻——敌人系统×4)---- var card = main.stage.enemy_box.get_child(0) if card != null and card._name_label.text == "": @@ -75,11 +74,12 @@ func _ready() -> void: slots_row.set_effects([]) if not slots_row._slots.is_empty(): problems.append("BUG-0001 回归:空态未清槽(%d)" % slots_row._slots.size()) - # ---- 图标键回归(HUD框架 图标键契约:状态表六行全映射、奖励 category 五值全映射; + # ---- 图标键回归(HUD框架 图标键契约:状态表七行全映射、格面资源 cell_type 五值全映射; # 查不到的键 fail-fast + ? 占位——此处钉死映射不残缺,未知键断言由 _icon_cfg/_badge_glyph 承担)---- - if not StatusSlotRow.EFFECT_ICON.has("enrage") or not StatusSlotRow.EFFECT_ICON.has("off_guard"): - problems.append("状态图标表缺暴走/失衡两键(states 表六行须全映射)") - for cat in ["instant_aoe", "instant_heal", "damage_item", "heal_pack", "buff"]: + if not StatusSlotRow.EFFECT_ICON.has("enrage") or not StatusSlotRow.EFFECT_ICON.has("off_guard") \ + or not StatusSlotRow.EFFECT_ICON.has("sapped"): + problems.append("状态图标表缺暴走/失衡/乏力键(states 表七行须全映射)") + for cat in ["attack", "defense", "heal", "blast", "weaken"]: if not BoardView.REWARD_BADGE.has(cat): problems.append("奖励图标表缺键(%s)" % cat) var probe_kb: Array = [