From afeb6cacd2110f100102074579decaf5410dfc69 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 26 Sep 2026 05:07:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(presentation):=20=E7=AB=99=E6=A1=A9?= =?UTF-8?q?=E6=9C=AC=E4=BD=93=20Node2D=20=E5=8C=96=EF=BC=88Phase=20B=20T3?= =?UTF-8?q?=EF=BC=89=E2=80=94=E2=80=94StandeeBody=20=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E7=B2=BE=E7=81=B5=E6=96=B0=E4=BB=B6+StandeeView=20=E9=80=80?= =?UTF-8?q?=E5=BD=B9=E5=88=A0=E9=99=A4+=E8=A1=8C=E4=B8=BA=E9=9D=A2=2017=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=A5=91=E7=BA=A6=E4=BF=9D=E6=8C=81+?= =?UTF-8?q?=E4=BC=B4=E9=9A=8F=E4=BB=B6=E5=A3=B3=E5=B1=82=E5=8C=96=EF=BC=88?= =?UTF-8?q?enemy=5Fbox=20=E8=AF=AD=E4=B9=89=E6=94=B9=E5=86=99=EF=BC=89+wor?= =?UTF-8?q?ld=5Fto=5Fshell=20=E5=8D=95=E7=82=B9=E5=90=8C=E6=AD=A5+ui=5Fpro?= =?UTF-8?q?be=20=E6=96=AD=E8=A8=80=E7=AD=89=E4=BB=B7=E6=94=B9=E5=86=99+?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=20171?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/views/battle_stage_view.gd | 180 +++++++--- game/presentation/views/standee_body.gd | 348 +++++++++++++++++++ game/presentation/views/standee_body.gd.uid | 1 + game/presentation/views/standee_view.gd | 324 ----------------- game/presentation/views/standee_view.gd.uid | 1 - game/smoke/ui_probe.gd | 27 +- production/session-state/active.md | 3 +- 7 files changed, 504 insertions(+), 380 deletions(-) create mode 100644 game/presentation/views/standee_body.gd create mode 100644 game/presentation/views/standee_body.gd.uid delete mode 100644 game/presentation/views/standee_view.gd delete mode 100644 game/presentation/views/standee_view.gd.uid diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index e287414e..4bf6e07f 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -4,29 +4,39 @@ # 底部结算明细带(布局决策 D1 常驻双舞台分屏))。 # Date: 2026-09-12(2026-09-20 站桩改造批:HeroCardView/EnemyCardView 退役,StandeeView 换装; # setter 面契约保持——set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 签名不变) -# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T2,ADR-0006 / board-node2d-migration-plan §3): -# 根壳 PanelContainer → Control(类名/尺寸 252px/树内位置不变),内部拆两半区—— -# 「Node2D 舞台半区 = SubViewportContainer + SubViewport + StageWorld(站桩本体 T3 迁入,本期空置)」 -# +「Control 文本半区 = round_label / stage_center / log_view 三件原样保留(C1 永久 Control)」。 -# SubViewport 逻辑分辨率 = 壳层像素 1:1(stretch,禁任何缩放系数——C2/C5);字体显式注入挂点 -# set_world_font(C6——SubViewport 不自动继承主场景主题,Phase A BoardView 同款)。 +# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T2 壳 + T3 站桩迁入;ADR-0006 / +# board-node2d-migration-plan §3):根壳 PanelContainer → Control(类名/尺寸 252px/树内位置 +# 不变),内部两半区——「Node2D 舞台半区 = SubViewportContainer + SubViewport + StageWorld +# (站桩本体 StandeeBody 世界精灵)」+「Control 文本半区 = round_label / stage_center / +# log_view 三件原样保留(C1 永久 Control)」。SubViewport 逻辑分辨率 = 壳层像素 1:1(stretch, +# 禁任何缩放系数——C2/C5);字体显式注入挂点 set_world_font(C6,Phase A BoardView 同款)。 +# T3 批内代拍板默认(待追认):①enemy_box 语义改写「站桩壳层伴随件容器」(HBoxContainer → +# Control,子件 = 各站桩的 StatusSlotRow 伴随件,手动定位随世界同步——探针 :97 计数面不变); +# ②hero_standee/enemy 站桩 = StandeeBody(Node2D 世界本体,行为面契约逐项保持 C3),其 +# slot_row 成员 = 壳层伴随件引用;③定位同步 = 世界→壳层换算单点 world_to_shell,重建/重排 +# 挂点调用(零每帧成本,C4)。旧 StandeeView(Control)随迁退役删除。 # 高内聚边界:容器+组合视图——站桩槽位管理、轮次指示、结算行演出分发改由 setter 面承接; # 结算流水格式化归 BattleLogView,核心状态→文案装配归 hud_presenter.gd。 class_name BattleStageView extends Control const _STAGE_WORLD := preload("res://presentation/views/stage_world.gd") +const _STANDEE_BODY := preload("res://presentation/views/standee_body.gd") const _CONTENT_MARGINS := Rect2(6, 4, 6, 4) # 原 PanelContainer stylebox 内容边距(子面几何零漂移) +const _STANDEE_W := 150.0 # 站桩盒宽(旧 StandeeView custom_minimum_size) +const _STANDEE_GAP := 8.0 # 敌站桩间距(旧 enemy_box HBox separation) -var hero_standee: StandeeView +var hero_standee: Node2D # StandeeBody——主角站桩本体(世界内;slot_row = 壳层伴随件引用) var round_label: Label var stage_center: Label -var enemy_box: HBoxContainer +var enemy_box: Control # 站桩壳层伴随件容器(T3 语义改写:HBoxContainer → Control) var log_view: BattleLogView -var _cards: Array = [] +var _cards: Array = [] # StandeeBody(世界本体,敌) +var _row: HBoxContainer # 文本半区行(stage_center 载体 + 世界布局基准) +var _companion_layer: Control # 壳层伴随件层(主角 slot_row 宿主;敌伴随件宿主 = enemy_box) var _svp_container: SubViewportContainer var _viewport: SubViewport -var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点;T3 站桩迁入) +var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点) func _init(db_ref) -> void: @@ -42,7 +52,7 @@ func _init(db_ref) -> void: # ── Node2D 舞台半区(ADR-0006 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)── _svp_container = SubViewportContainer.new() _svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率,恒 1:1 - _svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # B 区零输入(tooltip 归壳层伴随件) + _svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 世界零输入(tooltip 归壳层伴随件) _svp_container.set_anchors_preset(Control.PRESET_FULL_RECT) _svp_container.offset_left = _CONTENT_MARGINS.position.x _svp_container.offset_top = _CONTENT_MARGINS.position.y @@ -55,8 +65,7 @@ func _init(db_ref) -> void: world = _STAGE_WORLD.new() _viewport.add_child(world) - # ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control; - # hero_standee/enemy_box 仍为 Control 站桩,T3 迁世界后仅伴随件留壳层)── + # ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control)── var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) vb.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -70,17 +79,15 @@ func _init(db_ref) -> void: round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vb.add_child(round_label) - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 10) - row.size_flags_vertical = Control.SIZE_EXPAND_FILL - vb.add_child(row) - - hero_standee = StandeeView.new("hero") - hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT), true) # B 区立绘 48×48 朝右(2026-09-26 用户点名 hero_idle_01 换装+水平向右;素材缺位 null 回退字形「人」) - row.add_child(hero_standee) + _row = HBoxContainer.new() + _row.add_theme_constant_override("separation", 10) + _row.size_flags_vertical = Control.SIZE_EXPAND_FILL + vb.add_child(_row) + _row.resized.connect(_on_row_resized) # 明细带展开/收起改行高——世界布局随动 var center := CenterContainer.new() center.size_flags_horizontal = Control.SIZE_EXPAND_FILL + center.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行内只剩文本——鼠标全放行给壳层伴随件 var center_col := VBoxContainer.new() center_col.alignment = BoxContainer.ALIGNMENT_CENTER center.add_child(center_col) @@ -88,49 +95,133 @@ func _init(db_ref) -> void: stage_center.add_theme_color_override("font_color", UiKit.STONE_FAINT) stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER center_col.add_child(stage_center) - row.add_child(center) + _row.add_child(center) - enemy_box = HBoxContainer.new() - enemy_box.add_theme_constant_override("separation", 8) - enemy_box.alignment = BoxContainer.ALIGNMENT_END - row.add_child(enemy_box) + enemy_box = Control.new() # 站桩壳层伴随件容器(T3 语义改写) + enemy_box.mouse_filter = Control.MOUSE_FILTER_IGNORE + _row.add_child(enemy_box) log_view = BattleLogView.new(db_ref) vb.add_child(log_view) + # ── 壳层伴随件层(最顶——世界站桩之上的 Control 触达面)── + _companion_layer = Control.new() + _companion_layer.set_anchors_preset(Control.PRESET_FULL_RECT) + _companion_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_companion_layer) -# SubViewport 主题/字体显式注入挂点(C6——Phase A BoardView.set_world_font 同款; -# T3 起随站桩本体下发 world_font) -func set_world_font(f: Font) -> void: - world.world_font = f + # 主角站桩本体(世界)+ 状态行伴随件(壳层) + hero_standee = _STANDEE_BODY.new("hero") + hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT), true) # B 区立绘 48×48 朝右(2026-09-26 用户点名 hero_idle_01 换装+水平向右;素材缺位 null 回退字形「人」) + world.add_child(hero_standee) + hero_standee.slot_row = _make_slot_companion(_companion_layer) -# 两套坐标空间换算单点(C4——台账 160 纪律延伸):SubViewport 世界内 = 局部排布; -# 壳层/fx 层(结算演出 SettlementFxView 在 SubViewport 之外)= 全局含容器偏移。 -# 站桩重建/容器重排挂点调用(站桩无逐帧位移,零每帧成本)。 +func _make_slot_companion(parent: Control) -> StatusSlotRow: + # 状态效果唯一展示面(2026-09-26 C 区精简批迁入站桩)——compact 档伴随件,tooltip 全明细 + var comp := StatusSlotRow.new() + comp.compact = true + comp.alignment = BoxContainer.ALIGNMENT_CENTER + comp.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行容器放行鼠标事件给槽位(悬停 tooltip) + comp.visible = false # 空态 = 图标行隐藏不占位(增益状态 EC12) + parent.add_child(comp) + return comp + + +# ── 布局同步(世界→壳层单点——C4;站桩无逐帧位移,重建/重排挂点调用,零每帧成本)── + func world_to_shell(p: Vector2) -> Vector2: return _svp_container.global_position + p +func _on_row_resized() -> void: + _sync_layout.call_deferred() + + func _notification(what: int) -> void: if what == NOTIFICATION_RESIZED: # stretch=true 时视口尺寸由容器接管;树外(冒烟直建)才需手设保 1:1(Phase A 同款) if not _svp_container.is_inside_tree(): _viewport.size = _svp_container.size + _sync_layout.call_deferred() + + +func _ready() -> void: + _sync_layout.call_deferred() # 首轮布局落定后对位(row 几何此时才有效) + + +func _sync_layout() -> void: + if not is_inside_tree() or hero_standee == null: + return + var origin: Vector2 = _svp_container.global_position + var row_rect: Rect2 = _row.get_global_rect() + var local := row_rect.position - origin # 行原点 → 世界坐标(1:1,C2) + var box_h := row_rect.size.y # 站桩盒高 = 行高(旧 HBox FILL 拉伸等价) + hero_standee.box = Vector2(_STANDEE_W, box_h) + hero_standee.position = local + # 敌站桩右对齐(旧 HBox ALIGNMENT_END + separation 8 等价) + var n := _cards.size() + var total := n * _STANDEE_W + maxi(0, n - 1) * _STANDEE_GAP + var x := row_rect.size.x - total + for i in n: + var body: Node2D = _cards[i] + if not is_instance_valid(body): + continue + body.box = Vector2(_STANDEE_W, box_h) + body.position = local + Vector2(x + float(i) * (_STANDEE_W + _STANDEE_GAP), 0.0) + _sync_companions() + + +func _sync_companions() -> void: + # 状态行伴随件 = 世界站桩顶部(血条正下方偏移 (8,14)——旧 top VBox 几何)的壳层投影 + var layer_pos: Vector2 = _companion_layer.global_position + if hero_standee != null and hero_standee.slot_row != null: + var comp: StatusSlotRow = hero_standee.slot_row + comp.reset_size() + comp.position = world_to_shell(hero_standee.position + Vector2(8.0 + (_STANDEE_W - 16.0 - comp.size.x) * 0.5, 14.0)) - layer_pos + for i in _cards.size(): + var body: Node2D = _cards[i] + if not is_instance_valid(body) or body.slot_row == null: + continue + var comp2: StatusSlotRow = body.slot_row + comp2.reset_size() + comp2.position = world_to_shell(body.position + Vector2(8.0 + (_STANDEE_W - 16.0 - comp2.size.x) * 0.5, 14.0)) - layer_pos + + +# SubViewport 主题/字体显式注入挂点(C6——Phase A BoardView.set_world_font 同款;站桩本体随动) +func set_world_font(f: Font) -> void: + world.world_font = f + hero_standee.font = f + for c in _cards: + if is_instance_valid(c): + c.font = f + c.queue_redraw() + + +# 敌方站桩本体出口(探针等价改写用——T5;旧 enemy_box 子件 = 站桩 Control 改伴随件后的本体通路) +func enemy_body_at(i: int) -> Node2D: + if i < 0 or i >= _cards.size(): + return null + return _cards[i] # 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律); # 数量变化 = 新战斗域,重建站桩槽(轮内不增删,无回流) func set_enemies(cards_data: Array) -> void: if _cards.size() != cards_data.size(): + for body in _cards: + if is_instance_valid(body): + body.queue_free() for c in enemy_box.get_children(): c.queue_free() _cards.clear() for d in cards_data: - var standee := StandeeView.new("enemy") - standee.set_data(d) - enemy_box.add_child(standee) - _cards.append(standee) + var body: Node2D = _STANDEE_BODY.new("enemy") + world.add_child(body) + body.slot_row = _make_slot_companion(enemy_box) + body.set_data(d) + _cards.append(body) + _sync_layout.call_deferred() return for i in cards_data.size(): if is_instance_valid(_cards[i]): @@ -138,6 +229,9 @@ func set_enemies(cards_data: Array) -> void: func clear_enemies() -> void: + for body in _cards: + if is_instance_valid(body): + body.queue_free() for c in enemy_box.get_children(): c.queue_free() _cards.clear() @@ -155,7 +249,7 @@ func on_step_row_fx(row: Dictionary) -> void: var target := String(row.get("target", "")) if target == "hero" or target == "": return - var card: StandeeView = _card_by_name(target) + var card: Node2D = _card_by_name(target) if card == null: return card.flash_hit() @@ -163,25 +257,25 @@ func on_step_row_fx(row: Dictionary) -> void: card.mark_dead() -func _card_by_name(display_name: String) -> StandeeView: +func _card_by_name(display_name: String) -> Node2D: for c in _cards: if not is_instance_valid(c): continue - if String(c._name_label.text).contains(display_name): # 名字行图标化批 2026-09-26:名字 Label 独立节点(_name_line 已改 HBox 容器) + if c.display_name().contains(display_name): return c return null -# 跨区弹道目标坐标(结算演出链批——组合根传给 SettlementFxView 的全局锚点) +# 跨区弹道目标坐标(结算演出链批——组合根传给 SettlementFxView 的全局锚点;壳层口径 C4) func enemy_standee_center(display_name: String): var card := _card_by_name(display_name) if card == null or not is_instance_valid(card): return null - return card.get_global_rect().get_center() + return world_to_shell(card.position + card.box * 0.5) func hero_standee_center() -> Vector2: - return hero_standee.get_global_rect().get_center() + return world_to_shell(hero_standee.position + hero_standee.box * 0.5) # battle_over:全场敌方站桩灰化兜底(短路击杀等未走 killed 行的边角) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd new file mode 100644 index 00000000..13720af2 --- /dev/null +++ b/game/presentation/views/standee_body.gd @@ -0,0 +1,348 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99: +# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。 +# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T3):站桩本体自 Control(旧 StandeeView, +# 退役删除)平移为 Node2D 世界精灵(ADR-0006:B 区战斗舞台归 Node2D)。行为面契约逐项保持: +# set_data/set_hero/_set_hp/set_idle/set_effects/set_effects_cleared_feedback/set_order/set_pending/ +# _play_intent_motion/set_portrait_texture(tex,flip)/mark_dead/is_dead/flash_hit/_revive/ +# _update_name_line/_refresh_portrait_glyph(调用点 hud_presenter/main/探针零改造目标——C3)。 +# 等价平移基线 = 旧 Control 占位表现(C9):立绘贴图(flip/NEAREST/整数 2× 档)+ 底座 + 血条 +# (直角递减 tween)+ 顺位徽标 + 意图徽标(弹入+立绘前倾一次性动画)+ 名字行(纯名+护甲类型 +# 图标——2026-09-26 终裁 AC 不展示)+ 受击闪白 + 死亡灰化摇摆塌陷(禁瞬消)+ 复活 + 字形占位 +# 回退(人/▂/▆/█/✕/敌)。批内代拍板默认(待追认):①状态图标行(slot_row)与悬停 tooltip 保留 +# Control 载体挂壳层(tooltip 机制零返工)——本体的 slot_row 成员 = 壳层伴随件引用; +# ②状态行隐藏时序 = 随 mark_dead 同拍隐藏;③清空反馈微文案挂伴随件父层(壳层)。 +# 文字全部经 draw_string + 世界字体(world.world_font 或 ThemeDB.fallback_font——C6 挂点链)。 +# 坐标空间:本节点 position/box = SubViewport 世界内局部排布;壳层换算归 +# BattleStageView.world_to_shell 单点(C4)。 +# 无 class_name(类名缓存面纪律——BattleStageView 经 preload 使用)。 +extends Node2D + +const CHARGE_SKILL_IDS := ["heavy"] +# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 / +# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。(自旧 StandeeView 平移) +const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} + +var box := Vector2(150, 176) # 站桩盒尺寸(旧 Control custom_minimum_size;实际高随壳层 row 布局注入) +var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入) +var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界) + +var _kind := "enemy" +var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) +var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) +var _portrait_flip := false +var _order := 1 +var _intent_glyph := "" +var _intent_col := Color.WHITE +var _intent_visible := false +var _intent_scale := 1.0: # 意图弹入动画参数(旧 tween scale 等价) + set(v): + _intent_scale = v + queue_redraw() +var _glyph_rot := 0.0: # 立绘前倾角(旧只转字形 Label——贴图态无此动画,等价平移) + set(v): + _glyph_rot = v + queue_redraw() +var _display_name := "" +var _enemy_id := "" +var _armor := "light" +var _dead := false +var _hp_max := 1.0 +var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积) + set(v): + hp_value = clampf(v, 0.0, 1.0) + queue_redraw() +var _hp_tween: Tween +var _hp_flash_tw: Tween + + +func _init(kind: String = "enemy") -> void: + _kind = kind + + +func _efont() -> Font: + return font if font != null else ThemeDB.fallback_font + + +func display_name() -> String: + return _display_name + + +# 血条存在性探针出口(T5 断言等价改写——旧 _hp_bar ProgressBar 退役,世界直角血条常在) +var has_world_hp_bar := true + + +# 意图徽标探针出口(T5——旧 _intent_badge.visible/text 改判) +func intent_probe() -> Dictionary: + return {"visible": _intent_visible, "text": _intent_glyph} + + +# 名字行探针出口(T5——旧 _name_label/_armor_icon.text 改判) +func name_line_probe() -> Dictionary: + return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))} + + +# ---- setter 面(投影契约与旧 StandeeView 一致——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_max = float(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", [])) + _refresh_portrait_glyph() + if _dead and int(d.get("hp", 0)) > 0: + _revive() + queue_redraw() + + +func set_hero(hp: int, max_hp: int) -> void: + _hp_max = float(maxi(1, max_hp)) + _set_hp(hp) + + +# 血条短 tween 递减(直角递减占位——密度/缓动终稿归战斗表现批);逐行刷新时重启 tween 不累积 +func _set_hp(hp: int) -> void: + var target := float(clampi(hp, 0, int(_hp_max))) / _hp_max + if _hp_tween != null and _hp_tween.is_valid(): + _hp_tween.kill() + _hp_tween = create_tween() + _hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15)) + + +func set_idle() -> void: + _set_hp(0) + set_effects([]) + + +# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位) +func set_effects(effects: Array) -> void: + if slot_row == null: + return + slot_row.set_effects(effects) + slot_row.visible = not effects.is_empty() + + +# battle_over:状态图标行全清可见反馈(微文案挂伴随件父层 = 壳层) +func set_effects_cleared_feedback() -> void: + if slot_row == null: + return + 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 + var parent := slot_row.get_parent() + if parent != null: + parent.add_child(note) + note.position = slot_row.position + Vector2((slot_row.size.x - 134.0) * 0.5, slot_row.size.y) + 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: + # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) + _order = i + queue_redraw() + + +# 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff; +# "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。 +func set_pending(ptype: String, pid: String) -> void: + if _dead: + _intent_visible = false + queue_redraw() + return + if ptype == "" or ptype == "basic": + _intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + queue_redraw() + 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_glyph = glyph + _intent_col = col + _intent_visible = true + _play_intent_motion() + + +# 三档动作动画占位:摇定时一次性短动画(意图图标弹入 + 立绘前倾回正),随后静帧保持 +func _play_intent_motion() -> void: + var tw := create_tween() + tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10)) + tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16)) + tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10)) + tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14)) + + +# 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 +# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2× +# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。 +func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: + _portrait_flip = flip + if tex == null: + if _portrait_sprite != null: + _portrait_sprite.visible = false + queue_redraw() + return + _portrait_tex = tex + if _portrait_sprite == null: + _portrait_sprite = Sprite2D.new() + _portrait_sprite.centered = false + _portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊(棋盘小人贴图批 2026-09-25) + add_child(_portrait_sprite) + _portrait_sprite.texture = tex + _portrait_sprite.flip_h = flip + _portrait_sprite.visible = true + _place_portrait() + + +func _place_portrait() -> void: + # 立绘盒 = (0,44)-(box.x, box.y-36)(旧 FULL_RECT offsets 等价);48 源 ×2 整数档居中 + if _portrait_sprite == null or _portrait_tex == null: + return + var box_h := box.y - 44.0 - 36.0 + var w := _portrait_tex.get_width() * 2.0 + var h := _portrait_tex.get_height() * 2.0 + _portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5) + _portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5) + + +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() + _intent_visible = false # 死亡残位意图退位(旧同款) + _set_hp(0) + if slot_row != null: + slot_row.visible = false # 状态行随 mark_dead 同拍隐藏(批内代拍板②待追认) + + +func is_dead() -> bool: + return _dead + + +# 受击闪白(结算演出占位——§2.2 受击反馈) +func flash_hit() -> void: + if _hp_flash_tw != null and _hp_flash_tw.is_valid(): + _hp_flash_tw.kill() + _hp_flash_tw = create_tween() + _hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) + _hp_flash_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() + if slot_row != null: + slot_row.visible = true + queue_redraw() + + +func _refresh_portrait_glyph() -> void: + queue_redraw() + + +func _portrait_glyph() -> String: + if _kind == "hero": + return "人" + if _dead: + return "✕" + match _enemy_id: + "goblin": + return "▂" # 小体量墩形 + "bugbear": + return "▆" # 垂直延伸体量 + "ogre_chief": + return "█" # 最大体量 + _: + return "敌" + + +func _portrait_color() -> Color: + return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP + + +# 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用 +func _portrait_rect() -> Rect2: + return Rect2(0, 44, box.x, box.y - 80.0) + + +func _draw() -> void: + var f := _efont() + # 底座(旧 panel_flat(STONE.darkened(0.35), STONE.darkened(0.1), 6, 1) 等价——BOTTOM_WIDE 26/-26, -18/-8) + var plat := UiKit.panel_flat(UiKit.STONE.darkened(0.35), UiKit.STONE.darkened(0.1), 6, 1) + plat.draw(get_canvas_item(), Rect2(26, box.y - 18.0, box.x - 52.0, 10.0)) + + # 血条(直角递减):敌附顺位徽标(GOLD_DEEP ①②③)——旧 top VBox 几何等价(左 8 / 右 -8) + var bar_x := 8.0 + if _kind == "enemy": + var order_txt := String("①②③"[clampi(_order - 1, 0, 2)]) + f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt, + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_DEEP) + bar_x += 20.0 + var bar_w := box.x - 8.0 - bar_x + draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移) + var fill_w := bar_w * hp_value + if fill_w > 0.0: + draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减) + + # 立绘:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退 + if _portrait_sprite == null or _portrait_sprite.texture == null or not _portrait_sprite.visible: + var prect := _portrait_rect() + var glyph := _portrait_glyph() + var center := prect.get_center() + draw_set_transform(center, deg_to_rad(_glyph_rot), Vector2.ONE) + var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42) + f.draw_string(get_canvas_item(), center - Vector2(ts.x * 0.5, -ts.y * 0.35), glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color()) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + # 意图徽标(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 + if _intent_visible: + var icenter := Vector2(box.x * 0.5, 86.0) + draw_set_transform(icenter, 0.0, Vector2(_intent_scale, _intent_scale)) + var its := f.get_string_size(_intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15) + f.draw_string(get_canvas_item(), icenter - Vector2(its.x * 0.5, -its.y * 0.35), _intent_glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 15, _intent_col) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + # 底座下 护甲类型图标+名字 常驻行(敌 only——2026-09-26 用户终裁:AC 数值不展示,类型图标化) + if _kind == "enemy": + var armor_txt := String(ARMOR_ICONS.get(_armor, "◇")) + var col: Color = UiKit.STONE # 轻甲 = 灰石(中性轻薄) + match _armor: + "ward": + col = UiKit.HERO_BLUE # 魔法护甲 = 小人蓝 + "heavy": + col = UiKit.GOLD_DEEP # 重甲 = 深金 + var name_w := f.get_string_size(_display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x + var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x + var total_w := armor_w + 3.0 + name_w + var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0) + f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col) + f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), _display_name, + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.INK_TITLE) diff --git a/game/presentation/views/standee_body.gd.uid b/game/presentation/views/standee_body.gd.uid new file mode 100644 index 00000000..6693b923 --- /dev/null +++ b/game/presentation/views/standee_body.gd.uid @@ -0,0 +1 @@ +uid://dliuugrkyq2ra diff --git a/game/presentation/views/standee_view.gd b/game/presentation/views/standee_view.gd deleted file mode 100644 index 670d6aa8..00000000 --- a/game/presentation/views/standee_view.gd +++ /dev/null @@ -1,324 +0,0 @@ -# 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 = "敌" diff --git a/game/presentation/views/standee_view.gd.uid b/game/presentation/views/standee_view.gd.uid deleted file mode 100644 index 4e14b0bc..00000000 --- a/game/presentation/views/standee_view.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gfvj2sxds3ph diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 25d8b0c8..7bb08c04 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -110,21 +110,26 @@ func _ready() -> void: 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;2026-09-20 全站桩拍板)---- - var standee = main.stage.enemy_box.get_child(0) - if standee != null and (standee._name_label.text == "" or standee._armor_icon.text == ""): - problems.append("敌立绘名字行缺失(名字或护甲类型图标占位为空——2026-09-26 终裁:AC 不展示、类型图标化)") - if standee != null and standee._intent_badge == null: - problems.append("敌立绘意图指示缺失") - elif standee != null: + # B 区站桩 Node2D 迁移批 T3(2026-09-26)断言等价改写(台账 171 改判记录):enemy_box 子件 + # 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体 + # 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持 + var standee = main.stage.enemy_body_at(0) + if standee == null: + problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)") + else: + var name_line: Dictionary = standee.name_line_probe() + if String(name_line.name) == "" or String(name_line.armor_icon) == "": + problems.append("敌立绘名字行缺失(名字或护甲类型图标占位为空——2026-09-26 终裁:AC 不展示、类型图标化)") # 三档动作动画占位契约(表现层升级批拍板:不显示技能名——图标档位 + 一次性动画) 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]) + var intent_on: Dictionary = standee.intent_probe() + if not bool(intent_on.visible) or String(intent_on.text) != "⚔": + problems.append("攻击系意图未呈现(visible=%s text=%s)" % [str(intent_on.visible), str(intent_on.text)]) standee.set_pending("basic", "") - if standee._intent_badge.visible: + if bool(standee.intent_probe().visible): problems.append("普攻应零表示(意图指示应隐藏)") - if main.stage.hero_standee == null or main.stage.hero_standee._hp_bar == null: - problems.append("B 区主角站桩缺失") + if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar: + problems.append("B 区主角站桩缺失(世界血条面缺失)") # ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)---- var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源) var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1, diff --git a/production/session-state/active.md b/production/session-state/active.md index 27fde002..4d4b23f0 100644 --- a/production/session-state/active.md +++ b/production/session-state/active.md @@ -457,4 +457,5 @@ 168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。 169. **样张 v1 制作与机检批(2026-09-26,本会话——循环 1 T1;worktree battle-stage-node2d-20260926 批次本提交)**:①**四源对照表先行** production/mockups/画法参数四源对照表-2026-09-26.md(battle.md 布局/机制代码〔0 格无字形 board_view:586·旗只插未开 :589·脚锚自底 25%〕/引擎常量〔45px·1.5× 档·96px 盒〕/art-bible §1.2-§4.6 画法)——候选值偏差两笔登记(小人蓝 #2E86DE 候选 vs UiKit #2E6E8E=样张批冻结项非矛盾;格底 L≥0.30 系换皮槽参数域不复裁已验收 dungeon 瓦片)。②**脚本重写** tools/render_sample_sheet.py v1:方案甲词表查表拼装(16 情形+对角回落 nw>ne>sw>se+flip 镜像+变体 60/20/20 哈希)+三区全内容 1280×800 实比 mockup+S2 数字档 0.5× 抽检+S3 underdark 四槽换色+S4 图例;素材两级回退(worktree→主检出,素材不入 git)。③**机检 12 断言生成即验全绿**——F1 词表拼装零错位 15 种用件/F2 旗∈未开/F3 脚锚自底 25%(两态)〔预筛抓出素材底部 7 行全透明→裁除留白后锚定,视觉脚=锚线;**游戏内 BoardWorld.ensure_hero 同病(视觉脚浮锚线 ~10px)=呈现缺陷登记移交循环 2/接线批**〕/F4 0 格无字形/F5a 接触带微光⊆英雄格/F5b 类型微光禁整格染色(中心 50% 区零像素——角嵌臂长两轮收紧 L9→L5 留白砖心)/A1 色板对表+禁入带 300-345° 零命中(腐蚀两轮滤 AA 混色)/A2 周界闭合(墙侧 2px 带墨线存在性 min<55——45px 非整数缩放下均值判误报改最暗值判)/A3 墨×牌 11.8-12.0:1+图底分离 113 vs 51/A4 混排在库原件/A5 画布四缘净空(溢出永久断言)。④**visual-judge 预筛三轮**(≤3 预算内):R1 四 FATAL→triage=站桩横向重叠/小人脚浮(素材留白)/同格态色牌被小人盖(改 §1.2 栈序数字>小人)/缘件裸拼误报(1px 墨线逐像素 zoom 自证 end_n 三向墨线俱在+15 种用件机检反证,判官 3-6px 采样带均值漏 1px 线)+地精无徽标误报(普攻零表示=设计);R2 两 FATAL=画布右缘溢出×2(说明行+S4 末项)→画布 1560+A5 断言化;R3 窄域复核 pass。⑤**交付**=样张 v1+机检报告+对照表三件已拷主区 production/mockups/(评审产物不入 git 同 v0 先例),**用户异步终审**(过=冻结为循环 2 输入;不过=对表改参重出);终审不阻塞 T2-T7。轻闸全绿(当轮基线=T0 记档),进入 T2。 -170. **B 区战斗舞台壳改造批(Phase B T2,2026-09-26,本会话——循环 1;worktree battle-stage-node2d-20260926)**:BattleStageView 根壳 PanelContainer→**Control**(类名/252px/树内位置不变;面板样式=Panel 子面+原 stylebox 等价平移,子面几何沿原内容边距 Rect2(6,4,6,4));内部拆两半区——**Node2D 舞台半区**=SubViewportContainer(stretch=true 1:1 禁缩放系数)+SubViewport(transparent_bg)+**StageWorld**(新件 presentation/views/stage_world.gd,无 class_name 经 preload 接线,BoardWorld 同构:world_font 字体注入槽位+font_probe() C6 判据出口〔注入非空+has_char+宽度>0〕;T2 期世界空置待 T3 站桩迁入)+**Control 文本半区**=round_label/stage_center/log_view 三件原样保留(C1 永久 Control),hero_standee/enemy_box 本期仍 Control 原位。新挂点=set_world_font(f)(Phase A BoardView 同款,无调用方纯挂点)+world_to_shell(p) 坐标换算单点(C4——世界=局部排布/壳层=全局含容器偏移)。**C6 判据实证**(临时脚本跑完即删):未注入=回退字体 has_char=false(引擎默认无 CJK=文档既有口径);注入 fusion-pixel ja TTF(内存 load_dynamic_font 零素材入工程——C7)后 has_char=true 宽度 70>0 PASS。**验收**=轻闸全绿:editor --quit 0 error+冒烟 557/0(=基线)+五探针全绿+ui_layout_probe 外框逐位零漂移(stage pos=(0,0) size=(1280,252) min=(0,252)=T0 基线同值;B 区内部树结构变化记录:PanelContainer 自动布局→Control 锚点+VBox 同边距)+static_scan exit0+文档三闸零漂移。进入 T3。 \ No newline at end of file +170. **B 区战斗舞台壳改造批(Phase B T2,2026-09-26,本会话——循环 1;worktree battle-stage-node2d-20260926)**:BattleStageView 根壳 PanelContainer→**Control**(类名/252px/树内位置不变;面板样式=Panel 子面+原 stylebox 等价平移,子面几何沿原内容边距 Rect2(6,4,6,4));内部拆两半区——**Node2D 舞台半区**=SubViewportContainer(stretch=true 1:1 禁缩放系数)+SubViewport(transparent_bg)+**StageWorld**(新件 presentation/views/stage_world.gd,无 class_name 经 preload 接线,BoardWorld 同构:world_font 字体注入槽位+font_probe() C6 判据出口〔注入非空+has_char+宽度>0〕;T2 期世界空置待 T3 站桩迁入)+**Control 文本半区**=round_label/stage_center/log_view 三件原样保留(C1 永久 Control),hero_standee/enemy_box 本期仍 Control 原位。新挂点=set_world_font(f)(Phase A BoardView 同款,无调用方纯挂点)+world_to_shell(p) 坐标换算单点(C4——世界=局部排布/壳层=全局含容器偏移)。**C6 判据实证**(临时脚本跑完即删):未注入=回退字体 has_char=false(引擎默认无 CJK=文档既有口径);注入 fusion-pixel ja TTF(内存 load_dynamic_font 零素材入工程——C7)后 has_char=true 宽度 70>0 PASS。**验收**=轻闸全绿:editor --quit 0 error+冒烟 557/0(=基线)+五探针全绿+ui_layout_probe 外框逐位零漂移(stage pos=(0,0) size=(1280,252) min=(0,252)=T0 基线同值;B 区内部树结构变化记录:PanelContainer 自动布局→Control 锚点+VBox 同边距)+static_scan exit0+文档三闸零漂移。进入 T3。 +171. **站桩本体 Node2D 化批(Phase B T3,2026-09-26,本会话——循环 1 最重卡;worktree battle-stage-node2d-20260926)**:站桩本体自 Control(旧 StandeeView **退役删除**)平移为 Node2D 世界精灵 **standee_body.gd**(新件,无 class_name 经 preload)。**行为面 17 方法契约逐项保持(C3)**——set_data/set_hero/_set_hp/set_idle/set_effects/set_effects_cleared_feedback/set_order/set_pending/_play_intent_motion/set_portrait_texture(tex,flip)/mark_dead/is_dead/flash_hit/_revive/_update_name_line/_refresh_portrait_glyph+is_dead,调用点 hud_presenter:53-54/79-80、main:309/441/475/495/503、main:768/770 零改排。**等价平移清单(C9)**——立绘=set_portrait_texture 惰性 Sprite2D(NEAREST+flip_h+整数 2×=48 源盒 96px)缺位回退字形(人/▂/▆/█/✕/敌 draw_string);底座=UiKit.panel_flat(STONE 系) StyleBox.draw 直绘;血条=直角矩形 hp_value 属性 tween 0.15 递减(引擎默认主题深底/蓝填充近似平移——色值列目检清单);顺位徽标 ①②③ GOLD_DEEP;意图徽标=draw_set_transform 弹入 scale 1.6→1+字形前倾 4°→0(旧只转字形 Label——贴图态无动画等价保持);名字行=护甲图标+纯名 draw_string 居中(AC 不展示 2026-09-26 终裁);受击闪白 modulate 1.6/1.4/1.2→白/死灰;死亡=灰化+rotation 2.5/-1.5/0 摇摆塌陷+意图退位(禁瞬消)+**状态行随 mark_dead 同拍隐藏(代拍板②待追认)**;复活=全态重置。**批内代拍板默认(待追认)**——①slot_row+悬停 tooltip 保留 Control 载体挂壳层(_companion_layer 顶层,敌伴随件宿主=**enemy_box 语义改写「站桩壳层伴随件容器」HBoxContainer→Control**,手动定位随世界同步,探针 :97 计数面不变);②定位同步=world_to_shell 单点(T2 建挂),_sync_layout 挂点=ready/NOTIFICATION_RESIZED/_row.resized(明细带展收改行高随动)/set_enemies 重建,站桩盒高=行高(旧 HBox FILL 拉伸等价)、敌右对齐 +8 间距等价;③hero_standee/enemy 站桩成员类型 StandeeView→Node2D(StandeeBody),**hero_standee_center/enemy_standee_center 改 world_to_shell(本体中心)=壳层口径(C4)**。ui_probe 敌站桩断言等价改写(台账 170 前例):穿透明(enemy_box 子件 _name_label/_armor_icon/_intent_badge/_hp_bar)→本体出口 enemy_body_at(i)+name_line_probe()/intent_probe()/has_world_hp_bar——呈现口径逐字保持。**踩坑记录**——standee_body 漏声明 _portrait_tex 成员→编译连坐 main.gd 假性两错(w/h 推断失败),editor --quit 前置闸暴露后修复。**验证**——editor --quit 0 error+冒烟 557/0(=基线零增删)+五探针全绿(ui_probe/ui_click 问题=0/ui_tune/ui_input 十一断言/layout 外框零漂移 stage 1280×252 逐位=T0 基线)+static_scan exit0+文档三闸零漂移。进入 T4。 \ No newline at end of file