# VERTICAL SLICE - NOT FOR PRODUCTION # 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99: # 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。 # 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装) # + 底座;血条悬浮头顶;状态效果唯一展示面挂血条正下方(2026-09-26 C 区精简批:compact 档 # StatusSlotRow 组件整体迁入——定稿 PNG 图标+层数 ×N 角标+悬停 tooltip〔来源+语义+剩余〕, # tooltip 义务自 C 区状态槽并入;空态 = 图标行隐藏)。 # 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 / # 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持, # 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下 护甲类型图标+名字 # 常驻行(2026-09-26 用户终裁:AC 数值不展示、护甲类型图标化——敌人技能池批克制对策可读性 # 义务降级承载:类型保留可读、数值移除;HP 信息已足够)。 # 死亡呈现(敌人系统 资产下限):灰化 + 摇摆塌陷残位,禁瞬消;受击闪白沿用。 # Date: 2026-09-20(站桩改造批——hero_card_view/enemy_card_view 退役删除) class_name StandeeView extends Control const CHARGE_SKILL_IDS := ["heavy"] # 护甲类型图标占位——终稿图标资产归美术批(2026-09-26 用户终裁:AC 数值不展示,类型图标化)。 # 形状+色相双通道(art-bible):轻甲=◇ 空心菱(轻薄)·灰石中性 / 魔法护甲=◆ 实心晶菱·小人蓝 # (避开 Boss 专属紫赤 #8F2F6A 与妨害紫——两者各有专属语义)/ 重甲=▣ 实心方板(厚重板甲)·深金。 const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} var _kind := "enemy" var _portrait: Label var _portrait_tex: TextureRect = null # 资产挂点(null = 未落位,字形在位) var _platform: Panel var _hp_bar: ProgressBar var _order_badge: Label = null var _intent_badge: Label = null var _top_row: VBoxContainer var slot_row: StatusSlotRow # 状态效果唯一展示面(2026-09-26 C 区精简批——compact 档组件整体迁入; # 悬停 tooltip 来源+语义+剩余 义务随之从 C 区状态槽并入,主角/敌方共用) var _name_line: HBoxContainer = null var _armor_icon: Label = null var _name_label: Label = null var _display_name := "" var _enemy_id := "" var _armor := "light" var _dead := false var _hp_tween: Tween func _init(kind: String = "enemy") -> void: _kind = kind custom_minimum_size = Vector2(150, 176) mouse_filter = Control.MOUSE_FILTER_IGNORE # 头顶悬浮层:血条(敌附顺位徽标)+ 状态小图标行 var top := VBoxContainer.new() _top_row = top top.set_anchors_preset(Control.PRESET_TOP_WIDE) top.offset_left = 8.0 top.offset_right = -8.0 top.add_theme_constant_override("separation", 2) add_child(top) var hp_row := HBoxContainer.new() hp_row.add_theme_constant_override("separation", 4) top.add_child(hp_row) if kind == "enemy": _order_badge = UiKit.label("①", 13, true) _order_badge.add_theme_color_override("font_color", UiKit.GOLD_DEEP) hp_row.add_child(_order_badge) _hp_bar = ProgressBar.new() _hp_bar.custom_minimum_size = Vector2(0, 10) _hp_bar.show_percentage = false _hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL _hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER hp_row.add_child(_hp_bar) slot_row = StatusSlotRow.new() slot_row.compact = true # 站桩迷你档——图标 16px+层数角标,tooltip 全明细 slot_row.alignment = BoxContainer.ALIGNMENT_CENTER slot_row.size_flags_horizontal = Control.SIZE_SHRINK_CENTER slot_row.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行容器放行鼠标事件给槽位(悬停 tooltip) top.add_child(slot_row) # 立绘(程序占位字形) _portrait = UiKit.label("人" if kind == "hero" else "敌", 42, true) _portrait.add_theme_color_override("font_color", UiKit.HERO_BLUE if kind == "hero" else UiKit.RED_DEEP) _portrait.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _portrait.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _portrait.set_anchors_preset(Control.PRESET_FULL_RECT) _portrait.offset_top = 44.0 _portrait.offset_bottom = -26.0 _portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_portrait) # 意图指示(敌 only——立绘上半部居中悬浮) if kind == "enemy": _intent_badge = UiKit.label("", 15, true) _intent_badge.set_anchors_preset(Control.PRESET_CENTER_TOP) _intent_badge.offset_left = -20.0 _intent_badge.offset_right = 20.0 _intent_badge.offset_top = 74.0 _intent_badge.offset_bottom = 98.0 _intent_badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _intent_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE _intent_badge.visible = false add_child(_intent_badge) # 底座 _platform = Panel.new() _platform.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) _platform.offset_left = 26.0 _platform.offset_right = -26.0 _platform.offset_top = -18.0 _platform.offset_bottom = -8.0 _platform.mouse_filter = Control.MOUSE_FILTER_IGNORE _platform.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.STONE.darkened(0.35), UiKit.STONE.darkened(0.1), 6, 1)) add_child(_platform) # 底座下 护甲类型图标+名字 常驻行(敌 only——2026-09-26 用户终裁:AC 数值不展示,类型图标化) if kind == "enemy": _name_line = HBoxContainer.new() _name_line.alignment = BoxContainer.ALIGNMENT_CENTER _name_line.add_theme_constant_override("separation", 3) _name_line.mouse_filter = Control.MOUSE_FILTER_IGNORE _name_line.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) _name_line.offset_top = -8.0 add_child(_name_line) _armor_icon = UiKit.label("", 12, true) _armor_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE _name_line.add_child(_armor_icon) _name_label = UiKit.label("", 12, false) _name_label.add_theme_color_override("font_color", UiKit.INK_TITLE) _name_label.clip_text = true _name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS _name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE _name_line.add_child(_name_label) # ---- setter 面(投影契约与旧敌卡一致——presenter 装配面零改排) ---- # data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id, # pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)} func set_data(d: Dictionary) -> void: _display_name = String(d.get("display_name", "敌人")) _enemy_id = String(d.get("enemy_id", "")) _armor = String(d.get("armor_type", "light")) _hp_bar.max_value = maxi(1, int(d.get("max_hp", 1))) _set_hp(int(d.get("hp", 0))) set_order(int(d.get("order", 1))) set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", ""))) set_effects(d.get("effects", [])) _update_name_line() _refresh_portrait_glyph() if _dead and int(d.get("hp", 0)) > 0: _revive() func set_hero(hp: int, max_hp: int) -> void: _hp_bar.max_value = maxi(1, max_hp) _set_hp(hp) # 血条短 tween 递减(§2.2 直角递减占位——密度/缓动终稿归战斗表现批);逐行刷新时重启 tween 不累积 func _set_hp(hp: int) -> void: var target := float(clampi(hp, 0, int(_hp_bar.max_value))) if _hp_tween != null and _hp_tween.is_valid(): _hp_tween.kill() _hp_tween = create_tween() _hp_tween.tween_property(_hp_bar, "value", target, UiKit.motion(0.15)) func set_idle() -> void: _set_hp(0) set_effects([]) # 主角/敌方共用——状态效果唯一展示面(2026-09-26 C 区精简批:compact 档 StatusSlotRow 整体迁入, # 原 B 区迷你字形行退役;悬停 tooltip(来源+语义+剩余结算次数)义务自 C 区状态槽并入本组件; # 空态 = 图标行隐藏不占位(增益状态 EC12)) func set_effects(effects: Array) -> void: slot_row.set_effects(effects) slot_row.visible = not effects.is_empty() # battle_over:状态图标行全清可见反馈(增益状态×4④ 载体迁移——逐槽化光 + 微文案, # 自 C 区状态槽迁入;主角侧调用,敌方不适用) func set_effects_cleared_feedback() -> void: slot_row.visible = true slot_row.set_cleared_feedback() var note := UiKit.label("状态随战斗结束消散", 11, false) note.add_theme_color_override("font_color", UiKit.STONE_FAINT) note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER note.mouse_filter = Control.MOUSE_FILTER_IGNORE _top_row.add_child(note) var tw := create_tween() tw.tween_interval(1.2) tw.tween_property(note, "modulate:a", 0.0, UiKit.motion(0.8)) tw.tween_callback(note.queue_free) func set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) if _order_badge != null: _order_badge.text = String("①②③"[clampi(i - 1, 0, 2)]) # 摇定动作渲染(敌人技能池批 三档可见性;渲染映射唯一定义处归本类——code-review 2026-09-12 先例): # pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;"" = 未摇定/死亡残位)。 # 表现层升级批拍板:不显示技能名——类型级图标 + 一次性动作动画(_play_intent_motion)。 # 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。 func set_pending(ptype: String, pid: String) -> void: if _intent_badge == null or _dead: return if ptype == "" or ptype == "basic": _intent_badge.visible = false # 普通攻击零表示(完全不可预测——§2.4) return var glyph := "⚔" # 攻击系(damage) var col: Color = UiKit.FLAG_RED if ptype == "buff": glyph = "✦" # 增益系 col = UiKit.HERO_BLUE elif ptype == "debuff": glyph = "☠" # 妨害系(新效应词汇批 2026-09-19) col = UiKit.DEBUFF_PURPLE if pid in CHARGE_SKILL_IDS: col = UiKit.GOLD_DEEP # 重击类蓄力档(2026-09-18 用户拍板独立蓄力提示) _intent_badge.text = glyph _intent_badge.add_theme_color_override("font_color", col) _intent_badge.visible = true _play_intent_motion() # 三档动作动画占位:摇定时一次性短动画(意图图标弹入 + 立绘前倾回正),随后静帧保持—— # 挖掘期唯一在动元素 = 可挖域微光(art-bible §2.1);终稿动画密度归战斗表现样张批 func _play_intent_motion() -> void: _intent_badge.pivot_offset = _intent_badge.size * 0.5 _portrait.pivot_offset = _portrait.size * 0.5 var tw := create_tween() tw.tween_property(_intent_badge, "scale", Vector2(1.6, 1.6), UiKit.motion(0.10)) tw.tween_property(_intent_badge, "scale", Vector2.ONE, UiKit.motion(0.16)) tw.parallel().tween_property(_portrait, "rotation_degrees", 4.0, UiKit.motion(0.10)) tw.tween_property(_portrait, "rotation_degrees", 0.0, UiKit.motion(0.14)) # 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 # flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右——2026-09-26 用户点名英雄立绘水平向右)。 # 盒高 96px = 48px 立绘源整数 2×(圣经 §8.2 禁非整数缩放;offset_top 44 = 血条+状态行之下净空)—— # 源换代时随源重算盒高。 func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: if _portrait_tex == null: _portrait_tex = TextureRect.new() _portrait_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _portrait_tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED _portrait_tex.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊(棋盘小人贴图批 2026-09-25) _portrait_tex.set_anchors_preset(Control.PRESET_FULL_RECT) _portrait_tex.offset_top = 44.0 _portrait_tex.offset_bottom = -36.0 # 盒高 96 = 48 源 ×2 整数档(原 −26 盒高 106 系非整数缩放) _portrait_tex.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_portrait_tex) _portrait_tex.texture = tex _portrait_tex.flip_h = flip _portrait_tex.visible = tex != null _portrait.visible = tex == null func mark_dead() -> void: if _dead: return _dead = true modulate = Color(0.62, 0.62, 0.62, 0.9) var tw := create_tween() tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12)) tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1)) tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08)) _refresh_portrait_glyph() if _intent_badge != null: _intent_badge.visible = false _set_hp(0) func is_dead() -> bool: return _dead # 受击闪白(结算演出占位——§2.2 受击反馈) func flash_hit() -> void: var tw := create_tween() tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) func _revive() -> void: _dead = false modulate = Color(1, 1, 1) rotation_degrees = 0.0 _refresh_portrait_glyph() _update_name_line() func _update_name_line() -> void: if _name_line == null: return _name_label.text = _display_name var col: Color = UiKit.STONE # 轻甲 = 灰石(中性轻薄) match _armor: "ward": col = UiKit.HERO_BLUE # 魔法护甲 = 小人蓝(避开 Boss 专属紫赤/妨害紫) "heavy": col = UiKit.GOLD_DEEP # 重甲 = 深金(厚重达意金) _armor_icon.text = String(ARMOR_ICONS.get(_armor, "◇")) _armor_icon.add_theme_color_override("font_color", col) func _refresh_portrait_glyph() -> void: if _kind == "hero": _portrait.text = "人" return if _dead: _portrait.text = "✕" return match _enemy_id: "goblin": _portrait.text = "▂" # 小体量墩形 "bugbear": _portrait.text = "▆" # 垂直延伸体量 "ogre_chief": _portrait.text = "█" # 最大体量 _: _portrait.text = "敌"