diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd index b026700c..9b832726 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/hud_presenter.gd @@ -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 清零反馈(行动经济 义务) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index f324c24f..0ea7aadc 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -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) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd index 72219654..4819e9dc 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd @@ -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(): diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd deleted file mode 100644 index 5abb66d2..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd +++ /dev/null @@ -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 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd.uid deleted file mode 100644 index f8522432..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/enemy_card_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cmu3givbs3323 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd deleted file mode 100644 index 029679e2..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd +++ /dev/null @@ -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 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd.uid deleted file mode 100644 index a7da8a89..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/hero_card_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dj2024ofvmee0 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd new file mode 100644 index 00000000..0278df0c --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd @@ -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, "?"] diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd.uid new file mode 100644 index 00000000..4e14b0bc --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd.uid @@ -0,0 +1 @@ +uid://gfvj2sxds3ph diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd index 999bb7e9..ce7c6d65 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd @@ -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,