diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index a27469fb..585eb080 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -688,4 +688,4 @@ func _target_for(rule: String) -> Variant: func _ineffective_row(t: Dictionary, reason: String) -> void: emit_row_cb.call({"type": "reward_ineffective", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), - "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", ""))}) + "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", "")), "deferred": true}) diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 52def7f8..391f2bd0 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -41,8 +41,8 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int "independent": _instances.append(_new_instance(effect_id, 1, dur, source, snap)) return true - "refresh": - return _apply_refresh(effect_id, dur, snap, source) + "accumulate": + return _apply_accumulate(effect_id, dur, snap, source) "stack": return _apply_stack(effect_id, dur, snap, source, max_stack) push_warning("[增益状态] 未知 stack_policy") @@ -57,11 +57,11 @@ func _grant_snapshot(row: Dictionary, source: Dictionary) -> Dictionary: return {"m": snap_m, "unit": String(row.unit), "display": String(row.display_name)} -## refresh 臂:命中改 R 未命中追加(R ← D 取实例快照 D——同表值;L 不变;空刷新恒 true) -func _apply_refresh(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool: +## 累加臂:命中加时长未命中追加(R += D 取本次授予生效时长——覆盖值 ≥1 按覆盖值;L 不变;快照沿首授;每次授予必有剩余次数变化) +func _apply_accumulate(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool: for inst in _instances: if inst.effect_id == effect_id: - inst.remaining = dur + inst.remaining = int(inst.remaining) + dur return true _instances.append(_new_instance(effect_id, 1, dur, source, snap)) return true 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/schemas/status_effect_table.gd b/game/data/schemas/status_effect_table.gd index 4e1568cb..0c15dffe 100644 --- a/game/data/schemas/status_effect_table.gd +++ b/game/data/schemas/status_effect_table.gd @@ -8,7 +8,7 @@ extends Resource const TARGET_SIDES := ["hero", "enemy"] const UNITS := ["percent", "flat", "ac"] # ac——AC 阈值轴效果(ac_up),非池百分比非定点值 -const STACK_POLICIES := ["stack", "refresh", "independent"] +const STACK_POLICIES := ["stack", "accumulate", "independent"] func validate() -> Array: 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/data/tables/status_effects.tres b/game/data/tables/status_effects.tres index 91f46885..7833dfdd 100644 --- a/game/data/tables/status_effects.tres +++ b/game/data/tables/status_effects.tres @@ -11,7 +11,7 @@ rows = [{ "magnitude": 50, "unit": "percent", "duration_settlements": 1, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 2 }, { "effect_id": "ac_up", @@ -20,7 +20,7 @@ rows = [{ "magnitude": 1, "unit": "ac", "duration_settlements": 1, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 2 }, { "effect_id": "weaken", @@ -29,7 +29,7 @@ rows = [{ "magnitude": -65, "unit": "percent", "duration_settlements": 2, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 16 }, { "effect_id": "enrage", @@ -65,7 +65,7 @@ rows = [{ "magnitude": 100, "unit": "percent", "duration_settlements": 2, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 1 }, { "effect_id": "fortify", @@ -74,7 +74,7 @@ rows = [{ "magnitude": -50, "unit": "percent", "duration_settlements": 2, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 1 }, { "effect_id": "defensive_stance", @@ -83,7 +83,7 @@ rows = [{ "magnitude": -25, "unit": "percent", "duration_settlements": 2, -"stack_policy": "refresh", +"stack_policy": "accumulate", "max_stack": 1 }, { "effect_id": "alert", 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/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 6cfe6681..7e7fb434 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -173,7 +173,8 @@ func _build_status_bar(box: VBoxContainer) -> void: strip.add_child(abandon_btn) -## 头像域:定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) +## 头像域:定高 48px 框、宽随源图比自适应(源 = 原稿分辨率;该面为独立消费面—— +## 显示档批不改框尺寸,画面随素材分辨率自动变化) func _build_face_rect() -> TextureRect: var face := TextureRect.new() var face_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index f8c2e6e1..07ac8753 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -281,7 +281,11 @@ func _portrait_glyph() -> String: # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - return {"has_tex": mo._portrait_sprite != null and mo._portrait_sprite.texture != null} + var sp: Sprite2D = mo._portrait_sprite + if sp == null or sp.texture == null: + return {"has_tex": false, "display_size": Vector2.ZERO, "display_scale": 0.0} + var src := Vector2(sp.texture.get_width(), sp.texture.get_height()) + return {"has_tex": true, "display_size": src * sp.scale, "display_scale": sp.scale.x} # ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ---- diff --git a/game/presentation/views/standee_motion.gd b/game/presentation/views/standee_motion.gd index e162ac4e..84998245 100644 --- a/game/presentation/views/standee_motion.gd +++ b/game/presentation/views/standee_motion.gd @@ -12,6 +12,15 @@ const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": var _owner: Node2D = null var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) +# 站桩立绘显示档(三档绝对高——art-bible §8.2 显示档定值单点;九键逐键钉值,禁兜底字面量): +# 小型 96 = 地精/巨蜘蛛/灰矮人;中型 144 = 主角/钩爪恐魔/熊地精;大型 188 = 酋长/卓尔/石君王。 +# 显示倍率 = 档高 / 源高(宽随源比例);源高 = 原稿表值(Python 侧 STANDEE_SRC_H—— +# 跨语言逐键对账由 standee_present_values_test V17 承担)。 +const PORTRAIT_DISPLAY_H := { + "goblin": 96.0, "giant_spider": 96.0, "duergar": 96.0, + "hook_horror": 144.0, "bugbear": 144.0, "hero": 144.0, + "ogre_chief": 188.0, "drider": 188.0, "duergar_king": 188.0, +} # 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 # (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 var _hero_tex := {} @@ -35,8 +44,9 @@ func _init(owner: Node2D) -> void: # 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 -# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2× -# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。(box = 宿主冻结 var,由宿主传入) +# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。显示高 = PORTRAIT_DISPLAY_H +# 逐键绝对高(art-bible §8.2 三档),宽随源比例(高定宽随)。 +# (box = 宿主冻结 var,由宿主传入) func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void: if tex == null: if _portrait_sprite != null: @@ -56,14 +66,25 @@ func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void: func _place_portrait(box: Vector2) -> void: - # 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中 + # 立绘盒 = (0,44)-(box.x, box.y-36);显示档逐键取 PORTRAIT_DISPLAY_H 绝对高, + # 宽随源比例(高定宽随);高于盒者沿既有居中溢出机制((box_h-h)*0.5 为负即上溢) if _portrait_sprite == null or _portrait_tex == null: return + var eid: String = _owner.enemy_id() + var key := "hero" if eid.is_empty() else eid 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) + var disp_h: float = PORTRAIT_DISPLAY_H.get(key, -1.0) + if disp_h < 0.0: + # 缺键不崩、不静默兜档——push_error 报警后按 1× 落位(V18 键集自检在测试期报警) + push_error("[站桩] 显示档表缺键: %s(逐键登记纪律)" % key) + _portrait_sprite.scale = Vector2.ONE + _portrait_sprite.position = Vector2((box.x - _portrait_tex.get_width()) * 0.5, + 44.0 + (box_h - _portrait_tex.get_height()) * 0.5) + return + var s := disp_h / float(_portrait_tex.get_height()) + _portrait_sprite.scale = Vector2(s, s) + _portrait_sprite.position = Vector2((box.x - _portrait_tex.get_width() * s) * 0.5, + 44.0 + (box_h - disp_h) * 0.5) # 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理) 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/probe/ui_probe.gd b/game/probe/ui_probe.gd index 65d5b429..640d3a81 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -147,11 +147,11 @@ func _ready() -> void: problems.append("普攻应零表示(意图指示应隐藏)") # 站桩纹理非空断言(八敌真图落位;缺位回退链保持的对照面) if not bool(standee.portrait_probe().get("has_tex", false)): - problems.append("敌立绘纹理未落位(素材管线 48px 档——迭代 6 T2;缺位回退链不应在此态触发)") + problems.append("敌立绘纹理未落位(原稿分辨率入包——显示档批;缺位回退链不应在此态触发)") if main.ui.battle.stage.hero_standee == null or not main.ui.battle.stage.hero_standee.has_world_hp_bar: problems.append("B 区主角站桩缺失(世界血条面缺失)") elif not bool(main.ui.battle.stage.hero_standee.portrait_probe().get("has_tex", false)): - problems.append("主角立绘纹理未落位(48px 档——迭代 6 T2/T3)") + problems.append("主角立绘纹理未落位(显示档 hero=144——显示档批)") # ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)---- var slots_row = main.ui.battle.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源) var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1, 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/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index f3b343a5..b42bde9b 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -373,7 +373,7 @@ func _test_pool_clamp(db) -> void: var expected1: int = clampi(int(pool_eff.magnitude), lo, hi) Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1) var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src) - Kit.check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变", + Kit.check("累加政策重复授予受理且层数恒 1(同名重复授予加时长)——池值不变", second_ok and h.ctrl.clamped_pool("hero_offense") == expected1) # ac 型效果不入 percent 池 var ac_eff2 := {} diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index ac4365af..df8438c4 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -154,13 +154,18 @@ func _smoke_armor_roll() -> void: int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false))) Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)], int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk)) - # ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置 - # (削弱为续时语义,无叠层通道) - var weaken_probe := _weaken_refresh_probe() - Kit.check("削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)", - int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2) + # ③ 削弱累加(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数累加本次授予时长 + # (削弱为累加语义,无叠层通道) + var weaken_probe := _weaken_accumulate_probe() + Kit.check("削弱重复授予 → 层数保持 1、剩余结算数累加至 3(1+2——快照沿首授)〔状态累加 V1〕", + int(weaken_probe.get("after_regrant", -1)) == 3 and int(weaken_probe.get("layers", -1)) == 1 \ + and int(weaken_probe.get("snap_magnitude", -9999)) == -65) Kit.check("削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)", int(weaken_probe.get("pool", -999)) == int(_db.combat.status_effects.row_by_id("weaken").magnitude)) + Kit.check("削弱覆盖时长重授 → 按覆盖值累加至 6(3+3)〔状态累加 V3〕", + int(weaken_probe.get("after_override", -1)) == 6) + Kit.check("削弱同轮再授 → 再累加一段至 8(6+2)〔状态累加 V2〕", + int(weaken_probe.get("after_extra", -1)) == 8) # ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定 # (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值) var tiers := _ac11_tier_samples() @@ -215,10 +220,11 @@ func _first_hero_attack_rows(seed_val: int) -> Dictionary: return {} -# 削弱续时探测: -# 授予 → 衰减一轮 → 再授予,读 weaken 实例投影 {layers, remaining, pool}。 +# 削弱累加探测(五段式): +# 授予(2) → 衰减一轮(1) → 重授(1+2=3) → 覆盖值重授(3+3=6) → 同轮再授(6+2=8), +# 读 weaken 实例投影 {layers, pool, after_regrant, after_override, after_extra, snap_magnitude}。 # 直授通道(来源格真实坐标惯例——沿前例);depth 2 板(D2 熊地精)仅作宿主,行为与敌种无关。 -func _weaken_refresh_probe() -> Dictionary: +func _weaken_accumulate_probe() -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260916) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) @@ -227,11 +233,24 @@ func _weaken_refresh_probe() -> Dictionary: var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index} settlement._status_effects.apply_effect("weaken", src) settlement._status_effects.decay_round() # remaining 2 → 1(先兑付后衰减——只借衰减器,不结算) - settlement._status_effects.apply_effect("weaken", src) # 续时:remaining ← 2,层数不动 + settlement._status_effects.apply_effect("weaken", src) # 累加:remaining 1+2=3,层数不动 + var regrant := _weaken_inst(settlement) + settlement._status_effects.apply_effect("weaken", src, 3) # 覆盖时长按覆盖值累加:3+3=6 + var override_grant := _weaken_inst(settlement) + settlement._status_effects.apply_effect("weaken", src) # 同轮再授:6+2=8 + var extra := _weaken_inst(settlement) + if regrant.is_empty() or override_grant.is_empty() or extra.is_empty(): + return {} + # 走伤害控制器公有算术面 clamped_pool + return {"layers": int(extra.layers), "pool": int(settlement._damage.clamped_pool("enemy_output")), + "after_regrant": int(regrant.remaining_settlements), "after_override": int(override_grant.remaining_settlements), + "after_extra": int(extra.remaining_settlements), "snap_magnitude": int(regrant.snap_magnitude)} + + +func _weaken_inst(settlement: Object) -> Dictionary: for eff in settlement._status_effects.query_active("enemy"): if String(eff.effect_id) == "weaken": - # 走伤害控制器公有算术面 clamped_pool - return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._damage.clamped_pool("enemy_output"))} + return eff return {} @@ -339,7 +358,7 @@ func _smoke_pool_wiring() -> void: # 构造停手轮首行主角攻击行:opts = {effects: Array};hero_atk 覆盖 100 拉开量级 -# (floor 关系可精确断言);效果直授沿 _weaken_refresh_probe 前例。 +# (floor 关系可精确断言);效果直授沿 _weaken_accumulate_probe 前例。 func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 677a9f25..2542adac 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -464,7 +464,8 @@ func _row_phase(row: Dictionary, ri: int) -> int: "damage": return 2 # 案C:全部伤害兑付=战斗段(主角先手/敌方还手/地图账殿后统一结账) "reward_ineffective": - return 2 if String(row.get("forfeit", "")) == "hero_down" else 0 # 战斗段末地图账不兑留痕 vs 授予段未生效 + # 战斗段末地图账行(hero_down 兜底留痕 / deferred 直伤无目标——行载荷自描述键,迭代 48 卡①裁决补记) + return 2 if String(row.get("forfeit", "")) == "hero_down" or bool(row.get("deferred", false)) else 0 # 战斗段末地图账不兑留痕 vs 授予段未生效 match String(row.get("source_class", "")): "safe_cell", "mine_hit", "seal_hit", "seal_miss", "clear_reward": if int(row.get("round_index", ri)) < ri: diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index 7a06fa7a..ff0d7f8b 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -269,10 +269,10 @@ func _smoke_ac_swap_guard() -> void: # ① 三处无双轨 Kit.check("状态表 defense_up 行已退役(无双轨)", _db.combat.status_effects.row_by_id("defense_up").is_empty()) var ac_row: Dictionary = _db.combat.status_effects.row_by_id("ac_up") - Kit.check("ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)", + Kit.check("ac_up 行在位(hero · +1 ac · 累加语义——同名重复授予加时长不叠层)〔状态累加 V4〕", not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \ and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \ - and String(ac_row.get("stack_policy", "")) == "refresh") + and String(ac_row.get("stack_policy", "")) == "accumulate") var ac_reward := {} for row in _db.run.reward.rows: if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive": 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/standee_present_values_test.gd b/game/tests/unit/presentation/standee_present_values_test.gd index 80a9a1c2..22260714 100644 --- a/game/tests/unit/presentation/standee_present_values_test.gd +++ b/game/tests/unit/presentation/standee_present_values_test.gd @@ -11,6 +11,9 @@ # V10 名字行提档 FS_HINT 14(探针 name_font_size 实读 + 两绘制点源码锚 + FS_CAPTION 计数锚==1); # V11 退役常量不在册 + 掩尘 BRICK 逐位 + 顶缘线/口沿高光两绘制点改指源码锚; # V12 PLAZA_EDGE 逐位 #4C4038 + 回退描边改指源码锚。 +# V13–V18 立绘显示档(2026-11-01 迭代 48 卡②)——显示档 = 三档绝对高逐键钉值(96/144/188)+ +# 入包件 = 原稿紧裁零降采样(源高 == 原稿表值)+纵横倍率恒等+主角三态显示 144+ +# 跨语言显示档表逐键对账+键集恰九键(§8.2 显示档定值单点)。 # headless 无渲染面:像素采样以常量单点 + 源码锚 + 探针几何承载(观测面注记,验收对照表 V5 行)。 # 运行:godot --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd(expect 0) extends SceneTree @@ -110,4 +113,94 @@ func _init() -> void: else "护甲三枚入包(24×24)〔样张批 V9〕", tex9 != null and tex9.get_width() == 24 and tex9.get_height() == 24) + # ---- V13–V18 立绘显示档(迭代 48 卡②) ---- + _check_display_tier() + quit(Kit.finish("standee_present_values")) + + +# ---- V13–V18 立绘显示档(迭代 48 卡②——显示档 = 三档绝对高,入包件 = 原稿紧裁零降采样) ---- + +# 规格钉值(显示档定值单点 = art-bible §8.2;源高 = 原稿表值——旧降采样稿必红; +# 显示宽 = round(源宽×档高/源高),食人魔酋长 157 为已接受超宽项): +# 键 → [源高, 显示档高, 显示宽] +const _DISPLAY_SPEC := { + "goblin": [90, 96, 95], "giant_spider": [81, 96, 121], "duergar": [126, 96, 71], + "hook_horror": [94, 144, 113], "bugbear": [123, 144, 119], "hero": [92, 144, 138], + "ogre_chief": [189, 188, 157], "drider": [189, 188, 139], "duergar_king": [189, 188, 132], +} + + +func _enemy_tex_path(key: String) -> String: + return "res://assets/art/character/character_enemy_%s.png" % key + + +func _check_display_tier() -> void: + # V13/V15 九键逐一:真实入口 StandeeBody → set_data(enemy_id) → set_portrait_texture(真纹理) → portrait_probe + for key: String in _DISPLAY_SPEC: + var spec: Array = _DISPLAY_SPEC[key] + var body: Node2D = StandeeBodyScript.new("hero" if key == "hero" else "enemy") + root.add_child(body) + var tex: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT if key == "hero" else _enemy_tex_path(key)) + if key == "hero": + body.set_portrait_texture(tex, true) + else: + body.set_data({"display_name": key, "enemy_id": key, "hp": 3, "max_hp": 3, + "order": 1, "armor_type": "light"}) + body.set_portrait_texture(tex, false) + var pp: Dictionary = body.portrait_probe() + Kit.check("显示档 %s:源高 %d(原稿表值——旧降采样稿必红)+显示高 %d 逐键钉值〔立绘显示档 V13〕" + % [key, int(spec[0]), int(spec[1])], + tex != null and int(tex.get_height()) == int(spec[0]) + and is_equal_approx(float(pp.display_size.y), float(spec[1])) + and is_equal_approx(float(pp.display_scale) * float(tex.get_height()), float(spec[1]))) + Kit.check("显示档 %s:纵横倍率恒等(双轴同倍率)+显示宽随源比例(round 档 %d)〔立绘显示档 V15〕" + % [key, int(spec[2])], + is_equal_approx(float(pp.display_size.x), float(tex.get_width()) * float(pp.display_scale)) + and is_equal_approx(float(pp.display_size.y), float(tex.get_height()) * float(pp.display_scale)) + and absf(float(pp.display_size.x) - float(spec[2])) < 0.51) + body.queue_free() + # V14 静态源锚:显示档表读表单点 + 零硬编码 2× 倍率 + var motion_src := _read_source("res://presentation/views/standee_motion.gd") + Kit.check("显示档表单点在位(_place_portrait 读 PORTRAIT_DISPLAY_H)且零 Vector2(2.0, 2.0) 字面倍率〔立绘显示档 V14〕", + motion_src.contains("PORTRAIT_DISPLAY_H") and not motion_src.contains("Vector2(2.0, 2.0)")) + # V16 主角三态:组合根装载(hero_tex 注册链)→ 逐态 apply_hero_state 复读探针 + var stage_view: Node = load("res://presentation/views/battle_stage_view.gd").new(null, load("res://data/tables/battle_layout.tres")) + root.add_child(stage_view) + var hs: Node2D = stage_view.hero_standee + var pp0: Dictionary = hs.portrait_probe() + Kit.check("主角 normal 态:源高 ≥92 且显示高 144(组合根真实装载链)〔立绘显示档 V16〕", + bool(pp0.has_tex) and is_equal_approx(float(pp0.display_size.y), 144.0)) + for hstate: String in ["attack", "hit"]: + hs.set_hero_state(hstate) + var pph: Dictionary = hs.portrait_probe() + Kit.check("主角 %s 态:源高 ≥92 且显示高 144(三态同档)〔立绘显示档 V16〕" % hstate, + bool(pph.has_tex) and is_equal_approx(float(pph.display_size.y), 144.0)) + stage_view.queue_free() + # V17 跨语言对账:Python 显示档表(STANDEE_DISPLAY_H 八敌 + STANDEE_SLOT_H 主角)逐键 == GDScript 表 + # (读不到工具源 = L2 记录退回双端字面钉值+独立审计对账——禁当红亦禁当绿) + var py_src := "" + for py_path: String in ["../tools/pixelize_candidates.py", "tools/pixelize_candidates.py"]: + var pf := FileAccess.open(py_path, FileAccess.READ) + if pf != null: + py_src = pf.get_as_text() + break + if py_src.is_empty(): + print("[L2] V17 工具源不可读(../tools/ 路径不通)——退回双端字面钉值+独立审计对账(记录不判红)") + else: + var py_ok := true + for key: String in _DISPLAY_SPEC: + if key == "hero": + py_ok = py_ok and py_src.contains("STANDEE_SLOT_H = %d" % int(_DISPLAY_SPEC[key][1])) + else: + py_ok = py_ok and py_src.contains("\"%s\": %d" % [key, int(_DISPLAY_SPEC[key][1])]) + Kit.check("Python 显示档表与 GDScript PORTRAIT_DISPLAY_H 九键逐键相等(源文本对账)〔立绘显示档 V17〕", py_ok) + # V18 键集恰九键(八敌 ∪ hero,无多余键——防新敌种漏登记被 push_error 静默) + var motion_script: Script = load("res://presentation/views/standee_motion.gd") + var mcmap: Dictionary = motion_script.get_script_constant_map() + var disp_tbl: Dictionary = mcmap.get("PORTRAIT_DISPLAY_H", {}) + var keys_ok: bool = disp_tbl.size() == 9 + for k: String in ["bugbear", "drider", "duergar", "duergar_king", "giant_spider", + "goblin", "hook_horror", "ogre_chief", "hero"]: + keys_ok = keys_ok and disp_tbl.has(k) + Kit.check("显示档表键集 = 八敌 ∪ hero 恰九键(无多余键)〔立绘显示档 V18〕", keys_ok) 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/reports/iter48-代拍板追认单-2026-11-02.md b/reports/iter48-代拍板追认单-2026-11-02.md new file mode 100644 index 00000000..228706c1 --- /dev/null +++ b/reports/iter48-代拍板追认单-2026-11-02.md @@ -0,0 +1,33 @@ +# 迭代 48 代拍板追认单(2026-11-02) + +> 全部代拍板均落在各卡白名单内,无超白名单事项。本单按卡逐条备案追认(随 commit 留痕)。 + +## 卡①状态合并累加制(白名单 1–3 全用,零超出) + +| 白名单项 | 落地 | 追认 | +|---|---|---| +| 1 词表值命名 `accumulate` | schema `STACK_POLICIES := ["stack", "accumulate", "independent"]` | 追认 | +| 2 合并授予不改快照(首授快照恒定) | `_apply_accumulate` 零快照写入;V1 断言 snap_magnitude==−65 背书 | 追认 | +| 3 测试函数改名 `_weaken_accumulate_probe` 与 V 尾缀措辞 | 全仓旧名零残留;〔状态累加 V1/V2/V3/V4〕在案 | 追认 | +| 裁决补记随动(2026-11-02 用户甲案——非代拍板,用户裁决授权) | `_ineffective_row` 载荷 `deferred: true`+`_row_phase` 分类补 deferred→战斗段(卡①申报 5+R7 行) | 随用户裁决生效 | + +## 卡②立绘显示档(白名单 1–5 全用,零超出) + +| 白名单项 | 落地 | 追认 | +|---|---|---| +| 1 `portrait_probe()` 新读数键名 | `display_size`/`display_scale`(卡面拟定名照用) | 追认 | +| 2 `pixelize_candidates.py` 函数与 CLI 旗标改名 | `build_standee_display`/`--standee-display` | 追认 | +| 3 测试尾缀〔立绘显示档 Vn〕括注格式 | V13–V18 六尾缀 | 追认 | +| 4 对照图目录名 | `artwork/candidates/立绘显示档批-2026-11-02/` | 追认 | +| 5 主角取 key 实现写法 | `enemy_id()` 空值判 hero | 追认 | + +## 卡③战斗区背景替换(白名单 1–6 全用,零超出) + +| 白名单项 | 落地 | 追认 | +|---|---|---| +| 1 私有函数顺序/局部变量名 | `_build_ground`/`_load_stage_texture`/`set_board_type` 布局与命名 | 追认 | +| 2 测试 helper 顺序与断言消息措辞 | stage_background_test 18 checks 消息(判据值/阈值零改) | 追认 | +| 3 注释与日志文案 | 舞台顶注/背景域注释换代(零「光柱仍在」类误导、零变更史) | 追认 | +| 4 TextureRect expand/stretch 写法 | `EXPAND_IGNORE_SIZE`+`STRETCH_SCALE`(基准档 1×) | 追认 | +| 5 B 区 1px 面板描边可见性处理 | 平色 Panel 描边保留被背景覆盖(未另加描边层、零新增类变量) | 追认 | +| 6 既有错误扫描暴露的纯格式问题一次修净 | 无新增格式修(gdstyle 零新键) | 追认(空项) | diff --git a/reports/iter48-停机呈报-卡1状态相位断言-2026-11-02.md b/reports/iter48-停机呈报-卡1状态相位断言-2026-11-02.md new file mode 100644 index 00000000..be910a7a --- /dev/null +++ b/reports/iter48-停机呈报-卡1状态相位断言-2026-11-02.md @@ -0,0 +1,45 @@ +# 停机呈报:迭代 48 卡①(状态合并累加制批)——round_flow 相位单调断言红(L3②) + +> 日期:2026-11-02 · 性质:实现段停机呈报(缺陷移交,非提问)。呈报即卡①停摆,改动全部留在迭代工作区 `g-plan2-iter48` **未提交**,卡②卡③已各自提交收口。 +> **裁决闭合(2026-11-02 用户甲案)**:按生产行载荷自描述键方案补记卡①(申报 5+R7 行+写面两行+T1 随动步)后本区继续施工——`_ineffective_row` 加 `deferred: true`+`_row_phase` 分类随动,七套件复跑全绿,卡①收口提交 df173018。第六节待办清单状态回退拟稿作废不执行。本呈报留作裁决与根因记录。 + +## 一、停机点(哪条判据触发) + +卡①第十节 T1 末项:「孤立验证:六套件最窄复跑(…round_flow_skill_pool——EV 锚套件提前曝红)全绿或按 L3②停机」。round_flow_skill_pool_test 套件红(133 PASS / 1 FAIL,exit=1,两次复跑确定性复现同断言同行)→ 按卡①第十二节 L3② 停机呈报。 + +## 二、红项全貌 + +- 断言:`game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd:422`「每轮行相位单调(授予→增益/减益→战斗→衰减→判定)」,宿主函数 `_smoke_settlement_segments`。 +- 违序行(种子 42,L1·D0 自动局,round 2): + `{"batch_key":"magic_bolt","type":"reward_ineffective","source_class":"safe_cell","display_name":"魔法矢 未生效(无存活敌人)","value":0,"round_index":2}`——出现在同轮战斗段伤害行之后,被测试的相位分类器判为第 0 段(授予段),形成 0 段行后于 2 段行的读序。 +- 改前 CI(de5e31ed 回执 unit.log:3923)同断言 PASS——红确属本批新增,非存量红。 + +## 三、根因(两条事实链) + +1. **生产侧行为符合设计**:该行由 `damage_controller.gd:543/:568 → _ineffective_row(:686)` 在「案C 地图账殿后」补提交结算时发射(魔法矢找不到存活目标)——行本身落在战斗段末是案C 既有口径;累加制使削弱实例存续更长 → 战斗轨迹改变(该局敌人 round 2 内提前全灭)→ 这类「战斗段内未生效」行首次在该种子轨迹出现。 +2. **测试侧分类器存在潜伏缺口**:`_row_phase`(round_flow_skill_pool_test.gd:466-467)把无 `hero_down` forfeit 的 `reward_ineffective` 一律判第 0 段(授予段未生效);该假设在旧轨迹下从未被打破,累加制轨迹首次暴露。 + +## 四、分歧点与两种读法 + +- 读法 A(测试缺陷 exposed):断言冻结的五段全序不变量本身仍被生产行为满足(行序实际是 授予→…→战斗→地图账→衰减→判定),红的直接原因是分类器对「战斗段地图账未生效行」这一已存在行型缺映射。修复=把该行型正确归类到战斗段(相位 2)——属**断言改判**,须卡片申报(卡①测试削弱申报四条未含此项,写面亦不含 round_flow_skill_pool_test.gd)。 +- 读法 B(生产缺陷):「奖励直伤找不到目标」不应以第 0 段形态出现在战斗段——若设计意图是该行必须留在授予段,则生产发射时点错位。查证:案C 口径(combat_segment.gd:107-109 注释「主角死于怪物 → 地图账不兑,逐单留『未生效』行不作废静默」)明确未生效行属战斗段末地图账——读法 B 不成立,但归类标记(现仅 `hero_down` 一种 forfeit)是否扩词表属设计面裁量。 + +## 五、建议案(供裁决,二选一或另裁) + +- **甲案(推荐)**:卡①补申报一条测试随动——`_row_phase` 对「战斗段地图账未生效行」补归类。实现取其一:① 最小测试面:`reward_ineffective` 行按「同轮已出现战斗段行」状态化归类(分类器带一比特状态,不变量强度不降);② 生产观测面扩展:`_ineffective_row`(damage_controller.gd:686-690)载荷加自描述键(如 `"deferred": true`,零行为改动、行载荷观测面扩展),分类器读键归类——行自描述优于启发式,推荐②。两法都须写卡段补记进卡①对照表与削弱申报后方可施工。 +- **乙案**:判为累加制的设计后果重裁(例如「敌人全灭后地图直伤奖励该不该仍结账」属玩法问题)——若如此,卡①整体退回设计段,与本批解耦。 + +EV 锚三值(goblin 0.9148/bugbear 1.0012/ogre_chief 1.1948)改前改后逐位一致、全绿——**无期望值漂移**,乙案如走重锚通道无数据依据。 + +## 六、待办清单对应条目状态回退+缺口补记(拟稿,落笔归上游段/用户) + +条目「状态合并改累加制——同名状态重复授予改加时长」(待办清单 :20-32): + +- 状态回退:「文档设计已闭合+已写卡+已编迭代 48」→「已写卡(实现停机待卡补记——round_flow 相位断言随动申报未决)」。 +- 缺口补记:需求卡片-状态合并累加制批-2026-11-01.md 需补第十一节申报一条(round_flow `_row_phase` 战斗段未生效行归类,甲案①或②取一)+第七节文件结构图补 round_flow_skill_pool_test.gd(甲①)或 damage_controller.gd 观测面一行(甲②);补记后重新编入迭代。 + +## 七、卡①已完成面(停机不抹账) + +- 实装六处全落:tres 六行 accumulate/schema 词表/容器累加臂(`_apply_accumulate`,R += D)/roll_multiplier 探针五段式+V1/V2/V3 断言/table_structure_guard V4/damage_controller 标签换代。grep 四项可核查全过(值行 refresh 零残留、accumulate 恰六处、容器与全仓定义面 refresh 零残留)。 +- 六套件窄测:五套件全绿(roll_multiplier 42 checks/table_structure_guard/system_contracts/damage_controller 44/seal_polarity 70),round_flow 133 PASS+1 FAIL(本呈报主体)。 +- 改前基线:S13 两产品文件零基线键;EV 锚改前读数 goblin 0.9148(CI unit.log:3968)。 diff --git a/reports/iter48-反哺点-2026-11-02.md b/reports/iter48-反哺点-2026-11-02.md new file mode 100644 index 00000000..68b9af94 --- /dev/null +++ b/reports/iter48-反哺点-2026-11-02.md @@ -0,0 +1,28 @@ +# 迭代 48 反哺点清单(2026-11-02) + +> 本批未写 `design/`、未写根流程清单、未写 `game/tests/README.md`。以下逐条待用户裁决后由文档批落文,落文后本清单逐条清除。 + +## 卡②立绘显示档 + +1. **08-Alpha版本标准.md 覆盖行**:立绘清晰度条目「待做」→「在役」列呈裁——九件已按档落位+机检全绿,实机目检四项登记在案(实现报告第四节 1–5)。 +2. **art-prompts.md §5.4 产出状态**:「待实装批落位」→「已落位」(依据:九件原稿分辨率入包、`STANDEE_SRC_H` 表在案、`--standee-display` 派生管线)。 +3. **art-status-checklist.md/art-assets-checklist.md 站桩行**:「按档派生落位归本条实装批」→ ✅ 已交付(依据同上;闸件 standee_asset_check 已换显示档口径全绿)。 +4. **待办清单.md 本条**:状态推进已编迭代 48 → 完成删条(状态机推进,非实现批直改)。 +5. **术语表「显示档」是否立条**:呈裁——卡②三处口径(art-bible §8.2 定值单点/GDScript `PORTRAIT_DISPLAY_H`/Python `STANDEE_DISPLAY_H`)已稳定使用「显示档」一词,是否进术语表归用户裁。 +6. **体验验证清单.md**:实机目检五项(实现报告第四节 1–5)转正式条目呈裁。 + +## 卡③战斗区背景替换 + +7. **art-bible.md §6.1**:三句实装化——「程序化平色+光柱现行使用=占位」改为背景三关变体+连续地面已实装;「ground_h 由实装批新增并回填」改为已回填 100;「资产到位后四柱直接退役」改为已退役(依据:`TEX_STAGE_BG_FMT`/`TEX_STAGE_GROUND`/`set_board_type` 落码,光柱层与 BEAM_LIGHT 删除,断言 V2–V5 绿)。 +8. **art-bible.md §4.1 换色合并登记③**:`STAGE_BG` 由「随舞台背景插画批处置」改为已退役、平色缺图回退改指 `SLOT_BACKDROP`(#3E362C)——代码事实(常量表零键、回退点改指,断言 V5 绿)。 +9. **art-assets-checklist.md §4**:三张背景与地面「已出候选 v10」状态句 → 候选 v12 过审、终稿入包已实装消费;小石台保留句核对无误(原样保留、叠于地面之上)。 +10. **battle-stage-prompts.md §4/§5**:「ground_h 待实装批回填」→ 已回填 100;档 6「四柱退役(代码批)」→ 已退役。 +11. **design/gdd/ux/battle.md :118、:155**:布局字段清单补 `ground_h`(B 区连续地面层高,源=基准屏宽×本值);「现行溢出区为临时光柱氛围层,正式背景到位后该层退役」→ 已退役,溢出判定不变。 +12. **HUD框架.md :60、:247**:B 区内容补「三关背景+连续地面层」;布局几何清单补地面层高(读布局表 ground_h)。 +13. **数据配置.md :73**:布局表用途句补「地面层高度」。 +14. **体验验证清单.md**:实机目检五项(实现报告第四节 6–10)转正式条目呈裁。 + +## 卡①状态合并累加制(甲案裁决已闭合——随本批合回生效) + +15. **增益状态.md 字段表 stack_policy 取值行**:「stack / refresh / independent」→「stack / accumulate / independent」(依据:schema 词表与表行已换代——该落文随卡①合回生效,卡未闭合前禁落)。 +16. **增益状态.md「现在有哪些状态」节**:「现行九行」→ 十行+补 defensive_stance(护身)行(敌方输出减益 −25 percent、duration 2、累加、max_stack 1、来源敌方技能「孤注守势」)——本批考证发现的存量漂移,非本批造成。 diff --git a/reports/iter48-实现报告-2026-11-02.md b/reports/iter48-实现报告-2026-11-02.md new file mode 100644 index 00000000..42b0deaa --- /dev/null +++ b/reports/iter48-实现报告-2026-11-02.md @@ -0,0 +1,81 @@ +# 迭代 48 实现报告(2026-11-02) + +> 批性质:需求改动实现批(三卡:①状态合并累加制②立绘显示档③战斗区背景替换)。执行形态=整迭代单工作区 `g-plan2-iter48` 串行。开工基点 bfa0a8be→a5c89d67(批中并入 master 对齐绿基线)。卡① 曾按 L3② 停机呈报(`iter48-停机呈报-卡1状态相位断言-2026-11-02.md`),**2026-11-02 用户甲案裁决**=补记卡①申报(R7 行:`_ineffective_row` 载荷自描述键+`_row_phase` 分类随动)后本区继续施工——七套件复跑全绿后卡①收口。三卡全收口,合并走总收口流程。 + +## 一、各卡状态 + +| 卡 | 状态 | 提交 | 窄测证据 | +|---|---|---|---| +| ①状态合并累加制 | 收口(甲案裁决随动后) | df173018 | roll_multiplier 42 绿(V1/V2/V3)/table_structure_guard 绿(V4)/system_contracts 绿/damage_controller 44 绿/seal_polarity 70 绿/round_flow 全绿(相位单调 PASS——deferred 归类随动)/refresh_feats 绿;EV 三锚 0.9148/1.0012/1.1948 逐位不变 | +| ②立绘显示档 | 收口 | ec385772 | standee_present_values 50 checks 绿(V13–V18 全执行)+素材闸 11 件全 PASS+样张脚本 30/30 全绿(V10 并排图产出) | +| ③战斗区背景替换 | 收口 | 33e5b72d | stage_background 18 checks 绿(V1–V5)+六套件关联窄测全 PASS+layout_probe 两档 PASS+ui_contrast 36 测点绿+ground_h 工具读取=100 | + +## 二、实际改动—验收对照表(结果列) + +### 卡②(对照表 V1–V8) + +| 需求点 | 改动 | 验收断言(尾缀〔立绘显示档 Vn〕) | 结果 | +|---|---|---|---| +| 逐件显示档高 | standee_motion.gd `PORTRAIT_DISPLAY_H` 九键+`_place_portrait` 按档算倍率 | V13 九键逐一(源高==原稿表值+显示高≈档高双读数) | 绿(9 键 ×2 断言全 PASS) | +| 硬编码 ×2 退役 | 同上 | V14 源码锚(读表+零 `Vector2(2.0,2.0)`) | 绿 | +| 纵横倍率恒等 | 同上 | V15 双轴同倍率+显示宽 round 档 ±0.51 | 绿(9 键全 PASS) | +| 主角中档 144 三态 | 素材 2× 手修稿+register 链 | V16 组合根装载 normal/attack/hit 逐态复读 | 绿(3 断言) | +| 素材不降采样 | 9 件原稿紧裁+闸件新口径 | `standee_asset_check.py` 断言①源高==表值②显示宽≤150(酋长 157 例外逐字) | 绿(11 件 PASS) | +| 两表同值不漂移 | Python STANDEE_DISPLAY_H/STANDEE_SLOT_H ↔ GDScript 表 | V17 源文本逐键对账 | 绿(文件可读路径生效;L2 退路在件未触发) | +| 表键集完整 | 同上 | V18 恰九键无多余 | 绿 | +| 样张口径随动 | render_sample_sheet_v3.py 站桩段+A4 解冻重校 88×92 | 脚本 CHECKS 全绿+V10 改前改后并排 | 绿(30/30) | +| 实机观感 | 九件+两消费面 | 目检(登记不阻塞) | 见第四节 | + +### 卡③(对照表 V1–V8) + +| 需求点 | 改动 | 验收断言(尾缀〔舞台背景 Vn〕) | 结果 | +|---|---|---|---| +| V1 ground_h 字段与校验 | schema+tres 100+layout_config | `_test_layout_ground_field`(表值/三态拒绝族)+config_db_test | 绿 | +| V2 按 board_type 选图 | TEX_STAGE_BG_FMT+set_board_type+两处接板同步 | `_test_background_selection`+layout_probe L1 真实入口(路径 `_dungeon.png` 结尾) | 绿 | +| V3 缺图回退 | `_load_stage_texture`+回退链 | `_test_background_fallback`(子类缺图→dungeon;全缺→平色不崩) | 绿 | +| V4 地面层几何 | TEX_STAGE_GROUND+_build_ground | `_test_ground_layer_geometry`+layout_probe 两档 size.y==ground_h | 绿 | +| V5 光柱与舞台底退役 | 删层删双常量;回退改指 SLOT_BACKDROP | `_test_beams_retired`+theme_projection/ui_contrast 锚改读后全绿 | 绿 | +| V6 接板真实链路 | main.gd/main_debug_flow.gd 各一守卫行 | layout_probe(L1 进入后路径 dungeon)+源码锚断言(stage_background_test:两文件含 `stage.set_board_type(`) | 绿 | +| V7 既有呈现回归 | 零改站桩/明细/棋盘 | 六套件关联窄测+两探针 | 全 PASS | +| V8 能力行名实与登记 | run_all.gd 登记;README 写卡段行 | README:138 三用例名与套件函数逐字一致;套件 manifest 在册 exit 0 | 绿(capability_audit 归合回 CI) | + +卡①对照表 R1–R7:R1/R2/R3/R4 断言全部落地且绿(V1/V2/V3/V4 尾缀可 grep);R5 快照断言随 V1 绿;R6 既有断言零改动复跑绿;R7(裁决补记随动)=`_ineffective_row` 载荷 `deferred: true`+`_row_phase` deferred→战斗段,相位单调断言在 seed 42 真实轨迹(含该行型)当轮执行且 PASS,申报 1–4 与 R1–R6 断言零变化。 + +## 三、读数留痕 + +- **EV 锚改前改后对拍**:goblin 0.9148 / bugbear 1.0012 / ogre_chief 1.1948——改前(CI unit.log:3968 区)与改后(卡①两轮窄测)逐位一致,全绿,零漂移。 +- **S13**:卡①触面(status_effect_table/status_container/damage_controller)与卡③触面(battle_stage_view/battle_layout_table/ui_kit/main/main_debug_flow)改前均零基线键;批中 gdstyle 跑批「基线内 5、基线外新违规 0、较登记值恶化 0」。 +- **结构预算**:battle_stage_view 类变量 11→14(≤15);公开方法 12→13(≤20);validate 分支 7 不变(地面校验拆 `_ground_field_errors`)。 +- **素材九件**(改前→改后):bugbear 51x62→102x123/drider 67x90→140x189/duergar 34x46→93x126/duergar_king 66x94→133x189/giant_spider 48x38→102x81/goblin 36x36→89x90/hook_horror 46x58→74x94/ogre_chief 62x74→158x189/hero 46x48,48x48,46x48→88x92,92x92,88x92。F7 实证:改前管线产物与包上素材逐件同尺寸。 +- **舞台背景四件**:三关背景各 1280×252、地面 1280×100(T0 PIL 实读)。 +- **对照图**:`artwork/candidates/立绘显示档批-2026-11-02/改前改后三面对照.png`(站桩区/标题屏看台/地图头像三面);样张并排图 `artwork/mockups/sample_battle_screen_before_after.png`(改前基线由旧脚本+旧资产生成)。 + +## 四、实机目检总清单(登记不阻塞) + +1. 九件立绘清晰度可接受(含灰矮人 126→96 的 0.76× 抽行观感判读——头号回退候选)〔卡②〕 +2. 同屏体量顺序 小<中<大〔卡②〕 +3. 标题屏看台与关卡地图头像无可感劣化(地图头像 0.52× 降采样)〔卡②〕 +4. 食人魔酋长显示宽 157 不碰邻位/不压顶部轮次指示〔卡②〕 +5. 主角三态切换横跳读数(三态源宽 88/92/88,切换约 3–6px 横跳——重摆归另批)〔卡②〕 +6. 三关背景可辨为不同洞窟、远景有纵深、非贴墙〔卡③〕 +7. 背景无光柱/光带/光源实体(缺图态同)〔卡③〕 +8. 地面读作泥土/碎石非瓷砖、中低明度不刺眼〔卡③〕 +9. 站桩与小石台踩在连续地面上;结算期明细带压住地面底带〔卡③〕 +10. B 区文字(轮次牌/演出提示/明细带)对背景/地面可读〔卡③〕 + +## 五、实现决定记录(代拍板随 commit 留痕+备案) + +- 卡②:CLI 旗标 `--standee48`→`--standee-display`、函数 `build_standee48`→`build_standee_display`(代拍板 2);hero key=`enemy_id()` 空值判 hero(代拍板 5);V17=源文本逐键 contains 对账+L2 打印退路(读不到不判红);**V15 判据实现**=双轴同倍率比例恒等(is_equal_approx)+round 档 ±0.51 带——卡面字面 `display_size.x == round(...)` 在非整数比下物理不可达(如地精 94.93),取更强判据实现,缩放几何零改动;产物计数=八敌+主角立绘恰九(attack/hit 为伴随态不入计数——`STANDEE_SRC_H` 九键口径);样张 S6 语境 x 坐标随动(②③④+60、⑤+30,断言与 meta 全随动);闸件第三处复写兜底字面量删除(import 失败即崩——契约 2 单点纪律)。 +- 卡③:接板同步取「board 存局部变量→rebuild→守卫同步」三行形态;`_beams_rect` 判定用源码锚(函数不进 script constant map,卡内 haskey 备选按实况取源码形态);layout_probe 地面高容差 0.51(float 布局);卡②ui_kit.gd 注释「48px 档」残留未动(该件不在卡②写面——留观察)。 +- 卡①:停机呈报另件。 + +## 六、遗留读数义务与观察 + +- 卡①停机已随甲案裁决闭合(2026-11-02)——停机呈报第六节的待办清单状态回退拟稿**作废不执行**(卡已收口,条目状态走完成删条通道归批后收尾)。 +- status_effects.tres :99-141 批注含 refresh 陈旧叙事(用户 2026-11-01 恢复令在案)——本批零触碰,处置归用户另裁(卡①范围边界在案)。 +- ui_kit.gd TEX_HERO_PORTRAIT 行注释「48px 档」与 path_check.py 已改指外的一处陈旧表述——随下次触碰该件的批次顺带换代(反哺点清单未列——非文档面)。 +- 渲染闸(05)与 golden 零变化判据归合回后本地 CI(里程碑 --code 出口因卡①停机未跑——合并前置齐)。 + +## 七、独立验收审计 + +独立验收审计已执行(未参与实现的子代理,报告 `reports/iter48-验收审计-2026-11-02.md`):首轮结论=窄口径不通过,缺漏三项——①卡③ V6 源码锚断言未落地(实现遗漏,卡面本有 prescribed);②代拍板追认单未另立件;③两处甲案裁决前陈旧叙述。三项已随审计补差修齐(V6 锚断言入 stage_background_test 套件并跑绿;追认单另立 `reports/iter48-代拍板追认单-2026-11-02.md`;陈旧叙述订正),复审结论见审计报告末节。 diff --git a/reports/iter48-验收审计-2026-11-02.md b/reports/iter48-验收审计-2026-11-02.md new file mode 100644 index 00000000..5e2108c6 --- /dev/null +++ b/reports/iter48-验收审计-2026-11-02.md @@ -0,0 +1,150 @@ +# 迭代 48 验收审计(2026-11-02) + +> 审计性质:独立验收审计(未参与实现的会话执行,test-conventions §8.7 口径)。工作区=`g-plan2-iter48`(分支 iter48),基点 master=a5c89d67,四笔=f08b63a3(卡②立绘显示档批)/ ef8154b2(卡③战斗区背景替换批)/ 64b9d79a(卡①状态合并累加制批,含甲案裁决随动)/ 3dd93dde(收尾)。审计只读:可 Read/grep/跑测试,禁改源码/测试/卡片;唯一写面=本报告。 +> 审计输入:三份需求卡片、`iter48-实现报告-2026-11-02.md`、`iter48-停机呈报-卡1状态相位断言-2026-11-02.md`、`git diff a5c89d67..iter48` 逐笔。 + +## 一、总结论 + +**不通过(窄口径)。** 三卡改动—验收对照表共 24 行,23 行证据齐备、当轮真实跑批绿、测试削弱零申报外动作、R7 零行为改动与九件素材原稿分辨率两大专项均实证成立;唯卡③ V6 申报的「源码锚(main.gd 与 main_debug_flow.gd 均含 `stage.set_board_type(`)」未在任何测试/探针落地,且实现报告§二卡③ V6 行「+源码锚在套件」与实况不符(证据误报)。修复面极窄(补一条静态锚断言或由用户裁对该判据改口径),补齐后复审只需核该一点。 +> **复审结果(2026-11-02,同日):三项缺漏已修齐,终结论=通过——见文末「复审(2026-11-02)」节。** + +## 二、对照表逐行核对 + +### 2.1 卡①(状态合并累加制)R1–R7 + +| # | 需求点 | 断言存在+尾缀 grep | 真实入口 | 判读 | +|---|---|---|---|---| +| R1 | 六行换代+词表+加载期校验 | `table_structure_guard_test.gd`〔状态累加 V4〕在位(:272-275),值判 `accumulate`;实跑 66 checks 全绿=全表装载零红 | ConfigDB 加载链 → `row_by_id()`(套件经 `_db` 实装装载) | 通过 | +| R2 | 重复授予累加+层数恒 1 | `roll_multiplier_test.gd`〔状态累加 V1〕在位(remaining==3、layers==1、snap_magnitude==−65);实跑 PASS | `battle_orchestrator.new(_db,rng)`+`begin_battle` 真实编排宿主 → `apply_effect` | 通过 | +| R3 | 同轮双授予累加两段 | 〔状态累加 V2〕在位(6+2=8);实跑 PASS | 同上 | 通过 | +| R4 | 覆盖时长按覆盖值累加 | 〔状态累加 V3〕在位(3+3=6,`apply_effect(...,3)`);实跑 PASS | 同上(覆盖入参即 combat_segment 同口) | 通过 | +| R5 | 快照纪律不变 | V1 断言内 `snap_magnitude==−65` 承载;实跑 PASS | 同 R2 | 通过 | +| R6 | 两臂零变化=既有断言零改动回归 | diff 核实三套件零触碰(numstat:damage_controller_test 仅 :376 标签 1/1;seal_polarity、system_contracts 不在 diff);实跑三套件全绿(44/70/15 checks) | 各既有真实入口 | 通过 | +| R7 | 相位分类随动(甲案) | `round_flow_skill_pool_test.gd`〔:422 相位单调〕+:466-468 `deferred` 分支在位;实跑 134 checks 全绿含「每轮行相位单调」PASS | `_run_battle(1,0)` 真实结算流(seed 42 轨迹) | 通过(零行为改动自证见第六节 a) | + +### 2.2 卡②(立绘显示档批)对照表 9 行 + +| # | 需求点 | 断言存在+尾缀 grep | 真实入口 | 判读 | +|---|---|---|---|---| +| 1 | 逐件显示档高 | `_check_display_tier`〔立绘显示档 V13〕九键逐一在位;实跑 9 条 PASS(源高实读==原稿表值+显示高≈档高双读数) | `StandeeBody.new("enemy")`→`set_data(enemy_id)`→`set_portrait_texture(真纹理)`→`portrait_probe()`;主角经 `StandeeBody.new("hero")` 同链——**未直读常量,读数取自落位后 sprite 实测** | 通过 | +| 2 | 硬编码 ×2 退役 | 〔V14〕源码锚(读表+零 `Vector2(2.0, 2.0)`)在位;实跑 PASS;审计另 grep 全 presentation 确认零残留 ×2 立绘倍率 | `FileAccess` 静态判据(卡面即申报为静态判据) | 通过 | +| 3 | 纵横倍率恒等 | 〔V15〕双轴同倍率(is_equal_approx)+显示宽 round 档 ±0.51 带在位;九键实跑 PASS | 同 V13 | 通过(±0.51 带见第六节 b) | +| 4 | 主角中档 144 三态 | 〔V16〕三条在位(normal/attack/hit 逐态显示高 144);实跑 PASS | `BattleStageView` 组合根装载 → `hero_standee.portrait_probe()` → `set_hero_state`(内部即 `apply_hero_state`,同一真实链);「源高」腿由素材闸三态断言(92/92/92)承载 | 通过 | +| 5 | 素材不降采样 | `standee_asset_check.py` 断言①源高==`STANDEE_SRC_H` 表值在位;实跑 11 件全 PASS | 工具闸实跑(读盘上真实 PNG) | 通过(第六节 c) | +| 6 | 两表同值不漂移 | 〔V17〕源文本逐键对账在位;实跑 PASS(文件可读路径生效) | 读 `tools/pixelize_candidates.py` 源文本逐键 contains 对账 | 通过(L2 退路见第六节 b) | +| 7 | 表键集完整 | 〔V18〕恰九键无多余在位;实跑 PASS | `get_script_constant_map()` 读 `PORTRAIT_DISPLAY_H` | 通过 | +| 8 | 样张口径随动 | A4 冻结断言显式解冻重校到 88×92(diff 在案,卡 T3 明令「解冻重校、禁放宽凑绿」);站桩段按 `STANDEE_SLOT_H` 取数+S6 坐标随动在 diff。**脚本 30/30 未由审计复跑**(脚本落盘产物且资产根两级回退指向主检出,避免审计写盘)——依据=A4 冻结值 88×92 与盘上素材实读(素材闸 88×92)逐位一致+报告申报 | `python tools/render_sample_sheet_v3.py` | 通过(依据留痕,未复跑) | +| 9 | 实机观感 | 目检十项落实现报告第四节(登记不阻塞) | 真人目检 | 通过(登记义务完成) | + +### 2.3 卡③(战斗区背景替换)V1–V8 + +| # | 需求点 | 断言存在+尾缀 grep | 真实入口 | 判读 | +|---|---|---|---|---| +| V1 | ground_h 字段与校验 | `_test_layout_ground_field` 四条〔舞台背景 V1〕在位;实跑全 PASS;`config_db_test` 29 checks 绿;工具读取 `ground_h=100`(审计实跑输出 100) | `BattleLayoutTable.validate()`+`ConfigDB.load_tables()` | 通过 | +| V2 | 按 board_type 选图 | `_test_background_selection` 五条〔舞台背景 V2〕在位;实跑 PASS;`layout_probe` 两断言〔舞台背景 V2〕〔舞台背景 V2/V6〕在位且实跑 PASS | 探针 `main.ui.modals.level_screen.press_candidate(0)`(layout_probe.gd:32)→`board_changed` 真实选路进战斗屏后才断言 | 通过 | +| V3 | 缺图回退 | `_test_background_fallback` 两条在位;实跑 PASS(子类覆写 `_load_stage_texture` 构造缺图——卡面申报的观测法,不注入不改资产) | `set_board_type("underdark")`(缺图子类) | 通过 | +| V4 | 地面层几何 | `_test_ground_layer_geometry` 四条在位;实跑 PASS;`layout_probe` 两档 `size.y==ground_h`(容差 0.51,报告§五备案)实跑 PASS | 舞台构造+press_candidate(0) 真实进入 | 通过 | +| V5 | 光柱与舞台底退役 | `_test_beams_retired` 三条(源码锚+常量表+菜单斜光带在位)实跑 PASS;`theme_projection_test` 160 checks 绿(对比锚改读合并目标后 6.33:1/5.75:1 PASS);`ui_contrast_probe` 36 测点绿 | 构造+常量表+两探针 | 通过 | +| V6 | 接板真实链路 | layout_probe 真实入口腿 PASS(路径 `_dungeon.png` 结尾,尾缀〔舞台背景 V2/V6〕);**申报的源码锚断言(main.gd+main_debug_flow.gd 含 `stage.set_board_type(`)全仓测试/探针 grep 零命中=未落地**;实现报告§二 V6 行「+源码锚在套件」与 diff 实况不符 | press_candidate(0)(仅覆盖 main.gd 链);main_debug_flow 调试链零自动化断言 | **缺漏**(生产行在案可 grep——main.gd:388、main_debug_flow.gd:106;缺的是申报的验收断言) | +| V7 | 既有呈现回归 | 六套件+两探针实跑全绿(审计复跑其中 standee_present_values 50/theme_projection 160/layout_probe/ui_contrast PASS;diff 证实站桩/明细/棋盘/golden/shot_checker 零触碰) | 各既有真实入口 | 通过 | +| V8 | 能力行名实与登记 | run_all.gd:196 登记行在案;README:138 能力行三用例名与套件函数逐字一致(`_test_background_selection`/`_test_ground_layer_geometry`/`_test_beams_retired` 均实存);capability_audit 归合回 CI(报告已声明) | `res://tests/run_all.gd` | 通过 | + +## 三、抽查实跑记录(审计本人在工作区执行) + +| 测试 | 退出码 | 关键结果 | +|---|---|---| +| `--headless --path game --script res://tests/unit/battle_settlement/roll_multiplier_test.gd` | 0 | 42 checks 全绿;〔状态累加 V1〕「累加至 3(1+2——快照沿首授)」/〔V3〕「累加至 6(3+3)」/〔V2〕「累加至 8(6+2)」三条 PASS | +| `res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd` | 0 | 134 checks 全绿;「每轮行相位单调(授予→增益/减益→战斗→衰减→判定)」PASS——deferred 归类在 seed 42 真实轨迹生效 | +| `res://tests/unit/data_config/table_structure_guard_test.gd` | 0 | 66 checks 全绿;〔状态累加 V4〕「ac_up 行在位(hero · +1 ac · 累加语义)」PASS | +| `res://tests/unit/battle_settlement/damage_controller_test.gd` | 0 | 44 checks 全绿(R6 回归证词) | +| `res://tests/unit/battle_settlement/seal_polarity_test.gd` | 0 | 70 checks 全绿(R6) | +| `res://tests/unit/contracts/system_contracts_test.gd` | 0 | 15 checks 全绿;「开域后受理已知 id(布尔受理口径 true 通道)」PASS(R6) | +| `res://tests/unit/presentation/standee_present_values_test.gd` | 0 | 50 checks 全绿;V13 九键 ×2+V14+V16 三态+V17+V18 共 24 条显示档断言逐条 PASS | +| `python tools/standee_asset_check.py` | 0 | 11 件全 PASS(源高 90/81/126/94/123/189/189/189/92 逐件==原稿表值;酋长显示宽 157 例外逐字命中) | +| `res://tests/unit/presentation/stage_background_test.gd` | 0 | 18 checks 全绿(V1 四条/V2 五条/V3 两条/V4 四条/V5 三条) | +| `res://tests/unit/presentation/theme_projection_test.gd` | 0 | 160 checks 全绿;舞台底对比锚(SLOT_BACKDROP)6.33:1、5.75:1 ≥4.5 PASS | +| `res://tests/unit/data_config/config_db_test.gd` | 0 | 29 checks 全绿(V1 装载链腿) | +| `res://probe/layout_probe.tscn`(headless) | 0 | 「PASS — 2 档几何不变量全过」——含新增舞台背景/地面层断言两档通过 | +| `res://probe/ui_contrast_probe.tscn`(headless) | 0 | 「对比度 36 对测点全绿」 | +| `python -c "from tools.layout_config import load; print(load()['ground_h'])"` | 0 | 输出 `100`(V1 工具腿) | + +## 四、测试削弱申报核对(申报 vs diff,逐处) + +- 卡①申报 1–5:diff 逐处对上——①:160-161 削弱断言换代(2→3,含 layers/snap 双判据);②:157-158、:230、:358 三处注释与 `_weaken_refresh_probe`→`_weaken_accumulate_probe` 改名(全仓旧名零残留);③table_structure_guard :272-275 值+消息换代;④damage_controller_test :376 仅标签文字、断言数值逐字未动;⑤round_flow `_row_phase` 补 `or bool(row.get("deferred", false))` 分支(申报 5=甲案随动)。**申报外零删除、零放宽、零扫描面缩小、零换下层直调**(numstat:roll_multiplier 31/12 全属申报面;其余测试件 1-2 行均申报内)。 +- 卡②申报「不删不改任何既有断言」:`standee_present_values_test.gd` diff 93/0 纯新增;`ui_probe.gd` 2/2 仅文案(判据 `has_tex` 不变);样张 A4 冻结值 46×48→88×92 系卡 T3 明令「显式解冻重校到新档」(非放宽——旧值对原稿分辨率素材必红,新值与盘上素材逐位绑定)。无申报外改动。 +- 卡③申报「零删除零放宽,唯一改读=theme_projection_test 与 ui_contrast_probe 舞台底锚 STAGE_BG→SLOT_BACKDROP」:diff 恰为该两处 1 行/1 行改读,阈值与文案未动;`layout_probe` 14/1 纯追加(1 删=文件末补换行符);其余测试件零触碰。无申报外改动。 + +## 五、内部字段直查受控例外核对 + +| 直查点 | 是否申报/在案 | 判读 | +|---|---|---| +| 卡①探针 `settlement._status_effects` / `settlement._damage.clamped_pool` | 卡对照表 R2 真实入口列注明「经真实编排宿主 begin_battle」+「remaining/layers 只读投影(query_active)」;该直查为既有探针沿例(旧 `_weaken_refresh_probe` 同面) | 通过(在案) | +| 卡② `mo._portrait_sprite`(portrait_probe 扩两读数) | 卡§6 文件结构图「探针 portrait_probe() 扩显示读数(几何出口单点)」+§12 落位预检「复用既有探针出口」;旧 `has_tex` 即读同成员 | 通过(在案) | +| 卡③ layout_probe `stage.get("bg_texture")`/`get("ground_layer")` | 二者为本批新增公开 var(非 `_` 私有),非内部直查 | 不适用 | +| layout_probe `bv.get("_board")`(既有) | 探针源码 :102 注释自带例外理由(受控例外在案,非本批引入) | 通过(既有) | + +## 六、特别核三点 + +**(a)卡① R7 deferred 方案=零行为改动:成立。** +- 生产侧 diff 仅 `_ineffective_row` 一行:载荷 dict 在原 `"batch_key"` 键后追加 `"deferred": true`——行型(reward_ineffective)、source_class、origin_cell、highlight、display 文案、value 0、round_index 全部逐字未动。 +- 全仓 grep:生产码无任何消费方读取结算行的 `deferred` 键(其余 `deferred` 命中全属宝物域 treasure_items,异面无关);分类读取只在该测试 `_row_phase`。 +- 归类非一刀切:`grant_segment.gd:301` 的授予段未生效行(坑道小憩)不带 deferred 键,仍归第 0 段——分类器仍按行自描述区分「授予段未生效」与「战斗段地图账殿后」,五段全序不变量强度不降。 +- 非空转证据链:停机呈报在案的红(seed 42 round 2 magic_bolt 行判第 0 段致违序)发生先于随动;载荷加键不消耗 RNG、不改判定/数值路径 → 轨迹逐行不变,该行只能经新分支归入相位 2——套件由 133+1FAIL 转 134 全绿即该分支真实生效的直接证据。 + +**(b)卡② V17 L2 退路与 V15 ±0.51 带:均按备案执行。** +- V17:测试内置 `../tools/`→`tools/` 两级路径回退,双双不可读时打印 `[L2] V17 工具源不可读…(记录不判红)` 并跳过对账——与卡面「禁当红亦禁当绿」逐字一致;本轮文件可读、对账正常执行并 PASS(退路在件未触发)。 +- V15:实现=双轴同倍率 is_equal_approx+`absf(display_size.x − round 档) < 0.51`;卡面字面 `display_size.x == round(...)` 在非整数比(如地精 89×96/90=94.93)下物理不可达,取带式为更强可行判据——实现报告§五已备案且九键 round 档值(95/121/71/113/119/138/157/139/132)与源宽×档高/源高的四舍五入逐一吻合;±0.51 覆盖 round 语义 ±0.5 加浮点残差,未放宽体量语义(酋长 157 超宽例外仍由素材闸逐字校验)。 + +**(c)九件素材=原稿分辨率:实证成立。** +- 素材闸断言①逐件「源高 == STANDEE_SRC_H 表值」11 件全 PASS(含主角三态 92/92/92);旧降采样稿(八敌 36–94 高、主角 46–48 高)任一回填即在该断言红。 +- 管线侧 `build_standee_display` 紧裁后 `im.height != STANDEE_SRC_H` 即 `sys.exit(2)`+产物计数 ≠9 即停(L3#2 防回退顶替)双自证在 diff 在案。 +- 报告§三改前→改后九件读数(如 goblin 36×36→89×90、ogre_chief 62×74→158×189)与闸件实跑输出逐件一致。 + +## 七、缺漏清单与观察项 + +**缺漏(判不通过的依据):** + +1. 卡③ V6 源码锚断言未落地+证据误报:卡面 V6 行判据面含「源码锚 `main.gd` 与 `main_debug_flow.gd` 均含 `stage.set_board_type(`」,实现报告§二卡③ V6 行称「+源码锚在套件」;实测 `stage_background_test.gd:_test_background_selection` 与全仓测试/探针均无该断言。影响面:main.gd 链已由 layout_probe 真实入口覆盖;**main_debug_flow 调试链的同步行(main_debug_flow.gd:106)零自动化断言**。生产行本身在案(两处 grep 可定位、行为形态与卡 T2 一致),缺的是申报的验收证据。修复建议:在 `stage_background_test.gd` 补一条双文件源码锚断言(或经用户裁对该判据改口径并订正实现报告),随修后复审只核此点。 + +**记录级(不单独构成不通过,随批处置):** + +2. 卡② T3/卡③ T5 各要求产出物三份(实现报告/代拍板追认/反哺点),「代拍板追认单」未另立件;代拍板内容已在实现报告§五逐条备案(卡②五条/卡③三条,覆盖面与卡面白名单对得上)。补立或由用户裁认「报告§五代行追认单」二选一。 +3. 实现报告§七「当前合并被卡①停机阻塞」与反哺点清单卡①节标题「停机在案」均为甲案裁决前的陈旧叙述(同文头部已记卡①收口)——随补丁顺带订正,禁留两处记同一件事互相矛盾。 + +**观察项(无需动作):** + +- 卡②自报遗留:ui_kit.gd:233 注释「48px 档」残留(该件不在卡②写面)——实现报告§六已登记,随下次触碰顺带换代。 +- path_check.py 注释改名(`STANDEE_TIER_H`→`STANDEE_DISPLAY_H`)不在卡②文件结构图列面,属表改名的必要注释联动(1 行注释,零行为)——记录为申报外最微触碰,不判红。 +- 渲染闸(05)与 golden 零变化判据归合回后本地 CI(里程碑 --code 出口因卡①停机未跑,实现报告§六已声明)——本审计不含窗口化渲染闸,合并后以 CI 回执为准。 + +## 八、证据基线 + +审计只读声明:本审计未修改任何源码/测试/卡片/工具;写面仅本报告。实跑 14 项(第三节清单)全部退出码 0;关键 grep(refresh 残留、accumulate 恰 6 处、`Vector2(2.0, 2.0)` 零残留、`set_board_type` 锚点)均留存于本文各节。 + +**总结论(首轮):不通过(窄口径)——缺漏清单见第七节第 1 条(卡③ V6 源码锚未落地+报告证据误报),补齐该点并处置第 2/3 条记录项后,本批其余 23 行对照表证据、削弱申报合规性、R7 零行为改动与素材原稿分辨率专项均已实证通过。**(终结论随复审更新=通过,见下节。) + +## 九、复审(2026-11-02) + +> 复审面=窄口径,只核首轮缺漏清单三项修复,不改首轮其他结论。实现侧在审计补差后按卡序重塑重建分支:新链 db43f77e(卡①)→ 99095d88(卡②)→ 0478258d(卡③含审计补差)→ 7c9e5722(收尾),基点仍 a5c89d67。`git diff d2b034b1 HEAD` 输出为空(逐字节一致)自证成立——首轮对代码/测试/工具面的全部结论对新链继续有效;本报告文件本身已随收尾提交入库,复审写面仍仅本文件。 + +**修复 1——卡③ V6 源码锚断言落地:核验通过。** +- `stage_background_test.gd:_test_background_selection` 内新增循环断言(逐 `res://presentation/main.gd` 与 `res://presentation/main_debug_flow.gd` 读源,断言 `contains("stage.set_board_type(")`,尾缀〔舞台背景 V6〕两条)——grep 在位;读文件失败时 `contains` 必假(fail-closed,不空转)。 +- 实跑:`--headless --path game --script res://tests/unit/presentation/stage_background_test.gd` → **exit 0,「all green (20 checks)」**(首轮 18 → 20),两条 V6 PASS 逐条在案:「接板同步源码锚 main.gd 含 stage.set_board_type(」「接板同步源码锚 main_debug_flow.gd 含 stage.set_board_type(」。 +- 首轮缺漏 1 关闭:main.gd 与 main_debug_flow 两条接板链均有真实入口腿(layout_probe,首轮已绿)+静态锚腿(本轮绿),V6 判据面两腿齐备。 + +**修复 2——代拍板追认单另立件:核验通过。** +- `reports/iter48-代拍板追认单-2026-11-02.md` 在库:卡①白名单 1–3(词表命名 accumulate/快照不改/探针改名与尾缀)+甲案裁决随动单列且正确标注「非代拍板,用户裁决授权」;卡②白名单 1–5(probe 键名/CLI 与函数改名/尾缀格式/对照图目录名/hero key 判法)逐项与首轮实核落地一致;卡③白名单 1–6(私有函数布局/helper 与措辞/注释文案/TextureRect 写法/描边可见性=平色 Panel 描边被背景覆盖零新增类变量/格式修空项)逐项一致,无超白名单事项。 +- 首轮记录的实现决定(V15 ±0.51 带、V17 L2 退路形态、产物计数九件口径、样张 S6 坐标随动、闸件兜底字面量删除、接板三行形态、`_beams_rect` 源码锚形态、layout_probe 0.51 容差)与追认单不矛盾——其性质属卡面明令步骤或已在实现报告§五备案的判据实现(首轮第六节 b 已按备案口径核过),追认单按「白名单逐项」口径立单,覆盖面成立。首轮缺漏 2 关闭。 + +**修复 3——陈旧叙述订正:核验通过。** +- 实现报告§七已换代为「审计已执行(首轮窄口径不通过三项缺漏)→三项已随审计补差修齐→复审结论见审计报告末节」,与实况一致(含对本报告路径与追认单路径的正确指引);首轮指出的「§七仍称合并被卡①停机阻塞」矛盾句已消除。 +- 反哺点清单卡①节标题已改「(甲案裁决已闭合——随本批合回生效)」。首轮缺漏 3 关闭。 + +**复审实跑记录:** + +| 测试 | 退出码 | 关键结果 | +|---|---|---| +| `--headless --path game --script res://tests/unit/presentation/stage_background_test.gd` | 0 | 20 checks 全绿;两条〔舞台背景 V6〕源码锚 PASS | +| `git diff d2b034b1 HEAD` | 0 | 空输出(重塑后终树与审计补差树逐字节一致) | + +**总结论(终版):通过。** 首轮三项缺漏全部修齐并经当轮实跑核验;三卡 24 行对照表证据、测试削弱申报合规性、内部字段直查例外、R7 零行为改动、V17/V15 备案执行、九件素材原稿分辨率专项全部成立。本报告为合并前独立验收审计的最终口径。 diff --git a/reports/需求卡片-状态合并累加制批-2026-11-01.md b/reports/需求卡片-状态合并累加制批-2026-11-01.md index d0e59944..c13fa861 100644 --- a/reports/需求卡片-状态合并累加制批-2026-11-01.md +++ b/reports/需求卡片-状态合并累加制批-2026-11-01.md @@ -2,6 +2,7 @@ > 日期:2026-11-01 > 性质:需求改动实现批卡片;零待裁点,开工门=U0。 +> 裁决补记(2026-11-02 用户甲案):round_flow 相位单调断言红=测试分类器对「战斗段地图账未生效行」缺映射(生产行序符合案C)——补记第十一节申报第 5 条+第七节写面两行+第十节 T1 两步;方案=生产行载荷自描述键 `deferred`(零行为改动、观测面扩展),裁决原文见 reports/iter48-停机呈报-卡1状态相位断言-2026-11-02.md。 > 来源:待办清单「状态合并改累加制——同名状态重复授予改加时长」条(2026-10-31 用户立案并四点裁决闭合;登记批 git 提交 d216ae04,设计闭合批 870b2f2b)。 > 唯一事实源:[增益状态](../design/gdd/systems/战斗循环/增益状态.md) v1.3(2026-10-31)——§同名状态再拿到(合并策略表:叠加/累加/并存)· §剩余次数怎么倒数 · 「幅度与量纲」节盾牌加护行与字段表。本卡只实现上述设计,不改设计。 > 估时:T0–T2 ≈ **1.0~1.25 个工作日**(中值 ≈1.125)。 @@ -79,6 +80,8 @@ | game/tests/unit/battle_settlement/roll_multiplier_test.gd | 掷点/削弱/AC11 套件 | 行为改动(测试面):削弱累加探针换代+新增三条断言(对照表 R2–R4;申报见第十一节) | | game/tests/unit/data_config/table_structure_guard_test.gd | 表结构守卫 | 行为改动(测试面):ac_up 行断言换代(对照表 R1;申报见第十一节) | | game/tests/README.md | 能力清单与覆盖域表 | 写卡段维护件——本卡落位时已入表,实现批零改动 | +| game/core/damage_controller.gd `_ineffective_row` | 行载荷加自描述键 `deferred: true`(战斗段地图账未生效行——裁决补记 2026-11-02 甲案) | 非行为改动(观测面扩展,零绘制/结算变化;对照表 R7) | +| game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd `_row_phase` | reward_ineffective 分类补 deferred 分支(战斗段)——裁决补记随动(对照表 R7) | 行为改动(测试面,断言判据随申报 5 换代) | ## 八、落位与可复用性预检 @@ -107,6 +110,7 @@ - [ ] roll_multiplier_test.gd:③段 :157–158 注释与 :160–161 断言换代(重置 2 → 累加 3);`_weaken_refresh_probe` 改名 `_weaken_accumulate_probe`(:159 调用点与 :342 引用注释随改),探针改五段式——授予(2)→衰减(1)→重授(1+2=3,V1)→覆盖值重授(3+3=6,V3)→同轮再授(6+2=8,V2);返回 {layers, pool, after_regrant, after_override, after_extra, snap_magnitude} 六读数(pool 键保留——:163 池读数断言零改动在位) - [ ] 新增断言三条(尾缀〔状态累加 V1/V2/V3〕):V1=重授后 remaining==3、layers==1、snap_magnitude==−65(首授快照恒定);V3=覆盖值累加后 remaining==6;V2=同轮再授后 remaining==8 - [ ] table_structure_guard_test.gd:273–275:ac_up 行断言 `stack_policy == "accumulate"`+消息换代(累加语义句)——可核查:断言尾缀〔状态累加 V4〕可 grep、套件绿 +- [ ] 裁决补记随动(2026-11-02 甲案):`_ineffective_row` 载荷加 `"deferred": true`;`_row_phase` reward_ineffective 分类补 `or bool(row.get("deferred", false))` → 战斗段 - [ ] 孤立验证:六套件最窄复跑(roll_multiplier/table_structure_guard/system_contracts/damage_controller/seal_polarity/round_flow_skill_pool——EV 锚套件提前曝红)全绿或按 L3②停机、断言尾缀可 grep **T2 产出物落位(0.25 天)** @@ -126,6 +130,7 @@ | R4 | 覆盖时长按覆盖值累加 | apply_effect :37 override 入累加臂 | 〔状态累加 V3〕(覆盖值 3 重授 remaining==6) | 单元 | 同上(真实施放链 combat_segment :89 走同口) | remaining 累加覆盖值 | 新增:同名状态合并策略(叠加/累加/并存)与剩余次数语义 | | R5 | 快照纪律不变(首授快照恒定) | 非行为改动——累加臂沿用既有重授不换快照口径,零代码变化 | 〔状态累加 V1〕快照断言(snap_magnitude==−65) | 单元 | 同 R2 | 快照域取值(snap_magnitude) | 新增:同名状态合并策略(叠加/累加/并存)与剩余次数语义 | | R6 | 叠加/并存两臂零变化 | 非行为改动——两臂零代码变化 | damage_controller_test :391+seal_polarity_test :684+system_contracts_test 布尔受理——既有断言零改动复跑即回归证词 | 单元 | 各既有真实入口 | 既有断言零改动 | 既有:系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | +| R7 | 战斗段地图账未生效行归类随动(裁决补记 2026-11-02 甲案) | damage_controller.gd `_ineffective_row` 载荷加 `deferred: true`(非行为改动);round_flow `_row_phase` 分类补 deferred→战斗段 | round_flow_skill_pool_test 相位单调断言(同轮执行且通过——seed 42 轨迹含该行型)+五套件回归 | 单元 | `_run_battle(1,0)` 真实结算流 | 相位分类器按自描述键归类;既有申报 1–4 与 R1–R6 断言零变化 | 既有:战斗屏三区几何与棋盘格边长(该套件承载面)——行载荷观测面扩展不立新能力行 | **测试削弱申报**(允许的旧断言改动,全部为语义换代非削弱): @@ -133,6 +138,7 @@ 2. roll_multiplier_test.gd:157–158 注释、:230 探针注释与 :342 探针名引用注释换代(「续时:remaining ← 2」→累加口径;旧函数名 `_weaken_refresh_probe` 全仓清零)。 3. table_structure_guard_test.gd:274–275 ac_up 行断言值与消息换代(refresh→accumulate)——同上。 4. damage_controller_test.gd:376 断言标签「refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变」——行为面累加制下仍绿(层数恒 1、池值不变,断言数值零改动),仅标签文字换代为累加口径。 +5. **round_flow_skill_pool_test.gd `_row_phase` 分类补 deferred 分支(裁决补记 2026-11-02 用户甲案)**:「每轮行相位单调」断言的相位分类器对「战斗段地图账未生效行」(damage_controller `_ineffective_row` 发射、案C 口径在案)原判第 0 段——累加制改变战斗轨迹后该行型首次在种子 42 轨迹出现致断言红。随动=`_ineffective_row` 行载荷加 `deferred: true` 自描述键(非行为改动)+分类器按键归战斗段(相位 2)。五段全序不变量强度不降:归类的是既有设计口径行型,非放宽任何阈值。裁决原文=reports/iter48-停机呈报-卡1状态相位断言-2026-11-02.md。 ## 十二、异常分级 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): diff --git a/tools/path_check.py b/tools/path_check.py index 2f5dd793..60f979b5 100644 --- a/tools/path_check.py +++ b/tools/path_check.py @@ -44,7 +44,7 @@ ACTIVE_FILES = [ "fx/shovel_swing_f2.png", "board/seal_mark.png", ] -# 立绘八敌按数据表键生成(键集=STANDEE_TIER_H,见 standee_asset_check) +# 立绘八敌按数据表键生成(键集=STANDEE_DISPLAY_H,见 standee_asset_check) ENEMY_KEYS = ("bugbear", "drider", "duergar", "duergar_king", "giant_spider", "goblin", "hook_horror", "ogre_chief") ACTIVE_FILES += ["character/character_enemy_%s.png" % k for k in ENEMY_KEYS] diff --git a/tools/pixelize_candidates.py b/tools/pixelize_candidates.py index 5d397d47..c3124b50 100644 --- a/tools/pixelize_candidates.py +++ b/tools/pixelize_candidates.py @@ -12,9 +12,9 @@ v3 设计要点(对应四类问题:脏点/模糊/信息丢失/白边): 用法: python tools/pixelize_candidates.py <候选批次目录> # v3 批量像素化(现行行为) python tools/pixelize_candidates.py <目录> --fixer # +rust fixer 收网格+封闭灰袋清除 - python tools/pixelize_candidates.py <目录> --standee48 [--standee-out ] - # 九张 48px 站桩槽位档(八敌 pixel-1+主角 normal; - # 底中锚/NEAREST/统一比例保体量/画布高恒 48) + python tools/pixelize_candidates.py <目录> --standee-display [--standee-out ] + # 九张站桩原稿分辨率稿(八敌 pixel-1+主角手修 2x 稿; + # 内容紧裁原样存盘,零降采样——显示缩放归游戏侧按档) --fixer: rust pixelfixer(Retro-Diffusion, MIT, D:/code/pixel-art-fixer)对原图先做网格检测+重建(收网格),v3 去底后再跑封闭灰袋清除 (三闸=近底色∧中性灰∧组件不触图缘——防误伤盔甲与影);--fixer-exe 可换引擎路径。 @@ -25,25 +25,29 @@ from PIL import Image, ImageFilter FIXER_EXE_DEFAULT = r"D:\code\pixel-art-fixer\rust\target\release\pixelfixer.exe" FIXER_EXE = FIXER_EXE_DEFAULT # --fixer-exe 可覆写(__main__ 解析) -STANDEE_SLOT_H = 48 # 主角站桩源档高(×2 整数放大=盒高 96——圣经 §8.2) -STANDEE_SLOT_W_MAX = 75 # 盒宽 150 的一半(超宽才按宽收,现八敌+主角均不触) +STANDEE_SLOT_H = 144 # 主角显示档高(中型档——art-bible §8.2 三档绝对高) -# 八敌站桩源档高 = 生物体型,不是强度档位 -# 口径:体量按「这只生物本来多大」定;强度改由别的通道承载(Boss 的紫赤色相独占、 -# 精英的垂直延伸、HUD 意图徽标)。因此下表与 §5.5 的普通/精英/Boss 档位**不对齐**—— -# 例如钩爪恐魔是普通怪却比精英灰矮人大,灰矮人是精英却比普通/精英熊地精小。 -# 数值经实测校准(视觉体量 = 不透明像素面积,不看高度——蜘蛛宽扁,按高度排会失真): -# 地精 828 < 灰矮人 1014 < 巨蜘蛛 1390 < 钩爪恐魔 1538 -# < 熊地精 2145 < 食人魔酋长 2886 < 蛛化卓尔精灵 3513 < 灰矮人石君王 4385 -STANDEE_TIER_H = { - "goblin": 36, "giant_spider": 38, - "duergar": 46, "hook_horror": 58, - "bugbear": 62, "ogre_chief": 74, - "drider": 90, "duergar_king": 94, +# 八敌站桩显示档 = 三档绝对高(art-bible §8.2 显示档定值单点)——小型 96 / 中型 144 / 大型 188 +# 归档 = 生物体型语义(character-design §5.2):小 < 中 < 大,禁按强度重排。 +# 显示倍率 = 档高 / 原稿高,宽随源比例(高定宽随,NEAREST);入包件 = 原稿紧裁原样, +# 禁任何降采样(2026-10-14 降档裁决作废——现行入包稿与原稿逐像素同)。 +STANDEE_DISPLAY_H = { + "goblin": 96, "giant_spider": 96, "duergar": 96, + "hook_horror": 144, "bugbear": 144, + "ogre_chief": 188, "drider": 188, "duergar_king": 188, +} + +# 原稿高表(入包件逐件源高 == 此值——禁降采样纪律的机检锚;主角 = 手修 2x 稿紧裁后高) +STANDEE_SRC_H = { + "goblin": 90, "giant_spider": 81, "duergar": 126, + "hook_horror": 94, "bugbear": 123, + "ogre_chief": 189, "drider": 189, "duergar_king": 189, + "hero": 92, } # 中间产物(pixel-1)的生成分辨率——只决定中间图有多细,**不决定最终体量**。 -# 最终体量归 STANDEE_TIER_H。这里取得比最终档高越大,后续降采样越干净。 +# 站桩入包件 = 原稿紧裁原样(STANDEE_SRC_H),显示体量归 STANDEE_DISPLAY_H。这里取得 +# 比显示档越大,入包件细节越足。 # 注意:ogre_chief 取 192 是分辨率选择(要给它更高的中间精度),不是把它归为 Boss 档。 SIZES = {"goblin": 96, "giant_spider": 96, "hook_horror": 96, "bugbear": 128, "duergar": 128, @@ -456,60 +460,62 @@ def dom_block_resize(im, nw, nh): return out -def build_standee48(batch_root, out_dir, extra_hero_states=("attack", "hit")): - """出八敌 + 主角的站桩源档。源 = 八敌 pixel-1 + 主角 normal_1x(追加 attack/hit,默认启用)。 +def build_standee_display(batch_root, out_dir, extra_hero_states=("attack", "hit")): + """出八敌 + 主角的站桩入包稿(原稿分辨率,零降采样)。源 = 八敌 pixel-1 + 主角手修 2x 稿。 口径: - 1. 八敌各自按 STANDEE_TIER_H 定高——那是生物体型,不是强度档位。 - (实测可读门槛约 32px,48px 舒适;若全敌共用「48/最大值」一个系数, - 普通怪被压到 21–24px 低于门槛、钩爪恐魔糊成一团——禁用统一比例。) - 2. 主角仍按 STANDEE_SLOT_H(48)定高。 - 3. 画布高度 = 该角色自己的档高;内容裁透明边后底中锚贴底缘。 - 4. 降采样走 dom_block_resize(块内众数投票,零插值)。 + 1. 内容紧裁透明边后原样存盘——画布即内容,禁任何缩放与降采样(art-bible §8.2: + 显示按三档绝对高在游戏侧缩放,入包件保留原稿分辨率)。 + 2. 逐件自证:紧裁后高度 == STANDEE_SRC_H 表值;九件计数 != 9 即停 + (缺件即停——禁回退旧降采样稿顶替)。 """ os.makedirs(out_dir, exist_ok=True) enemy_srcs = {} - for key in sorted(SIZES.keys()): + for key in sorted(STANDEE_SRC_H.keys()): + if key == "hero": + continue p = os.path.join(batch_root, key, "pixel-1.png") if os.path.isfile(p): enemy_srcs[key] = Image.open(p).convert("RGBA") - hero_p = os.path.join(batch_root, "hero-ref", "edit", "normal_1x.png") + hero_p = os.path.join(batch_root, "hero-ref", "edit", "normal_2x.png") jobs = [(k, im, "character_enemy_%s.png" % k) for k, im in enemy_srcs.items()] if os.path.isfile(hero_p): jobs.append(("hero_normal", Image.open(hero_p).convert("RGBA"), "character_hero_portrait.png")) for st in extra_hero_states: - sp = os.path.join(batch_root, "hero-ref", "edit", "%s_1x.png" % st) + sp = os.path.join(batch_root, "hero-ref", "edit", "%s_2x.png" % st) if os.path.isfile(sp): jobs.append(("hero_%s" % st, Image.open(sp).convert("RGBA"), "character_hero_%s.png" % st)) made = [] + n_pieces = 0 # 九件 = 八敌 + 主角立绘(attack/hit 为立绘伴随态,不入计数) for key, im, out_name in jobs: l, t, r, b = _bbox_alpha(im) im = im.crop((l, t, r, b)) - is_hero = key == "hero_normal" or key.startswith("hero_") - slot_h = STANDEE_SLOT_H if is_hero else STANDEE_TIER_H.get(key, STANDEE_SLOT_H) - f = slot_h / float(im.height) - nw, nh = max(1, int(round(im.width * f))), max(1, int(round(im.height * f))) - if nw > STANDEE_SLOT_W_MAX: # 超宽兜底(现无触)——按宽收再贴底 - k2 = STANDEE_SLOT_W_MAX / float(nw) - nw, nh = STANDEE_SLOT_W_MAX, max(1, int(round(nh * k2))) - body = dom_block_resize(im, nw, min(nh, slot_h)) - canvas = Image.new("RGBA", (nw, slot_h), (0, 0, 0, 0)) - canvas.alpha_composite(body, (0, slot_h - body.height)) # 底中锚:脚贴画布底缘 + src_key = "hero" if key.startswith("hero") else key + if im.height != STANDEE_SRC_H.get(src_key): + print("FAIL 站桩原稿高失配 %s: 实得 %d, 表值 %s(禁降采样——查源稿)" + % (key, im.height, STANDEE_SRC_H.get(src_key))) + sys.exit(2) outp = os.path.join(out_dir, out_name) - canvas.save(outp) - print("standee %-14s %sx%s -> %s" % (key, nw, slot_h, outp)) + im.save(outp) + print("standee %-14s %dx%d -> %s" % (key, im.width, im.height, outp)) made.append(outp) + if out_name.startswith("character_enemy_") or out_name == "character_hero_portrait.png": + n_pieces += 1 + if n_pieces != len(STANDEE_SRC_H): + print("FAIL 站桩产物计数 %d != %d(缺件即停——禁回退旧降采样稿顶替)" + % (n_pieces, len(STANDEE_SRC_H))) + sys.exit(2) return made def main(argv): root = argv[0] if argv and not argv[0].startswith("--") else "artwork/remote-gen-2026-09-26" args = argv[1:] if argv and not argv[0].startswith("--") else argv - if "--standee48" in args: + if "--standee-display" in args: out = args[args.index("--standee-out") + 1] if "--standee-out" in args \ else os.path.join("game", "assets", "art", "character") - build_standee48(root, out) + build_standee_display(root, out) return use_fixer = "--fixer" in args thumbs = [] diff --git a/tools/render_sample_sheet_v3.py b/tools/render_sample_sheet_v3.py index ae560eb0..f540deb8 100644 --- a/tools/render_sample_sheet_v3.py +++ b/tools/render_sample_sheet_v3.py @@ -51,6 +51,7 @@ import hashlib from collections import deque from layout_config import load as load_layout +from pixelize_candidates import STANDEE_SLOT_H # 站桩显示档单点(主角=中型档——art-bible §8.2) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" @@ -772,7 +773,7 @@ def _armor_tex(armor): def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", hp=1.0, intent="", statuses=(), dead=False): - """站桩 150×176(standee_body.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。""" + """站桩 150×176(standee_body.gd 几何):血条+状态行头顶/立绘盒 96px 槽位(显示档居中溢出)/底座/敌名行。""" d = ImageDraw.Draw(canvas) W, H = 150, 176 # 头顶:顺位徽标(敌,FS_HINT 14)+血条(填充=危险红 #A8321F 不因敌我分色;条底=HP_BAR_TRACK) @@ -788,11 +789,13 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", for i, ic in enumerate(statuses): icon = STATUS_ICONS[ic].resize((16, 16), Image.NEAREST) canvas.alpha_composite(icon, (sx + i * 17, oy + 18)) - # 立绘盒 96px(48 源 ×2 整数——C5) + # 立绘盒 96px 槽位(hero 显示档 = STANDEE_SLOT_H 绝对高、宽随源比例——§8.2,居中溢出同 _place_portrait) pbox_y = oy + 44 if kind == "hero": - por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST) - canvas.alpha_composite(por, (ox + (W - 96) // 2, pbox_y)) + disp_h = STANDEE_SLOT_H + disp_w = int(round(PORTRAIT_SRC.width * disp_h / float(PORTRAIT_SRC.height))) + por = PORTRAIT_SRC.resize((disp_w, disp_h), Image.NEAREST) + canvas.alpha_composite(por, (ox + (W - disp_w) // 2, pbox_y + (96 - disp_h) // 2)) else: glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌") col = (150, 60, 48) if not dead else (110, 104, 98) @@ -1131,11 +1134,11 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None): edge_hits = edge_content(canvas_img) check("A5", "画布四缘 2px 净空(零溢出)", edge_hits == 0, "缘带内容像素 %d" % edge_hits) # A4 混排相融:小人/立绘/状态图标=在库原件;语义色==UiKit 常量 - # A4 尺寸基线按现行资产实态固化——立绘 46×48/棋盘小人源 32×32 + # A4 尺寸基线按现行资产实态固化——立绘 88×92(原稿分辨率)/棋盘小人源 32×32 # 硬等断言落锁(资产升档换代须显式解冻重校,断言失败=预期闸)。 - check("A4", "混排相融(在库原件可载+UiKit 语义色对表+尺寸基线冻结:立绘 46×48/小人源 32×32)", - PORTRAIT_SRC.size == (46, 48) and HERO_SRC.size == (32, 32), - "小人源 %s(裁透明留白后 %s)/立绘 %s[冻结 46×48]/状态图标原件;INK/封印黑/奖励金 == ui_kit.gd" + check("A4", "混排相融(在库原件可载+UiKit 语义色对表+尺寸基线冻结:立绘 88×92/小人源 32×32)", + PORTRAIT_SRC.size == (88, 92) and HERO_SRC.size == (32, 32), + "小人源 %s(裁透明留白后 %s)/立绘 %s[冻结 88×92]/状态图标原件;INK/封印黑/奖励金 == ui_kit.gd" % (str(HERO_SRC.size), str(HERO.size), str(PORTRAIT_SRC.size))) return all(c[2] for c in CHECKS) @@ -1497,42 +1500,42 @@ def render_outline_overlap(): img = Image.new("RGBA", (W, H), (24, 20, 17, 255)) d = ImageDraw.Draw(img) meta = {"hero_boxes": [], "overlap": None, "armor": {}} - # ① 战斗舞台语境:主角立绘 96px + 深墨描边 - por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST) + # ① 战斗舞台语境:主角立绘显示档(STANDEE_SLOT_H 绝对高、宽随源比例)+深墨描边 + por = PORTRAIT_SRC.resize((int(round(PORTRAIT_SRC.width * STANDEE_SLOT_H / float(PORTRAIT_SRC.height))), STANDEE_SLOT_H), Image.NEAREST) img.alpha_composite(_outline_underlay(por, (40, 19, 13)), (18, 14)) img.alpha_composite(por, (18, 14)) - meta["hero_boxes"].append((18, 14, 18 + 96, 14 + 96)) - d.text((18, 116), "主角·战斗舞台语境\n描边=深墨 (40,19,13)", font=F_LBL2, fill=(225, 215, 195)) + meta["hero_boxes"].append((18, 14, 18 + por.size[0], 14 + por.size[1])) + d.text((18, 166), "主角·战斗舞台语境 描边=深墨 (40,19,13)", font=F_LBL2, fill=(225, 215, 195)) # ② 棋盘格语境:小人 48px 压已开格底 - d.rectangle([150, 14, 214, 78], fill=(24, 20, 16)) + d.rectangle([210, 14, 274, 78], fill=(24, 20, 16)) fl = FLOORS[0].resize((64, 64), Image.NEAREST) - img.paste(fl.convert("RGB"), (150, 14)) + img.paste(fl.convert("RGB"), (210, 14)) h48 = HERO.resize((48, int(48 * HERO.size[1] / HERO.size[0])), Image.NEAREST) - img.alpha_composite(h48, (150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8)) - meta["hero_boxes"].append((150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8, - 150 + (64 - h48.size[0]) // 2 + h48.size[0], 14 + 64 - 8)) - d.text((150, 116), "棋盘格语境\n(隔离已开格底)", font=F_LBL2, fill=(225, 215, 195)) + img.alpha_composite(h48, (210 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8)) + meta["hero_boxes"].append((210 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8, + 210 + (64 - h48.size[0]) // 2 + h48.size[0], 14 + 64 - 8)) + d.text((210, 116), "棋盘格语境\n(隔离已开格底)", font=F_LBL2, fill=(225, 215, 195)) # ③ 结算明细带语境:小人 24px 压明细带底 - d.rectangle([238, 14, 302, 78], fill=(46, 39, 32)) + d.rectangle([298, 14, 362, 78], fill=(46, 39, 32)) h24 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST) - img.alpha_composite(h24, (238 + 8, 14 + 52 - h24.size[1])) - meta["hero_boxes"].append((238 + 8, 14 + 52 - h24.size[1], 238 + 8 + h24.size[0], 14 + 52)) - d.text((238, 116), "结算明细带语境\n(同剪影 24px)", font=F_LBL2, fill=(225, 215, 195)) + img.alpha_composite(h24, (298 + 8, 14 + 52 - h24.size[1])) + meta["hero_boxes"].append((298 + 8, 14 + 52 - h24.size[1], 298 + 8 + h24.size[0], 14 + 52)) + d.text((298, 116), "结算明细带语境\n(同剪影 24px)", font=F_LBL2, fill=(225, 215, 195)) # ④ 敌方描边对照:字形多向偏移暖白垫底+本体深朱 for ddx, ddy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]: - d.text((350 + 32 + ddx, 14 + 32 + ddy), "█", font=F_STANDEE, fill=(240, 226, 200), anchor="mm") - d.text((350 + 32, 14 + 32), "█", font=F_STANDEE, fill=(150, 60, 48), anchor="mm") - d.text((350, 116), "敌方描边=暖白族\n(非黑非深褐)", font=F_LBL2, fill=(225, 215, 195)) + d.text((410 + 32 + ddx, 14 + 32 + ddy), "█", font=F_STANDEE, fill=(240, 226, 200), anchor="mm") + d.text((410 + 32, 14 + 32), "█", font=F_STANDEE, fill=(150, 60, 48), anchor="mm") + d.text((410, 116), "敌方描边=暖白族\n(非黑非深褐)", font=F_LBL2, fill=(225, 215, 195)) # ⑤ 护甲三枚展示(源 24×24 挂 HP_BAR_BACK 素底——工单 §5.3 挂载底色口径) for i, key in enumerate(["light", "ward", "heavy"]): - ax = 470 + i * 60 + ax = 500 + i * 60 d.rectangle([ax, 14, ax + 40, 54], fill=(40, 34, 32)) tex = _armor_tex(key) if tex is not None: img.paste(tex, (ax + 8, 22), tex) meta["armor"][key] = (ax + 8, 22, ax + 8 + 24, 22 + 24) d.text((ax + 4, 58), {"light": "轻甲", "ward": "魔甲", "heavy": "重甲"}[key], font=F_LBL2, fill=(225, 215, 195)) - d.text((470, 116), "护甲三枚 24×24\n挂载底=名字行素底", font=F_LBL2, fill=(225, 215, 195)) + d.text((500, 116), "护甲三枚 24×24\n挂载底=名字行素底", font=F_LBL2, fill=(225, 215, 195)) # ⑥ 遮挡对照组:敌像(最底)→旗→小人+蓝框→数字(最顶)——P5 断言:顶层三件仍可辨 cx, cy0 = 700, 10 d.rectangle([cx, cy0, cx + 68, cy0 + 68], fill=(24, 20, 16)) diff --git a/tools/standee_asset_check.py b/tools/standee_asset_check.py index 0be3626d..710313f4 100644 --- a/tools/standee_asset_check.py +++ b/tools/standee_asset_check.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""站桩素材机检(B1 硬闸升级)。 +"""站桩素材机检(B1 硬闸,显示档口径)。 -升级依据=art-bible §8.3③ 挂点规格(审计 §07-2/3): - - 逐键校验档高(逐键断言 STANDEE_TIER_H),宽度按「源宽×2 ≤ 站桩盒宽 150」 - 物理约束校验; +判据(art-bible §8.2 站桩立绘显示档 = 三档绝对高): + - 逐键校验源高(逐件断言 STANDEE_SRC_H——入包件 = 原稿紧裁原样,禁降采样); + - 显示宽按「round(源宽 × 档高 / 源高) ≤ 站桩盒宽 150」校验 + (食人魔酋长 157 超宽 8px 为已接受项,登记例外表逐字校验); - 像素域色数 ≤32(**排除全透明像素**,见下方口径注); - 无非整数倍缩放痕迹(alpha 二值 + 无中间 α); - 描边亮度达标(亮描边暖白族,character-design §10硬规则「禁黑描边」)。 @@ -13,8 +14,9 @@ naive `len(set(所有像素))` 会把全透明 (0,0,0,0) 计成一个色 ⇒ 报33 假红灯。 正确口径 = 只统计 alpha>0 像素的唯一 RGB。机检必须如此,否则天天误报。 -档高表= tools/pixelize_candidates.py 的 STANDEE_TIER_H(定值单点纪律 §8:本表 -不在检查器里复写一份,改由 import 读,避免两处数字漂移)。 +表单点 = tools/pixelize_candidates.py 的 STANDEE_DISPLAY_H / STANDEE_SRC_H / +STANDEE_SLOT_H(定值单点纪律 §8:本件不在检查器里复写一份,改由 import 读; +import 失败即崩——禁兜底字面量造第三处复写)。 用法: python tools/standee_asset_check.py [角色素材目录] @@ -29,16 +31,7 @@ import sys from PIL import Image sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -try: - from pixelize_candidates import STANDEE_TIER_H, STANDEE_SLOT_H -except ImportError: # pragma: no cover - STANDEE_TIER_H = { - "goblin": 36, "giant_spider": 38, - "duergar": 46, "hook_horror": 58, - "bugbear": 62, "ogre_chief": 74, - "drider": 90, "duergar_king": 94, - } - STANDEE_SLOT_H = 48 +from pixelize_candidates import STANDEE_DISPLAY_H, STANDEE_SRC_H, STANDEE_SLOT_H ENEMY_KEYS = ("bugbear", "drider", "duergar", "duergar_king", "giant_spider", "goblin", "hook_horror", "ogre_chief") @@ -48,9 +41,10 @@ REQUIRED = ["character_enemy_%s.png" % k for k in ENEMY_KEYS] + \ # ── 契约常量(全部有出处,禁复写第二份) ────────────────────────────── COLOR_MAX = 32 # 圣经 §4.1/fixer 32 色冻结;机检口径=alpha>0 唯一 RGB -DISPLAY_SCALE = 2 # standee_body.gd:237 _portrait_sprite.scale = (2,2) BOX_W = 150 # standee_body.gd:29 var box := Vector2(150, 176) -SRC_W_MAX = BOX_W // DISPLAY_SCALE # =75,源宽上限(×2 后须塞进盒宽) +# 显示宽例外(已接受项——art-bible §8.2 槽位条):食人魔酋长显示宽 157 超桩盒 150, +# 逐字校验 = 实算宽必须恰等于登记值,源稿一变即红。 +DISPLAY_W_EXCEPT = {"character_enemy_ogre_chief.png": 157} ALPHA_BIN = {0, 255} # 二值 alpha 域 # 描边亮度阈值(实测标定): # 敌人暖白边占比实测 54.1%–84.3%,主角 portrait/hit 实测 0.4%(无暖白描边,见下)。 @@ -72,12 +66,19 @@ def _sat(c): return 0.0 if mx == 0 else (mx - mn) / float(mx) -def expected_tier_h(fn): - """按文件名返回应有档高。""" +def src_key_of(fn): + """按文件名返回 STANDEE_SRC_H 键(敌=键名;主角三态同键 hero)。""" if fn.startswith("character_enemy_") and fn.endswith(".png"): - key = fn[len("character_enemy_"):-len(".png")] - return STANDEE_TIER_H.get(key) - return STANDEE_SLOT_H # 主角三态同档 + return fn[len("character_enemy_"):-len(".png")] + return "hero" # 主角三态同源稿高 + + +def display_h_of(fn): + """按文件名返回显示档高(敌=逐键显示档;主角=中型档 144)。""" + key = src_key_of(fn) + if key == "hero": + return STANDEE_SLOT_H + return STANDEE_DISPLAY_H.get(key) def check(path, fn): @@ -87,15 +88,19 @@ def check(path, fn): w, h = im.size px = im.load() - # 断言①逐键档高 - tier = expected_tier_h(fn) - if tier is not None and h != tier: - problems.append("档高应 %d(§8.2 逐键定档),实得 %d" % (tier, h)) + # 断言① 逐件源高 = 原稿表值(禁降采样——旧降采样稿必红) + src_h = STANDEE_SRC_H.get(src_key_of(fn)) + if src_h is not None and h != src_h: + problems.append("源高应 %d(原稿表值——禁降采样),实得 %d" % (src_h, h)) - # 断言② 宽度物理约束:源宽 ×2 须塞进站桩盒宽 150 - if w > SRC_W_MAX: - problems.append("源宽 %d ×2=%d 超站桩盒宽 %d(§8.2 高定宽随但不得溢出盒)" - % (w, w * DISPLAY_SCALE, BOX_W)) + # 断言② 显示宽物理约束:round(源宽 × 档高 / 源高) ≤ 盒宽 150(酋长 157 例外逐字校验) + disp_h = display_h_of(fn) + disp_w = -1 + if disp_h is not None and src_h: + disp_w = int(round(w * disp_h / float(src_h))) + if disp_w > BOX_W and DISPLAY_W_EXCEPT.get(fn) != disp_w: + problems.append("显示宽 %d 超站桩盒宽 %d(§8.2 高定宽随;例外表未登记)" + % (disp_w, BOX_W)) if w < 1: problems.append("源宽 %d 非法" % w) @@ -119,7 +124,7 @@ def check(path, fn): if nb: edges.append(c) - # 断言③ 底中锚(脚贴画布底缘) + # 断言③ 底中锚(脚贴画布底缘——紧裁件底行即脚底) if bottom_opaque < 1: problems.append("底行无不透明像素(底中锚失守——脚未贴画布底缘)") @@ -148,7 +153,9 @@ def check(path, fn): info = { "size": [w, h], - "tier_h": tier, + "src_h": src_h, + "display_h": disp_h, + "display_w": disp_w, "colors": len(colors), "bottom_opaque": bottom_opaque, "edge_px": len(edges), @@ -188,9 +195,10 @@ def main(): for pr in r["problems"]: print("FAIL %-36s %s" % (r["file"], pr)) else: - print("PASS %-36s %dx%d 档高%d 色数%d 暖白边%.1f%%%s" - % (r["file"], i["size"][0], i["size"][1], i["tier_h"], - i["colors"], i["outline_warm_pct"], + print("PASS %-36s %dx%d 源高%d 显示%dx%d 色数%d 暖白边%.1f%%%s" + % (r["file"], i["size"][0], i["size"][1], i["src_h"], + i["display_w"], i["display_h"], i["colors"], + i["outline_warm_pct"], "(豁免)" if i["outline_exempt"] else "")) print("机检完成:%d 件必需,%d 件未过" % (len(REQUIRED), n_fail)) if not as_json: @@ -200,4 +208,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()