Files
gplan-2/game/presentation/hud_presenter.gd
T

152 lines
7.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(四区呈现装配器)——读核心只读面(战斗结算 呈现快照 / runner)→
# 组装文案/投影 → 驱动各视图 setter。
# 高内聚边界:"核心状态 → 呈现文案"的全部翻译集中于此;视图保持零核心依赖;
# 静态无状态——呈现不持久化任何核心句柄。
# E1 封装收口(2026-09-13):核心状态读取统一经 战斗结算.presentation_snapshot() 成文快照面——
# 禁再链式穿透 settlement.enemy_system.hero / settlement.enemy_system.enemies / settlement.status_effects.query_active 一类路径(战斗结算 协作对象已私有化,
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/pending_*/effects 字段——2026-09-16 敌人技能池批批 2 定稿)。
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(排雷玩法 发射面封闭)——
# 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查;
# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点。
# Date: 2026-09-12(战斗屏改造批;基础 = 2026-09-11 HUD 装配器;2026-09-13 快照面改造)
class_name HudPresenter
extends RefCounted
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null,
keystone_label: Label = null, keystone_balance: int = 0) -> void:
# GP 余额常驻(GP+商店房实施批 2026-09-21——A 区顶栏邻接位;runner 缺位 = 非冒险态按 0 读数)
if gp_label != null:
gp_label.text = "金币 %d" % (int(runner.gp) if runner != null else 0)
# 钥石余额常驻(元进度实现批 2026-09-21——A 区顶栏邻接位;数据源 MetaProgression 账本,购买/收束后随全量重查刷新)
if keystone_label != null:
keystone_label.text = "钥石 %d" % keystone_balance
if settlement == null:
_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, 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)
# ---- B 区主角站桩 + C 区数据框 ----
static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void:
var hero: Dictionary = 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 = []
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 区站桩只挂血条+状态小图标(2026-09-20 全站桩拍板)——攻击值等规划数据归 C 区数据框
stage.hero_standee.set_hero(hp, max_hp)
stage.hero_standee.set_effects(s.hero_effects)
info.set_hero(hp, max_hp)
info.set_attack(atk, mod_text)
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态) ----
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
# RAP 活值投影(C 区点阵)——清零反馈分支(行动经济 D1 义务)依赖本处持续投喂才会可达
info.set_rap(int(s.rap), int(db.action_economy.rap_refresh_cost))
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
ap_bar.set_refresh_state(refresh_remaining,
bool(s.can_refresh) and digging) # Waiting 代理已随 OQ12 退役——UI 仅挖掘期受理
ap_bar.set_stop_enabled(bool(s.hero.get("alive", false)) and digging)
# 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值
static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView,
runner, level_active: bool, db) -> void:
if not level_active:
return
if runner != null:
info.set_hero(runner.hero_hp, runner.effective_max_hp())
info.set_attack(runner.effective_atk(), "(关卡推进中)")
stage.hero_standee.set_hero(runner.hero_hp, runner.effective_max_hp())
stage.hero_standee.set_effects([])
stage.clear_enemies() # 节点间隙清敌方站桩残位(死亡瞬间的灰化呈现已在结算行驱动完成)
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
# 三源拒绝可感知(排雷玩法 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": "不能移动:目标未开",
"flag_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;code-review 2026-09-12 批) ----
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 = []
lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct])
lines.append("通关率 [color=#80d080]%.0f%%[/color](%d/%d)" % [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 = []
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()))])