feat(presentation): 站桩改造批——B 区全站桩形态落地(2026-09-20 用户拍板,台账 99 裁决执行)

① 新建 StandeeView(敌我两用单类):无框立绘+底座;血条悬浮头顶;状态小图标一排挂血条正下方
(词汇单点复用 StatusSlotRow.EFFECT_ICON,层数角标同 C 区键,未知键同款 fail-fast);
敌方专属:顺位徽标悬浮头顶、意图三档动作动画占位(表现层升级批拍板——不显示技能名:
普攻零表示/攻击系⚔/增益系✦/妨害系☠/蓄力⚔金档;摇定一次性短动画后静帧,挖掘期不循环守能量纪律)、
底座下名字·护甲·AC 常驻行(克制对策可读性义务平移);死亡=灰化+摇摆塌陷残位(禁瞬消);
set_portrait_texture 资产挂点——战斗表现样张批落位零代码换装
② HeroCardView/EnemyCardView 退役删除(连 .uid);BattleStageView 句柄 hero_card→hero_standee,
set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 签名不变;hud_presenter 两个装配点改
set_hero+set_effects(英雄卡文本摘要函数随卡退役——攻击值/修正角标归 C 区数据框,规格既定口径)
③ ui_probe 断言随形态契约换装:意图=三档图标断言(攻击系⚔可见+普攻零表示隐藏)+主角站桩存在性;
click/layout/tune 探针零改动自然通过(layout 几何转储核对:悬浮层 y=0/12/74、底座 y=172、名字行 y=182 无重叠)
④ 验收:冒烟 355 项 0 失败 + ui_probe/tune_probe PASS + click_probe 问题=0
注记:主角护甲等级(AC)此前仅英雄卡显示——卡片退役后无处呈现,规格 C 区数据框本无 AC 行;
是否补归后续批次裁决,不在本批擅动
This commit is contained in:
DKChan committed 2026-09-20 14:39:09 +08:00
1 parent 603037a724
commit 675eca83a9
10 files changed
+330 -292

No files matched your search

