## VERTICAL SLICE - NOT FOR PRODUCTION ## 呈现装配器:核心状态 → 呈现文案的全部翻译集中于此(战斗屏 B/C/D 区文案与投影),静态无状态。 ## 核心读取只走 settlement.presentation_snapshot() 成文快照面——禁链式穿透 settlement.enemy_system.* / status_effects 一类内部路径。 ## 封印/拔/移动反馈与 AP/RAP 无推送信号——组合根每次动作/结算返回后必须重查 refresh,漏调即陈旧呈现。 class_name HudPresenter extends RefCounted # 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配) # 钱包 GP/钥石不在本装配面投喂(中/大循环资源迁出战斗屏——信息归属单一展示面原则); # HP/状态效果单投喂 B 区站桩(B/C 双投喂属重复展示——单一展示面原则) static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, settlement, runner, level_active: bool, db, refresh_remaining: int) -> void: # 特性列表(runner 统一加算区投影:商店购买与关卡大奖励 # 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。 # 呈现只读投影零核心触碰;行文格式=白名单项) if runner != null and level_active: var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger var lines: Array[String] = [] if int(b.get("atk", 0)) != 0: lines.append("攻击 %+d" % int(b.get("atk", 0))) if int(b.get("max_hp", 0)) != 0: lines.append("生命上限 %+d" % int(b.get("max_hp", 0))) if int(b.get("armor_class", 0)) != 0: lines.append("护甲 AC %+d" % int(b.get("armor_class", 0))) info.set_level_bonuses(lines) else: info.set_level_bonuses([]) # 特性持有投影(「局内奖励选了什么」须可读: # 逐件行「件名 ×N」;常驻可读+空态隐藏) if runner != null and level_active: info.set_feat_holdings(feat_holdings_lines(db, runner.feats_held())) else: info.set_feat_holdings([]) if settlement == null: info.set_board_variants([]) # 战斗域角标出战斗即隐(空态纪律) _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) return var s: Dictionary[Variant, Variant] = settlement.presentation_snapshot() # 地形变异常驻角标投影(整图变异快照 → 文案行; # 空态隐藏=特性段先例;战斗全程常驻防中局遗忘) info.set_board_variants(board_variant_lines(db, s.get("board_variants", []))) var hero: Dictionary[Variant, Variant] = s.hero if hero.is_empty() or not bool(hero.get("alive", false)): _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) return _refresh_hero(stage, info, s) _refresh_economy(ap_bar, s, db, refresh_remaining) stage.set_enemies(s.enemies) # ---- B 区主角站桩(HP/状态唯一展示面)+ C 区数据框(攻击/AC) ---- static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void: var hero: Dictionary[Variant, Variant] = s.hero var hp: int = int(hero.hp) var max_hp: int = int(hero.max_hp) var atk: int = int(hero.atk) # 修正 = attack_up 池有效值(hero_offense);来源角标 = 效果 display_name(快照 hero_effects 投影) var mod_total := 0 var sources: Array[String] = [] for eff in s.hero_effects: if String(eff.effect_id) == "attack_up": mod_total += int(eff.effective_value) sources.append("%s×%d" % [String(eff.display_name), int(eff.layers)]) var mod_text := "" if mod_total != 0: mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)] # B 区站桩 = HP 与状态效果唯一展示面(单一展示面原则)——C 区不重复投喂 stage.hero_standee.set_hero(hp, max_hp) stage.hero_standee.set_effects(s.hero_effects) info.set_attack(atk, mod_text) # 我方 AC 唯一展示面 = C 区——快照 armor_class 含战斗内 ac_up 折算 info.set_armor(int(hero.get("armor_class", 0))) # ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态——RAP 读数在本条) ---- static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void: ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round)) ap_bar.set_rap(int(s.rap)) var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE ap_bar.set_refresh_state(refresh_remaining, bool(s.can_refresh) and digging) # 无 Waiting 代理——UI 仅挖掘期受理 ap_bar.set_stop_enabled(bool(s.hero.get("alive", false)) and digging) # 特性持有文案(战斗 C 区与关卡选择页两处共用;空持有=空数组。 # 行形态=逐件「件名 ×N」(非逐流派聚合)——表行 # 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制) static func feat_holdings_lines(db, held: Array) -> Array[String]: var rlines: Array[String] = [] if db == null or db.feats == null or (db.feats.rows as Array).is_empty() or held.is_empty(): return rlines var counts := {} for rid in held: var rrow: Dictionary[Variant, Variant] = db.feat_row(String(rid)) var fname := String(rrow.get("display_name", String(rid))) counts[fname] = int(counts.get(fname, 0)) + 1 for fname in counts.keys(): rlines.append("%s ×%d" % [String(fname), int(counts[fname])]) return rlines # 地形变异常驻角标文案(整图变异 id → 文案行; # 空集 = 空数组=空态隐藏。三行制:变异名+两行效果说明(C 区 240px 宽防裁切)。 # 文案用常规表述,不用俚语 static func board_variant_lines(db, ids: Array) -> Array[String]: var lines: Array[String] = [] if db == null or db.variation == null: return lines for vid in ids: var row: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) if not row.is_empty(): lines.append(String(row.get("display_name", vid))) lines.append("回复与护甲资源减少") lines.append("攻击资源增多") return lines # ---- 战斗收尾反馈与结算视觉同步(组合根直改视图一律收敛入此) ---- # battle_over 收尾反馈:死亡兜底灰化 → 主角态回 idle → RAP 清零脉冲(调试收尾路)→ 状态全清可见反馈。 # 调试/冒险两路共用;reset_rap=false = 冒险收束(RAP 清零由战斗末拍已毕,收束拍不再重放脉冲)。 static func battle_end_feedback(stage: BattleStageView, ap_bar: ApBarView, reset_rap: bool) -> void: stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) stage.settle_hero_idle() # 主角态回 idle(三态收尾) if reset_rap: ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体=D 区 AP 条) stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩) # 结算阶段视觉同步(round_end → 下一轮):D 区去饱和 + 禁挖光 + 明细带 settle/dig 换挡 + 轮次文案。 # 光带演出(play_round_sweep)属时序敏感触发,留在组合根(上升沿检测随行)——本条只做态驱动 setter。 static func sync_settlement_visuals(stage: BattleStageView, board_view: BoardView, log_view: BattleLogView, settling: bool, phase_text: String) -> void: board_view.set_desaturated(settling) board_view.set_dig_glow_enabled(not settling) stage.set_settle_focus(settling) log_view.set_mode("settle" if settling else "dig") stage.set_round_text(phase_text) # 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值 static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, runner, level_active: bool, db) -> void: if not level_active: return if runner != null: info.set_attack(runner.effective_atk(), "(关卡推进中)") info.set_armor(runner.effective_ac()) stage.hero_standee.set_hero(runner.hero_hp, runner.effective_max_hp()) stage.hero_standee.set_effects([]) stage.clear_enemies() # 节点间隙清敌方站桩残位(死亡瞬间的灰化呈现已在结算行驱动完成) ap_bar.set_rap(0) # battle_over RAP 清零反馈(行动经济 义务——载体=AP 条) # 三源拒绝可感知(排雷玩法 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈) static func outcome_text(outcome: Dictionary) -> String: if outcome.accepted: return "执行完成。" match String(outcome.get("reject_reason", "")): "board_face": var sub := String(outcome.get("sub_reason", "")) var texts := { "not_adjacent": "不能执行:目标与小人不相邻", "target_revealed": "不能执行:目标已开", "target_concealed": "不能移动:目标未开", "seal_present": "不能封印:此格已有封印印记(封印为一次性承诺——相邻时左键直接挖开)", "no_path": "不能移动:已探明区不连通", "out_of_catalog": "无效目标", } return texts.get(sub, "棋盘面拒绝: " + sub) "resource_face": return "AP 不足。" "phase": return "结算中,非行动阶段。" "board_invalid": return "棋盘数据非法(fail-fast)。" _: return "动作被拒。" # ---- 批量模拟报告装配(文案归 presenter——视图只 set_text) ---- static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String: var lines: Array[String] = [] lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct]) lines.append("通关率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n]) lines.append("整关承伤:均 %.1f · p90 %.0f(基础 HP %d)" % [_mean(dmg_all), _p90(dmg_all), base_hp]) if not hp_left_win.is_empty(): lines.append("通关剩余 HP 均 %.1f · 进 Boss HP 均 %.1f" % [_mean(hp_left_win), _mean(hp_boss)]) lines.append("平均轮数 %.1f" % _mean(rounds_all)) if death_nodes.is_empty(): lines.append("败北节点:—") else: var parts: Array[String] = [] for k in death_nodes.keys(): parts.append("%s×%d" % [k, death_nodes[k]]) lines.append("败北节点:%s(0-3=战斗序,4=Boss)" % " ".join(parts)) return "\n".join(lines) static 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 / a.size() static func _p90(a: Array) -> float: if a.is_empty(): return 0.0 var b := a.duplicate() b.sort() return float(b[mini(b.size() - 1, int(0.9 * b.size()))])