203 lines
10 KiB
GDScript
203 lines
10 KiB
GDScript
# 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
|