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

203 lines
10 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→定稿 PNG 纹理图标、
# 层数角标、剩余结算次数点、悬停 tooltip(来源+语义+剩余);空态 = 槽隐藏(数据框上移补位,
# EC12 空态裁决);极性映射 = 战斗结算 SettlementTable 效果语义而非 magnitude 符号〔增益状态 D1×③〕)。
# 2026-09-24 状态图标接入批:程序化占位字形(▲◇▼◎⚔✕☾⇧▣)+ 按极性染色退役——定稿九枚 PNG
# (白剑/白盾/白眼+绿↑红↓箭头语言,用户逐键验收,圣经 §4.6.1 v2.2 纹样系统)自带极性,裸图标呈现。
# Date: 2026-09-12
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;增益状态 纯被动容器零信号——刷新绑 战斗结算
# 结算编排完成时点(battle.md 更新触发注记)。
class_name StatusSlotRow
extends HBoxContainer
const MAX_SLOTS := 4
# 纹理映射(2026-09-24 状态图标接入批):effect_id→定稿纹理,全局恒定禁换图(图标键契约 §4.6 的
# 文件侧延伸——文件键 = status_<effect_id>.png,命名规范圣经 §8.3「对象键 = 数据表主键」)。
# 状态表九行全映射(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": preload("res://assets/ui/icons/status_attack_up.png"),
"ac_up": preload("res://assets/ui/icons/status_ac_up.png"),
"weaken": preload("res://assets/ui/icons/status_weaken.png"),
"alert": preload("res://assets/ui/icons/status_alert.png"),
"enrage": preload("res://assets/ui/icons/status_enrage.png"),
"off_guard": preload("res://assets/ui/icons/status_off_guard.png"),
"sapped": preload("res://assets/ui/icons/status_sapped.png"),
"empower": preload("res://assets/ui/icons/status_empower.png"),
"fortify": preload("res://assets/ui/icons/status_fortify.png"),
}
# 极性词(tooltip 第三通道;运行时不再按极性染色/画框——绿↑红↓极性语言已内化于定稿图标本体)
const EFFECT_POLARITY := {
"attack_up": "益", "ac_up": "益", "weaken": "益",
"alert": "害", "enrage": "害", "off_guard": "害",
"sapped": "害", "empower": "害", "fortify": "害",
}
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框架:占位也要可见,禁静默空图标——debug 断言先行,release 回退槽内 ? 文本)
const ICON_PLACEHOLDER := "?"
var _tip: PanelContainer
var _tip_label: Label
var _slots: Array[PanelContainer] = [] # 槽引用数组持有——tooltip _tip 悬停后入子节点表,按位序 get_child 会误取(BUG-0001)
var _cleared_feedback := false
var compact := false # B 区站桩迷你档(2026-09-26 C 区精简批):16px 图标+层数角标、无 meta 行——tooltip 义务随组件迁入站桩
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 恢复常态;
# 2026-09-24 状态图标接入批:原字形 "✕ " 前缀随占位字形退役,灰化即清场反馈)
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)
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()
if compact:
# 槽底不透明(2026-09-26 用户反馈:图标显示不清晰——半透明底叠 B 区米色面板吃掉白图标对比度;
# 改深墨棕实底 = 白色纹样图标对比承载,槽界同时可辨)
slot.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.SLOT_BACKDROP, UiKit.PANEL_BORDER, 2, 1, Vector4(2, 1, 2, 1)))
else:
slot.custom_minimum_size = Vector2(50, 46)
slot.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER))
slot.mouse_entered.connect(_on_slot_hover.bind(slot))
slot.mouse_exited.connect(_hide_tip)
var col := VBoxContainer.new()
col.alignment = BoxContainer.ALIGNMENT_CENTER
if compact:
col.add_theme_constant_override("separation", 0)
slot.add_child(col)
# 定稿纹理图标(32px 源 → 24px 槽内呈现;线性过滤匹配抗锯齿矢量风源图)
var icon := TextureRect.new()
icon.custom_minimum_size = Vector2(24, 24) if not compact else Vector2(16, 16)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
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 TextureRect
var tex := _icon_tex(String(eff.effect_id))
icon.visible = tex != null
icon.texture = tex
var layers := int(eff.get("layers", 1))
if compact:
# 迷你档:层数角标挂图标下方(×N)——槽随内容自适应,无 meta 行/剩余点(悬停 tooltip 承载全部明细)
var tag := col.get_child(1) as Label
tag.text = "×%d" % layers if layers > 1 else ""
tag.add_theme_font_size_override("font_size", 10)
if tex == null:
tag.text = (ICON_PLACEHOLDER + tag.text).strip_edges()
return
var meta := col.get_child(1) as Label
# 层数角标 + 剩余结算次数点(● 未耗余量)——叠层数字呈现逻辑保持不动
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" % [layers, dots]
if tex == null:
meta.text = ICON_PLACEHOLDER + " " + meta.text # release 回退占位(debug 已断言)
func _on_slot_hover(slot: Control) -> void:
# tooltip:来源 + 语义 + 剩余(≥13px;悬停只读反馈通道——无点击无写)
if not slot.has_meta("eff"):
return
var eff: Dictionary = slot.get_meta("eff")
var eid := 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(eid, String(eff.get("display_name", ""))))
_tip_label.text = "%s〔%s〕\n%s\n剩余结算 %d 次\n来源:%s(第 %d 轮)" % [
String(eff.get("display_name", "")), String(EFFECT_POLARITY.get(eid, ICON_PLACEHOLDER)), 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_tex(effect_id: String) -> Texture2D:
if EFFECT_ICON.has(effect_id):
return EFFECT_ICON[effect_id]
push_error("[状态槽] 未知图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知图标键: " + effect_id)
return null
func _hide_tip() -> void:
_tip.visible = false