iter48-卡2:卡②(立绘显示档批): 九件素材原稿分辨率入包(紧裁零降采样,STANDEE_SRC_H九键+产物计数自证)+standee_motion显示档PORTRAIT_DISPLAY_H九键钉值(_place_portrait按档算倍率,缺键push_error按1×)+portrait_probe扩display_size/display_scale+闸件standee_asset_check换口径(源高==原稿表值/显示宽≤150酋长157例外逐字)+样张站桩段按显示档取数(A4解冻重校88×92,V10并排30/30)+V13–V18全绿(50checks)+三面对照图

This commit is contained in:
DKChan committed 2026-11-02 01:58:23 +08:00
1 parent 1e35ab947b
commit 4c3f0f6082
9 files changed
+255 -119

No files matched your search

+2 -1
View File
@@ -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)
+5 -1
View File
@@ -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 帧数档挂点) ----
+28 -7
View File
@@ -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 不注册=该态保持现纹理)
+2 -2
View File
@@ -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,
@@ -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)