合并(迭代48·状态合并累加制+立绘显示档+战斗区背景替换三卡): 卡①六行accumulate+累加臂+V1–V4断言+甲案裁决随动(deferred自描述键+相位分类归绿,EV三锚零漂移)/卡②九件原稿分辨率入包+显示档九键钉值+V13–V18+样张30/30+三面对照图/卡③ground_h单源+背景三关选图+连续地面层+光柱与STAGE_BG/BEAM_LIGHT退役+V1–V6含审计补差源码锚; 独立验收审计通过(首轮窄口径不通过三项缺漏修齐后复审通过); 合回后post-merge钩子自动跑CI全量域闸
This commit is contained in:
commit
4c94eee369
36 files changed
+909
-187
No files matched your search
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 帧数档挂点) ----
|
||||
|
||||
@@ -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 不注册=该态保持现纹理)
|
||||
|
||||
@@ -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→程序化占位,冒烟安全)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
# 舞台背景与地面层(真实选路入口进战斗屏后的装载断言——迭代 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)])
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 迁入)
|
||||
|
||||
@@ -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 := {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://dskho0aqpongv
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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 零新键) | 追认(空项) |
|
||||
@@ -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)。
|
||||
@@ -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、来源敌方技能「孤注守势」)——本批考证发现的存量漂移,非本批造成。
|
||||
@@ -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`;陈旧叙述订正),复审结论见审计报告末节。
|
||||
@@ -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 备案执行、九件素材原稿分辨率专项全部成立。本报告为合并前独立验收审计的最终口径。
|
||||
@@ -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。
|
||||
|
||||
## 十二、异常分级
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
+1
-1
@@ -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]
|
||||
|
||||
@@ -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 <game/assets/art/character>]
|
||||
# 九张 48px 站桩槽位档(八敌 pixel-1+主角 normal;
|
||||
# 底中锚/NEAREST/统一比例保体量/画布高恒 48)
|
||||
python tools/pixelize_candidates.py <目录> --standee-display [--standee-out <game/assets/art/character>]
|
||||
# 九张站桩原稿分辨率稿(八敌 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 = []
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
main()
|
||||
Reference in new issue
Block a user