Files
gplan-2/game/presentation/views/status_slot_row.gd
T

169 lines
8.0 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(C 区状态效果槽 ×4〔增益状态 D1×①〕——effect_id→icon 形状+色相双通道、
# 层数角标、剩余结算次数点、悬停 tooltip(来源+语义+剩余);空态 = 槽隐藏(数据框上移补位,
# EC12 空态裁决);极性映射 = 战斗结算 SettlementTable 效果语义而非 magnitude 符号〔增益状态 D1×③〕)。
# Date: 2026-09-12
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;增益状态 纯被动容器零信号——刷新绑 战斗结算
# 结算编排完成时点(battle.md 更新触发注记)。
class_name StatusSlotRow
extends HBoxContainer
const MAX_SLOTS := 4
# 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name):
# 益 = ▲/◆/▼/◇(金/蓝族);害 = ⚡/⚔/✕/☾/⇧/▣(旗朱族;⇧/▣ = 敌方增益威胁纹样)——形状+色相双通道,
# 文字名常驻第三通道。状态表九行全映射(attack_up/ac_up/weaken/alert/enrage/off_guard/sapped/empower/fortify)
# ——HUD框架 图标键契约"一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"
# (2026-09-15 格面资源批补 sapped;2026-09-16 护甲等级掷点批 defense_up → ac_up 换装;
# 同日 敌人技能池批补 empower/fortify 敌方增益两键——敌方增益显示 = 敌卡状态行文本 + 本图标键词汇)。
const EFFECT_ICON := {
"attack_up": ["▲", UiKit.GOLD_DEEP, "益"],
"ac_up": ["◇", UiKit.HERO_BLUE, "益"],
"weaken": ["▼", UiKit.HERO_BLUE, "益"],
"alert": ["◎", UiKit.FLAG_RED, "害"],
"enrage": ["⚔", UiKit.FLAG_RED, "害"],
"off_guard": ["✕", UiKit.FLAG_RED, "害"],
"sapped": ["☾", UiKit.FLAG_RED, "害"],
"empower": ["⇧", UiKit.FLAG_RED, "害"],
"fortify": ["▣", UiKit.PANEL_BORDER, "害"],
}
const EFFECT_SEMANTIC := {
"attack_up": "主角攻击加成(hero_offense 池加算)",
"ac_up": "护甲等级 +1/层(掷点折扣判定阈值——AC 数值轴,受击低掷按档位打折)",
"weaken": "敌方输出削弱(enemy_output 池乘算)",
"alert": "敌方护甲等级 +1/层(blast 型负向——我方普通攻击掷点更易落入折扣档)",
"enrage": "敌方输出加成(enemy_output 池加算——attack 型负向)",
"off_guard": "主角承伤加深(hero_defense 池加算——defense 型负向)",
"sapped": "主角输出降低(hero_offense 池加算——weaken 型负向)",
"empower": "敌方输出 ×2(enemy_output 池加算——蓄力技施放;持续 1 个有效结算)",
"fortify": "敌方防御 −50(enemy_defense 池——硬化技施放,主角攻击 ×0.5;持续 1 个有效结算)",
}
const SOURCE_NAMES := {"safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励"}
# 未知图标键的占位纹样(HUD框架:占位也要可见,禁静默空图标)
const ICON_PLACEHOLDER := ["?", UiKit.PANEL_BORDER, "?"]
var _tip: PanelContainer
var _tip_label: Label
var _slots: Array[PanelContainer] = [] # 槽引用数组持有——tooltip _tip 悬停后入子节点表,按位序 get_child 会误取(BUG-0001)
var _cleared_feedback := false
func _init() -> void:
add_theme_constant_override("separation", 6)
_tip = PanelContainer.new()
_tip.z_index = 10
_tip.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tip.visible = false
var sb := UiKit.panel_flat(UiKit.TIP_BG, UiKit.GOLD, 2, 1, Vector4(6, 3, 6, 3))
_tip.add_theme_stylebox_override("panel", sb)
_tip_label = UiKit.label("", 13, false)
_tip_label.add_theme_color_override("font_color", UiKit.WARM)
_tip_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_tip_label.custom_minimum_size = Vector2(210, 0)
_tip.add_child(_tip_label)
# effects = 增益状态 query_active 投影数组(含空态);空槽隐藏,非空按插入序填充(最多 4 槽可见——超出截断)
func set_effects(effects: Array) -> void:
_cleared_feedback = false
_sync_slots(effects.size())
for i in _slots.size():
_fill_slot(_slots[i], effects[i])
_slots[i].visible = true
# battle_over 反馈(增益状态×4④):逐槽化光收回 + 微文案(下次 set_effects 恢复常态)
func set_cleared_feedback() -> void:
_cleared_feedback = true
for slot in _slots:
slot.visible = true
slot.modulate = Color(0.7, 0.7, 0.7, 0.5)
var col := slot.get_child(0) as VBoxContainer
var icon := col.get_child(0) as Label
icon.text = "✕ " + icon.text
func _sync_slots(n: int) -> void:
# 移除旧槽(槽生命周期由 _slots 数组唯一管理——子节点表含 tooltip 节点,禁按位序索引;BUG-0001)
for slot in _slots:
remove_child(slot)
slot.queue_free()
_slots.clear()
var count := mini(maxi(n, 0), MAX_SLOTS)
for i in count:
var slot := _make_slot()
_slots.append(slot)
add_child(slot)
func _make_slot() -> PanelContainer:
var slot := PanelContainer.new()
slot.custom_minimum_size = Vector2(50, 46)
var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER)
slot.add_theme_stylebox_override("panel", sb)
slot.mouse_entered.connect(_on_slot_hover.bind(slot))
slot.mouse_exited.connect(func(): _tip.visible = false)
var col := VBoxContainer.new()
col.alignment = BoxContainer.ALIGNMENT_CENTER
slot.add_child(col)
var icon := UiKit.label("", 16, true)
icon.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
col.add_child(icon)
var meta := UiKit.label("", 11, false)
meta.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
col.add_child(meta)
return slot
func _fill_slot(slot: PanelContainer, eff: Dictionary) -> void:
slot.modulate = Color(1, 1, 1)
slot.set_meta("eff", eff)
var col := slot.get_child(0) as VBoxContainer
var icon := col.get_child(0) as Label
var meta := col.get_child(1) as Label
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
icon.text = String(icon_cfg[0])
icon.add_theme_color_override("font_color", icon_cfg[1])
# 层数角标 + 剩余结算次数点(● 未耗余量)
var dots := ""
var left: int = int(eff.get("remaining_settlements", 0))
for i in maxi(left, 1):
dots += "●" if i < left else "○"
meta.text = "×%d %s" % [int(eff.get("layers", 1)), dots]
func _on_slot_hover(slot: Control) -> void:
# tooltip:来源 + 语义 + 剩余(≥13px;悬停只读反馈通道——无点击无写)
if not slot.has_meta("eff"):
return
var eff: Dictionary = slot.get_meta("eff")
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
var src: String = String(SOURCE_NAMES.get(String(eff.get("source_class", "")), String(eff.get("source_class", ""))))
var semantic: String = String(EFFECT_SEMANTIC.get(String(eff.effect_id), String(eff.get("display_name", ""))))
_tip_label.text = "%s〔%s〕\n%s\n剩余结算 %d 次\n来源:%s(第 %d 轮)" % [
String(eff.get("display_name", "")), String(icon_cfg[2]), semantic,
int(eff.get("remaining_settlements", 0)), src, int(eff.get("round_index", 0))]
if not _tip.is_inside_tree():
add_child(_tip)
_tip.reset_size()
# 防出屏定位(缺陷①家族回归——边缘格 tooltip 硬编码上偏移会被视口裁切):
# 默认悬浮槽位上方,越上缘翻转到槽位下方,再钳制在视口内;top_level 脱离容器布局管理
_tip.top_level = true
var slot_rect := slot.get_global_rect()
var desired := slot_rect.position + Vector2(0.0, -_tip.size.y - 6.0)
var vp := get_viewport_rect().size
desired.x = clampf(desired.x, 4.0, maxf(4.0, vp.x - _tip.size.x - 4.0))
if desired.y < 4.0:
desired.y = slot_rect.end.y + 6.0
desired.y = clampf(desired.y, 4.0, maxf(4.0, vp.y - _tip.size.y - 4.0))
_tip.global_position = desired
_tip.visible = true
# 图标键解析(HUD框架:查不到的键按错误处理——立即断言 + ? 占位纹样 + 诊断,禁止静默显示空图标)
func _icon_cfg(effect_id: String) -> Array:
if EFFECT_ICON.has(effect_id):
return EFFECT_ICON[effect_id]
push_error("[状态槽] 未知图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知图标键: " + effect_id)
return ICON_PLACEHOLDER