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

197 lines
9.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
## 状态效果槽行:效果图标+层数角标+剩余点+悬停 tooltip;空态整行隐藏不占位。
## 极性映射=战斗结算效果语义(非 magnitude 符号);效果投影装配归 HudPresenter,本类纯被动零信号。
class_name StatusSlotRow
extends HBoxContainer
const MAX_SLOTS := 4
# 纹理映射:effect_id→定稿纹理,全局恒定禁换图(图标键契约 §4.6 的
# 文件侧延伸——文件键 = status_<effect_id>.png,命名规范圣经 §8.3「对象键 = 数据表主键」)。
# 状态表九行全映射(attack_up/ac_up/weaken/alert/enrage/off_guard/sapped/empower/fortify)
# ——HUD框架 图标键契约"一键一枚专属纹样,查不到的键立即断言 + 占位 + 诊断"
const EFFECT_ICON := {
"attack_up": "res://assets/ui/icons/status_attack_up.png",
"ac_up": "res://assets/ui/icons/status_ac_up.png",
"weaken": "res://assets/ui/icons/status_weaken.png",
"alert": "res://assets/ui/icons/status_alert.png",
"enrage": "res://assets/ui/icons/status_enrage.png",
"off_guard": "res://assets/ui/icons/status_off_guard.png",
"sapped": "res://assets/ui/icons/status_sapped.png",
"empower": "res://assets/ui/icons/status_empower.png",
"fortify": "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": "安全格", "seal_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 区站桩迷你档: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,UiKit.RADIUS_OBJECT, 1, Vector4(6, 3, 6, 3))
_tip.add_theme_stylebox_override("panel", sb)
_tip_label = UiKit.label("",UiKit.FS_HINT, false, 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 反馈:逐槽化光收回 + 微灰化(下次 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)
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:
# 槽底不透明(图标显示不清晰——半透明底叠 B 区米色面板吃掉白图标对比度;
# 改深墨棕实底 = 白色纹样图标对比承载,槽界同时可辨)
slot.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.SLOT_BACKDROP, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 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 源 → 常规档 32px=1×;mini(站桩)档 16px=0.5×
# 共用 §8.2 例外登记——线性过滤匹配抗锯齿矢量风源图)
var icon := TextureRect.new()
icon.custom_minimum_size = Vector2(32, 32) 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("",UiKit.FS_CAPTION, 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", UiKit.FS_CAPTION)
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[Variant, Variant] = 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框架:查不到的键按错误处理——立即断言 + 占位 + 诊断,禁止静默显示空图标)
# 装载走 UiKit.load_tex 守卫(呈现层素材 preload 红线=0——
# 素材缺位树上 preload 直接断言炸脚本;路径串表 + exists 守卫,缺位 null 走占位回退)
func _icon_tex(effect_id: String) -> Texture2D:
if EFFECT_ICON.has(effect_id):
return UiKit.load_tex(EFFECT_ICON[effect_id])
push_error("[状态槽] 未知图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知图标键: " + effect_id)
return null
func _hide_tip() -> void:
_tip.visible = false