diff --git a/game/data/schemas/battle_layout_table.gd b/game/data/schemas/battle_layout_table.gd index 2095fb2d..3e557b08 100644 --- a/game/data/schemas/battle_layout_table.gd +++ b/game/data/schemas/battle_layout_table.gd @@ -5,6 +5,7 @@ class_name BattleLayoutTable extends Resource @export var stage_h: int = 252 # B 区高(全宽战斗画面,含底部明细带) +@export var ground_h: int = 100 # B 区连续地面层高(源 = 基准屏宽 × 本值) @export var info_panel_w: int = 240 # C 区宽(左下角色信息列) @export var ap_bar_h: int = 44 # D 区 AP 条高 @export var log_band_h_dig: int = 24 # 明细带挖掘期收起高(单行上轮摘要) @@ -27,4 +28,15 @@ func validate() -> Array: errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) < log_band_h_dig(%d)——展开态不得矮于收起态" % [log_band_h_settle, log_band_h_dig]}) if log_band_h_settle >= stage_h: errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) ≥ stage_h(%d)——明细带不得占满 B 区" % [log_band_h_settle, stage_h]}) + errs.append_array(_ground_field_errors()) + return errs + + +## 地面层域校验(拆私有辅助——validate 分支预算内;ground_h 单源 = 布局表) +func _ground_field_errors() -> Array: + var errs: Array[Dictionary] = [] + if ground_h < 16 or ground_h > stage_h / 2: + errs.append({"category": "domain", "message": "ground_h 越域 [16, stage_h/2=%d]: %d" % [stage_h / 2, ground_h]}) + if ground_h <= log_band_h_dig: + errs.append({"category": "cross_field", "message": "ground_h(%d) ≤ log_band_h_dig(%d)——地面层不得被收起态明细带整条盖住" % [ground_h, log_band_h_dig]}) return errs diff --git a/game/data/tables/battle_layout.tres b/game/data/tables/battle_layout.tres index 0a784bc0..cf30df23 100644 --- a/game/data/tables/battle_layout.tres +++ b/game/data/tables/battle_layout.tres @@ -5,6 +5,7 @@ [resource] script = ExtResource("1") stage_h = 252 +ground_h = 100 info_panel_w = 240 ap_bar_h = 44 log_band_h_dig = 24 diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 8e277baf..09caad6f 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -382,7 +382,10 @@ func _on_board_changed() -> void: run_scope.settlement = run_scope.runner.current_battle() if run_scope.settlement == null: return - ui.battle.board_zone.board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) + var board = run_scope.settlement.minesweeper.current_board() + ui.battle.board_zone.board_view.rebuild(board, run_scope.settlement.minesweeper) + if board != null: + ui.battle.stage.set_board_type(String(board.board_type)) # 舞台背景选图随板(C8——既有重建点单行同步) _check_board_hints() _refresh_presentations() diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd index 7114ca29..949b96ed 100644 --- a/game/presentation/main_debug_flow.gd +++ b/game/presentation/main_debug_flow.gd @@ -100,6 +100,9 @@ static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrches static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: root.run_scope.settlement = settlement_ref - root.ui.battle.board_zone.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper) + var board = root.run_scope.settlement.minesweeper.current_board() + root.ui.battle.board_zone.board_view.rebuild(board, root.run_scope.settlement.minesweeper) + if board != null: + root.ui.battle.stage.set_board_type(String(board.board_type)) # 舞台背景选图随板(C8——调试就绪重建点单行同步) root._check_board_hints() root._refresh_presentations() diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index 251baffa..5638748b 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -1,5 +1,6 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## B 区战斗画面:主角/敌方站桩舞台(SubViewport 世界)+ 轮次·阶段指示 + 结算明细带宿主。 +## B 区战斗画面:舞台场景构成(§6.1——背景三关变体→连续地面→世界站桩/小石台)+ 轮次·阶段指示 + 结算明细带宿主。 +## 光柱层已退役,不迁入背景;平色缺图回退走合并目标 SLOT_BACKDROP(art-bible §4.1 换色合并③)。 ## 本类零文案装配:结算流水格式化归 BattleLogView、核心状态→文案装配归 HudPresenter——禁在此拼文案字符串。 class_name BattleStageView extends Control @@ -16,44 +17,80 @@ var round_label: Label var stage_center: Label var enemy_box: Control # 站桩壳层伴随件容器(T3 语义改写:HBoxContainer → Control) var log_view: BattleLogView +var bg_texture: TextureRect # 三关背景纹理层(set_board_type 按 board_type 选图) +var ground_layer: TextureRect # 连续地面层(三关共用——高度读布局表 ground_h) var _cards: Array[Node2D] = [] # StandeeBody(世界本体,敌) var _row: HBoxContainer # 文本半区行(stage_center 载体 + 世界布局基准) var _companion_layer: Control # 壳层伴随件层(主角 slot_row 宿主;敌伴随件宿主 = enemy_box) var _svp_container: SubViewportContainer var _viewport: SubViewport +var _bg_board_type := "" # 当前背景板型(set_board_type 归一后缓存——同型早退) var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点) func _init(db_ref, layout: BattleLayoutTable) -> void: custom_minimum_size = Vector2(0, layout.stage_h) _build_backdrop() + _build_ground(layout) _build_world_stage() _build_text_half(db_ref, layout) _build_companion_layer(db_ref) -## 背景域(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景 -## =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿) +## 背景域(§6.1 舞台场景构成:背景三关变体 → 连续地面 → 世界站桩;平色 Panel 保留为 +## 缺图回退底(改指合并目标 SLOT_BACKDROP——art-bible §4.1 换色合并③);描边=PANEL_BORDER。 +## 光柱层已退役,不迁入背景) func _build_backdrop() -> void: var bg := Panel.new() - var sb := UiKit.panel_flat(UiKit.STAGE_BG, UiKit.PANEL_BORDER, 0, 1) + var sb := UiKit.panel_flat(UiKit.SLOT_BACKDROP, UiKit.PANEL_BORDER, 0, 1) bg.add_theme_stylebox_override("panel", sb) bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(bg) - _build_beams_layer() + bg_texture = TextureRect.new() + bg_texture.set_anchors_preset(Control.PRESET_FULL_RECT) + bg_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + bg_texture.stretch_mode = TextureRect.STRETCH_SCALE + bg_texture.mouse_filter = Control.MOUSE_FILTER_IGNORE + bg_texture.visible = false + add_child(bg_texture) + set_board_type("dungeon") -## 顶光柱氛围层域(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 -## UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ -## 世界站桩之下——层序=bg < 光柱 < 世界) -func _build_beams_layer() -> void: - var beams := Control.new() - beams.set_anchors_preset(Control.PRESET_FULL_RECT) - beams.mouse_filter = Control.MOUSE_FILTER_IGNORE - beams.draw.connect(_draw_light_beams.bind(beams)) - beams.resized.connect(beams.queue_redraw) - add_child(beams) +## 背景选图单点(C1——只按 board_type 三值选图,禁按关卡序号/节点深度另造选择逻辑; +## 空串归 dungeon;目标缺图先回 dungeon,再缺保留平色底;缺图不崩不刷产品日志) +func set_board_type(board_type: String) -> void: + var bt := "dungeon" if board_type.is_empty() else board_type + if bt == _bg_board_type: + return + _bg_board_type = bt + var tex := _load_stage_texture(UiKit.TEX_STAGE_BG_FMT % bt) + if tex == null and bt != "dungeon": + tex = _load_stage_texture(UiKit.TEX_STAGE_BG_FMT % "dungeon") + bg_texture.texture = tex + bg_texture.visible = tex != null + + +## 缺图回退可测单点(背景与地面装载唯一经此——load_tex exists 守卫承载) +func _load_stage_texture(path: String) -> Texture2D: + return UiKit.load_tex(path) + + +## 连续地面层(C3——底边贴 B 区底、全宽、高度读布局表 ground_h;三关共用; +## 位于背景之上/世界之下——小石台在世界内站桩底绘制,天然在地面之上) +func _build_ground(layout: BattleLayoutTable) -> void: + ground_layer = TextureRect.new() + ground_layer.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) + ground_layer.offset_left = 0 + ground_layer.offset_right = 0 + ground_layer.offset_bottom = 0 + ground_layer.offset_top = -float(layout.ground_h) + ground_layer.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + ground_layer.stretch_mode = TextureRect.STRETCH_SCALE + ground_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE + ground_layer.texture = _load_stage_texture(UiKit.TEX_STAGE_GROUND) + ground_layer.visible = ground_layer.texture != null + add_child(ground_layer) ## Node2D 舞台半区域(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5) @@ -131,7 +168,7 @@ func _build_companion_layer(db_ref) -> void: # type-hint: ignore(db 鸭子型 add_child(_companion_layer) hero_standee = _STANDEE_BODY.new("hero") var hero_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) - hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘 48px 档·normal 态朝右(素材缺位 null 回退字形「人」) + hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘显示档 hero=144(§8.2 三档)·normal 态朝右(素材缺位 null 回退字形「人」) # 主角三态注册:attack/hit 缺位不注册=该态保持现纹理,缺位回退链延伸 hero_standee.register_hero_state("idle", hero_tex) hero_standee.register_hero_state("attack", UiKit.load_tex(UiKit.TEX_HERO_ATTACK)) @@ -140,21 +177,6 @@ func _build_companion_layer(db_ref) -> void: # type-hint: ignore(db 鸭子型 hero_standee.slot_row = _make_slot_companion(_companion_layer) -# 顶光柱绘制(§6.1——方向稿同构:每柱 60 段叠层矩形,宽度 ±46px 随 i//3 收窄、高度 40+3i 向下延伸、 -# α 26→0 渐隐;同柱相邻段重叠区 α 自然叠加=顶部亮下部渐隐的光井观感;位置随层宽等比例) -func _draw_light_beams(c: Control) -> void: - var w := c.size.x - if w <= 0.0: - return - for bx: float in [110.0, 900.0, 1055.0, 1210.0]: - var x0: float = bx / 1280.0 * w - for i in 60: - var a := int(26.0 * (1.0 - float(i) / 60.0)) - var half := 46.0 - float(i) / 3.0 - c.draw_rect(Rect2(x0 - half, 0.0, half * 2.0, 40.0 + float(i) * 3.0), - Color(UiKit.BEAM_LIGHT, float(a) / 255.0), true) - - func _make_slot_companion(parent: Control) -> StatusSlotRow: # 状态效果唯一展示面——compact 档伴随件,tooltip 全明细 var comp := StatusSlotRow.new() diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 74110af8..7ce53dd1 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -182,8 +182,6 @@ const TOAST_BG := Color(0.94, 0.91, 0.84, 0.95) # toast 面底 const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底 # 呈现杂色档(各视图裸 Color8 字面量单点化) -const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 render_battle_screen——battle_stage_view 面板底) -const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层) const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色) const PLAZA_EDGE := Color8(76, 64, 56) # 小石台描边(art-bible §4.1 换色合并②合并目标——贴图已落此值,缺位回退同值) const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep) @@ -247,6 +245,10 @@ const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_ const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王 # 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint) const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png" +# 三关舞台背景(§6.1——按 board_type 选图、不走 tint;源 = 基准屏宽 × stage_h) +const TEX_STAGE_BG_FMT := "res://assets/art/scene/scene_stage_bg_%s.png" +# 连续地面层(§6.1——三关共用、无 board_type 后缀、不吃 tint;源 = 基准屏宽 × ground_h) +const TEX_STAGE_GROUND := "res://assets/art/scene/scene_stage_ground.png" # 营地场景十层(art-bible §6.2——z0 bg 不透明全幅+z1 围墙透明细条+八物件透明层;布局矩形/命中矩形/z 序 # 唯一事实源 = data/tables/camp_scene_layout.tres;素材名跟本组常量走(先定常量防断链); # 缺位 load_tex→null→程序化占位,冒烟安全) diff --git a/game/probe/layout_probe.gd b/game/probe/layout_probe.gd index 965732fd..0346ae24 100644 --- a/game/probe/layout_probe.gd +++ b/game/probe/layout_probe.gd @@ -117,4 +117,17 @@ func _check_one(label: String, main: Node) -> void: "13": BoardViewScript._cell_for(13, bv.size), "14": BoardViewScript._cell_for(14, bv.size), "avail": str(bv.size), - } \ No newline at end of file + } + # 舞台背景与地面层(真实选路入口进战斗屏后的装载断言——迭代 48 卡③ V2/V4/V6) + var stage: Object = battle_ui.get("stage") + var bg_tex: TextureRect = stage.get("bg_texture") + if bg_tex == null or bg_tex.texture == null: + _problems.append("[%s] 舞台背景纹理未装载(bg_texture 缺位或空纹理)〔舞台背景 V2〕" % label) + elif not String(bg_tex.texture.resource_path).ends_with("_dungeon.png"): + _problems.append("[%s] 舞台背景路径非 dungeon 变体:%s〔舞台背景 V2/V6〕" % [label, String(bg_tex.texture.resource_path)]) + var ground: TextureRect = stage.get("ground_layer") + var layout = main.db.presentation.battle_layout + if ground == null or not ground.visible: + _problems.append("[%s] 地面层缺位或不可见〔舞台背景 V4〕" % label) + elif absf(ground.size.y - float(layout.ground_h)) > 0.51: + _problems.append("[%s] 地面层高 %s ≠ 布局表 ground_h %d〔舞台背景 V4〕" % [label, str(ground.size.y), int(layout.ground_h)]) \ No newline at end of file diff --git a/game/probe/ui_contrast_probe.gd b/game/probe/ui_contrast_probe.gd index eaae9ffb..98de808f 100644 --- a/game/probe/ui_contrast_probe.gd +++ b/game/probe/ui_contrast_probe.gd @@ -27,7 +27,7 @@ func _pairs() -> Array: ["RAP标签", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # N1-7(T0 实测已合规,显式化) ["锚·正文墨/数据板", UiKit.INK, UiKit.PANEL_DATA, 1.0], # 全站锚点:正文墨对数据板 ["锚·暖白/屏根", UiKit.WARM, UiKit.BG_ROOT, 1.0], # 全站锚点:暗底正文 - ["锚·暖白弱/舞台底", UiKit.WARM_FAINT, UiKit.STAGE_BG, 1.0], # 全站锚点:B区辅助墨 + ["锚·暖白弱/舞台底", UiKit.WARM_FAINT, UiKit.SLOT_BACKDROP, 1.0], # 全站锚点:B区辅助墨(合并目标——art-bible §4.1 换色合并③) ["钥石徽·深墨/烛光黄", UiKit.INK_ON_GOLD, UiKit.CANDLE, 1.0], # badge 工厂件文字(营地徽+状态面板余额行同形制) ["生涯页·行名墨/暖亮页底", UiKit.WARM, UiKit.BG_BRIGHT, 1.0], # 生涯页行名列/提示文(WARM_SOFT 同族亮墨) ["生涯页·行值墨/暖亮页底", UiKit.CANDLE, UiKit.BG_BRIGHT, 1.0], # 生涯页数值列(烛光黄 bold) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index a74234bf..e7ce2470 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -193,6 +193,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) {"path": "res://tests/unit/presentation/camp_scene_layout_test.gd", "expect": 0}, # 营地场景布局数据件校验(V14——fail-fast 判据+七命中区几何红线) + {"path": "res://tests/unit/presentation/stage_background_test.gd", "expect": 0}, # B 区舞台背景三关变体与连续地面层(V1–V5/V8——ground_h 表单源/board_type 选图/缺图回退/地面几何/光柱退役) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入) diff --git a/game/tests/unit/presentation/stage_background_test.gd b/game/tests/unit/presentation/stage_background_test.gd new file mode 100644 index 00000000..0f61ad31 --- /dev/null +++ b/game/tests/unit/presentation/stage_background_test.gd @@ -0,0 +1,165 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# B 区舞台背景与连续地面层单元套件(迭代 48 卡③)——布局字段/选图/缺图回退/地面几何/光柱退役。 +# 判据(改动—验收对照表 V1–V5/V8):ground_h 表单源与校验拒绝、board_type 三值选图与空串归一、 +# 缺图 dungeon 回退与平色兜底、地面层底贴 B 区底读表、光柱层与两源常量退役且菜单斜光带在位。 +# headless 直建 BattleStageView(null db——背景/地面/文本半区不消费 db;选路真实入口归 layout_probe)。 +# 缺图观测 = 子类覆写 _load_stage_texture(不改包内资产、不注入破坏)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/stage_background_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const StageViewScript := preload("res://presentation/views/battle_stage_view.gd") + + +# 缺图观测子类一:仅 underdark 路径缺图(deep_underdark/地面/其余走真实 load_tex) +class MissingUnderdarkStage extends BattleStageView: + func _load_stage_texture(path: String) -> Texture2D: + if path.ends_with("scene_stage_bg_underdark.png"): + return null + return UiKit.load_tex(path) + + +# 缺图观测子类二:全缺图(任何板型与地面都缺 → 平色兜底面) +class AllMissingStage extends BattleStageView: + func _load_stage_texture(_path: String) -> Texture2D: + return null + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + _test_layout_ground_field() + await _test_background_selection() + await _test_background_fallback() + await _test_ground_layer_geometry() + _test_beams_retired() + quit(Kit.finish("stage_background_test")) + + +# ---- V1 ground_h 字段与校验(表值/默认表 validate 空/三态拒绝族) ---- +func _test_layout_ground_field() -> void: + var layout = load("res://data/tables/battle_layout.tres") + Kit.check("布局表 ground_h = 100 且默认表 validate 为空〔舞台背景 V1〕", + int(layout.ground_h) == 100 and layout.validate().is_empty()) + var t := BattleLayoutTable.new() + t.ground_h = 20 # ≤ log_band_h_dig=24(收起态明细带整条盖住) + var errs_low: Array = [] + for e in t.validate(): + if String(e.get("message", "")).contains("ground_h"): + errs_low.append(e) + Kit.check("ground_h=20 → 恰一条含 ground_h 的错误(≤收起带盖住)〔舞台背景 V1〕", errs_low.size() == 1) + t.ground_h = 200 # > stage_h/2 = 126 + var errs_high: Array = [] + for e in t.validate(): + if String(e.get("message", "")).contains("ground_h"): + errs_high.append(e) + Kit.check("ground_h=200 → 恰一条含 ground_h 的错误(超 B 区一半)〔舞台背景 V1〕", errs_high.size() == 1) + t.ground_h = 100 + var errs_ok: Array = [] + for e in t.validate(): + if String(e.get("message", "")).contains("ground_h"): + errs_ok.append(e) + Kit.check("ground_h=100 → 零 ground 相关错误〔舞台背景 V1〕", errs_ok.is_empty()) + + +func _make_stage() -> Node: + var stage: Node = StageViewScript.new(null, load("res://data/tables/battle_layout.tres")) + root.add_child(stage) + return stage + + +# ---- V2 背景按 board_type 选图(直建默认 dungeon + 三值路径逐值对应 + 层序在世界之下) ---- +func _test_background_selection() -> void: + var layout = load("res://data/tables/battle_layout.tres") + var stage: Node = _make_stage() + await process_frame + var bg: TextureRect = stage.bg_texture + Kit.check("直建默认装载 dungeon 背景(纹理非空且 1280×stage_h)〔舞台背景 V2〕", + bg != null and bg.texture != null + and bg.texture.get_width() == 1280 and bg.texture.get_height() == int(layout.stage_h)) + for bt: String in ["underdark", "deep_underdark", "dungeon"]: + stage.set_board_type(bt) + var tex: Texture2D = bg.texture + Kit.check("set_board_type(%s) → 纹理路径以 _%s.png 结尾且尺寸不变〔舞台背景 V2〕" % [bt, bt], + tex != null and String(tex.resource_path).ends_with("_%s.png" % bt) + and tex.get_width() == 1280 and tex.get_height() == 252) + var world_idx: int = stage.world.get_parent().get_parent().get_index() + Kit.check("背景层序在世界之下(bg index < world 容器 index)〔舞台背景 V2〕", + bg.get_index() < world_idx) + # V6 源码锚:两处接板重建点(main 既有链+调试就绪链)均同步舞台板型 + for src_path: String in ["res://presentation/main.gd", "res://presentation/main_debug_flow.gd"]: + var sf := FileAccess.open(src_path, FileAccess.READ) + var ssrc := sf.get_as_text() if sf != null else "" + Kit.check("接板同步源码锚 %s 含 stage.set_board_type(〔舞台背景 V6〕" % src_path.get_file(), + ssrc.contains("stage.set_board_type(")) + stage.queue_free() + await process_frame + + +# ---- V3 缺图回退(目标缺→dungeon;全缺→平色兜底不崩不刷产品日志) ---- +func _test_background_fallback() -> void: + var s1: Node = MissingUnderdarkStage.new(null, load("res://data/tables/battle_layout.tres")) + root.add_child(s1) + await process_frame + s1.set_board_type("underdark") + var bg1: TextureRect = s1.bg_texture + Kit.check("目标缺图 → 回落 dungeon 路径〔舞台背景 V3〕", + bg1.texture != null and String(bg1.texture.resource_path).ends_with("_dungeon.png")) + s1.queue_free() + await process_frame + # 全 null 子类——任何板型都缺图 → 平色兜底(texture null、visible false、无异常) + var s2: Node = AllMissingStage.new(null, load("res://data/tables/battle_layout.tres")) + root.add_child(s2) + await process_frame + s2.set_board_type("dungeon") + var bg2: TextureRect = s2.bg_texture + Kit.check("全缺图 → texture null 且 visible false(平色兜底无异常)〔舞台背景 V3〕", + bg2.texture == null and not bg2.visible) + s2.queue_free() + await process_frame + + +# ---- V4 连续地面层几何(底贴 B 区底、高读表、全宽、位于背景与世界之间) ---- +func _test_ground_layer_geometry() -> void: + var layout = load("res://data/tables/battle_layout.tres") + var stage: Node = _make_stage() + await process_frame + var ground: TextureRect = stage.ground_layer + Kit.check("地面纹理可加载且 (1280, ground_h)〔舞台背景 V4〕", + ground.texture != null and ground.texture.get_width() == 1280 + and ground.texture.get_height() == int(layout.ground_h)) + Kit.check("地面锚点 0/1/1/1 且 offset 四位归零顶=-ground_h〔舞台背景 V4〕", + ground.anchor_left == 0.0 and ground.anchor_top == 1.0 + and ground.anchor_right == 1.0 and ground.anchor_bottom == 1.0 + and ground.offset_left == 0.0 and ground.offset_right == 0.0 + and ground.offset_bottom == 0.0 and int(ground.offset_top) == -int(layout.ground_h)) + stage.size = Vector2(1280, int(layout.stage_h)) + await process_frame + Kit.check("1280 档地面实读 size=(1280, ground_h)、position.y=stage_h-ground_h〔舞台背景 V4〕", + ground.size == Vector2(1280, float(layout.ground_h)) + and absf(ground.position.y - (float(layout.stage_h) - float(layout.ground_h))) < 0.51) + var world_idx: int = stage.world.get_parent().get_parent().get_index() + Kit.check("图层序 bg < ground < world〔舞台背景 V4〕", + ground.get_index() > stage.bg_texture.get_index() and ground.get_index() < world_idx) + stage.queue_free() + await process_frame + + +# ---- V5 光柱层与舞台底源常量退役(源码锚 + 常量表 + 菜单斜光带在位) ---- +func _test_beams_retired() -> void: + var f := FileAccess.open("res://presentation/views/battle_stage_view.gd", FileAccess.READ) + var src := f.get_as_text() if f != null else "" + Kit.check("舞台源码零光柱残留且平色回退含合并目标 SLOT_BACKDROP〔舞台背景 V5〕", + not src.is_empty() + and not src.contains("_build_beams_layer") and not src.contains("_draw_light_beams") + and not src.contains("BEAM_LIGHT") and src.contains("UiKit.SLOT_BACKDROP")) + var uikit_s: Script = load("res://presentation/views/ui_kit.gd") + var cmap: Dictionary = uikit_s.get_script_constant_map() + Kit.check("UiKit 常量表退役 BEAM_LIGHT/STAGE_BG 零键〔舞台背景 V5〕", + not cmap.has("BEAM_LIGHT") and not cmap.has("STAGE_BG")) + var uf := FileAccess.open("res://presentation/views/ui_kit.gd", FileAccess.READ) + var usrc := uf.get_as_text() if uf != null else "" + Kit.check("菜单斜光带 _beams_rect 仍在(不在退役面)〔舞台背景 V5〕", + usrc.contains("_beams_rect")) diff --git a/game/tests/unit/presentation/stage_background_test.gd.uid b/game/tests/unit/presentation/stage_background_test.gd.uid new file mode 100644 index 00000000..18ae1532 --- /dev/null +++ b/game/tests/unit/presentation/stage_background_test.gd.uid @@ -0,0 +1 @@ +uid://dskho0aqpongv diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index eddc7162..03d3812a 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -665,7 +665,7 @@ func _smoke_texture_filter() -> void: func _smoke_contrast_inks() -> void: print("--- UI 残余 <4.5:1 文字位换墨(V4 对比度锚定)---") var uikit_s = load("res://presentation/views/ui_kit.gd") - var stage: Color = uikit_s.STAGE_BG + var stage: Color = uikit_s.SLOT_BACKDROP # 舞台底对比锚改读合并目标(art-bible §4.1 换色合并③——阈值与判据不变) var r_order := _theme_contrast(uikit_s.GOLD_BRIGHT, stage) Kit.check("V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" % r_order, r_order >= 4.5) var r_ward := _theme_contrast(uikit_s.HERO_BLUE_BRIGHT, stage) diff --git a/tools/layout_config.py b/tools/layout_config.py index 28436e0c..79da2757 100644 --- a/tools/layout_config.py +++ b/tools/layout_config.py @@ -9,7 +9,7 @@ import re ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TABLE_PATH = os.path.join(ROOT, "game", "data", "tables", "battle_layout.tres") -FIELDS = ("stage_h", "info_panel_w", "ap_bar_h", "log_band_h_dig", "log_band_h_settle") +FIELDS = ("stage_h", "ground_h", "info_panel_w", "ap_bar_h", "log_band_h_dig", "log_band_h_settle") def load(path=TABLE_PATH):