@@ -31,7 +31,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB
info.set_status_effects(s.hero_effects)
# ---- B 区英雄卡 + C 区数据框 ----
# ---- B 区主角站桩 + C 区数据框 ----
static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void:
var hero: Dictionary = s.hero
@@ -48,18 +48,13 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
var mod_text := ""
if mod_total != 0:
mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)]
stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects), int(hero.get("armor_class", -1)))
# B 区站桩只挂血条+状态小图标(2026-09-20 全站桩拍板)——攻击值等规划数据归 C 区数据框
stage.hero_standee.set_hero(hp, max_hp)
stage.hero_standee.set_effects(s.hero_effects)
info.set_hero(hp, max_hp)
info.set_attack(atk, mod_text)
static func _hero_status_summary(effects: Array) -> String:
var parts: Array = []
for eff in effects:
parts.append("%s×%d(剩 %d)" % [String(eff.display_name), int(eff.layers), int(eff.remaining_settlements)])
return " ".join(parts) if not parts.is_empty() else "状态:无"
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态) ----
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
@@ -80,8 +75,9 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
if runner != null:
info.set_hero(runner.hero_hp, runner.effective_max_hp())
info.set_attack(runner.effective_atk(), "(关卡推进中)")
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "", int(db.hero.armor_class))
stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成)
stage.hero_standee.set_hero(runner.hero_hp, runner.effective_max_hp())
stage.hero_standee.set_effects([])
stage.clear_enemies() # 节点间隙清敌方站桩残位(死亡瞬间的灰化呈现已在结算行驱动完成)
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
@@ -246,7 +246,7 @@ func _on_level_finished(payload: Dictionary) -> void:
route_overlay.visible = false # 终局兜底(选路口非 Boss 节点才有——防御性收层)
_sync_settlement_visuals() # → 全关
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
if result == "victory":
# 胜利终局 overlay(金蜡封落于棋盘中央)
_end_overlay.open_victory(Narrator.level_end_text("victory", payload, runner.effective_max_hp()),
@@ -355,7 +355,7 @@ func _on_battle_finished(payload: Dictionary) -> void:
# 仅调试模式接线(完整关卡的结束归 关卡节点地图 runner._on_level_finished)
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
info_panel.set_rap(0, int(db.action_economy.rap_refresh_cost)) # RAP 清零滚动 + 微文案
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
var decision: Dictionary = _debug.on_battle_finished(payload)
@@ -1,14 +1,15 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区战斗画面 252px 全宽——英雄卡(左)+ 兑现演出区(中)+
# 敌人卡 ×1–3(右);顶部中央轮次·阶段指示「第 N 轮 · 挖掘中/结算中/道具窗口」;
# 实现 GDD:design/ux/battle.md(B 区战斗画面 252px 全宽——主角站桩(左)+ 兑现演出区(中)+
# 敌方站桩 ×1–3(右)〔2026-09-20 全站桩拍板——卡片式退役〕;顶部中央轮次·阶段指示;
# 底部结算明细带(布局决策 D1 常驻双舞台分屏))。
# Date: 2026-09-12
# 高内聚边界:容器+组合视图——敌人卡槽位管理、轮次指示、结算行演出分发改由 setter 面承接;
# Date: 2026-09-12(2026-09-20 站桩改造批:HeroCardView/EnemyCardView 退役,StandeeView 换装;
# setter 面契约保持——set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 签名不变)
# 高内聚边界:容器+组合视图——站桩槽位管理、轮次指示、结算行演出分发改由 setter 面承接;
# 结算流水格式化归 BattleLogView,核心状态→文案装配归 hud_presenter.gd。
class_name BattleStageView
extends PanelContainer
var hero_card: HeroCardView
var hero_standee: StandeeView
var round_label: Label
var stage_center: Label
var enemy_box: HBoxContainer
@@ -33,8 +34,8 @@ func _init(db_ref) -> void:
row.size_flags_vertical = Control.SIZE_EXPAND_FILL
vb.add_child(row)
hero_card = HeroCardView.new()
row.add_child(hero_card)
hero_standee = StandeeView.new("hero")
row.add_child(hero_standee)
var center := CenterContainer.new()
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -56,18 +57,18 @@ func _init(db_ref) -> void:
vb.add_child(log_view)
# 敌卡投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等卡上动画不被逐行刷新重置——禁瞬消纪律);
# 数量变化 = 新战斗域,重建卡槽(轮内不增删,无回流)
# 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律);
# 数量变化 = 新战斗域,重建站桩槽(轮内不增删,无回流)
func set_enemies(cards_data: Array) -> void:
if _cards.size() != cards_data.size():
for c in enemy_box.get_children():
c.queue_free()
_cards.clear()
for d in cards_data:
var card := EnemyCardView.new()
card.set_data(d)
enemy_box.add_child(card)
_cards.append(card)
var standee := StandeeView.new("enemy")
standee.set_data(d)
enemy_box.add_child(standee)
_cards.append(standee)
return
for i in cards_data.size():
if is_instance_valid(_cards[i]):
@@ -84,7 +85,7 @@ func set_round_text(text: String) -> void:
round_label.text = text
# 结算行演出分发(占位级):伤害行 → 目标卡闪白 / 击杀 → 卡体灰化碎裂
# 结算行演出分发(占位级):伤害行 → 目标站桩闪白 / 击杀 → 灰化塌陷残位
func on_step_row_fx(row: Dictionary) -> void:
var t := String(row.get("type", ""))
if t != "damage":
@@ -92,7 +93,7 @@ func on_step_row_fx(row: Dictionary) -> void:
var target := String(row.get("target", ""))
if target == "hero" or target == "":
return
var card: EnemyCardView = _card_by_name(target)
var card: StandeeView = _card_by_name(target)
if card == null:
return
card.flash_hit()
@@ -100,16 +101,16 @@ func on_step_row_fx(row: Dictionary) -> void:
card.mark_dead()
func _card_by_name(display_name: String) -> EnemyCardView:
func _card_by_name(display_name: String) -> StandeeView:
for c in _cards:
if not is_instance_valid(c):
continue
if String(c._name_label.text).contains(display_name):
if String(c._name_line.text).contains(display_name):
return c
return null
# battle_over:全场敌人卡灰化兜底(短路击杀等未走 killed 行的边角)
# battle_over:全场敌方站桩灰化兜底(短路击杀等未走 killed 行的边角)
func gray_all_enemies() -> void:
for c in _cards:
if is_instance_valid(c) and not c.is_dead():
@@ -1,173 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区敌人卡 ×1–3——敌像占位、血条、顺位徽标、意图徽标〔常驻:挖掘期全程可见〕、
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——战斗结算 流水经 增益状态 投影〕)。
# Date: 2026-09-12(2026-09-16 敌人技能池批改型:行为环环点拆除〔proposal §2.4〕——
# 意图徽标语义 = 本拍摇定结果的动画占位:普通攻击零表示 / 特殊技类型级提示(攻击系 ⚔ / 增益系 ✦),
# 池与权重不公开(D4/D5);环点行退役,护甲类型 + AC 常驻;敌方增益(蓄力/硬化等)入状态行)
# 敌人死亡呈现(敌人系统 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
# 卡体保留灰化残位——定长卡槽,死亡动画不回流布局。
# 意图线索映射目录(敌人系统×4① 成文占位——资产定稿归 战斗表现,本处按 enemy_id 程序化占位)。
class_name EnemyCardView
extends PanelContainer
const COL_INK := UiKit.INK
# 摇定动作呈现映射(敌人技能池批 §2.4 三档可见性;新效应词汇批 2026-09-19 扩四档):键 = pending_type(快照投影)。
# 普通攻击零表示("—" 中性色——完全不可预测);伤害特殊技 = 攻击系 ⚔(朱红);
# 增益特殊技 = 防御/增益系 ✦(蓝);妨害特殊技 = 妨害系 ☠(紫——对主角施减益,proposal-status-vocabulary §1.4)。
# nat20 暴击无前置表示(掷点层,与技能重击严格区分)。
const PENDING_STYLE := {
"basic": ["—", UiKit.PANEL_BORDER],
"damage": ["⚔", UiKit.FLAG_RED],
"buff": ["✦", UiKit.HERO_BLUE],
"debuff": ["☠", UiKit.DEBUFF_PURPLE],
}
# 蓄力档技能集(2026-09-18 用户拍板:重击类独立蓄力提示——伤害技 ⚔ / 增益技 ✦ / 重击类 蓄力 三档占位)
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型呈现(攻击类型×护甲克制批——可读性前提:玩家须能看出该用穿刺还是魔法打它)
const ARMOR_NAMES := {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"}
var _portrait: Label
var _name_label: Label
var _hp_bar: ProgressBar
var _intent_badge: Label
var _armor_line: Label
var _status_label: Label
var _armor := "light" # 护甲类型快照(set_data 写入、set_pending 渲染——护甲行常驻标签)
var _armor_class := 0 # 护甲等级快照(2026-09-16 护甲等级掷点批——护甲行常驻;与护甲类型两轴分立)
var _dead := false
func _init() -> void:
custom_minimum_size = Vector2(190, 176)
var sb := UiKit.panel_flat(UiKit.PANEL_ENEMY, UiKit.RED_DEEP, 4, 1, Vector4(8, 6, 8, 6))
add_theme_stylebox_override("panel", sb)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
add_child(row)
_portrait = UiKit.label("敌", 34, true)
_portrait.add_theme_color_override("font_color", UiKit.RED_DEEP)
_portrait.custom_minimum_size = Vector2(48, 0)
_portrait.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_portrait.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_child(_portrait)
var col := VBoxContainer.new()
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
col.add_theme_constant_override("separation", 2)
row.add_child(col)
_name_label = UiKit.label("敌人", 13, true)
col.add_child(_name_label)
_hp_bar = ProgressBar.new()
_hp_bar.custom_minimum_size = Vector2(0, 10)
_hp_bar.show_percentage = false
col.add_child(_hp_bar)
_intent_badge = UiKit.label("意图 —", 13, true)
_intent_badge.add_theme_color_override("font_color", COL_INK)
col.add_child(_intent_badge)
_armor_line = UiKit.label("", 12, false)
col.add_child(_armor_line)
_status_label = _fixed_label(30)
col.add_child(_status_label)
# data = 战斗结算 presentation_snapshot 投影:{order, display_name, hp, max_hp, enemy_id,
# pending_skill/pending_type/pending_name(摇定动作三字段), effects[](敌方增益,含警觉 ac 行)}
func set_data(d: Dictionary) -> void:
_name_label.text = "%s" % String(d.get("display_name", "敌人"))
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
_hp_bar.value = int(d.get("hp", 0))
_set_order(int(d.get("order", 1)))
_armor = String(d.get("armor_type", "light"))
_armor_class = int(d.get("armor_class", 0))
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
# 状态行:敌方增益状态(蓄力/硬化/削弱/暴走 percent + 警觉 ac——side-wide,八型批 2026-09-20)
var parts: Array = []
for eff in d.get("effects", []):
parts.append("%s ×%d" % [String(eff.get("display_name", "")), int(eff.get("layers", 1))])
_status_label.text = " · ".join(parts)
_portrait.text = _portrait_glyph(String(d.get("enemy_id", "")))
if _dead and int(d.get("hp", 0)) > 0:
_revive()
func _set_order(i: int) -> void:
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
var base: String = _name_label.text.trim_prefix("① ").trim_prefix("② ").trim_prefix("③ ")
_name_label.text = "%s %s" % [String("①②③"[clampi(i - 1, 0, 2)]), base]
# 摇定动作渲染(敌人技能池批——presenter 不翻译,渲染映射唯一定义处归本类〔code-review 2026-09-12 先例〕):
# pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;"" = 未摇定/死亡残位)。
# 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。
func set_pending(ptype: String, pid: String) -> void:
if _dead:
return
var style: Array = PENDING_STYLE.get(ptype, ["—", UiKit.PANEL_BORDER])
_intent_badge.add_theme_color_override("font_color", style[1])
if ptype == "" or ptype == "basic":
_intent_badge.text = "意图:—" # 普通攻击零表示(不可预测——§2.4)
elif pid in CHARGE_SKILL_IDS:
_intent_badge.text = "意图:蓄力" # 重击类蓄力档(2026-09-18 用户三档拍板占位;终稿动画归战斗表现样张批)
else:
_intent_badge.text = "意图:%s" % String(style[0]) # 特殊技类型级占位——不预告技能名与数值(HUD框架;2026-09-18 剧透堵口)
# 护甲行(环点拆除——护甲类型 + AC 恒常驻,克制对策与掷点阈值的可读前提)
_armor_line.text = "%s · AC %d" % [_armor_tag(_armor), _armor_class]
func _armor_tag(armor: String) -> String:
return String(ARMOR_NAMES.get(armor, "轻甲"))
# 死亡呈现:卡体灰化 + 碎裂退场(禁瞬消)——灰化残位 + 裂纹字符 + 击破标记
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, 0.12)
tw.tween_property(self, "rotation_degrees", -1.5, 0.1)
tw.tween_property(self, "rotation_degrees", 0.0, 0.08)
_portrait.text = "✕"
_intent_badge.text = "—— 击破 ——"
_intent_badge.add_theme_color_override("font_color", UiKit.STONE)
_armor_line.text = ""
_status_label.text = ""
_hp_bar.value = 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), 0.08)
tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), 0.2)
func _revive() -> void:
_dead = false
modulate = Color(1, 1, 1)
rotation_degrees = 0.0
_portrait.text = "敌"
func _portrait_glyph(enemy_id: String) -> String:
# enemy_id 恒定映射(敌人系统 Rule 8③)——程序化占位字形:小体量墩形/垂直延伸/最大体量
match enemy_id:
"goblin":
return "▂"
"bugbear":
return "▆"
"ogre_chief":
return "█"
return "敌"
func _fixed_label(height: float) -> Label:
var l := UiKit.label("", 11, false)
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
l.custom_minimum_size = Vector2(0, height)
l.clip_contents = true
return l
@@ -1 +0,0 @@
uid://cmu3givbs3323
@@ -1,78 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区英雄卡——小人像占位/HP 条/攻击值含修正显示/状态效果槽引用)。
# Date: 2026-09-12
# 高内聚边界:纯视图 setter 面——状态装配(含修正文案)归 hud_presenter.gd;定长卡体无布局回流。
class_name HeroCardView
extends PanelContainer
const COL_INK := UiKit.INK
const COL_HERO := UiKit.HERO_BLUE
var _portrait: Label
var _hp_bar: ProgressBar
var _hp_label: Label
var _atk_label: Label
var _ac_label: Label
var _status_label: Label
func _init() -> void:
custom_minimum_size = Vector2(216, 176)
var sb := UiKit.panel_flat(UiKit.PANEL_HERO, UiKit.PANEL_BORDER, 4, 1, Vector4(8, 6, 8, 6))
add_theme_stylebox_override("panel", sb)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
add_child(row)
_portrait = UiKit.label("人", 40, true)
_portrait.add_theme_color_override("font_color", COL_HERO)
_portrait.custom_minimum_size = Vector2(56, 0)
_portrait.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_portrait.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_child(_portrait)
var col := VBoxContainer.new()
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
col.add_theme_constant_override("separation", 3)
row.add_child(col)
col.add_child(UiKit.label("探险者", 14, true))
_hp_bar = ProgressBar.new()
_hp_bar.custom_minimum_size = Vector2(0, 12)
_hp_bar.show_percentage = false
col.add_child(_hp_bar)
_hp_label = UiKit.label("HP —", 13, false)
col.add_child(_hp_label)
_atk_label = UiKit.label("攻击 —", 13, false)
col.add_child(_atk_label)
_ac_label = UiKit.label("护甲 —", 13, false)
col.add_child(_ac_label)
_status_label = _fixed_label(46) # 缺陷①约束:固定槽高 + clip——状态文案切换零回流
col.add_child(_status_label)
func set_data(hp: int, max_hp: int, atk: int, atk_mod: String, status_summary: String, ac: int = -1) -> void:
_hp_bar.max_value = maxi(1, max_hp)
_hp_bar.value = hp
_hp_label.text = "HP %d / %d" % [hp, max_hp]
_atk_label.text = "攻 %d %s" % [atk, atk_mod]
_ac_label.text = "护甲 %d" % ac if ac >= 0 else "护甲 —"
_status_label.text = status_summary
func set_idle() -> void:
_hp_bar.value = 0
_hp_label.text = "HP —"
_atk_label.text = "攻击 —"
_ac_label.text = "护甲 —"
_status_label.text = ""
# battle_over 反馈(增益状态×4④):状态逐槽化光收回 + 微文案——英雄卡侧紧凑引用同步
func set_status_cleared_feedback() -> void:
_status_label.text = "状态随战斗结束消散"
func _fixed_label(height: float) -> Label:
var l := UiKit.label("", 12, false)
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
l.custom_minimum_size = Vector2(0, height)
l.clip_contents = true
return l
@@ -1 +0,0 @@
uid://dj2024ofvmee0
@@ -0,0 +1,282 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
# + 底座;血条悬浮头顶;状态小图标一排挂血条正下方(形状+色相双通道、层数角标同 C 区状态槽键——
# 词汇单点复用 StatusSlotRow.EFFECT_ICON,未知键同款 fail-fast + ? 占位)。
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
# 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下名字·护甲·AC 常驻行
# (护甲类型+AC 恒常驻——敌人技能池批克制对策可读性义务)。
# 死亡呈现(敌人系统 资产下限):灰化 + 摇摆塌陷残位,禁瞬消;受击闪白沿用。
# Date: 2026-09-20(站桩改造批——hero_card_view/enemy_card_view 退役删除)
class_name StandeeView
extends Control
const CHARGE_SKILL_IDS := ["heavy"]
const ARMOR_NAMES := {"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 _status_row: HBoxContainer
var _name_line: Label = null
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _armor_class := 0
var _dead := false
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.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)
_status_row = HBoxContainer.new()
_status_row.alignment = BoxContainer.ALIGNMENT_CENTER
_status_row.add_theme_constant_override("separation", 4)
top.add_child(_status_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)
# 底座下名字·护甲·AC 常驻行(敌 only)
if kind == "enemy":
_name_line = UiKit.label("", 12, false)
_name_line.add_theme_color_override("font_color", UiKit.INK_TITLE)
_name_line.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_name_line.clip_text = true
_name_line.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_name_line.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_name_line.offset_top = -8.0
_name_line.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_name_line)
# ---- 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"))
_armor_class = int(d.get("armor_class", 0))
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
_hp_bar.value = 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)
_hp_bar.value = hp
func set_idle() -> void:
_hp_bar.value = 0
set_effects([])
# 主角/敌方共用——状态小图标行重建(B 区迷你版:icon + 层数角标;完整 tooltip 义务归 C 区状态槽)
func set_effects(effects: Array) -> void:
for c in _status_row.get_children():
_status_row.remove_child(c)
c.queue_free()
for eff in effects:
var cfg: Array = _icon_cfg(String(eff.get("effect_id", "")))
var layers := maxi(1, int(eff.get("layers", 1)))
var lab := UiKit.label(String(cfg[0]) + ("×%d" % layers if layers > 1 else ""), 12, true)
lab.add_theme_color_override("font_color", cfg[1])
lab.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_row.add_child(lab)
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), 0.10)
tw.tween_property(_intent_badge, "scale", Vector2.ONE, 0.16)
tw.parallel().tween_property(_portrait, "rotation_degrees", 4.0, 0.10)
tw.tween_property(_portrait, "rotation_degrees", 0.0, 0.14)
# 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形
func set_portrait_texture(tex: Texture2D) -> 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.set_anchors_preset(Control.PRESET_FULL_RECT)
_portrait_tex.offset_top = 44.0
_portrait_tex.offset_bottom = -26.0
_portrait_tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_portrait_tex)
_portrait_tex.texture = tex
_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, 0.12)
tw.tween_property(self, "rotation_degrees", -1.5, 0.1)
tw.tween_property(self, "rotation_degrees", 0.0, 0.08)
_refresh_portrait_glyph()
if _intent_badge != null:
_intent_badge.visible = false
_hp_bar.value = 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), 0.08)
tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), 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:
_name_line.text = "%s · %s·AC%d" % [_display_name, String(ARMOR_NAMES.get(_armor, "轻甲")), _armor_class]
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 = "敌"
# 状态图标键解析(词汇单点复用 C 区 StatusSlotRow——HUD框架:查不到的键按错误处理,
# 立即断言 + ? 占位纹样 + 诊断,禁止静默显示空图标)
func _icon_cfg(effect_id: String) -> Array:
if StatusSlotRow.EFFECT_ICON.has(effect_id):
return StatusSlotRow.EFFECT_ICON[effect_id]
push_error("[站桩] 未知状态图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知状态图标键: " + effect_id)
return ["?", UiKit.PANEL_BORDER, "?"]
@@ -0,0 +1 @@
uid://gfvj2sxds3ph
@@ -2,7 +2,8 @@
# 实现 GDD:design/ux/battle.md(场景级 headless 探针——四区布局实例化断言;entry 17②③)。
# Date: 2026-09-12(战斗屏改造批扩展;基础 = 2026-09-11 首战棋盘探针——既有断言全保留)
# 项目运行模式(autoload 在位)实例化 main.tscn,直接驱动 _start_full_level(),
# 断言四区真实构建(棋盘/进度条/英雄卡/敌人卡/AP 点阵/明细带/数据框;E 区道具面板已删除)。
# 断言四区真实构建(棋盘/进度条/主角站桩/敌方站桩/AP 点阵/明细带/数据框;E 区道具面板已删除;
# 2026-09-20 全站桩拍板——英雄卡/敌人卡断言随站桩改造批换装)。
# 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn
extends Node
@@ -33,7 +34,7 @@ func _ready() -> void:
if not main.stage.round_label.text.contains("第 1 轮"):
problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text)
if main.stage.enemy_box.get_child_count() < 1:
problems.append("B 区敌人卡未构建(%d 张)" % main.stage.enemy_box.get_child_count())
problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count())
if main.ap_bar._dots.get_child_count() != int(main.db.action_economy.ap_per_round):
problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count())
if main.ap_bar.refresh_remaining_text() == "":
@@ -45,12 +46,22 @@ func _ready() -> void:
if main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0:
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
# E 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖
# ---- 敌人卡投影(顺位/意图徽标常驻——敌人系统×4)----
var card = main.stage.enemy_box.get_child(0)
if card != null and card._name_label.text == "":
problems.append("敌人卡名称为空")
if card != null and not card._intent_badge.text.begins_with("意图"):
problems.append("敌人卡意图徽标缺失(%s)" % card._intent_badge.text)
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4;2026-09-20 全站桩拍板)----
var standee = main.stage.enemy_box.get_child(0)
if standee != null and standee._name_line.text == "":
problems.append("敌立绘名字行为空")
if standee != null and standee._intent_badge == null:
problems.append("敌立绘意图指示缺失")
elif standee != null:
# 三档动作动画占位契约(表现层升级批拍板:不显示技能名——图标档位 + 一次性动画)
standee.set_pending("damage", "")
if not standee._intent_badge.visible or standee._intent_badge.text != "⚔":
problems.append("攻击系意图未呈现(visible=%s text=%s)" % [str(standee._intent_badge.visible), standee._intent_badge.text])
standee.set_pending("basic", "")
if standee._intent_badge.visible:
problems.append("普攻应零表示(意图指示应隐藏)")
if main.stage.hero_standee == null or main.stage.hero_standee._hp_bar == null:
problems.append("B 区主角站桩缺失")
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
var slots_row = main.info_panel.slot_row
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,