合并(迭代54·三卡): iter54 三笔卡提交(96123978 QA收口/4c424af3 营地交互/f9c9caf5 精英专属池)+审计报告笔(53588800)合入——QA harness 缺陷与覆盖遗漏收口批(A2 终局结算框容错+B1 停手真输入+B2 方法口 41 处全清+死接缝 11 方法+candidate_rect 读口)/营地交互修复批(开局隐藏点收一处+按钮位次镜像+顶栏栏体 camp_hud_band_surface 出口)/精英专属池批(elite 行类+池继承合并+寻址门暂不接线+goblin_elite 落表+图鉴适配)。批内凭据: 卡1 批末--code 六闸绿+卡3 T3.8 --code 六闸绿【验收通过】+独立验收审计三卡全通过(reports/iter54-验收审计-2026-11-07.md); 区内已并 master a2e500a4(测试层合并批, 冲突并集解+终树逐字节一致); S13 ui_kit 基线 23→24 随卡2预申报; 本批零 MC 零数值改动。roadmap 行换代/待办清单推进/当前位换代归文档批(本批产实现报告+反哺点两条+目检登记于 reports/)
ci / gates (push) Successful in 8m5s
ci / gates (push) Successful in 8m5s
This commit is contained in:
commit
5b5e629706
39 files changed
+1260
-171
No files matched your search
@@ -191,7 +191,7 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var pool_n := {"basic": 0, "buff": 0, "debuff": 0, "uncond_special": 0}
|
||||
for entry in row.get("pool", []):
|
||||
for entry in enemy_table.effective_pool(row): # 合并池(elite=基础池+附加技;battle/boss=自身 pool——池构成纪律校验合并后的池)
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
|
||||
@@ -38,7 +38,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
if L < 1 or D < 0:
|
||||
_diag.call("fatal", "request_oob", "enemy 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [L, D])
|
||||
return []
|
||||
if not EnemyTable.ROW_KINDS.has(row_kind):
|
||||
if not EnemyTable.ENCOUNTER_ROW_KINDS.has(row_kind): # 开战寻址行类门——elite 行静置合法、任何寻址拿不到它(敌人系统.md:151)
|
||||
_diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind])
|
||||
return []
|
||||
var rows: Array[Dictionary] = []
|
||||
|
||||
@@ -8,8 +8,10 @@ extends Resource
|
||||
|
||||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||||
|
||||
# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集
|
||||
const ROW_KINDS := ["battle", "boss"]
|
||||
# 行类封闭枚举:表内允许出现的行类(battle/boss 对齐关卡节点 type 词表,elite=精英行)
|
||||
const ROW_KINDS := ["battle", "boss", "elite"]
|
||||
# 开战寻址可读的行类——elite 接线前不在此列(敌人系统 v1.9 暂不接线;resolver 行类门单点消费)
|
||||
const ENCOUNTER_ROW_KINDS := ["battle", "boss"]
|
||||
|
||||
# 护甲类型封闭枚举(克制矩阵本体在 SettlementTable;
|
||||
# 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
|
||||
@@ -49,6 +51,7 @@ func validate() -> Array:
|
||||
errs.append_array(_validate_row_numbers(row, eid2))
|
||||
errs.append_array(_validate_row_atk_range(row, eid2))
|
||||
errs.append_array(_validate_row_pool(row, eid2))
|
||||
errs.append_array(_validate_elite_fields(row, eid2))
|
||||
errs.append_array(_validate_row_types(row, eid2))
|
||||
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
|
||||
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
|
||||
@@ -67,6 +70,7 @@ func validate() -> Array:
|
||||
boss_ok[L2] = true
|
||||
if not anchor_ok.has(L2):
|
||||
anchor_ok[L2] = false
|
||||
errs.append_array(_validate_elite_refs())
|
||||
errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok))
|
||||
return errs
|
||||
|
||||
@@ -85,7 +89,7 @@ func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]:
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss, elite})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
@@ -136,25 +140,99 @@ func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var no_errs: Array[Dictionary] = []
|
||||
if String(row.get("row_kind", "")) == "elite":
|
||||
return no_errs # elite 行不带 pool 键(pool_extra 承载附加技——条目域校验随 _validate_elite_fields)
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool: Array[Variant] = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
errs.append_array(_validate_pool_entries(pool, eid, "pool"))
|
||||
return errs
|
||||
|
||||
|
||||
## 池条目域共用校验(pool 与 pool_extra 共用——权重上限 9999 不出现第二份,契约 3 单点);
|
||||
## field 承载报错定位段("pool"=既有文案逐字不变,"pool_extra"=附加技清单定位)。
|
||||
func _validate_pool_entries(entries: Array, eid: String, field: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
for entry_v in entries:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
errs.append({"category": "domain", "message": "%s %s 条目非字典: %s" % [eid, field, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary[Variant, Variant] = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
errs.append({"category": "domain", "message": "%s %s 条目 skill_id 空" % [eid, field]})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
errs.append({"category": "dup_key", "message": "%s %s 内 skill_id 重复: %s" % [eid, field, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
errs.append({"category": "domain", "message": "%s %s 权重越域 (0,9999]: %d" % [eid, field, w]})
|
||||
return errs
|
||||
|
||||
|
||||
## 精英行字段互锁(敌人系统 v1.9 :281/:282/:283):elite 行必备 base_ref/pool_extra、禁 pool、
|
||||
## enemy_id 不与 base_ref 同名;非 elite 行禁带 base_ref/pool_extra。base_ref 引用存在性归 _validate_elite_refs。
|
||||
func _validate_elite_fields(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if String(row.get("row_kind", "")) == "elite":
|
||||
var base_ref := String(row.get("base_ref", ""))
|
||||
if base_ref == "":
|
||||
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 缺或空(基础敌种引用必带)" % eid})
|
||||
if base_ref == eid:
|
||||
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 与自身 enemy_id 同名(禁)" % eid})
|
||||
if not row.has("pool_extra") or (row.get("pool_extra") as Array).is_empty():
|
||||
errs.append({"category": "cross_field", "message": "%s elite 行 pool_extra 缺键或空(附加技清单非空必带)" % eid})
|
||||
if row.has("pool"):
|
||||
errs.append({"category": "cross_field", "message": "%s elite 行带 pool 键(禁——池走继承+附加)" % eid})
|
||||
if row.has("pool_extra") and (row.get("pool_extra") as Array).size() > 0:
|
||||
errs.append_array(_validate_pool_entries(row.get("pool_extra"), eid, "pool_extra"))
|
||||
else:
|
||||
if row.has("base_ref"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 base_ref 键(禁——基础敌种引用为 elite 行专属)" % eid})
|
||||
if row.has("pool_extra"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 pool_extra 键(禁——附加技清单为 elite 行专属)" % eid})
|
||||
return errs
|
||||
|
||||
|
||||
## 精英行跨行引用校验(敌人系统 v1.9 :148/:282):base_ref 指向同关 battle 行类敌种(缺/空已由
|
||||
## _validate_elite_fields 报——此处跳过);指向 elite 行判别同门 _min_band_row;附加技与基础池
|
||||
## skill_id 交集非空报 dup_key(敌人系统.md:149 附加技与基础池重复拒绝)。
|
||||
func _validate_elite_refs() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if String(row.get("row_kind", "")) != "elite":
|
||||
continue
|
||||
var base_ref := String(row.get("base_ref", ""))
|
||||
if base_ref == "":
|
||||
continue
|
||||
var level := int(row.get("level_index", 0))
|
||||
var eid := String(row.get("enemy_id", ""))
|
||||
var base := _min_band_row(level, base_ref, "battle")
|
||||
if base.is_empty():
|
||||
if not _min_band_row(level, base_ref, "elite").is_empty():
|
||||
errs.append({"category": "dangling_base", "message": "%s base_ref 指向 elite 行: %s(基础敌种须为 battle 行类)" % [eid, base_ref]})
|
||||
else:
|
||||
errs.append({"category": "dangling_base", "message": "%s base_ref 同关 battle 行类无此敌种: %s" % [eid, base_ref]})
|
||||
continue
|
||||
errs.append_array(_extra_overlap_errs(row, base, eid))
|
||||
return errs
|
||||
|
||||
|
||||
## 附加技与基础池重叠判(_validate_elite_refs 交集段提出——分支预算内聚)
|
||||
func _extra_overlap_errs(row: Dictionary, base_row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var base_ids := {}
|
||||
for entry_v in base_row.get("pool", []):
|
||||
if entry_v is Dictionary:
|
||||
base_ids[String(entry_v.get("skill_id", ""))] = true
|
||||
for entry_v in row.get("pool_extra", []):
|
||||
if entry_v is Dictionary:
|
||||
var sid := String(entry_v.get("skill_id", ""))
|
||||
if sid != "" and base_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool_extra 与基础池 skill_id 重复: %s" % [eid, sid]})
|
||||
return errs
|
||||
|
||||
|
||||
@@ -193,6 +271,43 @@ func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[
|
||||
return errs
|
||||
|
||||
|
||||
# 基础行查找单点(按 (关, 敌种, 行类) 取深度带最小行——精英有效池与「指向 elite 判别」共用,
|
||||
# 禁第二份查找算法):无匹配返回空字典。行字段一律 row.get 读取(夹具行可缺键)。
|
||||
func _min_band_row(level: int, eid: String, kind: String) -> Dictionary:
|
||||
var best := {}
|
||||
var best_band := -1
|
||||
for row in rows:
|
||||
if int(row.get("level_index", -1)) != level or String(row.get("row_kind", "")) != kind \
|
||||
or String(row.get("enemy_id", "")) != eid:
|
||||
continue
|
||||
var band := int(row.get("depth_band", -1))
|
||||
if best.is_empty() or band < best_band:
|
||||
best = row
|
||||
best_band = band
|
||||
return best
|
||||
|
||||
|
||||
## 精英有效池(按需现算、零缓存零写回——敌人系统.md:149 继承不复制):非 elite 行 = 自身 pool
|
||||
## 字典条目副本;elite 行 = 基础行(同关 battle 行类基础敌种深度带最小行)pool 条目 + 本行
|
||||
## pool_extra 条目(均只取 is Dictionary 条目并 duplicate——返回值改写不回灌表内行)。
|
||||
func effective_pool(row: Dictionary) -> Array[Dictionary]:
|
||||
var out: Array[Dictionary] = []
|
||||
if String(row.get("row_kind", "")) != "elite":
|
||||
for entry_v in row.get("pool", []):
|
||||
if entry_v is Dictionary:
|
||||
out.append((entry_v as Dictionary).duplicate())
|
||||
return out
|
||||
var base := _min_band_row(int(row.get("level_index", 0)), String(row.get("base_ref", "")), "battle")
|
||||
if not base.is_empty():
|
||||
for entry_v in base.get("pool", []):
|
||||
if entry_v is Dictionary:
|
||||
out.append((entry_v as Dictionary).duplicate())
|
||||
for entry_v in row.get("pool_extra", []):
|
||||
if entry_v is Dictionary:
|
||||
out.append((entry_v as Dictionary).duplicate())
|
||||
return out
|
||||
|
||||
|
||||
# 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的
|
||||
# depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。
|
||||
# D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例;
|
||||
|
||||
@@ -207,6 +207,25 @@ rows = [{
|
||||
"armor_type": "light",
|
||||
"armor_class": 8
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"row_kind": "elite",
|
||||
"depth_band": 0,
|
||||
"enemy_id": "goblin_elite",
|
||||
"display_name": "精英·地精",
|
||||
"unit_count": 1,
|
||||
"hp": 20,
|
||||
"atk": 2,
|
||||
"atk_min": 1,
|
||||
"atk_max": 3,
|
||||
"base_ref": "goblin",
|
||||
"pool_extra": [{
|
||||
"skill_id": "heavy",
|
||||
"weight": 2
|
||||
}],
|
||||
"attack_type": "normal",
|
||||
"armor_type": "light",
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 0,
|
||||
@@ -943,6 +962,7 @@ rows = [{
|
||||
; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0,
|
||||
; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。
|
||||
;
|
||||
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集;
|
||||
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss, elite}——battle=战斗节点配池,boss=终点行集,elite=精英行(独立 enemy_id、池走继承+附加,暂不接线任何节点寻址);
|
||||
; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。
|
||||
; (1,3) 大地精行 2026-11-04 用户裁退役,改地精+熊地精混编(行序熊地精在前、行值整行复用 (1,2)/(1,0)、静态骰和 3);沿革见 git 提交史
|
||||
; 精英专属池批首只精英=地精精英(1,elite,0,goblin_elite),行值 provisional(投值所有权归难度曲线与Boss 统一修正),来源=2026-11-06 用户点头草案 v0
|
||||
@@ -190,8 +190,8 @@ func _start_adventure_from_menu() -> void:
|
||||
# 主菜单「开始冒险」钮已随三入口化撤出(卡② mn6——主菜单侧断点互斥确认链同步摘除,
|
||||
# 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针
|
||||
# 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝
|
||||
# 由卡① sv3 承接(结算不落盘)。
|
||||
ui.screens.start_overlay.visible = false
|
||||
# 由卡① sv3 承接(结算不落盘)。开始层隐藏已收进 _start_adventure()(营地交互修复批契约 1
|
||||
# ——开局路径隐藏点唯一出口)。
|
||||
_start_adventure()
|
||||
|
||||
|
||||
@@ -290,6 +290,7 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
|
||||
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
|
||||
# 信号连接只在组合根)
|
||||
func _start_adventure() -> void:
|
||||
ui.screens.start_overlay.visible = false # 开局路径开始层隐藏唯一出口(营地交互修复批契约 1——三处调用方经此收口;失败分支开始层终态=隐藏,致命屏接管)
|
||||
ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
|
||||
begin_session(true)
|
||||
_wire_adventure()
|
||||
|
||||
@@ -189,20 +189,10 @@ func is_refresh_ready() -> bool:
|
||||
return not _btn_refresh.disabled
|
||||
|
||||
|
||||
# 探针接缝(合成输入走真实翻译出口口径):刷新按钮点击 → 确认 chips 全链路
|
||||
func press_refresh_for_probe() -> void:
|
||||
_on_refresh_pressed()
|
||||
|
||||
|
||||
func confirm_visible() -> bool:
|
||||
return _confirm_row.visible
|
||||
|
||||
|
||||
func press_confirm_for_probe() -> void:
|
||||
if _arm_timer.is_armed():
|
||||
_disarm_and_fire()
|
||||
|
||||
|
||||
func refresh_remaining_text() -> String:
|
||||
return _refresh_sub.text
|
||||
|
||||
|
||||
@@ -191,10 +191,11 @@ func close() -> void:
|
||||
st.seal.visible = false
|
||||
st.seal.modulate = Color.WHITE
|
||||
st.cele.visible = false
|
||||
var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀)
|
||||
if tw != null and tw.is_valid():
|
||||
tw.kill()
|
||||
remove_meta(&"celebrate_tw")
|
||||
if has_meta(&"celebrate_tw"): # 循环句柄随节点存(跨 open/close:有则必杀、无则无事——缺 meta 跳过)
|
||||
var tw: Tween = get_meta(&"celebrate_tw")
|
||||
if tw.is_valid():
|
||||
tw.kill()
|
||||
remove_meta(&"celebrate_tw")
|
||||
_clear_crest_bricks()
|
||||
|
||||
|
||||
@@ -328,9 +329,10 @@ func _spawn_dust_mote(i: int, base: Vector2) -> void:
|
||||
func _play_celebrate() -> void:
|
||||
if UiKit.motion_scale < 1.0:
|
||||
return
|
||||
var prev: Tween = get_meta(&"celebrate_tw", null)
|
||||
if prev != null and prev.is_valid():
|
||||
prev.kill()
|
||||
if has_meta(&"celebrate_tw"): # 有句柄才取并 kill(缺 meta 静默跳过——败走后首次胜利同款容错)
|
||||
var prev: Tween = get_meta(&"celebrate_tw")
|
||||
if prev.is_valid():
|
||||
prev.kill()
|
||||
var tw := st.cele.create_tween()
|
||||
set_meta(&"celebrate_tw", tw)
|
||||
tw.set_loops()
|
||||
|
||||
@@ -11,10 +11,15 @@ func _init() -> void:
|
||||
set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
custom_minimum_size = Vector2(0, 48) # 高 48px 浮层覆盖、不挤压画面(camp.md §顶栏)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE # HUD 栏不挡场景命中区(浮层覆盖语义)
|
||||
UiKit.camp_hud_band_surface(self) # 栏体色面——整件外观单一出口(色值与 α、铺满、不吃点击、挂载全在出口内);先入=绘制序底,徽章后入不被盖
|
||||
_badge = UiKit.keystone_badge("钥石 0")
|
||||
_badge.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
|
||||
_badge.position = Vector2(-160, 8) # 顶栏右位次(UiKit 现行件域内——代拍板 3)
|
||||
add_child(_badge)
|
||||
var dock := HBoxContainer.new() # 右对齐容器承载徽章(Q2 已裁乙——位数增宽自然向左展;摆位机制先例=confirm_dialog 钮行同款容器 alignment)
|
||||
dock.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dock.offset_right = -12.0 # 右缘内缩 12px(UiKit 钮间距同档——白名单 3)
|
||||
dock.alignment = BoxContainer.ALIGNMENT_END
|
||||
dock.mouse_filter = Control.MOUSE_FILTER_IGNORE # 容器自身默认 STOP——与出口内 IGNORE 是两个点,必须同批落(漏一处顶排物件点不动)
|
||||
dock.add_child(_badge)
|
||||
add_child(dock)
|
||||
|
||||
|
||||
## 余额写入(格式串=既有消费面同款「钥石 %d」——badge 工厂约定单点,禁第二格式串)
|
||||
|
||||
@@ -30,13 +30,15 @@ func _init() -> void:
|
||||
box.add_child(btn_row)
|
||||
_cancel_btn = UiKit.button("取消")
|
||||
_cancel_btn.pressed.connect(_emit_cancelled)
|
||||
btn_row.add_child(_cancel_btn)
|
||||
_secondary_btn = UiKit.button("次选项")
|
||||
_secondary_btn.pressed.connect(_emit_secondary)
|
||||
btn_row.add_child(_secondary_btn)
|
||||
_confirm_btn = UiKit.button("确认")
|
||||
_confirm_btn.pressed.connect(_emit_confirmed)
|
||||
# 位次定值(HUD框架.md:90 三条纪律·其三——推进钮在左、次推进居中、维持在右;四处消费方
|
||||
# 共用本件同一序。信号按按钮对象路由、不按位次——换序不改路由〔营地交互修复批契约 3〕)
|
||||
btn_row.add_child(_confirm_btn)
|
||||
btn_row.add_child(_secondary_btn)
|
||||
btn_row.add_child(_cancel_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
|
||||
@@ -153,26 +153,6 @@ func _on_confirm_pressed() -> void:
|
||||
pick_confirmed.emit(_selected)
|
||||
|
||||
|
||||
# 探针/测试复用:以真实按钮路径触发点选、确认与继续(树序遮挡纪律——断言走真实信号路径)
|
||||
func press_card(index: int) -> void:
|
||||
# M7:卡载体 Button→bcard(gui_input 点选)——真实路径=同一 _select_card 处理器,探针语义不变
|
||||
if index >= 0 and index < _card_meta.size():
|
||||
_select_card(String(_card_meta[index].get("id", "")))
|
||||
|
||||
|
||||
func press_confirm() -> void:
|
||||
_confirm_btn.pressed.emit()
|
||||
|
||||
|
||||
func press_continue() -> void:
|
||||
_continue_btn.pressed.emit()
|
||||
|
||||
|
||||
# 探针/测试复用:以真实按钮路径触发刷新意图(信号外发——受理归组合根)
|
||||
func press_refresh() -> void:
|
||||
_refresh_btn.pressed.emit()
|
||||
|
||||
|
||||
# 兑付反馈态(curio_resolved 到达——结果行追加在头部之后;唯一出口 = 继续)
|
||||
func show_result(text: String) -> void:
|
||||
_label.text = _header + "\n" + text
|
||||
|
||||
@@ -123,20 +123,6 @@ func output_text() -> String:
|
||||
return st.sim_out.get_parsed_text()
|
||||
|
||||
|
||||
# ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)----
|
||||
|
||||
func press_apply_for_probe() -> void:
|
||||
_apply_btn_pressed()
|
||||
|
||||
|
||||
func press_reset_for_probe() -> void:
|
||||
_reset_btn_pressed()
|
||||
|
||||
|
||||
func press_simulate_for_probe() -> void:
|
||||
_sim_btn.pressed.emit()
|
||||
|
||||
|
||||
# ---- 内部(信号 → 外发)----
|
||||
|
||||
func _apply_btn_pressed() -> void:
|
||||
|
||||
@@ -465,18 +465,20 @@ func _submit_choice(nid: int) -> void:
|
||||
route_chosen.emit(nid)
|
||||
|
||||
|
||||
# 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造)
|
||||
func press_candidate(index: int) -> void:
|
||||
# 探针面命中矩形读口:候选节点的图上命中矩形(全局坐标——真输入 push_click_at 落点)。
|
||||
# 命中面与 _candidate_at 同源(节点方块+外扩容差);index 越界或候选未就绪返回空 Rect2(调用方跳过)。
|
||||
func candidate_rect(index: int) -> Rect2:
|
||||
if index < 0 or index >= _cand_ids.size() or _map.runner == null:
|
||||
return
|
||||
return Rect2()
|
||||
var nodes: Array[Variant] = _map.runner.nodes()
|
||||
var node := _node_by_id(int(_cand_ids[index]), nodes)
|
||||
if node.is_empty():
|
||||
return
|
||||
return Rect2()
|
||||
var pos: Vector2 = _layout(nodes, _map.canvas)[node]
|
||||
var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块)
|
||||
if nid >= 0:
|
||||
_submit_choice(nid)
|
||||
var half := NODE_HALF + Vector2(HIT_PAD, HIT_PAD)
|
||||
var local := Rect2(pos - half, half * 2.0)
|
||||
var xform := _map.canvas.get_global_transform()
|
||||
return Rect2(xform * local.position, local.size)
|
||||
|
||||
|
||||
# ---- 视口锚点(布局决策 5:当前节点居中;入口停点尚无当前节点=锚入口组包围盒居中) ----
|
||||
|
||||
@@ -29,10 +29,6 @@ func show_rest(healed: int, hp_after: int, max_hp: int) -> void:
|
||||
_btn.text = "休息完毕,继续"
|
||||
visible = true
|
||||
|
||||
# 探针复用:以真实按钮路径触发继续
|
||||
func press_continue() -> void:
|
||||
_btn.pressed.emit()
|
||||
|
||||
|
||||
func _emit_continue_pressed() -> void:
|
||||
continue_pressed.emit()
|
||||
@@ -193,6 +193,7 @@ const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底
|
||||
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)
|
||||
const CAMP_HUD_BAND := Color(CAMP_HEADER_BAND, 0.80) # 营地顶栏栏体=头带深棕×α0.80(camp.md §顶栏定值——派生收口单点,非色板新增;同文件 TOAST_BG/TIP_BG 带 α 先例)
|
||||
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
|
||||
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
|
||||
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台)
|
||||
@@ -656,6 +657,19 @@ static func keystone_badge(text: String, icon_path := TEX_ICON_KEYSTONE, icon_px
|
||||
return KeystoneBadge.new(text, icon_path, icon_px)
|
||||
|
||||
|
||||
## 营地顶栏栏体面(整件外观单一出口——营地交互修复批契约 4/5):色值与 α、铺满、不吃点击、
|
||||
## 挂上宿主五件事全写死在体内;形参只有 host——调用方没有可以忘记的形参。返回句柄供调用方
|
||||
## 读取与断言;徽章摆位不由本出口负责(归顶栏件内右对齐容器)。须在宿主入树前调用
|
||||
## (set_anchors_preset 是入树前动作——_modal_dim/overlay_bg 同形)。
|
||||
static func camp_hud_band_surface(host: Control) -> ColorRect:
|
||||
var rect := ColorRect.new()
|
||||
rect.color = CAMP_HUD_BAND
|
||||
rect.set_anchors_preset(Control.PRESET_FULL_RECT) # 四边 offset 归零铺满宿主
|
||||
rect.mouse_filter = Control.MOUSE_FILTER_IGNORE # 栏体不吃点击(顶排物件命中区不受阻——契约 5)
|
||||
host.add_child(rect)
|
||||
return rect
|
||||
|
||||
|
||||
# 按钮皮·五态矩阵(逐实例 override 不动主题层)
|
||||
# 悬停:常态图+modulate 提亮(0.06 档与 primary_button 现行 lightened(0.06) 同纪律,零新素材);
|
||||
# 禁用:本族底+α0.45 幽灵化(与 font_disabled_color 同纪律);
|
||||
|
||||
@@ -45,7 +45,7 @@ func _ready() -> void:
|
||||
return
|
||||
for i in 5:
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
for i in 20:
|
||||
await get_tree().process_frame
|
||||
var st = main.run_scope.settlement
|
||||
|
||||
@@ -30,7 +30,7 @@ func _ready() -> void:
|
||||
for i in range(8):
|
||||
await get_tree().process_frame
|
||||
# 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
for i in range(20):
|
||||
await get_tree().process_frame
|
||||
for vp in VIEWPORTS:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档,
|
||||
# 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 设置面(settingsbtn
|
||||
# 真实信号路径)→ 生涯页(函数直调中间态——卡② mn10)→ 营地(进档开层新路径——mn9)→ 关卡画面(函数直调中间态→停点出图)→
|
||||
# 战斗(level_screen.press_candidate(0) 真实选路提交)→ 状态面板(地图屏状态角标真实信号路径)。
|
||||
# 战斗(level_screen candidate_rect 命中矩形真点选路提交)→ 状态面板(地图屏状态角标真实信号路径)。
|
||||
# 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录;
|
||||
# 最小自检 = 七件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。
|
||||
# 商店房不纳屏(真实路径口径):真实路径必经 L1 打穿→L2 商店节点
|
||||
@@ -149,10 +149,10 @@ func _ready() -> void:
|
||||
var expect := "◈ %d" % int(main.ui.modals.run.level_screen.get("_runner").gp)
|
||||
if gp_label.text != expect:
|
||||
_problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect])
|
||||
# ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款)
|
||||
# ④ 战斗(节点一——candidate_rect 命中矩形真点选路提交,ui_click_probe 同款)
|
||||
# oracle: design/gdd/ux/battle.md:122 B 区构建:站桩/明细带/轮次指示(战斗出图与几何锚组)
|
||||
if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible:
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
await _snap("battle")
|
||||
|
||||
@@ -37,7 +37,7 @@ func _ready() -> void:
|
||||
return
|
||||
for i in 5:
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
for i in 20:
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.settlement == null:
|
||||
|
||||
+111
-20
@@ -40,6 +40,8 @@ var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用
|
||||
var _locktest_state := 0 # T1 D1 锁期/重放断言状态机(0=待注入 1=等重放 2=完结)——每 run 一次
|
||||
var _locktest_cell := Vector2i(-9, -9)
|
||||
var _locktest_frames := 0
|
||||
var _stoptest_state := 0 # 停手钮定型化覆盖段状态机(0=待命 1=已点击待核效 2=完结)——每 run 一次
|
||||
var _stoptest_frames := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -421,13 +423,13 @@ func _ready() -> void:
|
||||
var wc3_txt: String = main.ui.battle.log_view._log.get_parsed_text()
|
||||
if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"):
|
||||
_problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)")
|
||||
# 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战
|
||||
# 关卡开始 = 入口选路停点——candidate_rect 命中矩形中心真点选入口候选进首战
|
||||
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
|
||||
if main.ui.modals.run.level_screen.visible:
|
||||
# 独立性断言:独立屏激活期战斗屏宿主整体隐藏
|
||||
if main.ui.screens.battle_root.visible:
|
||||
_problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var t_entry := Time.get_ticks_msec() - t1
|
||||
@@ -449,7 +451,7 @@ func _ready() -> void:
|
||||
action_budget -= 1
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.node_overlay.visible:
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.ui.modals.run.curio_overlay.visible:
|
||||
@@ -474,15 +476,17 @@ func _ready() -> void:
|
||||
if not main.ui.flow.locks_input():
|
||||
_problems.append("三选一停点打开时输入未锁定")
|
||||
if main.ui.modals.run.curio_overlay._continue_btn.visible:
|
||||
main.ui.modals.run.curio_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
main.ui.modals.run.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
|
||||
main.ui.modals.run.curio_overlay.press_confirm()
|
||||
ProbeUiUtil.push_click_at(main, (main.ui.modals.run.curio_overlay._bcards[0] as Control).get_global_rect().get_center()) # 奖励节点待选态:真点首卡+确认(gui_input 点选——与关卡完成停点同形)
|
||||
await get_tree().process_frame # 选卡当帧确认钮才现形、容器重排在途——排版帧后矩形成形再真点
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._confirm_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.ui.modals.run.curio_overlay.press_card(0)
|
||||
main.ui.modals.run.curio_overlay.press_confirm()
|
||||
ProbeUiUtil.push_click_at(main, (main.ui.modals.run.curio_overlay._bcards[0] as Control).get_global_rect().get_center())
|
||||
await get_tree().process_frame # 同上:确认钮排版帧
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._confirm_btn)
|
||||
else:
|
||||
main.ui.modals.run.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn) # 停点已消费的残面——继续关屏
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.ui.modals.menu.shop_overlay.visible:
|
||||
@@ -537,7 +541,7 @@ func _ready() -> void:
|
||||
if String(nd.get("type", "")) == "reward" and main.ui.modals.run.level_screen._cand_ids.has(nd_id):
|
||||
pick = main.ui.modals.run.level_screen._cand_ids.find(nd_id)
|
||||
break
|
||||
main.ui.modals.run.level_screen.press_candidate(pick)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(pick).get_center())
|
||||
_route_clicks += 1
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
@@ -626,11 +630,13 @@ func _ready() -> void:
|
||||
_ap_spent_this_round = 0
|
||||
_last_state = st
|
||||
_locktest_tick()
|
||||
_stoptest_tick()
|
||||
if main.run_scope.settlement.battle_result == "" and main.run_scope.settlement.action_economy.phase == "Active":
|
||||
# T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在
|
||||
# 帧末 deferred 重放之前改变英雄位(seal 拒收 not_adjacent 实证)——「注入→重放」
|
||||
# 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。
|
||||
if _locktest_state != 1:
|
||||
# 停手定型化段同款冻结:核效窗口内零挖掘提交,排除 AP/盘面漂移对收轮判据的混淆。
|
||||
if _locktest_state != 1 and _stoptest_state != 1:
|
||||
_run_modal_probes_once()
|
||||
_play_one_action()
|
||||
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
|
||||
@@ -651,14 +657,15 @@ func _ready() -> void:
|
||||
break
|
||||
elif main.ui.modals.run.curio_overlay.visible:
|
||||
if main.ui.modals.run.curio_overlay._continue_btn.visible:
|
||||
main.ui.modals.run.curio_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.ui.modals.run.curio_overlay.press_card(0)
|
||||
main.ui.modals.run.curio_overlay.press_confirm()
|
||||
ProbeUiUtil.push_click_at(main, (main.ui.modals.run.curio_overlay._bcards[0] as Control).get_global_rect().get_center())
|
||||
await get_tree().process_frame # 选卡当帧确认钮才现形、容器重排在途——排版帧后矩形成形再真点
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._confirm_btn)
|
||||
else:
|
||||
main.ui.modals.run.curio_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
elif main.ui.modals.run.node_overlay.visible:
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
elif main.ui.modals.menu.shop_overlay.visible:
|
||||
var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if shop_leave2 != null:
|
||||
@@ -737,10 +744,30 @@ func _ready() -> void:
|
||||
_problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center())
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」→开始新冒险
|
||||
await get_tree().process_frame
|
||||
# R4 位次真点(营地交互修复批——位次与语义绑定钉):真点前消费方前置读(gate_new 在册)→
|
||||
# 按类型定位按钮行 child(0) 全局矩形中心真点——位次若错接,此点命中「否」,下方主证(翻转)即红。
|
||||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 确认弹窗位次定值:推进在左、维持居右(R4 位次真点)
|
||||
if String(main._confirm_consumer) != "gate_new":
|
||||
_problems.append("R4 前置:确认消费方非 gate_new(分派面漂移)〔营地交互批 V3〕")
|
||||
var gate_row := _first_hbox(main.ui.modals.menu.confirm_dialog)
|
||||
if gate_row == null or gate_row.get_child_count() != 3:
|
||||
_problems.append("R4:按钮行缺失或子节点数≠3(位次判据面塌)〔营地交互批 V3〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click_at(main, (gate_row.get_child(0) as Control).get_global_rect().get_center()) # 真点推进钮(左位)
|
||||
for i in 3:
|
||||
await get_tree().process_frame # 停点成形帧口径(先例 3 帧——screen_flow_probe 选路停点成形窗)
|
||||
if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible:
|
||||
_problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕")
|
||||
if String(main._confirm_consumer) != "":
|
||||
_problems.append("R4 主证:确认后消费方未清零〔营地交互批 V3〕")
|
||||
# 开局终态两钉(营地交互修复批——开始层隐藏收一处后的落点断言):
|
||||
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:78 关卡开始 → 呈现节点图选路停点(开局终态两钉)
|
||||
# R1=确认后开始层不可见(被测改动特有提交面——修复前该句恒红=「开局被开始层盖住」缺陷本体);
|
||||
# R2=关卡画面(选路停点)在位(开局终态呈现面,与既有 is_adventure 断言同批缺一即红)
|
||||
if main.ui.screens.start_overlay.visible:
|
||||
_problems.append("大门无断点确认后:开始层滞留可见(开局被开始层盖住缺陷回潮)〔营地交互批 V1〕")
|
||||
if not main.ui.modals.run.level_screen.visible:
|
||||
_problems.append("大门无断点确认后:关卡画面(选路停点)未在位〔营地交互批 V2〕")
|
||||
# 有断点三出口(选路停点形注入——同开场行③ set_breakpoint 先例)
|
||||
main.save_manager.set_breakpoint({
|
||||
"master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0,
|
||||
@@ -978,10 +1005,13 @@ func _run_modal_probes_once() -> void:
|
||||
_tested_refresh_chain = true
|
||||
var rap_before: int = int(main.run_scope.settlement.action_economy.rap)
|
||||
var board_before = main.run_scope.settlement.minesweeper.current_board()
|
||||
main.ui.battle.board_zone.ap_bar.press_refresh_for_probe()
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_refresh) # 真点弃图刷新(disabled 守卫=真实拒绝路径)
|
||||
if not main.ui.battle.board_zone.ap_bar.confirm_visible():
|
||||
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
|
||||
main.ui.battle.board_zone.ap_bar.press_confirm_for_probe()
|
||||
for c in main.ui.battle.board_zone.ap_bar._confirm_row.get_children():
|
||||
if c is Button:
|
||||
ProbeUiUtil.push_click(main, c) # 真点确认 chips 真钮(异位确认——_confirm_row 内 Button 子件)
|
||||
break
|
||||
if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8:
|
||||
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)])
|
||||
if main.run_scope.settlement.action_economy.phase != "Active":
|
||||
@@ -1155,6 +1185,67 @@ func _locktest_pick_cell(minesweeper) -> Vector2i:
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
|
||||
# ---- 停手钮定型化覆盖段(B1——每 run 一次:使能窗内真点停手钮,钉命中登记与收轮落效) ----
|
||||
# 触发条件=战局挖掘期(相位 Active+停手钮使能+盘面有挖可走+本 run 未测过);触发帧冻结
|
||||
# 自动玩家其余随机行为,仅提交停手(驱动与 _click_stop 同型——AP 条真钮 push_input)。
|
||||
# 断言两钉:①覆盖登记件停手键在场(零命中=驱动空转假绿,即红);②相位离开 Active(停手
|
||||
# 收轮)。前提消失守卫:点击后战局收场(终结演播竞态)→ 静默弃测不计 problem(正常局不触发)。
|
||||
func _stoptest_tick() -> void:
|
||||
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:167 (停手=玩家主动收轮——判定上报通道)
|
||||
if _stoptest_state == 2:
|
||||
return
|
||||
var settlement = main.run_scope.settlement
|
||||
if settlement == null or settlement.minesweeper == null or settlement.minesweeper.current_board() == null:
|
||||
return
|
||||
if _stoptest_state == 0:
|
||||
if String(settlement.battle_result) != "" or main.ui.modals.run.end_overlay.is_open():
|
||||
return # 战局已收场——本 run 无停手窗(静置)
|
||||
if not main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
return # 演播期不出手:点击会进锁期缓存被吞(同 _locktest_tick 前提)
|
||||
var ae = settlement.action_economy
|
||||
if String(ae.phase) != "Active" or int(ae.ap) < 1:
|
||||
return
|
||||
var stop_btn: Button = main.ui.battle.board_zone.ap_bar._btn_stop
|
||||
if stop_btn == null or stop_btn.disabled:
|
||||
return # 停手钮未使能——下帧重试
|
||||
if AutoPlayer.choose_dig(settlement.minesweeper).is_empty():
|
||||
return # 盘面已无可挖——不算挖掘期触发窗(防清图混淆)
|
||||
_stoptest_state = 1
|
||||
_stoptest_frames = 0
|
||||
_click_stop()
|
||||
elif _stoptest_state == 1:
|
||||
_stoptest_frames += 1
|
||||
if _stoptest_frames > 120:
|
||||
_problems.append("停手定型化段:点击后 120 帧相位仍 Active(收轮落效失守)〔QA收口 V2〕")
|
||||
_stoptest_state = 2
|
||||
return
|
||||
if String(settlement.battle_result) != "" or main.ui.modals.run.end_overlay.is_open():
|
||||
_stoptest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测
|
||||
return
|
||||
if String(settlement.action_economy.phase) != "Active":
|
||||
_stoptest_assert_hit()
|
||||
_stoptest_state = 2
|
||||
|
||||
|
||||
func _stoptest_assert_hit() -> void:
|
||||
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:167 (停手钮真输入命中登记——判定上报通道)
|
||||
var stop_btn: Button = main.ui.battle.board_zone.ap_bar._btn_stop
|
||||
var stop_key := String(_recorder._node_keys.get(stop_btn.get_instance_id(), ""))
|
||||
if stop_key == "" or not _recorder._hits.has(stop_key):
|
||||
_problems.append("停手定型化段:停手钮真点零命中登记(驱动空转假绿)〔QA收口 V2〕")
|
||||
|
||||
|
||||
func _first_hbox(node: Node) -> HBoxContainer:
|
||||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 (位次真点定位——R4 判据面辅助)
|
||||
if node is HBoxContainer:
|
||||
return node
|
||||
for ch in node.get_children():
|
||||
var found := _first_hbox(ch)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _report_stall(st: Dictionary) -> void:
|
||||
# oracle: design/engineering/test-conventions.md:30 (判定上报通道——验证函数组)
|
||||
var why := ""
|
||||
|
||||
@@ -75,7 +75,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
# ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层;
|
||||
# press_candidate 图上命中路径 pick 入口单候选进首战)----
|
||||
# candidate_rect 命中矩形真点 pick 入口单候选进首战)----
|
||||
# oracle: design/gdd/ux/level-map.md:11 入口停点候选数=入口个数(入口单候选断言)
|
||||
# ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)----
|
||||
if not main.ui.modals.run.level_screen.visible:
|
||||
@@ -86,7 +86,7 @@ func _ready() -> void:
|
||||
or main.run_scope.runner.next_candidate_ids().size() != 1:
|
||||
problems.append("入口停点状态失守(选路态 + 入口单候选)")
|
||||
else:
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if not main.ui.screens.battle_root.visible or main.ui.modals.run.level_screen.visible:
|
||||
|
||||
@@ -272,7 +272,7 @@ func _ready() -> void:
|
||||
if not main.ui.modals.run.node_overlay.visible:
|
||||
problems.append("[node] 节点结算层未打开(rest 兑付路径中断)")
|
||||
await _audit("node_overlay", [main.ui.modals.run.node_overlay])
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
_settle()
|
||||
if main.ui.modals.run.node_overlay.visible:
|
||||
problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)")
|
||||
|
||||
@@ -58,16 +58,47 @@ func _ready() -> void:
|
||||
problems.append("生效预览未联动(%s)" % main.ui.modals.menu.debug_overlay.effect_text())
|
||||
# ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照
|
||||
# oracle: design/engineering/test-conventions.md:49 调试面加成链路:滑条联动/应用/重置、hero 表运行期只读(②③④⑦组)
|
||||
main.ui.modals.menu.debug_overlay.press_apply_for_probe()
|
||||
var apply_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "应用并关闭")
|
||||
if apply_btn == null:
|
||||
problems.append("调试浮窗「应用并关闭」钮缺失")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, apply_btn) # 真点应用(组合根应用=关浮窗——落点断言由 ④ 重开承载)
|
||||
# ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线)
|
||||
main._show_debug_overlay()
|
||||
main.ui.modals.menu.debug_overlay.press_reset_for_probe()
|
||||
var reset_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "重置为 0")
|
||||
if reset_btn == null:
|
||||
problems.append("调试浮窗「重置为 0」钮缺失")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, reset_btn)
|
||||
# ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位;
|
||||
# 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕)
|
||||
# oracle: design/engineering/test-conventions.md:49 调试面批量模拟链路:N=10 输出关卡完成率/剩余 HP/败北节点(⑤组)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(100, 0)
|
||||
main.ui.modals.menu.debug_overlay.set_sim_count(10)
|
||||
main.ui.modals.menu.debug_overlay.press_simulate_for_probe()
|
||||
# 折叠区控件真输入通道(test-conventions §2——先滚轮滚入可视区再点):sim 钮在滚动列表 boot
|
||||
# 滚位下跨出视口底边(rect y=639 对 scroll 底 641、钮高 40——hit 面被裁),真点必落空;
|
||||
# 滚轮下探滚入可视区后真点(实证一档入视口、同帧 is_running 翻真)
|
||||
var sim_btn: Button = main.ui.modals.menu.debug_overlay._sim_btn
|
||||
var sim_scroll: ScrollContainer = null
|
||||
var sim_stack: Array = [main.ui.modals.menu.debug_overlay]
|
||||
while not sim_stack.is_empty():
|
||||
var sn: Node = sim_stack.pop_back()
|
||||
if sn is ScrollContainer:
|
||||
sim_scroll = sn
|
||||
break
|
||||
for sch in sn.get_children():
|
||||
sim_stack.append(sch)
|
||||
if sim_scroll == null:
|
||||
problems.append("调试浮窗滚动列表缺失(sim 钮滚入通道无载体)")
|
||||
else:
|
||||
for i in 12:
|
||||
if sim_scroll.get_global_rect().encloses(sim_btn.get_global_rect()):
|
||||
break
|
||||
ProbeUiUtil.push_wheel(main, sim_scroll.get_global_rect().get_center(), 1)
|
||||
await get_tree().process_frame
|
||||
if not sim_scroll.get_global_rect().encloses(sim_btn.get_global_rect()):
|
||||
problems.append("sim 钮滚轮滚入可视区失败(12 档未入——折叠区真输入通道断)")
|
||||
ProbeUiUtil.push_click(main, sim_btn) # 真点「运行模拟」(菜单态未锁可点——disabled 守卫=真实拒绝路径)
|
||||
var guard := SIM_GUARD_FRAMES
|
||||
while main.ui.modals.menu.debug_overlay.is_running() and guard > 0:
|
||||
guard -= 1
|
||||
@@ -97,7 +128,12 @@ func _ready() -> void:
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(100, 50)
|
||||
main.ui.modals.menu.debug_overlay.press_apply_for_probe()
|
||||
main._show_debug_overlay() # ⑥ 关闭后重开——⑧ 应用真点(应用=组合根关窗,⑧b 显式关浮窗断言照旧承载)
|
||||
var apply_btn2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "应用并关闭")
|
||||
if apply_btn2 == null:
|
||||
problems.append("调试浮窗「应用并关闭」钮缺失(⑧)")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, apply_btn2)
|
||||
main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed)
|
||||
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
|
||||
await get_tree().process_frame
|
||||
@@ -107,9 +143,9 @@ func _ready() -> void:
|
||||
problems.append("开冒险路径未显式关浮窗(T2 防护失守)")
|
||||
if int(main.run_scope.adventure.master_seed) != 42:
|
||||
problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed))
|
||||
# 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照
|
||||
# 关卡开始 = 入口选路停点——candidate_rect 命中矩形真点选入口候选进首战再读呈现快照
|
||||
if main.ui.modals.run.level_screen.visible:
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
## 运行:Godot_console.exe --headless --path . --script res://sim/compendium_dump.gd -- [out=<绝对路径>] [tables_root=<res:// 路径>]
|
||||
## out 缺省 = 仓库根 sim-data/compendium/dump.json(res:// 的上一级);目录不存在则创建。
|
||||
## tables_root 缺省 = res://data/tables;fail-fast 路径验证时可指向临时表目录。
|
||||
## 导出形态:顶层 counts 带两表行数计数 + enemy_rows/skill_rows 行字典原样(零新增字段、零改名;
|
||||
## 表内可省略字段缺席即缺席)。本件零随机、零数值硬编码、零判定逻辑。
|
||||
## 导出形态:顶层 counts 带两表行数计数 + enemy_rows/skill_rows 行字典原样,elite 行另补合并后的
|
||||
## pool 供展示(表内行零改写;battle/boss 行=自身 pool 原样;表内可省略字段缺席即缺席)。
|
||||
## 本件零随机、零数值硬编码、零判定逻辑。
|
||||
## fail-fast:表加载失败(缺表/行域校验/跨表悬空引用)、行缺关键字段、pool.skill_id 在技能表无对应行
|
||||
## → 逐条报错并以退出码 1 中止,禁软回退。
|
||||
extends SceneTree
|
||||
@@ -29,15 +30,23 @@ func _initialize() -> void:
|
||||
return
|
||||
var enemy_rows: Array = db.combat.enemy.rows
|
||||
var skill_rows: Array = db.combat.skill.rows
|
||||
var errs := _validate_rows(enemy_rows, skill_rows)
|
||||
# 导出行数组:逐行 duplicate——elite 行副本写合并后的 pool(按需现算,表内行不被改写);
|
||||
# battle/boss 行=自身 pool 原样(读法等价面)
|
||||
var export_enemy_rows: Array = []
|
||||
for row_v in enemy_rows:
|
||||
var row: Dictionary = (row_v as Dictionary).duplicate()
|
||||
if String(row.get("row_kind", "")) == "elite":
|
||||
row["pool"] = db.combat.enemy.effective_pool(row_v)
|
||||
export_enemy_rows.append(row)
|
||||
var errs := _validate_rows(export_enemy_rows, skill_rows)
|
||||
if not errs.is_empty():
|
||||
for e in errs:
|
||||
push_error("compendium_dump: %s" % e)
|
||||
quit(1)
|
||||
return
|
||||
var payload := {
|
||||
"counts": {"enemy_rows": enemy_rows.size(), "skill_rows": skill_rows.size()},
|
||||
"enemy_rows": enemy_rows,
|
||||
"counts": {"enemy_rows": export_enemy_rows.size(), "skill_rows": skill_rows.size()},
|
||||
"enemy_rows": export_enemy_rows,
|
||||
"skill_rows": skill_rows,
|
||||
}
|
||||
var out_path := String(opt.out)
|
||||
|
||||
+42
-16
@@ -10,9 +10,9 @@
|
||||
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
|
||||
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
|
||||
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
|
||||
# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。
|
||||
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
|
||||
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。
|
||||
# 驱动纪律:按钮/场景物件/地图候选全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道;
|
||||
# 地图候选=LevelMapScreen.candidate_rect 命中矩形中心 push_click_at——方法口已清零)。
|
||||
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)=余下唯一方法口。
|
||||
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
|
||||
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
|
||||
extends Node
|
||||
@@ -41,6 +41,10 @@ var _map_status_done := false # 地图屏状态角标一次性
|
||||
var _legend_done := false # 图例开合一次性(每战斗面一次)
|
||||
var _status_done := false # 战斗屏状态面板一次性
|
||||
var _refresh_done := false # 刷新确认链一次性
|
||||
var _stop_done := false # 停手行为一次性(每战至多一次——挖掘期确定性触发)
|
||||
var _stop_settlement: Variant = null # 停手基线战局(换战局重置一次性)
|
||||
var _stop_round := -1 # 停手轮次基线(轮换重置已花计数)
|
||||
var _digs_this_round := 0 # 本轮已提交挖掘数(停手条件——AP 已花 ≥1)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -376,11 +380,11 @@ func _phase_adventure() -> void:
|
||||
_record_screens()
|
||||
var st := _screen_state()
|
||||
if bool(st["节点"]):
|
||||
_last_action = "节点层「继续」(方法-视图口)"
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
_last_action = "真点节点层「继续」钮"
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
continue
|
||||
if bool(st["三选一"]):
|
||||
_drive_curio()
|
||||
await _drive_curio()
|
||||
continue
|
||||
if bool(st["商店"]):
|
||||
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
@@ -437,30 +441,42 @@ func _drive_map_once() -> void:
|
||||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||||
ProbeUiUtil.push_click(main, resumed)
|
||||
_last_action = "地图候选命中(方法-地图命中口)"
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
_last_action = "地图候选命中(真实输入-命中矩形)"
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
|
||||
|
||||
func _drive_curio() -> void:
|
||||
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
|
||||
# 三选一停点:待选态真点首卡+确认(gui_input 点选——确认钮现形当帧容器重排在途,排版帧后再点);
|
||||
# 兑付/已消费态真点继续
|
||||
var co: Control = main.ui.modals.run.curio_overlay
|
||||
_last_action = "三选一停点分发(方法-视图口)"
|
||||
_last_action = "三选一停点分发(真输入)"
|
||||
if co._continue_btn.visible:
|
||||
co.press_continue()
|
||||
ProbeUiUtil.push_click(main, co._continue_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center())
|
||||
await _frames(2)
|
||||
ProbeUiUtil.push_click(main, co._confirm_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center())
|
||||
await _frames(2)
|
||||
ProbeUiUtil.push_click(main, co._confirm_btn)
|
||||
else:
|
||||
co.press_continue()
|
||||
ProbeUiUtil.push_click(main, co._continue_btn)
|
||||
|
||||
|
||||
func _drive_battle_once() -> void:
|
||||
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
|
||||
var settlement = main.run_scope.settlement
|
||||
var diag: Dictionary = settlement.diagnostic_snapshot()
|
||||
if settlement != _stop_settlement: # 换战局——停手一次性与轮内计数重置
|
||||
_stop_settlement = settlement
|
||||
_stop_done = false
|
||||
_stop_round = -1
|
||||
_digs_this_round = 0
|
||||
var round_now := int(diag.round)
|
||||
if round_now != _stop_round:
|
||||
_stop_round = round_now
|
||||
_digs_this_round = 0
|
||||
var st := {
|
||||
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
|
||||
"revealed": diag.minesweeper_revealed,
|
||||
@@ -514,6 +530,15 @@ func _drive_battle_once() -> void:
|
||||
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
|
||||
var minesweeper = settlement.minesweeper
|
||||
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
|
||||
# 停手行为(每战至多一次——挖掘期确定性触发,无随机占比;触发时点微调在代拍板白名单):
|
||||
# 相位 Active+停手钮使能+AP 已花 ≥1+盘面有挖可走 → 真点停手钮收轮(真输入行为之一)
|
||||
if not _stop_done and String(diag.action_economy.phase) == "Active" \
|
||||
and not ap_bar._btn_stop.disabled and _digs_this_round >= 1 and not plan.is_empty():
|
||||
_stop_done = true
|
||||
_last_action = "真点「停手」钮(挖掘期定型化触发)"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
|
||||
await _frames(2)
|
||||
return
|
||||
if plan.is_empty():
|
||||
plan = AutoPlayer.gamble_pick(minesweeper)
|
||||
if plan.is_empty():
|
||||
@@ -525,6 +550,7 @@ func _drive_battle_once() -> void:
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||||
_digs_this_round += 1
|
||||
await _frames(1)
|
||||
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/run_time_fatal_test.gd", "expect": 1, "negative": true}, # 卡② wr5/wr6/wr7 运行期违约通道(fatal 封印传导/未知标签/未播种)
|
||||
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/data_config/table_structure_guard_test.gd", "expect": 0}, # 表结构守卫宿主(adventure 配额与同调目录/enemy 带键行序/AC 单轨与掷点参数/variation 门控互锁/difficulty 修饰/reward 九型/meta 行族)
|
||||
{"path": "res://tests/unit/data_config/elite_row_test.gd", "expect": 0}, # 精英行校验/有效池合并/继承不复制/合并池纪律/暂不接线(精英专属池批)
|
||||
{"path": "res://tests/unit/data_config/rng_stream_boot_test.gd", "expect": 0}, # 启动初始化与统一 RNG 流宿主(data/tables 加载链/统一流黄金向量 m=42/derive_at 恒等/open_stream 取号序/new_adventure 计数器重置——批 B 自 smoke_all 迁入;源 :70 事件日志落盘条按裁决一转办跨系统闭环宿主)
|
||||
{"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面
|
||||
{"path": "res://tests/unit/core/node_map_generator_test.gd", "expect": 0}, # 节点图生成器产出合格/可复现/岔路取舍/恰一店/归属锁定/战斗保底/出口结构/RNG 纯流面(卡① V1·V3·V5–V7·V9·V10)
|
||||
@@ -200,6 +201,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线)
|
||||
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V12(血条危险红/名字行素底不压地台与提档 FS_HINT/徽标/护甲 PNG 绑定/素材入包/地台色合并退役)
|
||||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
|
||||
{"path": "res://tests/unit/presentation/confirm_dialog_test.gd", "expect": 0}, # 确认弹窗按钮位次与语义同钉(推进/次推/维持子节点序——营地交互修复批 R3)
|
||||
{"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 判据+七命中区几何红线)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(lifecycle 活战斗驱动;smoke 库件迁移期按现路径 preload)
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 真输入合成单点(tests 面复用共用件——禁复制第二份合成输入件)
|
||||
|
||||
var _failures: int = 0
|
||||
var _checks: int = 0
|
||||
@@ -43,6 +44,7 @@ func _split_tail() -> void:
|
||||
await _smoke_run_lifecycle()
|
||||
await _smoke_sim_guards()
|
||||
await _smoke_lifecycle_matrix()
|
||||
await _smoke_camp_hud_band()
|
||||
else:
|
||||
check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)")
|
||||
print("[system_test] 摘要:%d 项检查,%d 项失败" % [_checks, _failures])
|
||||
@@ -436,8 +438,8 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
await process_frame
|
||||
await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null",
|
||||
{"mf_start": false, "level_vis": true})
|
||||
# 阶段③地图选路→战斗中(真实选路提交 press_candidate)
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
# 阶段③地图选路→战斗中(真实选路提交——candidate_rect 命中矩形真点)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||||
for i in range(10):
|
||||
await process_frame
|
||||
await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false})
|
||||
@@ -602,4 +604,49 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp",
|
||||
"user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
|
||||
func _smoke_camp_hud_band() -> void:
|
||||
# oracle: design/gdd/ux/camp.md:41 (营地顶栏栏体定值与落座——几何/色值/吃点击五钉组)
|
||||
print("--- 营地交互修复批 T3 顶栏栏体(R5:色值/铺满/不吃点击/首子件/徽章落座)---")
|
||||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
await process_frame
|
||||
main._camp_pres.open_camp() # 营地屏 boot 态为隐——先开营地(装配链 main_ui_build→camp_scene_view 落座)
|
||||
for i in range(3):
|
||||
await process_frame # 排版帧(容器重排在途读数不可信)
|
||||
var band: Control = main.ui.modals.camp.scene.hud_band
|
||||
check(band != null and band.is_inside_tree(), "R5 前置:顶栏件经装配链在树(camp_scene_view.hud_band 公开句柄)")
|
||||
if band == null:
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
return
|
||||
var badge_min: float = band.get_child(band.get_child_count() - 1).get_child(0).get_combined_minimum_size().y if band.get_child_count() > 0 else -1.0
|
||||
print("[system_test] 徽章 get_combined_minimum_size().y 实测 = %s(T3.0 前置读数——48=顶格落座前提)" % str(badge_min))
|
||||
check(band.get_child_count() >= 2 and band.get_child(0) is ColorRect,
|
||||
"R5④ 栏体色面是顶栏件第一个子节点(子节点序=绘制序——色面先入不盖徽章)")
|
||||
if band.get_child_count() >= 2 and band.get_child(0) is ColorRect:
|
||||
var surface: ColorRect = band.get_child(0)
|
||||
check(String(surface.color.to_html(true)) == "6e4526cc",
|
||||
"R5① 栏体色=#6E4526×α0.80(to_html 字面编码——禁浮点分量直等)〔营地交互批 V4〕")
|
||||
var band_rect: Rect2 = band.get_global_rect()
|
||||
var surf_rect: Rect2 = surface.get_global_rect()
|
||||
print("[system_test] 顶栏件实测 get_global_rect=%s、栏体=%s(R5② 承重=ui_registry FULL_RECT 注册)" % [band_rect, surf_rect])
|
||||
check(absf(surf_rect.size.x - band_rect.size.x) < 0.51 and absf(surf_rect.size.y - band_rect.size.y) < 0.51
|
||||
and absf(surf_rect.size.y - 48.0) < 0.51,
|
||||
"R5② 栏体铺满宿主且高 48(全局矩形等值)〔营地交互批 V4〕")
|
||||
check(surface.mouse_filter == Control.MOUSE_FILTER_IGNORE,
|
||||
"R5③ 栏体不吃点击(MOUSE_FILTER_IGNORE——顶排物件命中区不受阻)〔营地交互批 V4〕")
|
||||
var dock: Control = band.get_child(band.get_child_count() - 1)
|
||||
var badge: Control = dock.get_child(0) if dock.get_child_count() > 0 else null
|
||||
check(badge != null,
|
||||
"R5⑤ 前置:徽章经右对齐容器在座(dock 尾子件)")
|
||||
if badge != null:
|
||||
var brect: Rect2 = badge.get_global_rect()
|
||||
check(absf(brect.position.y - band_rect.position.y) < 0.51
|
||||
and absf(brect.end.x - (band_rect.end.x - 12.0)) < 0.51,
|
||||
"R5⑤ 徽章顶格右位(y=栏顶格、end.x=栏右缘−12——全局矩形不与局部 position 混写)〔营地交互批 V4〕")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
@@ -527,7 +527,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
and is_equal_approx(float(venom.rider_mult), 0.75))
|
||||
var venom_sole_ok := true
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
|
||||
venom_sole_ok = false
|
||||
Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok)
|
||||
@@ -538,7 +538,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4)
|
||||
var snare_sole_ok := true
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
|
||||
snare_sole_ok = false
|
||||
Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok)
|
||||
@@ -557,7 +557,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
|
||||
var bash_sole_ok := true # 盾击只挂灰矮人族三行
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
var sid := String(entry.skill_id)
|
||||
if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \
|
||||
and int(row.depth_band) > spider_lead_band):
|
||||
@@ -608,6 +608,8 @@ func _smoke_skill_pool_data() -> void:
|
||||
"3_boss_11_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
"3_boss_12_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
"3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
# 精英专属池批:首只精英=地精精英(有效池=基础池 goblin 9/1/1+附加技 heavy 2——expect_pools 比对读 effective_pool)
|
||||
"1_elite_0_goblin_elite": {"attack": 9, "lunge": 1, "desperate_guard": 1, "heavy": 2},
|
||||
}
|
||||
var pools_ok := true
|
||||
for row in _db.combat.enemy.rows:
|
||||
@@ -616,15 +618,15 @@ func _smoke_skill_pool_data() -> void:
|
||||
pools_ok = false
|
||||
continue
|
||||
var got := {}
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
got[String(entry.skill_id)] = int(entry.weight)
|
||||
if got != expect_pools[rk]:
|
||||
pools_ok = false
|
||||
Kit.check("二十九行十种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
|
||||
Kit.check("三十行十一种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
|
||||
var basic_count_ok := true
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n := 0
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) == "basic":
|
||||
n += 1
|
||||
@@ -637,7 +639,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n_buff := 0
|
||||
var n_debuff := 0
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if srow.is_empty():
|
||||
continue
|
||||
@@ -652,7 +654,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
var uncond_ok := true
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n_uncond := 0
|
||||
for entry in row.pool:
|
||||
for entry in _db.combat.enemy.effective_pool(row):
|
||||
var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
|
||||
n_uncond += 1
|
||||
|
||||
@@ -711,6 +711,7 @@ func _smoke_level_graph() -> void:
|
||||
# ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) =
|
||||
# L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用),
|
||||
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
|
||||
var base_n: int = _db.combat.enemy.rows.size() # 注入前基线行数(生产表行数不写死——精英行随批入表)
|
||||
var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
_db.combat.enemy.rows.append(mix_row_a)
|
||||
@@ -729,7 +730,7 @@ func _smoke_level_graph() -> void:
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
Kit.check("混编断言段注入行全量还原(表回 29 行——L1 混编批后的基线行数;首批混编投放基线 21 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 29)
|
||||
Kit.check("混编断言段注入行全量还原(表回注入前基线行数 %d——相对基数断言,生产表行数不写死)" % base_n, int(_db.combat.enemy.rows.size()) == base_n)
|
||||
|
||||
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
|
||||
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
|
||||
|
||||
@@ -256,16 +256,16 @@ func _test_rowset_discipline() -> void:
|
||||
if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("unit_count"):
|
||||
dom_hit = true
|
||||
Kit.check("单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)", dom_hit)
|
||||
# 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据)
|
||||
# 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据;elite 随精英专属池批转合法,非法样本换 miniboss)
|
||||
var t_kind = EnemyTableScript.new()
|
||||
t_kind.rows.assign([mk_row.call(1, "elite", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"),
|
||||
t_kind.rows.assign([mk_row.call(1, "miniboss", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"),
|
||||
mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
errs = t_kind.validate()
|
||||
var kind_hit := false
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("row_kind"):
|
||||
kind_hit = true
|
||||
Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕", kind_hit)
|
||||
Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss, elite})〔fv7〕", kind_hit)
|
||||
# 负向:anchor 两半——缺 battle band 0 锚行、缺 Boss 行集,各自报 anchor
|
||||
var t_no_anchor = EnemyTableScript.new()
|
||||
t_no_anchor.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
# tests/unit/data_config/elite_row_test.gd — 精英行单元套(精英专属池批)
|
||||
# 断言面 = 精英行加载期校验(字段互锁/引用校验/错误类别)·有效池合并(基础池取深度带最小行
|
||||
# +附加技)·继承不复制(改基础池传到精英、改写返回不回灌)·合并池过池构成纪律(跨表件)·
|
||||
# 暂不接线(寻址行类门只放 battle/boss,elite 行静置合法)·生产表 goblin_elite 行(随 T3 落表追加)。
|
||||
# 断言从公开入口进出:EnemyTable.validate()/effective_pool()/validate_skill_pool_links()/
|
||||
# resolve_enemy_rowset()/ConfigDB.load_tables()。夹具一律 rows.assign([...])(定型数组直赋会触发
|
||||
# 脚本错误静默中断——config_db_test 同款坑注)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/data_config/elite_row_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
|
||||
const ValidatorScript := preload("res://core/config_db_cross_validator.gd")
|
||||
const SkillTableScript := preload("res://data/schemas/skill_table.gd")
|
||||
const StatusTableScript := preload("res://data/schemas/status_effect_table.gd")
|
||||
const ResolverScript := preload("res://core/config_db_resolver.gd")
|
||||
|
||||
var _diags: Array[String] = [] # T2.4 夹具 resolver 诊断捕获
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_elite_validation_rejects()
|
||||
_test_effective_pool_merge()
|
||||
_test_base_pool_change_propagates()
|
||||
_test_merged_pool_composition()
|
||||
_test_elite_unreachable_by_encounter()
|
||||
_test_production_goblin_elite()
|
||||
quit(Kit.finish("elite_row_test", "PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过"))
|
||||
|
||||
|
||||
# 行构造助手(照 config_db_test mk_row 写全字段;elite 行形态=去掉 pool、加 base_ref/pool_extra)
|
||||
func _mk_row(l: int, kind: String, band: int, eid: String) -> Dictionary:
|
||||
return {"level_index": l, "row_kind": kind, "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
|
||||
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
|
||||
|
||||
func _mk_elite_row(l: int, band: int, eid: String, base_ref: String, extra: Array) -> Dictionary:
|
||||
return {"level_index": l, "row_kind": "elite", "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
|
||||
"unit_count": 1, "hp": 20, "atk": 2, "base_ref": base_ref, "pool_extra": extra}
|
||||
|
||||
|
||||
# 合法夹具(三行——battle 锚/boss/elite 引用同关 battle 敌种)
|
||||
func _legal_rows() -> Array:
|
||||
var goblin := _mk_row(1, "battle", 0, "goblin")
|
||||
goblin["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||||
return [goblin, _mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}])]
|
||||
|
||||
|
||||
# category 计数(错误列表按类别筛)
|
||||
func _count_cat(errs: Array, cat: String) -> int:
|
||||
var n := 0
|
||||
for e in errs:
|
||||
if String((e as Dictionary).get("category", "")) == cat:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# ---- R1:elite 行类合法与字段互锁、引用校验(合法零错+十一违例各自「含」预期类别)----
|
||||
func _test_elite_validation_rejects() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:325 elite 行字段互锁与基础引用违例加载期返回错误
|
||||
var t_ok = EnemyTableScript.new()
|
||||
t_ok.rows.assign(_legal_rows())
|
||||
var errs: Array = t_ok.validate()
|
||||
Kit.check("合法夹具 validate 零错(battle 锚+boss+elite 引用在位——实得 %s)" % str(errs), errs.is_empty())
|
||||
# ① elite 删 base_ref → cross_field
|
||||
var rows1 := _legal_rows()
|
||||
rows1[2].erase("base_ref")
|
||||
Kit.check("① elite 删 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows1), "cross_field") >= 1)
|
||||
# ② base_ref 与自身同名 → cross_field
|
||||
var rows2 := _legal_rows()
|
||||
rows2[2]["base_ref"] = "goblin_elite"
|
||||
Kit.check("② base_ref 与 enemy_id 同名报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows2), "cross_field") >= 1)
|
||||
# ③ pool_extra 为空数组 → cross_field
|
||||
var rows3 := _legal_rows()
|
||||
rows3[2]["pool_extra"] = []
|
||||
Kit.check("③ pool_extra 空数组报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows3), "cross_field") >= 1)
|
||||
# ④ elite 删 pool_extra 键 → cross_field
|
||||
var rows4 := _legal_rows()
|
||||
rows4[2].erase("pool_extra")
|
||||
Kit.check("④ elite 删 pool_extra 键报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows4), "cross_field") >= 1)
|
||||
# ⑤ elite 加 pool → cross_field
|
||||
var rows5 := _legal_rows()
|
||||
rows5[2]["pool"] = [{"skill_id": "attack", "weight": 9}]
|
||||
Kit.check("⑤ elite 带 pool 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows5), "cross_field") >= 1)
|
||||
# ⑥ battle 行加 base_ref → cross_field
|
||||
var rows6 := _legal_rows()
|
||||
rows6[0]["base_ref"] = "ogre_chief"
|
||||
Kit.check("⑥ battle 行带 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows6), "cross_field") >= 1)
|
||||
# ⑦ battle 行加 pool_extra → cross_field
|
||||
var rows7 := _legal_rows()
|
||||
rows7[0]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}]
|
||||
Kit.check("⑦ battle 行带 pool_extra 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows7), "cross_field") >= 1)
|
||||
# ⑧ base_ref="hobgoblin"(同关无此 battle 敌种)→ dangling_base
|
||||
var rows8 := _legal_rows()
|
||||
rows8[2]["base_ref"] = "hobgoblin"
|
||||
Kit.check("⑧ base_ref 悬空报 dangling_base〔精英批 V1〕", _count_cat(_validate_rows(rows8), "dangling_base") >= 1)
|
||||
# ⑨ 再加 elite2 行、base_ref 改指它 → dangling_base 且 message 含「elite」
|
||||
var rows9 := _legal_rows()
|
||||
rows9.append(_mk_elite_row(1, 0, "goblin_elite2", "goblin", [{"skill_id": "heavy", "weight": 2}]))
|
||||
rows9[2]["base_ref"] = "goblin_elite2"
|
||||
var errs9: Array = _validate_rows(rows9)
|
||||
var has_elite_base := false
|
||||
for e in errs9:
|
||||
if String((e as Dictionary).get("category", "")) == "dangling_base" \
|
||||
and String((e as Dictionary).get("message", "")).contains("elite"):
|
||||
has_elite_base = true
|
||||
Kit.check("⑨ base_ref 指向 elite 行报 dangling_base 且 message 含 elite〔精英批 V1〕", has_elite_base)
|
||||
# ⑩ pool_extra 含基础池已有 lunge → dup_key
|
||||
var rows10 := _legal_rows()
|
||||
rows10[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}, {"skill_id": "lunge", "weight": 1}]
|
||||
Kit.check("⑩ 附加技与基础池重复报 dup_key〔精英批 V1〕", _count_cat(_validate_rows(rows10), "dup_key") >= 1)
|
||||
# ⑪ pool_extra 条目权重 0 → domain 且 message 含 pool_extra
|
||||
var rows11 := _legal_rows()
|
||||
rows11[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 0}]
|
||||
var errs11: Array = _validate_rows(rows11)
|
||||
var has_extra_domain := false
|
||||
for e in errs11:
|
||||
if String((e as Dictionary).get("category", "")) == "domain" \
|
||||
and String((e as Dictionary).get("message", "")).contains("pool_extra"):
|
||||
has_extra_domain = true
|
||||
Kit.check("⑪ pool_extra 权重越域报 domain 且定位含 pool_extra〔精英批 V1〕", has_extra_domain)
|
||||
|
||||
|
||||
func _validate_rows(rows: Array) -> Array:
|
||||
var t = EnemyTableScript.new()
|
||||
t.rows.assign(rows)
|
||||
return t.validate()
|
||||
|
||||
|
||||
# ---- R2:有效池=基础池+附加技,基础池取深度带最小行(深带行在前防取首行假绿)----
|
||||
func _test_effective_pool_merge() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 精英有效池=同关战斗行类基础敌种深度带最小行的池+附加技
|
||||
var rows := [
|
||||
_mk_row(1, "battle", 3, "goblin"), # 深带行在前(取首行的错误实现必红)
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||||
]
|
||||
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
|
||||
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||||
var t = EnemyTableScript.new()
|
||||
t.rows.assign(rows)
|
||||
var got := {}
|
||||
for e in t.effective_pool(rows[3]):
|
||||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||||
Kit.check("精英有效池=浅带基础池+附加技({attack9,lunge1,heavy2}——非深带行池)〔精英批 V2〕",
|
||||
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 3)
|
||||
var got_b0 := {}
|
||||
for e in t.effective_pool(rows[1]):
|
||||
got_b0[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||||
Kit.check("battle 行有效池=自身 pool(读法等价面——{attack9,lunge1})〔精英批 V2〕",
|
||||
got_b0.get("attack", 0) == 9 and got_b0.get("lunge", 0) == 1 and got_b0.size() == 2)
|
||||
|
||||
|
||||
# ---- R3:继承不复制——基础池改表传到精英、改写返回数组不回灌 ----
|
||||
func _test_base_pool_change_propagates() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 继承不复制——基础池调参自动传到精英
|
||||
var rows := [
|
||||
_mk_row(1, "battle", 3, "goblin"),
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||||
]
|
||||
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
|
||||
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||||
var t = EnemyTableScript.new()
|
||||
t.rows.assign(rows)
|
||||
var first: Array = t.effective_pool(rows[3])
|
||||
for e in first:
|
||||
if String((e as Dictionary).get("skill_id", "")) == "attack":
|
||||
e["weight"] = 99 # 改写返回数组条目——表内行不得被回灌
|
||||
rows[1]["pool"][0]["weight"] = 7 # 基础行(depth_band==0)attack 权重改 7
|
||||
var got := {}
|
||||
for e in t.effective_pool(rows[3]):
|
||||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||||
Kit.check("基础池改权重 7 传到精英有效池(按需现算零缓存)〔精英批 V3〕", got.get("attack", 0) == 7)
|
||||
Kit.check("改写返回数组不回灌表内行(基础行 attack 仍=7)〔精英批 V3〕",
|
||||
int(rows[1]["pool"][0]["weight"]) == 7)
|
||||
|
||||
|
||||
# ---- R4:合并池过池构成纪律(跨表件校验对象=合并后的池)----
|
||||
func _test_merged_pool_composition() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:150 池构成纪律校验对象是合并后的池
|
||||
var skills := SkillTableScript.new()
|
||||
skills.rows.assign([
|
||||
{"skill_id": "attack", "type": "basic"},
|
||||
{"skill_id": "lunge", "type": "damage"},
|
||||
{"skill_id": "heavy", "type": "damage"},
|
||||
{"skill_id": "harden", "type": "buff"},
|
||||
{"skill_id": "charge", "type": "buff"},
|
||||
])
|
||||
var status := StatusTableScript.new()
|
||||
status.rows.assign([]) # 空状态表——buff 无 buff_ref 可解析时 cast_side 不报(cross_validator :179-180)
|
||||
# 合规精英:基础 attack/lunge+附加 heavy → 恰一普攻/增益≤1/无条件特殊≥1 全合规
|
||||
var enemy_ok = EnemyTableScript.new()
|
||||
enemy_ok.rows.assign([
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||||
])
|
||||
enemy_ok.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||||
enemy_ok.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池(纪律对全行生效——本组只数精英违规)
|
||||
var errs_ok: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_ok, status)
|
||||
Kit.check("合规精英合并池 pool_composition=0(实得 %s)" % str(errs_ok), _count_cat(errs_ok, "pool_composition") == 0)
|
||||
# 附加第二普攻:基础 attack+附加 attack → basic=2 → pool_composition=1 且 message 含 goblin_elite
|
||||
var enemy_dup_basic = EnemyTableScript.new()
|
||||
enemy_dup_basic.rows.assign([
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "attack", "weight": 2}]),
|
||||
])
|
||||
enemy_dup_basic.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||||
enemy_dup_basic.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
|
||||
var errs_dup: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_basic, status)
|
||||
Kit.check("附加第二普攻报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
|
||||
_count_cat(errs_dup, "pool_composition") == 1 and _msgs(errs_dup).any(func(m: String) -> bool: return m.contains("goblin_elite")))
|
||||
# 附加第二增益:基础 attack/lunge/harden+附加 charge → buff=2 → pool_composition=1
|
||||
var enemy_dup_buff = EnemyTableScript.new()
|
||||
enemy_dup_buff.rows.assign([
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "charge", "weight": 2}]),
|
||||
])
|
||||
enemy_dup_buff.rows[0]["pool"] = [
|
||||
{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}, {"skill_id": "harden", "weight": 2}]
|
||||
enemy_dup_buff.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
|
||||
var errs_buff: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_buff, status)
|
||||
Kit.check("合并后增益超 1 报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
|
||||
_count_cat(errs_buff, "pool_composition") == 1 and _msgs(errs_buff).any(func(m: String) -> bool: return m.contains("goblin_elite")))
|
||||
|
||||
|
||||
func _msgs(errs: Array) -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
for e in errs:
|
||||
out.append(String((e as Dictionary).get("message", "")))
|
||||
return out
|
||||
|
||||
|
||||
# ---- R5:暂不接线——表里有 elite 行时寻址拿不到它(行类门改动特有判据)----
|
||||
func _test_elite_unreachable_by_encounter() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:151 精英行接线前不会被任何节点读到
|
||||
_diags.clear()
|
||||
var resolver = ResolverScript.new(
|
||||
func(level, category, message) -> void:
|
||||
_diags.append("%s/%s/%s" % [str(level), str(category), str(message)]),
|
||||
func() -> Array: return [],
|
||||
func() -> Array: return [
|
||||
_mk_row(1, "battle", 0, "goblin"),
|
||||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||||
],
|
||||
func(): return null,
|
||||
)
|
||||
# ① 请求 elite → 空数组且诊断含 fatal 与 request_oob(只改枚举不改行类门时此请求会返回 goblin_elite——判据绑行类门改动)
|
||||
var hit: Array = resolver.resolve_enemy_rowset(1, 0, "elite")
|
||||
var has_fatal_oob := false
|
||||
for d in _diags:
|
||||
if d.contains("fatal") and d.contains("request_oob"):
|
||||
has_fatal_oob = true
|
||||
Kit.check("请求 elite 返回空且诊断含 fatal/request_oob〔精英批 V5〕", hit.is_empty() and has_fatal_oob)
|
||||
# ② 同夹具 battle/boss 查询返回行中 elite 计数=0(非空前置:合计行数>0)
|
||||
var battle_rows: Array = resolver.resolve_enemy_rowset(1, 0, "battle")
|
||||
var boss_rows: Array = resolver.resolve_enemy_rowset(1, 9, "boss")
|
||||
var elite_n := 0
|
||||
for r in battle_rows + boss_rows:
|
||||
if String((r as Dictionary).get("row_kind", "")) == "elite":
|
||||
elite_n += 1
|
||||
Kit.check("battle/boss 查询合计>0 且零 elite 行(非空前置在判据内)〔精英批 V5〕",
|
||||
battle_rows.size() + boss_rows.size() > 0 and elite_n == 0)
|
||||
|
||||
|
||||
# ---- R6:生产表地精精英落表(真实加载零错+行值逐位+有效池字典)----
|
||||
func _test_production_goblin_elite() -> void:
|
||||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:147 精英=敌人表行类为 elite 的独立一行
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var ok: bool = db.load_tables("res://data/tables", true)
|
||||
Kit.check("生产表 ConfigDB.load_tables 装载(is_ready=%s)〔精英批 V6〕" % str(db.is_ready), ok and db.is_ready)
|
||||
Kit.check("生产表加载 error 级诊断计数=0(实得 %d)〔精英批 V6〕" % db.diagnostics_count("error"),
|
||||
db.diagnostics_count("error") == 0)
|
||||
var elite_rows: Array = []
|
||||
for row in db.combat.enemy.rows:
|
||||
if String(row.get("enemy_id", "")) == "goblin_elite":
|
||||
elite_rows.append(row)
|
||||
Kit.check("goblin_elite 行恰 1 条(实得 %d)〔精英批 V6〕" % elite_rows.size(), elite_rows.size() == 1)
|
||||
if elite_rows.is_empty():
|
||||
return
|
||||
var r: Dictionary = elite_rows[0]
|
||||
var checks := {
|
||||
"level_index": 1, "row_kind": "elite", "depth_band": 0,
|
||||
"display_name": "精英·地精", "hp": 20, "atk": 2, "atk_min": 1, "atk_max": 3,
|
||||
"armor_class": 2, "armor_type": "light", "unit_count": 1, "base_ref": "goblin",
|
||||
}
|
||||
var bad: Array[String] = []
|
||||
for k in checks:
|
||||
if r.get(k) != checks[k]:
|
||||
bad.append("%s=%s≠%s" % [k, str(r.get(k)), str(checks[k])])
|
||||
Kit.check("行值 12 字段逐位相等(失配项 %s)〔精英批 V6〕" % [bad if not bad.is_empty() else "无"], bad.is_empty())
|
||||
Kit.check("无 unit_count_max 键、无 pool 键(精英行形态面)〔精英批 V6〕",
|
||||
not r.has("unit_count_max") and not r.has("pool"))
|
||||
var got := {}
|
||||
for e in db.combat.enemy.effective_pool(r):
|
||||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||||
Kit.check("有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 %s)〔精英批 V6〕" % str(got),
|
||||
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("desperate_guard", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 4)
|
||||
@@ -1,7 +1,7 @@
|
||||
# tests/unit/data_config/table_structure_guard_test.gd — 表结构守卫单元测试宿主
|
||||
# 断言面 = data/tables 各表的结构锚与守卫,七套:
|
||||
# adventure 冒险表(档位配额/件值对/同调目录 12 件/装配→战斗贯通)
|
||||
# enemy 敌人表(29 行带键行序/混编行集/三 Boss 行)
|
||||
# enemy 敌人表(30 行带键行序/混编行集/三 Boss 行/精英行)
|
||||
# AC 单轨与掷点参数(status_effect/reward/settlement 三处无双轨/免伤墙护栏/敌 AC 带宽)
|
||||
# variation 地形变异(行数/候选池门控/跨字段互锁负样本)
|
||||
# difficulty 难度表(空表零行为/修饰套乘/越域 clamp)
|
||||
@@ -208,18 +208,18 @@ func _smoke_adventure_table() -> void:
|
||||
v18_ok and locked_n == 12)
|
||||
|
||||
|
||||
# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:29 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单)----
|
||||
# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:30 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单·精英行)----
|
||||
|
||||
func _smoke_enemy_bands() -> void:
|
||||
# oracle: design/gdd/systems/冒险循环/难度曲线与Boss.md:316 难度与敌人带校验(敌人带组)
|
||||
print("--- 敌人表深度带键结构(29 行键序 · (1,3) 混编 · Boss 带键覆盖表)---")
|
||||
# ① 29 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前)
|
||||
print("--- 敌人表深度带键结构(30 行键序 · (1,3) 混编 · Boss 带键覆盖表 · 精英行)---")
|
||||
# ① 30 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前;精英行随精英专属池批)
|
||||
# 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、
|
||||
# Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动
|
||||
var expect_keys: Array[String] = [
|
||||
"1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|bugbear", "1|battle|3|goblin",
|
||||
"1|battle|6|bugbear",
|
||||
"1|boss|4|ogre_chief", "1|boss|6|ogre_chief",
|
||||
"1|boss|4|ogre_chief", "1|boss|6|ogre_chief", "1|elite|0|goblin_elite",
|
||||
"2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider",
|
||||
"2|battle|5|bugbear", "2|battle|5|giant_spider", "2|battle|6|giant_spider", "2|battle|7|giant_spider",
|
||||
"2|battle|9|giant_spider",
|
||||
@@ -232,8 +232,8 @@ func _smoke_enemy_bands() -> void:
|
||||
for row in _db.combat.enemy.rows:
|
||||
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
|
||||
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
|
||||
Kit.check("敌人表 29 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前)〔L1混编 v1〕",
|
||||
_db.combat.enemy.rows.size() == 29 and got_keys == expect_keys)
|
||||
Kit.check("敌人表 30 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前;精英行接 boss 行后)〔L1混编 v1〕",
|
||||
_db.combat.enemy.rows.size() == 30 and got_keys == expect_keys)
|
||||
# ② (1,3) 混编构成与行序+静态最坏骰和+行值整行复用(R1-R4)
|
||||
var rowset13: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 3, "battle")
|
||||
var ids13: Array[String] = []
|
||||
@@ -386,6 +386,8 @@ func _smoke_ac_swap_guard() -> void:
|
||||
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2})
|
||||
var l1_totals := {}
|
||||
for row in _db.combat.enemy.rows:
|
||||
if String(row.get("row_kind", "")) == "elite":
|
||||
continue # 曲线护栏管遭遇曲线——elite 不进遭遇(敌人系统.md:151),不跳过则 band0 失真
|
||||
if int(row.get("level_index", -1)) != 1:
|
||||
continue
|
||||
var cnt_lo: int = int(row.get("unit_count", 1))
|
||||
|
||||
@@ -11,7 +11,14 @@ const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest"
|
||||
|
||||
func _initialize() -> void:
|
||||
# oracle: design/gdd/ux/camp.md:29 (营地场景布局数据件——纵深与地平线组)
|
||||
call_deferred("_test_layout_hotspot_geometry")
|
||||
call_deferred("_run_all")
|
||||
|
||||
|
||||
func _run_all() -> void:
|
||||
# oracle: design/gdd/ux/camp.md:29 (套件统一收尾——双测试函数单进程单 quit)
|
||||
await _test_layout_hotspot_geometry()
|
||||
await _test_hud_band_contrast_over_bg()
|
||||
quit(Kit.finish("camp_scene_layout_test", "PASS — 布局几何+顶栏栏体对比 全过"))
|
||||
|
||||
|
||||
func _test_layout_hotspot_geometry() -> void:
|
||||
@@ -19,7 +26,6 @@ func _test_layout_hotspot_geometry() -> void:
|
||||
var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres")
|
||||
Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null)
|
||||
if table == null:
|
||||
quit(Kit.finish("camp_scene_layout_test"))
|
||||
return
|
||||
var problems: Array[String] = table.validate()
|
||||
Kit.check("V14 校验函数通过(问题清单空,实得 %s)〔营地图批 V14〕" % [problems], problems.is_empty())
|
||||
@@ -88,4 +94,54 @@ func _test_layout_hotspot_geometry() -> void:
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
Kit.check("V14 ConfigDB 装载含营地布局表且校验过(is_ready=%s)〔营地图批 V14〕" % db.is_ready,
|
||||
load_ok and db.is_ready and db.presentation.camp_scene_layout != null)
|
||||
quit(Kit.finish("camp_scene_layout_test"))
|
||||
|
||||
|
||||
# 顶栏栏体规格侧对比核验(R6——与 R5 分工:R5 核实装等值,本行核规格值本身达标):
|
||||
# 按规格句在测试内写死 #6E4526 与 α0.80,与背景贴图顶带实测均色做 α 合成后算 WCAG 对比 ≥3.0。
|
||||
# 禁把视图件的 color 直接当预期值喂进算式(oracle 镜像——test-conventions §8.7 判红)。
|
||||
func _test_hud_band_contrast_over_bg() -> void:
|
||||
# oracle: design/gdd/ux/camp.md:41 (顶栏栏体定值——对落位带底色对比 ≥3:1 组)
|
||||
var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres")
|
||||
var band_h := 48
|
||||
if table != null:
|
||||
band_h = int(table.hud_band_h) # 带高取数据出口(禁字面量)
|
||||
var tex = load("res://assets/art/scene/scene_camp_bg.png")
|
||||
var img: Image = null
|
||||
if tex != null and tex.get_image() != null:
|
||||
img = tex.get_image()
|
||||
var scan_n := 0
|
||||
var r_sum := 0.0
|
||||
var g_sum := 0.0
|
||||
var b_sum := 0.0
|
||||
if img != null:
|
||||
for y in range(0, mini(band_h, img.get_height())):
|
||||
for x in range(img.get_width()): # 双层全像素扫描(非步长抽样——terrain_tile_sets_test 同款)
|
||||
var c := img.get_pixel(x, y)
|
||||
r_sum += c.r
|
||||
g_sum += c.g
|
||||
b_sum += c.b
|
||||
scan_n += 1
|
||||
Kit.check("R6 扫描面非空前置(顶带像素 %d > 0——贴图装载且带高在界)〔营地交互批 V5〕" % scan_n, scan_n > 0)
|
||||
if scan_n == 0:
|
||||
return
|
||||
var bg := Color(r_sum / float(scan_n), g_sum / float(scan_n), b_sum / float(scan_n))
|
||||
# 规格侧算式(camp.md:41 写死——#6E4526 × α0.80 对落位带背景合成)
|
||||
var fg := Color(0x6e / 255.0, 0x45 / 255.0, 0x26 / 255.0, 0.80)
|
||||
var mixed := Color(
|
||||
fg.r * fg.a + bg.r * (1.0 - fg.a),
|
||||
fg.g * fg.a + bg.g * (1.0 - fg.a),
|
||||
fg.b * fg.a + bg.b * (1.0 - fg.a))
|
||||
var lum_fg: float = _wcag_lum(mixed)
|
||||
var lum_bg: float = _wcag_lum(bg)
|
||||
var ratio: float = (maxf(lum_fg, lum_bg) + 0.05) / (minf(lum_fg, lum_bg) + 0.05)
|
||||
print("[camp_scene_layout_test] 顶带均色=%s 合成后=%s WCAG 对比=%.2f:1(规格 ≥3:1)" % [bg, mixed, ratio])
|
||||
Kit.check("R6 栏体合成色对落位带底色对比 ≥3.0(实测 %.2f)〔营地交互批 V5〕" % ratio, ratio >= 3.0)
|
||||
|
||||
|
||||
## WCAG 相对亮度(sRGB 线性化——命名函数形态,避多行 lambda 嵌套三元解析坑)
|
||||
func _wcag_lum(c: Color) -> float:
|
||||
return 0.2126 * _srgb_lin(c.r) + 0.7152 * _srgb_lin(c.g) + 0.0722 * _srgb_lin(c.b)
|
||||
|
||||
|
||||
func _srgb_lin(v: float) -> float:
|
||||
return v / 12.92 if v <= 0.03928 else pow((v + 0.055) / 1.055, 2.4)
|
||||
@@ -0,0 +1,59 @@
|
||||
extends SceneTree
|
||||
# 确认弹窗按钮位次与语义同钉单元套——
|
||||
# 守 ConfirmDialog 公开入口 open()/open_three()(四处消费方实际用的两个公开方法)的按钮行子节点序:
|
||||
# 推进钮恒 child(0)、次推进 child(1)、维持 child(2)(HUD框架.md:90 三条纪律·其三——营地交互修复批 R3)。
|
||||
# 判据面=树的公开子节点序+Button 公开 text,零私有成员直读;次钮在 open() 里只置 visible=false
|
||||
# 不从子节点表移除——双钮形态下子节点仍为 3,按下标断必取 child(2)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/confirm_dialog_test.gd(expect 0)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const ConfirmDialogScript := preload("res://presentation/views/confirm_dialog.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 (确认弹窗按钮位次定值——位次与语义同钉组)
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
# 按类型向下定位第一个 HBoxContainer(该弹窗树内唯一横排——modal_shell 产 CenterContainer+
|
||||
# PanelContainer+VBox 不产横排;HSeparator 与 UiKit.button 产件也不是 HBoxContainer)
|
||||
func _find_btn_row(node: Node) -> HBoxContainer:
|
||||
if node is HBoxContainer:
|
||||
return node
|
||||
for ch in node.get_children():
|
||||
var found := _find_btn_row(ch)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _test_two_button_row_order() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 双钮形态位次=推进在左、维持在右(两钮同钉组)
|
||||
var ov: Control = ConfirmDialogScript.new()
|
||||
root.add_child(ov)
|
||||
ov.open("t", "推进", "维持")
|
||||
var row := _find_btn_row(ov)
|
||||
Kit.check("双钮形态按钮行子节点数=3(次钮只隐藏不移除——非空前置)", row != null and row.get_child_count() == 3)
|
||||
if row != null and row.get_child_count() == 3:
|
||||
Kit.check("双钮形态位次 child(0)=推进", String((row.get_child(0) as Button).text) == "推进")
|
||||
Kit.check("双钮形态位次 child(2)=维持(次钮占 child(1) 位——按下标断必取 child(2))", String((row.get_child(2) as Button).text) == "维持")
|
||||
ov.queue_free()
|
||||
|
||||
|
||||
func _test_three_button_row_order() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 三钮形态位次=推进/次推/维持(三钮同钉组)
|
||||
var ov: Control = ConfirmDialogScript.new()
|
||||
root.add_child(ov)
|
||||
ov.open_three("t", "推进", "次推", "维持")
|
||||
var row := _find_btn_row(ov)
|
||||
Kit.check("三钮形态按钮行子节点数=3(非空前置)", row != null and row.get_child_count() == 3)
|
||||
if row != null and row.get_child_count() == 3:
|
||||
Kit.check("三钮形态位次 child(0)=推进", String((row.get_child(0) as Button).text) == "推进")
|
||||
Kit.check("三钮形态位次 child(1)=次推", String((row.get_child(1) as Button).text) == "次推")
|
||||
Kit.check("三钮形态位次 child(2)=维持", String((row.get_child(2) as Button).text) == "维持")
|
||||
ov.queue_free()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_test_two_button_row_order()
|
||||
_test_three_button_row_order()
|
||||
quit(Kit.finish("confirm_dialog_test", "PASS — 双钮/三钮形态位次与语义同钉 全过"))
|
||||
@@ -6,6 +6,7 @@ extends SceneTree
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0)
|
||||
# 断言走 test_kit 计数(Kit.check)+ 失败即停早退协议(顺序状态机——坏态下后继断言无效)。
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 真输入合成单点(tests 面复用共用件——禁复制第二份合成输入件)
|
||||
|
||||
# 断言 + 失败即停(原直写 print FAIL + quit(1) + return 样板单点化——早退语义逐位保持)
|
||||
func _req(desc: String, ok: bool) -> bool:
|
||||
@@ -60,13 +61,15 @@ func _run() -> void:
|
||||
if not _req("付费档余额可付:免费位 0 但钮点亮(对齐商店——GP 档可刷)",
|
||||
ov._refresh_btn.visible and not ov._refresh_btn.disabled):
|
||||
return
|
||||
ov.press_refresh()
|
||||
if not _req("press_refresh 未外发 refresh_requested", refreshed.size() == 1):
|
||||
await process_frame # 刷新钮现形当帧容器重排在途——排版帧后矩形成形再真点
|
||||
ProbeUiUtil.push_click(root, ov._refresh_btn) # 真点刷新钮(disabled 守卫=真实拒绝路径)
|
||||
if not _req("真点刷新未外发 refresh_requested", refreshed.size() == 1):
|
||||
return
|
||||
# ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准
|
||||
ov.press_card(0)
|
||||
ov.press_card(1)
|
||||
ov.press_confirm()
|
||||
# ② 点选→高亮→确认外发(真输入管线:bcard gui_input 点选——与 press_card 同一 _select_card 处理器);确认前改选以最后点选为准
|
||||
ProbeUiUtil.push_click_at(root, (ov._bcards[0] as Control).get_global_rect().get_center())
|
||||
ProbeUiUtil.push_click_at(root, (ov._bcards[1] as Control).get_global_rect().get_center())
|
||||
await process_frame # 确认钮现形当帧容器重排在途——排版帧后矩形成形再真点
|
||||
ProbeUiUtil.push_click(root, ov._confirm_btn)
|
||||
if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked),
|
||||
ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"):
|
||||
return
|
||||
@@ -76,7 +79,8 @@ func _run() -> void:
|
||||
if not _req("结果态应点亮继续且收三卡与刷新钮",
|
||||
ov._continue_btn.visible and not ov._choice_box.visible and not ov._refresh_btn.visible):
|
||||
return
|
||||
ov.press_continue()
|
||||
await process_frame # 继续钮现形当帧容器重排在途——排版帧后矩形成形再真点
|
||||
ProbeUiUtil.push_click(root, ov._continue_btn)
|
||||
if not _req("继续未外发 continue_pressed", continued.size() == 1):
|
||||
return
|
||||
ov.close_pick()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# 迭代 54 反哺点清单(2026-11-07)
|
||||
|
||||
> 本清单逐卡增量落笔;用户在会话中裁决后由文档批写入 `design/` 并把该条清掉(AGENTS 红线:实现批禁写 `design/`,迭代实现报告与反哺点归 `reports/`)。
|
||||
|
||||
## 卡 1|QA harness 缺陷与覆盖遗漏收口批——申报:零条
|
||||
|
||||
卡 1 §6 申报「无关联文档」——A2 为容错修复、B1/B2 为测试驱动面,零设计语义变化。核过面:`design/gdd/systems/排雷循环/排雷玩法.md`(停手语义句 :162/:167 与实装一致)、`design/engineering/test-conventions.md`(真输入管线条文 §2 与实装一致)——两处无需改句。
|
||||
|
||||
## 卡 2|营地交互修复批——两条
|
||||
|
||||
1. **`design/engineering/architecture/architecture.md` 呈现层模块表 CampHudBand 行(约 :175)**:现句「顶栏 48px 全宽窄条:钥石余额 badge 唯一呈现」已失真——本批落地后顶栏呈现物=栏体色面(`CAMP_HEADER_BAND`×α0.80 全宽)+钥石徽章(右位顶格)两件。建议改成:栏体与徽章两呈现物的实际形态描述;依据=实装读数(栏体 1280×48 全局矩形、徽章 end.x=栏右缘−12,system_test R5 五钉读数)。实现批禁写该文档,由文档批落文。
|
||||
2. **根 `体验验证清单.md` 增一行观测项**:营地顶栏观感(「成一条可辨窄带、徽章落在带内」)——需真人试玩下结论,已在实现报告目检总清单登记;由文档批落进清单正式在册。依据=卡 2 §6 同步面表行。
|
||||
|
||||
(未列项核对:camp.md §顶栏 与 art-bible §6.2 —— α0.80/栏高 48/色值零改值,按卡 2 §6「未改值不列」不列;HUD框架 §提示与弹层·其三「四处共用同一件同一序」——实装后句码一致,无发现不列;architecture.md :168 D1 HUD 视图单元接口——CampHudBand 仍为信号外发+setter 只读注入形态、新出口为 UiKit 工厂面(工厂单点先例在案),未失真不列。)
|
||||
|
||||
## 卡 3|精英专属池——一条
|
||||
|
||||
1. **`design/gdd/03-内容规模.md` 第 66 行「精英节点与词缀系统——只预留了字段位」**:精英怪本体已落敌人表精英行(行类 elite+池继承合并附加)——「只预留字段位」的表述落后于实况;建议改成「精英怪本体已落敌人表(行类+池继承附加),精英节点与投放仍未接线」。依据=本批实现(enemy.tres (1,elite,0,goblin_elite) 行+卡 3 对照表 R1–R6 全绿);由文档批落文。
|
||||
|
||||
(未列项核对:敌人系统.md v1.9——实现与文档零出入(V6 生产表 12 字段逐位+有效池四键精确);数据配置.md 加载期错误类别表——dangling_base 属「悬空引用」既有类别的实例,表述不变,不列。)
|
||||
@@ -0,0 +1,146 @@
|
||||
# 迭代 54 实现报告(2026-11-07)
|
||||
|
||||
> 三卡:QA harness 缺陷与覆盖遗漏收口批 → 营地交互修复批 → 精英专属池。本报告随各卡末卡增量落笔(卡 1 QA 收口段先行,卡 2/卡 3 段随后补齐),总收口前为完整版。
|
||||
> 独立验收审计入口预约:`reports/iter54-验收审计-2026-11-07.md`(未参与实现的只读会话/子代理执行,未过不合回)。审计深度:卡 1=中档(对照表逐行全量、最窄复跑省略以远端 CI 现查代证);卡 2=中档;卡 3=高档(全量)。
|
||||
|
||||
## 卡 1|QA harness 缺陷与覆盖遗漏收口批——状态:已完成
|
||||
|
||||
### 各任务卡状态
|
||||
|
||||
| 任务卡 | 状态 | 摘要 |
|
||||
|---|---|---|
|
||||
| T1 A2 关闭容错修复 | 完成 | battle_end_overlay.gd close(:194) 与 _play_celebrate(:331) 两处 `get_meta(&"celebrate_tw", null)` 改 `has_meta` 前置判断,语义不变(有则必杀、无则无事)〔提交 57f3cc75〕 |
|
||||
| T2 B1 探针定型化停手段 | 完成 | ui_click_probe 新增 `_stoptest_tick` 状态机(镜像 `_locktest_tick` 形态)+`_stoptest_assert_hit`,挖掘期触发窗真点停手钮,断言①覆盖登记停手键在场②相位离开 Active〔提交 816aaae5〕 |
|
||||
| T3 B1 journey 停手+S4c 8 处转换 | 完成 | journey_player 新增停手行为(每战至多一次,挖掘期确定性触发)+8 处真输入转换+头注方法口句换代〔提交 2f5c734e〕 |
|
||||
| T4 B2 ui_click_probe 16 处+candidate_rect | 完成 | LevelMapScreen 删 press_candidate 增 candidate_rect(index)->Rect2(命中面与 _candidate_at 同源,全局坐标,越界空 Rect2);ui_click_probe 候选 2+节点继续 2+三选一 10+弃图刷新链 2 全转真输入,既有落点断言逐条原样〔提交 afcfbf37〕 |
|
||||
| T5 B2 其余 7 探针 11 处 | 完成 | 候选 6+停点 1+调试浮窗 4;shot_probe 头注同步〔提交 22903043〕 |
|
||||
| T6 B2 非探针层 6 处+死接缝清理 | 完成 | system_test 1 处+reward_overlay_test 5 处真输入;死接缝 11 方法删除(ap_bar_view 2+curio_pick_overlay 4+node_overlay 1+debug_overlay 3,level_map_screen 1 随 T4)〔提交 710df6ce〕 |
|
||||
| T7 产出物落位 | 完成 | 本报告+反哺点清单+批末选档跑闸(读数见下)+审计入口预约 |
|
||||
|
||||
### 实际对照表(R1–R9,补「结果」格)
|
||||
|
||||
| 行 | 验收测试 | 结果 |
|
||||
|---|---|---|
|
||||
| R1 终局结算框关闭动线零引擎报错 | fx_probe+screen_flow_probe 直跑 grep「SCRIPT ERROR」「celebrate_tw」 | 绿——两探针各 exit=0、SCRIPT ERROR 0、celebrate_tw 0(提交 57f3cc75 读数) |
|
||||
| R2 停手钮真输入命中与落效 | ui_click_probe 直跑(_stoptest_tick 断言①②) | 绿——exit=0、问题=0、覆盖落盘停手键命中 1 次(确定性触发实证;提交 816aaae5 读数) |
|
||||
| R3 journey 停手+方法口清零 | journey 全线跑批 | 绿(读数判据满足)——fatal 0、上报结构不变(2 条=已知缺陷 A1 双指纹,改前基线同源)、覆盖登记停手键 6 次+停点键在场(结算面继续 6/确认 3)、屏到达 16 类含战斗/终局;exit=1 为 A1 基线态(营地交互修复批承接中,本卡不做动作——卡 1 §5 范围边界) |
|
||||
| R4 停点链真输入化(ui_click_probe 16 处) | ui_click_probe 直跑+既有落点断言+登记键集不回退 | 绿——exit=0、问题=0、覆盖 43 命中/30 控件较改前 39/28 不回退、新增停点链键(curio 确认选择 2/继续 2)、冒险终局在案选路口点击 9 次 |
|
||||
| R5 到达型候选+停点(7 探针) | 7 探针逐件孤立直跑 | 绿——headless 五件(ui_struct/layout/ui_probe/t17_defer/ui_tune)exit=0+SCRIPT ERROR 0;窗口化两件(shot 七屏 PNG 齐、fx_shot PASS 演出帧 7 张挖格 3)exit=0 |
|
||||
| R6 调试浮窗面(4 处) | ui_tune_probe 直跑 | 绿——PASS 全链路(含模拟 N=10 输出与种子三分断言) |
|
||||
| R7 系统测试宿主候选真输入化 | 系统测试直跑 | 绿——69 项检查 0 失败、三闸过(V3c 阶段真实选路提交在跑批中执行) |
|
||||
| R8 三选一覆盖层单元真输入化 | 单元与集成套件直跑 | 绿——82 套件 all green、exit=0、0 FAIL(reward_verify 断言在册) |
|
||||
| R9 死接缝同批清理(5 文件 11 方法) | 全仓 .gd 双模式 grep | 绿——定义清 `func press_(candidate\|continue\|card\|confirm\|refresh\|apply\|reset\simulate)` 与调用清 `.press_(…)` 双零命中 |
|
||||
|
||||
### 卡 1 批末选档跑闸读数(T7)
|
||||
|
||||
`bash tools/run_all_gates.sh --code`(游戏码批选档)=**【验收通过】闸况与基线一致,无新增红**。回执六闸全绿:冒烟测试(smoke_boot.log)=绿/单元与集成测试(unit.log)=绿/系统测试(system_test.log)=绿/场景探针(probe.log)=绿/渲染闸(render.log)=绿/静态扫描(static_scan.log)=绿。改前改后对拍:改前基线=master 40a3ac12 远端 CI run 99 绿;改后=本跑批无新增红(闸况与基线一致)。
|
||||
|
||||
### 实机目检总清单(卡 1)
|
||||
|
||||
| 目检项 | 登记处 | 义务口径 |
|
||||
|---|---|---|
|
||||
| 覆盖面板控件下钻停手键与停点链键在场 | 本表(数据面读数:probe_hits_ui_click.js 停手 1 次、停点链键在案) | 真人检测只登记不阻塞(AGENTS 红线)——登记即完成义务 |
|
||||
|
||||
### 遗留读数义务(卡 1)
|
||||
|
||||
- 无 MC/verifier 义务(本卡零数值零随机面)。
|
||||
- 远端 CI 现查读数(合回后):审计与追绿按 process-conventions §1 第 3 条按需现查。
|
||||
|
||||
### 实现决定记录(卡 1,超白名单事项备案)
|
||||
|
||||
1. **reward_overlay_test.gd 五处行号就地校正**:卡面申报 :60/:64/:65/:66/:76,实查为 :63/:67/:68/:69/:79(漂移 +3,内容与描述全一致)——依卡内 T1 开工核对条款就地校正,未呈报(漂移远小于一屏)。
|
||||
2. **ui_click_probe 停手段与 system_test 行号就地校正**:停手触发/驱动 :1041-1045/:1063-1065 → 实查 :1051-1055/:1074-1077;system_test :433 → :440。同上依据。
|
||||
3. **排版帧插入**:三选一选卡→确认之间、reward_overlay_test 刷新/确认/继续点击前、journey _drive_curio 卡→确认之间插入 1–2 帧等待——确认钮/继续钮现形当帧容器重排在途,矩形未成形真点必落空。属驱动机制细节(白名单「使能窗等待时长、触发条件的具体写法」同族)。
|
||||
4. **ui_tune ⑤ sim 真点的折叠区滚轮通道**:初跑发现 sim 钮真点落空——根因=滚动列表 boot 滚位下钮矩形跨出 ScrollContainer 视口底边(hit 面被裁;emit 路径历史性掩盖)。按 test-conventions §2「折叠区控件先以真输入滚轮滚入可视区再点」落滚轮通道(一档滚入实证后定装),并配 12 档上限+滚入失败 problem 上报(正面效力自检)。⑧ 应用前置重开浮窗(同款机制先例=④ 重开浮窗)。属真输入通道的实现形态(卡片白名单「具体写法」族),随提交留痕。
|
||||
5. **候选/bcard 覆盖登记键的结构性缺口**:覆盖登记件自动登记通道=BaseButton pressed+营地自绘物件两源;地图画布与 BCard 非 BaseButton、无 pressed 信号,真输入命中不产键(登记件零改动——卡 1 §13 申报)。候选/弃图确认键的对照表括注预期键与登记件能力不符:效力由既有落点断言承载(R4 判据绑定面=既有落点断言逐条+键集不回退,两者均成立)。弃图确认链在 ui_click_probe 受 is_refresh_ready(RAP≥8)门槛四种子未触发——改前同门槛不可达,链面真输入效力由 journey 既有真点链承载。此为观测面局限申报,非缺陷。
|
||||
6. **T2 断言①的登记件读取形态**:`_stoptest_assert_hit` 经 `_recorder._node_keys`/`_recorder._hits` 直读登记件命中表(登记件为探针共用面、卡 1 §13 申报零改动——不新增公开读口,探针面读私有沿现役惯例:ap_bar._btn_stop 同型先例)。
|
||||
7. **卡 2 T4.4「等用户发令再走收口」与迭代单合并语义的冲突处置**:卡 2 写卡时为独立批(编入迭代 54 前),其末卡收口语义已被编排段换代为迭代级总收口(iter 技能第 5 步:一次合并、五步登记)。本批按迭代级收口执行,不逐卡等发令;此冲突为编排换代遗留,随本报告呈报。
|
||||
|
||||
## 卡 2|营地交互修复批——状态:已完成
|
||||
|
||||
### 各任务卡状态
|
||||
|
||||
| 任务卡 | 状态 | 摘要 |
|
||||
|---|---|---|
|
||||
| T1 开局路径隐藏点收一处 | 完成 | `_start_adventure()` 首句加开始层隐藏(开局唯一出口,契约 1)+删 `_start_adventure_from_menu()` 冗余句;探针大门无断点段追加 R1/R2〔提交 4d9687eb〕 |
|
||||
| T2 确认弹窗按钮位次镜像 | 完成 | `add_child` 三句次序镜像(确认→次→取消,其余零动——契约 3);新单元套 confirm_dialog_test 入册;探针 R4 位次真点〔提交 46287043 区段〕 |
|
||||
| T3 营地顶栏可见栏体 | 完成 | ui_kit 新增 CAMP_HUD_BAND+camp_hud_band_surface(host) 出口(契约 4/5);camp_hud_band 调出口+徽章右对齐容器落座;R5 五钉(系统测试)+R6 对比(单元套)〔提交 1958d329〕 |
|
||||
| T4 产出物落位 | 完成 | 本报告卡 2 段+反哺点+gdstyle 基线修订(见下)+能力行名实核对 |
|
||||
|
||||
### 实际对照表(R1–R8,补「结果」格)
|
||||
|
||||
| 行 | 验收测试 | 结果 |
|
||||
|---|---|---|
|
||||
| R1 大门无断点按「是」后开始层不可见 | ui_click_probe 大门段〔营地交互批 V1〕 | 绿——探针直跑问题=0,断言在跑批中执行(修复前该句恒红判据零命中) |
|
||||
| R2 开局落节点图选路停点 | ui_click_probe 大门段〔营地交互批 V2〕 | 绿——同批(3 帧口径下 level_screen.visible 断言过) |
|
||||
| R3 两/三钮形态位次=推进→次→维持 | confirm_dialog_test 两函数 | 绿——直跑 exit=0 七断言全 PASS |
|
||||
| R4 左位真点即推进 | ui_click_probe R4〔营地交互批 V3〕 | 绿——真点前 consumer=="gate_new" 前置读+child(0) 中心真点+真点后 is_adventure 翻转+consumer 清零,全部在跑批中执行 |
|
||||
| R5 顶栏栏体几何与徽章落座 | system_test._smoke_camp_hud_band | 绿——76 检查 0 失败;T3.0 前置读数=徽章 min 高 48.0;五钉全过(色面 6e4526cc、铺满 1280×48、IGNORE、首子件、徽章顶格右位 end.x−12) |
|
||||
| R6 栏体对落位带底色对比 ≥3:1 | camp_scene_layout_test._test_hud_band_contrast_over_bg | 绿——直跑 exit=0,规格侧算式实测 4.29:1(与卡面复核推算一致,顶带均色≈#DEEAE7) |
|
||||
| R7 回归·件1 未动三条动线 | 既有大门两分支段+篝火段断言 | 绿——探针直跑问题=0(既有断言原样通过) |
|
||||
| R8 回归·件2 路由与件数断言 | 探针次钮隐藏断言+system_test 三钮计数 | 绿——system_test 76/0(三钮计数断言在册);探针既有次钮断言原样通过 |
|
||||
|
||||
### 卡 2 批末读数
|
||||
|
||||
- 静态扫描(S13)直跑:首跑出 `ui_kit.gd|quality/max-public-methods = 24(基线登记 23)` 恶化键——卡 2 §13 预申报的例外(用户 11-06 终裁「整件外观封公共件一个具名出口」的直接代价),按申报把 `tools/gdstyle_baseline.txt` 该行 23→24 修订后复跑绿(基线内 7 条、恶化 0)。基线修订的批准依据=卡 2 §13「本条即申报与请求」+U0 批卡。
|
||||
- 系统测试 76 检查 0 失败;单元侧 confirm_dialog_test/camp_scene_layout_test 直跑绿;探针直跑问题=0。卡 3 T3.8 的里程碑出口跑闸(--code 全档)覆盖本卡改动面的全档回归。
|
||||
|
||||
### 实机目检总清单(卡 2)
|
||||
|
||||
| 目检项 | 登记处 | 义务口径 |
|
||||
|---|---|---|
|
||||
| 营地顶栏观感——成一条可辨窄带、徽章落在带内 | 本表(渲染闸七屏段营地截图随批产出可对照;卡 2 §1 真人目检项) | 真人检测只登记不阻塞——登记即完成义务 |
|
||||
|
||||
### 遗留读数义务(卡 2)
|
||||
|
||||
- 远端 CI 现查读数(合回后,按需现查)。
|
||||
|
||||
### 实现决定记录(卡 2 新增条目)
|
||||
|
||||
8. **T2.3「前置重开」步重构为原位 R4**:卡面 T2.3 设计=既有确认真点后重开营地再测 R4;但该流程下 is_adventure 已为真、R4 主证「翻转」恒空转(且 `_on_confirm_declined` 同样清零 consumer,清零判据无判别力)。R4 对照表行的判据绑定面(主证=is_adventure 翻转)优先于任务卡步骤机制——改为真点前 is_adventure=false 的原位确认:既有确认成员点击替换为按钮行 child(0) 坐标真点,翻转成为真判据。既有断言(is_adventure+营地隐藏)原样保留。
|
||||
9. **T1.5 `--quick` 闸壳步以探针直跑代证**:改完即测铁规频率口径(轮内不跑闸壳全套含 --quick)下,T1.4 探针直跑即该件最窄确认(同一判据同源读数),未另起 --quick 闸壳跑批。
|
||||
10. **T3.0 读数实测 48.0**(卡面 L3 第 5 条警戒项未触发)——顶格落座前提成立,R5⑤ 判据照卡落装。
|
||||
|
||||
## 卡 3|精英专属池——状态:已完成
|
||||
|
||||
### 各任务卡状态
|
||||
|
||||
| 任务卡 | 状态 | 摘要 |
|
||||
|---|---|---|
|
||||
| T1 敌人表 schema | 完成 | ROW_KINDS 增 elite+ENCOUNTER_ROW_KINDS 行类门常量+_min_band_row+effective_pool+_validate_pool_entries 提取(pool 文案逐字不变)+_validate_elite_fields/_validate_elite_refs(dangling_base 新类别)+_extra_overlap_errs;新套件 elite_row_test 入册;config_db_test fv7 样本换代〔T1+T2 合并提交〕 |
|
||||
| T2 跨表纪律+寻址门 | 完成 | cross_validator :194 改读 effective_pool(合并池过纪律);resolver :41 行类门改读 ENCOUNTER_ROW_KINDS(文案保持 {battle, boss}) |
|
||||
| T3 地精精英落表+存量同步 | 完成 | enemy.tres 新行 (1,elite,0,goblin_elite)(卡面 §2 表值逐字段)+尾批注 provisional 来源行;round_flow 七处读法换代+expect_pools 增键+文案换代;guard 行数 30+键序插行+l1_totals skip elite;shop 还原断言改相对基数〔提交 e538e2cb〕 |
|
||||
| T4 图鉴导出 | 完成 | compendium_dump 导出行数组 elite 副本写 effective_pool(表内行零改写);gen.py 词表三处〔提交 f4865ad8〕 |
|
||||
| T5 产出物落位 | 完成 | 本报告卡 3 段+反哺点+能力行名实核对 |
|
||||
|
||||
### 实际对照表(R1–R7,补「结果」格)
|
||||
|
||||
| 行 | 验收测试 | 结果 |
|
||||
|---|---|---|
|
||||
| R1 elite 行类合法与字段互锁、引用校验 | elite_row_test._test_elite_validation_rejects〔精英批 V1〕 | 绿——合法夹具零错+十一违例各「含」预期类别(cross_field×7/dangling_base×2/dup_key×1/domain+pool_extra 定位×1);config_db_test fv7 样本换代后直跑绿 |
|
||||
| R2 有效池=基础池+附加技、基础池取深度带最小行 | _test_effective_pool_merge〔精英批 V2〕 | 绿——深带行在前夹具下 {attack9,lunge1,heavy2}(取首行错误实现必红);battle 行读法等价面 {attack9,lunge1} |
|
||||
| R3 继承不复制 | _test_base_pool_change_propagates〔精英批 V3〕 | 绿——基础池权重改 7 传到精英;改写返回数组条目后表内行不变 |
|
||||
| R4 合并池过池构成纪律 | _test_merged_pool_composition〔精英批 V4〕 | 绿——合规精英计数 0;附加第二普攻/第二增益两夹具各计 1 且 message 含 goblin_elite |
|
||||
| R5 暂不接线:寻址拿不到 elite | _test_elite_unreachable_by_encounter〔精英批 V5〕 | 绿——请求 elite 返回空+诊断含 fatal/request_oob;同夹具 battle/boss 查询合计>0 且零 elite 行 |
|
||||
| R6 地精精英落表且全表加载零错 | _test_production_goblin_elite〔精英批 V6〕 | 绿——error 级诊断计数 0;行恰 1 条、12 字段逐位、无 unit_count_max/pool 键;有效池 {attack9,lunge1,desperate_guard1,heavy2} |
|
||||
| R7 存量生产表冒烟随新行同步 | round_flow/guard/shop/roll_multiplier/trigger_cond 五件直跑 | 绿——五件 exit=0 零 SCRIPT ERROR;既有断言原判据照旧(精确键集/行数精确等/键序逐位/注入全量还原),仅扩期望集与读法 |
|
||||
|
||||
### 卡 3 批末读数(T3.8 里程碑出口)
|
||||
|
||||
`bash tools/run_all_gates.sh --code`=**【验收通过】闸况与基线一致,无新增红**(冒烟/单元与集成/系统/场景探针/渲染闸/静态扫描六闸全绿)——卡 3 全改动面(schema+core+enemy.tres+存量测试+图鉴工具)全档回归在案。
|
||||
|
||||
### 实机目检总清单(卡 3)
|
||||
|
||||
**零项**——理由:精英暂不接线(敌人系统.md:151),无玩家可见面(卡 3 §8 测试方式两分法申报在案)。
|
||||
|
||||
### 遗留读数义务(卡 3)
|
||||
|
||||
- 精英 EV 重锚与数值终值 → 难度曲线与Boss(Alpha 阶段零读数;本批零 MC——卡 3 §5 申报)。
|
||||
- 开战快照改读有效池(敌人系统.md:325 半句)→ 待办清单「战斗节点遭遇编排方式」条目承接(本批无入口可验)。
|
||||
|
||||
### 实现决定记录(卡 3 新增条目)
|
||||
|
||||
11. **T2.3 夹具的 ogre_chief 行补合规池**:卡面 T2.3 夹具规格未点 boss 行池值——_mk_row 默认池(仅普攻)自身违反「至少一行无条件特殊技」纪律,pool_composition 误红。boss 行补 attack+lunge(自身合规),断言只数精英违规——夹具 mechanics 决策(白名单「夹具助手函数的拆法」族),V4 判据面不变。
|
||||
12. **round_flow 七处转换的批次内第 4 处豁免核**:卡面申报「另两处遍历(:571-575、:791-794)有前置过滤」;实查另核卡面未列的 :844(resolve_rowset 结果池读取)——经行类门 resolver elite 行结构性不可达,同豁免逻辑,不改不改判据。全文件 row.pool 直取残留恰 1 处(:574)与卡面申报一致。
|
||||
13. **expect_pools 键与文案的完成形态**:卡面 T3.4 定「:623 文案改『三十行』」;实改「三十行十一种池模板」——池模板计数同句失真(精英合并池为第 11 种),一并换代。键插入位置=字典尾(顺序无关,判据面=精确键集比对)。
|
||||
14. **S13 基线零新键确认**:enemy_table.gd/config_db_cross_validator.gd/config_db_resolver.gd 三件静态扫描零新增键(卡 3 §13 预算全数守住——validate() 局部变量 10/10 零新增、各私有函数分支在预算内),--code 跑闸静态扫描段绿为凭。
|
||||
@@ -0,0 +1,169 @@
|
||||
# 迭代 54 独立验收审计报告(2026-11-07)
|
||||
|
||||
> **审计者身份**:独立验收审计子代理——未参与本批实现的只读会话,全量凭据自取(diff 逐笔自读、grep 自跑、窄复跑自跑),未采信任何报告自述的「已全绿」。
|
||||
> **对象**:分支 `iter54`(工作区 `D:\code\g-plan2-iter54`),基底=master `a2e500a4`,三笔卡提交:`96123978`=卡 1(QA 缺陷收口批)、`4c424af3`=卡 2(营地交互修复批)、`f9c9caf5`=卡 3(精英专属池)。原史(逐 T 步提交与当轮跑批读数)在 `backup/iter54-20261107-030512` 与 `iter54-premerge-backup` 两分支在案。
|
||||
> **审计输入**:三张需求卡片(reports/需求卡片-QA缺陷收口批-2026-11-06.md/需求卡片-营地交互修复批-2026-11-05.md/需求卡片-精英专属池-2026-11-06.md)+实现报告+反哺点+三笔提交完整 diff。
|
||||
> **执行证据口径(申报在案)**:卡 1/卡 2 为中档审计,最窄复跑省略——执行证据=逐 T 步提交信息内的当轮跑批读数(`iter54-premerge-backup` 在案,T1–T7 每步读数齐全)+合回后远端 CI 现查(合回落地后按 process-conventions §1 第 3 条现查)。卡 3 为高档审计,窄复跑由审计者本机自跑(§3.5 原样读数)。
|
||||
|
||||
---
|
||||
|
||||
## 卡 1|QA harness 缺陷与覆盖遗漏收口批——判定:通过
|
||||
|
||||
### 1.0 申报验收档与实际执行深度
|
||||
|
||||
卡面申报验收档=中。实际执行:对照表 R1–R9 全量逐行核对+测试削弱检查(diff 删除行全扫)+内部字段直查受控例外核对逐条+死接缝删除判据(双模式 grep)审计者自跑。窄复跑按中档口径省略(证据口径见头部申报)。
|
||||
|
||||
### 1.1 对照表逐行核对(R1–R9)
|
||||
|
||||
| 行 | 核对结论 | 证据 |
|
||||
|---|---|---|
|
||||
| R1 终局结算框关闭动线零引擎报错 | 通过 | close() 与 _play_celebrate() 两处均改 `has_meta` 前置判断(battle_end_overlay.gd,diff 确认 :191–198/:329–336 两处,语义=有则必杀、无则跳过);验收形态按卡面自陈=grep 读数(「该缺陷唯一提交面=报错打印本体」),当轮读数〔T1 提交 57f3cc75〕=fx_probe/screen_flow_probe 各 exit=0、SCRIPT ERROR 0、celebrate_tw 0 |
|
||||
| R2 停手钮真输入命中与落效 | 通过 | `_stoptest_tick`+`_stoptest_assert_hit` 在案(ui_click_probe.gd:1172–1236),断言①停手键命中登记、②相位离开 Active、120 帧超时红、终局竞态静默弃测守卫,尾缀〔QA收口 V2〕grep 命中 2 处;驱动=`_click_stop()`(`ap_bar._btn_stop` 真钮 push_input);当轮读数〔T2 提交 816aaae5〕=ui_click_probe exit=0、问题=0、停手键命中 1 次 |
|
||||
| R3 journey 停手+方法口清零 | 通过(候选键一项按申报例外记) | journey_player 停手行为(每战至多一次,Active+停手钮使能+本轮已挖 ≥1+盘面有挖)+8 处方法口转换逐处核对在 diff(:380→node_overlay._btn 真点、:441→candidate_rect(0) 矩形中心、:449/:451/:452/:454/:455/:457→curio 真钮/bcard 矩形中心);头注「方法口已清零」换代在案。当轮读数〔T3 提交 67f16cc2〕=fatal 0、停手键命中 6 次、停点键在场、屏到达 16 类含战斗/终局、exit=1 为已知缺陷 A1 基线态(卡 1 §5 明示不收件,卡 2 承接)。「候选键在场」未产出——登记件登记通道=BaseButton pressed+营地自绘物件两源,地图画布与 BCard 非 BaseButton(结构性不可产出),实现报告决定记录 5 申报为观测面局限;效力承载面=屏到达 16 类+段间推进(当轮在跑批执行)。审计认定:判据字面一项偏差已申报且有替代效力证据,不构成缺漏 |
|
||||
| R4 停点链真输入化(16 处) | 通过 | 逐处清点 diff:候选 2(:426/:536→push_click_at(candidate_rect))+节点继续 2(:448/:656→push_click(node_overlay._btn))+三选一 10(:473/:475/:476/:478/:479/:481/:649/:651/:652/:654→bcard 矩形中心/真钮)+弃图刷新链 2(:972→`_btn_refresh`、:975→`_confirm_row` 内 Button 子件)=16,全部经 ProbeUiUtil;既有落点断言删除行全扫=0 条。当轮读数〔T4 提交 2067461f〕=exit=0、问题=0、覆盖 43 命中/30 控件较改前 39/28 不回退、新增停点链键。弃图确认链四种子未触发(is_refresh_ready RAP≥8 门槛,改前同不可达)——申报在案,效力由 journey 真点链承载 |
|
||||
| R5 到达型候选+停点(7 探针 11 处) | 通过 | 7 件 diff 逐件核对:fx_shot_probe:47/layout_probe:32/shot_probe:147/ui_probe:83/t17_defer_probe:39/ui_tune_probe:143(原 :109 行号就地校正)→ push_click_at(candidate_rect(0).get_center());ui_struct_probe:275(原 :257)→ push_click(node_overlay._btn);shot_probe:9 头注同步。当轮读数〔T5 提交 46493f73〕=headless 五件 exit=0+SCRIPT ERROR 0,窗口化两件 exit=0 |
|
||||
| R6 调试浮窗面(4 处) | 通过 | ui_tune_probe:③应用→find_button 真点、④重置→find_button 真点、⑤模拟→`_sim_btn` 真点(折叠区滚轮滚入通道=test-conventions §2 现役条文,12 档上限+失败 problem 上报正面效力自检)、⑧应用→前置重开+真点=4 处全转真输入。当轮读数〔T5〕=ui_tune PASS 全链路(含 N=10 模拟输出与种子三分断言) |
|
||||
| R7 系统测试宿主候选真输入化 | 通过 | system_test.gd:440(原 :433)→ push_click_at(candidate_rect(0).get_center()),`const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")` 复用单点;当轮读数〔T6 提交 97d6b0a5〕=69 检查 0 失败三闸过(V3c 阶段真实选路提交在跑批中执行) |
|
||||
| R8 三选一覆盖层单元真输入化(5 处) | 通过 | reward_overlay_test.gd 刷新/首卡/次卡/确认/继续 5 处真输入(bcard 矩形中心+真钮),断言语义逐位保持(diff 确认仅驱动行与消息文案换代,判定表达式零改);经 preload 复用 ProbeUiUtil。当轮读数〔T6〕=82 套件 all green、exit=0、0 FAIL |
|
||||
| R9 死接缝同批清理(5 文件 11 方法) | 通过 | diff 确认删除:ap_bar_view 2+curio_pick_overlay 4+node_overlay 1+debug_overlay 3+level_map_screen 1=11。**审计者自跑双模式 grep**:定义清 `func press_(candidate\|continue\|card\|confirm\|refresh\|apply\|reset\|simulate)` 与调用清 `\.press_(…)`(--include=*.gd,全仓)双双零命中 |
|
||||
|
||||
尾缀 grep 口径说明:〔QA收口 V2〕在码 2 处(T2 新断言的唯一在码尾缀);V1(grep 读数)、V3(journey 跑批读数)、V9(grep 判据)按卡面对照表自陈为读数型判据、无在码断言载体;V4–V8 验收载体=既有断言保持+当轮直跑读数(卡面未为转换行立新尾缀)。与卡面 §11 白名单「尾缀编号分配」一致,无缺漏。
|
||||
|
||||
### 1.2 测试削弱检查
|
||||
|
||||
- 三笔提交 diff 删除行全扫(`^-` 行筛 `_problems.append`/`Kit.check`/`check(`):卡 1 删除断言行=**0 条**。
|
||||
- 允许删除面(只验证被删 press_* 死接缝的护栏):实际无此类护栏被删——死接缝方法体内无断言。
|
||||
- 驱动行替换未触判定表达式;reward_overlay_test 仅消息文案「press_refresh 未外发」→「真点刷新未外发」换代,判定 `refreshed.size() == 1` 原样。
|
||||
- ui_click_probe 主循环冻结条件由 `_locktest_state != 1` 扩为 `_locktest_state != 1 and _stoptest_state != 1`——新增冻结窗,非弱化。
|
||||
- 判据不放宽面核对:无阈值改动、无断言删除、无跳过逻辑新增。
|
||||
|
||||
### 1.3 例外核对(内部字段直查受控例外逐条)
|
||||
|
||||
- 产品码私取新增=0(A2 为视图内部容错;candidate_rect 为卡面 §13 申报的唯一新公开面,命中面与 `_candidate_at` 同源——审计比对 level_map_screen.gd:452 与 :478 的半宽常量 `NODE_HALF + Vector2(HIT_PAD, HIT_PAD)` 逐字节一致)。
|
||||
- 探针/测试面私有直读逐条:`ap_bar._btn_stop`/`_btn_refresh`/`_confirm_row`、`node_overlay._btn`、curio `_bcards`/`_confirm_btn`/`_continue_btn`、`debug_overlay._sim_btn`、登记件 `_recorder._node_keys`/`_hits`(决定记录 6 申报)——全部属探针族现役惯例(ui_click_probe 既有 `ap_bar._btn_stop` 同型先例),封装扫描件射程不含探针(presentation_encapsulation_sweep_test 只套五个 flow 件)。
|
||||
- tests 面合成输入件检查:game/tests 全目录 grep `InputEvent`=**零命中**;system_test 与 reward_overlay_test 均经 preload 复用 ProbeUiUtil(禁第二份合成输入件契约成立)。probe/sim 面持 push_input 的文件(ui_input_probe、screen_flow_probe 等)全部为改前既有件,本批零新增。
|
||||
- 改动面越界检查:卡 1 提交触面 17 文件=卡面 §7 结构图逐项+产出物 2 件,零越申报面。
|
||||
|
||||
---
|
||||
|
||||
## 卡 2|营地交互修复批——判定:通过
|
||||
|
||||
### 2.0 申报验收档与实际执行深度
|
||||
|
||||
卡面申报验收档=中。实际执行:对照表 R1–R8 全量逐行核对+测试削弱检查+允许改动 0 条既有断言核对+判据形态核对+UiKit 出口形参核对。窄复跑按中档口径省略(证据口径见头部申报)。
|
||||
|
||||
### 2.1 对照表逐行核对(R1–R8)
|
||||
|
||||
| 行 | 核对结论 | 证据 |
|
||||
|---|---|---|
|
||||
| R1 大门无断点按「是」后开始层不可见 | 通过 | `_start_adventure()` 首句加 `ui.screens.start_overlay.visible = false`(main.gd:293,在 `ui.flow.close` 之前——契约 1 落点);`_start_adventure_from_menu()` 冗余句删除(:194 注释换代)。探针 R1 断言〔营地交互批 V1〕在案(ui_click_probe 大门段),真实入口=大门命中区真点→R4 推进钮真点→同链断言。当轮读数〔T1 提交 bf8d30c1〕=探针 exit=0、问题=0(修复前该句恒红判据零命中) |
|
||||
| R2 开局落节点图选路停点 | 通过 | R2 断言〔营地交互批 V2〕(level_screen.visible)与既有 is_adventure 断言同批在案;await 1 帧→3 帧帧口径落实(T1 提交信息记「停点成形最小帧数实测=3 帧内成形」)。判据绑定面按卡面 R2 自陈=开局终态在位断言,特有提交面由 R1 承载——两行同批缺一即红结构保持 |
|
||||
| R3 两/三钮形态位次=推进→次→维持 | 通过 | confirm_dialog.gd 三句 `add_child` 次序镜像为 确认→次→取消,对象创建、三句 connect、成员声明、open()/open_three() 签名零改动(diff 确认,契约 3);新套 confirm_dialog_test.gd=7 断言(双钮 3+三钮 4),判据=公开子节点序+Button 公开 text,零私有直读,双钮形态 child(2) 非 child(1) 硬知识与非空前置(子节点数=3)在案;入册 run_all.gd(+1 行)。当轮读数〔T2 提交 af127b15〕=直跑 exit=0 七断言全 PASS |
|
||||
| R4 左位真点即推进 | 通过 | 探针 R4〔营地交互批 V3〕:真点前 `_confirm_consumer == "gate_new"` 前置读(探针既有直读先例)、按钮行 child(0) 全局矩形中心 push_click_at(非行矩形左位——卡面轮次 4 硬伤修正的落法)、真点后 is_adventure 翻转+consumer 清零。T2.3「前置重开」→「原位 R4」重构=实现报告决定记录 8 申报(主证绑定翻转才有判别力),既有确认断言原样保留。当轮读数〔T2〕=R4 在跑批执行且通过 |
|
||||
| R5 顶栏栏体几何与徽章落座 | 通过 | `_smoke_camp_hud_band()` 接进 `_split_tail()` await 链(system_test.gd:47,非 _initialize——卡面 T3.4 硬伤修正落法);五钉:①`to_html(true) == "6e4526cc"`(禁浮点分量直等判据形态成立,全文件无 `.a == 0.80` 型断言)②铺满(栏体矩形对顶栏矩形等值+高 48)③IGNORE④栏体为第一子件⑤徽章顶格(y 对齐栏顶)+右位(end.x=栏右缘−12),几何容差 0.51。T3.0 前置读数=徽章 min 高实测 48.0(L3 第 5 条未触发,决定记录 10)。当轮读数〔T3 提交 8f2eab7c〕=76 检查 0 失败 |
|
||||
| R6 栏体对落位带底色对比 ≥3:1 | 通过 | `_test_hud_band_contrast_over_bg()`:规格侧算式——测试内写死 `#6E4526`/α0.80(Color(0x6e/255.0, …) 字面构造),**未把视图件 color 喂进算式**(oracle 镜像禁令成立);带高取 `camp_scene_layout_table.gd` 数据出口;双层全像素扫描+扫描面非空前置;WCAG ≥3.0。当轮读数〔T3〕=实测 4.29:1,与卡面复核推算一致 |
|
||||
| R7 回归·件 1 未动三条动线 | 通过 | 既有大门两分支段与篝火段断言零改写(diff 确认无删除行);三处未改动隐藏点核在位:`_resume_from_menu()`(main.gd:246)、boot 默认(:137)、其余 11 处自管点全部原样(grep 全列与卡面 13 处清单一致)。当轮读数〔T1〕=既有断言原样通过 |
|
||||
| R8 回归·件 2 路由与件数断言 | 通过 | 探针次钮隐藏断言(ui_click_probe.gd:739,〔营地图批 V3〕原文原样)与 system_test 三钮计数断言(:193–201,V2d 三钮在位+渲染面可见钮计数==3)均未被本批触碰(两笔 diff 删除行全扫=0)。位次改动后二者仍绿=契约 3 未被违。当轮读数=探针问题=0、系统测试 76/0 |
|
||||
|
||||
### 2.2 测试削弱检查
|
||||
|
||||
- diff 删除断言行=**0 条**(三笔提交中卡 2 段全扫)。
|
||||
- camp_scene_layout_test 入口重构(单 quit 迁至 `_run_all` 统一收尾):断言集零增删,早退路径从 quit 改 return 后仍会被 `_run_all` 收尾 quit 接住,无静默跳过——非削弱。
|
||||
- 允许改动 0 条既有断言:成立。
|
||||
|
||||
### 2.3 例外与申报面核对
|
||||
|
||||
- 契约 1:开局隐藏点唯一(`_start_adventure()`),续玩唯一(`_resume_from_menu()`),禁改的 13 处自管点逐一 grep 在位,`camp_shop_presenter.gd:85` 语义句未动(契约 2)。
|
||||
- 契约 4:`camp_hud_band_surface(host)` 形参只有 host——色值、α、铺满、IGNORE、挂载、返回句柄六事全在体内(ui_kit.gd:666–676 审读);无 color/pass_input 形参(开了即违——未开);盘上三处同形铺面(ui_assembler.gd overlay_bg、ui_kit `_modal_dim`、page_shell 底板)零触碰(不在 diff 文件清单)。
|
||||
- 契约 5:IGNORE 在出口内写死;camp_hud_band.gd 内右对齐容器自身 IGNORE 同批落(漏一处即顶排点不动的双点纪律成立);本文件内 `ColorRect.new()` grep=零命中。
|
||||
- S13 基线修订:ui_kit max-public-methods 23→24——卡 2 §13 预申报的例外(「本条即申报与请求」+U0 批卡),diff 仅此一行;非越权改基线。
|
||||
- 改动面越界检查:提交触面=卡面 §7 结构图 9 件+run_all 入册+gdstyle_baseline+产出物 2 件,零越申报面;design/ 与两份根流程清单零触碰(契约 6)。
|
||||
- 能力行名实核对:README:158/:159 两行与实际落地(confirm_dialog_test 两函数、`_smoke_camp_hud_band`、`_test_hud_band_contrast_over_bg`)逐字相符。
|
||||
|
||||
---
|
||||
|
||||
## 卡 3|精英专属池——判定:通过(高档全量)
|
||||
|
||||
### 3.0 申报验收档与实际执行深度
|
||||
|
||||
卡面申报验收档=高。实际执行:①对照表 R1–R7 逐行核对;②判据绑定面抽查;③测试削弱检查(含 4 条申报测试改动逐条 diff 核对);④直查例外核对+契约核四条;⑤窄复跑抽查——审计者自跑两件;⑥oracle 出处核对抽查 5 条。
|
||||
|
||||
### 3.1 对照表逐行核对(R1–R7)
|
||||
|
||||
| 行 | 核对结论 | 证据 |
|
||||
|---|---|---|
|
||||
| R1 elite 行类合法与字段互锁、引用校验 | 通过 | elite_row_test `_test_elite_validation_rejects`=合法夹具零错+11 个违例夹具(①删 base_ref ②同名 ③pool_extra 空 ④删键 ⑤带 pool ⑥battle 带 base_ref ⑦battle 带 pool_extra ⑧悬空 ⑨指向 elite ⑩重叠 ⑪权重 0)各「含」预期类别,与卡面 T1.11 逐条对应;`_validate_elite_fields`/`_validate_elite_refs`/`dangling_base` 实装与 T1.7/T1.9 一致。config_db_test fv7 样本 miniboss 换代在 diff(申报第 1 条) |
|
||||
| R2 有效池=基础池+附加技、基础池取深度带最小行 | 通过 | `_test_effective_pool_merge`:夹具深带行(band 3, attack5/charge2)在前、浅带行(band 0, attack9/lunge1)在后——取首行的错误实现必红;期望 {attack9,lunge1,heavy2}+size 3;battle 行读法等价面 {attack9,lunge1}。实装 `_min_band_row`(遍历取 band 最小)与 `effective_pool`(基础池条目+pool_extra 条目,均 duplicate)与 T1.3/T1.4 一致 |
|
||||
| R3 继承不复制 | 通过 | `_test_base_pool_change_propagates`:基础行 attack 改 7 后再查=7(按需现算);改写首次返回数组条目为 99 后表内行仍=7(duplicate 隔离)。enemy_table.gd 类变量 grep=仍仅 `rows`+`_resolver` 两个,**零新增类变量**(契约 1) |
|
||||
| R4 合并池过池构成纪律 | 通过 | `_test_merged_pool_composition`:合规精英 pool_composition=0;附加第二普攻/第二增益两夹具各=1 且 message 含 goblin_elite。boss 行补合规池=决定记录 11 申报(夹具 mechanics,`_mk_row` 默认单普攻池自身违纪律会误红);跨表件 `:194` 改读 `enemy_table.effective_pool(row)` 单行 diff(T2.1) |
|
||||
| R5 暂不接线 | 通过 | `_test_elite_unreachable_by_encounter`:夹具 resolver 敌行回调返回含 elite 行的三行行集;①`resolve_enemy_rowset(1, 0, "elite")` 返回空+诊断含 fatal/request_oob——判据绑行类门改动(只改枚举不改为门时该请求会返回 goblin_elite);②battle/boss 查询合计>0 且零 elite 行(非空前置内嵌判据)。resolver :41 改读 `ENCOUNTER_ROW_KINDS`、文案保持「row_kind 域 {battle, boss}」逐字(T2.2);该常量全仓 grep 仅 resolver 一处消费(卡面 §12 单点申报成立);`unit_container.gd`/`battle_orchestrator.gd`/`level_runner.gd`/`level_table.gd` 零触碰(契约 2) |
|
||||
| R6 地精精英落表且全表加载零错 | 通过 | `_test_production_goblin_elite`:生产表 load_tables(true) 后 error 级诊断=0;行恰 1 条;12 字段逐位断言表+无 unit_count_max/无 pool 键+有效池四键精确。enemy.tres diff:行插 (1,boss,6,ogre_chief) 之后、(2,battle,0,bugbear) 之前(T3.1),15 键中 12 判据字段与卡面 §2 表逐值一致(hp 20/atk 2/1/3/armor 2/light/unit 1/base_ref goblin/pool_extra heavy 2),attack_type normal 由 roll_multiplier_test 既有「全行 normal」断言覆盖 |
|
||||
| R7 存量冒烟随新行同步、判据不放宽 | 通过 | 逐条核对见 3.3——行数 29→30 精确等、键序逐位插行、池模板精确键集比对、曲线护栏阈 ≤2.5 未动、注入还原改相对基数(加强);五件存量套件当轮读数〔T3 提交 75a4afe2〕=exit=0 零 SCRIPT ERROR |
|
||||
|
||||
### 3.2 判据绑定面抽查
|
||||
|
||||
- V5 绑定改动特有面:判据①在「只做 T1.1 不做 T2.2」的反事实下必红(elite 请求会返回行而非空)——断言钉在行类门改动本身,非改动前后双绿的常真断言。审计按卡面反事实推演复核成立。
|
||||
- V2 绑定取带语义:夹具行序(深带在前)使「取首行」实现必红——钉在 `_min_band_row` 的取最小带逻辑,非常真。
|
||||
- V3 绑定零缓存:改表后再查+改写返回不回灌,两钉分别排除缓存实现与引用泄漏——绑契约 1。
|
||||
- V4 绑定合并池:单看基础行或单看附加技都不违规,唯合并后违规——钉在「纪律校验对象=合并池」。
|
||||
|
||||
### 3.3 测试改动申报 4 条逐条 diff 核对(无越面改判)
|
||||
|
||||
| 申报条 | diff 实况 | 结论 |
|
||||
|---|---|---|
|
||||
| 1. config_db_test fv7 样本 elite→miniboss+文案 {battle, boss, elite} | 恰两处(样本行+断言消息),判定逻辑(domain+含 "row_kind")零改 | 相符 |
|
||||
| 2. round_flow 七处 row.pool→effective_pool+expect_pools 增键+文案 | diff 恰 7 处读法换代(:530/:541/:560/:621/:627/:642/:657 区段)+expect_pools 增 `"1_elite_0_goblin_elite"` 键+文案「二十九行十种」→「三十行十一种」(十种→十一种为决定记录 13 申报的完成形态,池模板计数同句失真一并换代);期望字典判定 `got != expect_pools[rk]` 零改 | 相符 |
|
||||
| 3. guard 行数 30+键序插行+l1_totals skip elite | 行数断言 `== 30` 精确等(未放宽为 ≥);expect_keys 在 `"1|boss|6|ogre_chief"` 后恰插 `"1|elite|0|goblin_elite"`;`:389` 循环首加 elite 跳过(两行);头注与 print 文案同步 | 相符 |
|
||||
| 4. level_shop 还原断言改相对基数 | 注入前记 `base_n`,断言 `== base_n`,文案去写死数——卡面自陈「属加强」 | 相符(加强) |
|
||||
|
||||
三笔提交删除断言行全扫:恰上述 4 条申报改判的旧行,无第 5 条。**无越面改判。**
|
||||
|
||||
### 3.4 契约核(四条全过)与例外核对
|
||||
|
||||
- 契约 1 继承不复制:enemy_table.gd 类变量 grep=2 个(rows/_resolver),零新增;effective_pool 每次现算、返回条目 duplicate、零缓存零写回(V3 断言实证)。
|
||||
- 契约 3 算法唯一落点:合并算法 grep `pool_extra` 全仓仅 enemy_table.gd/elite_row_test/round_flow/compendium_dump 四处(后三处为消费/测试/导出面,非第二份算法);权重上限 9999 在 schemas 面仅 enemy_table.gd:171 一处(meta_tree 的 9999 为价格域既有件,非池权重)。
|
||||
- 契约 4 既有文案逐字不变:`_validate_row_pool` 四条报错文案经 field 参数化后对 field="pool" 渲染逐字节等值(「%s pool 条目非字典」「%s pool 条目 skill_id 空」「%s pool 内 skill_id 重复」「%s pool 权重越域 (0,9999]」——diff 左右对比确认);既有错误类别零改名,新增类别仅 dangling_base;`:88` row_kind 文案域换代=卡面 T1.1 申报项。
|
||||
- 直查例外:产品码零私取新增(新增公开面=effective_pool+ENCOUNTER_ROW_KINDS,均卡面 §12 申报);elite_row_test 全部经公开入口(validate/effective_pool/validate_skill_pool_links/resolve_enemy_rowset/load_tables)进出,零私有直读。
|
||||
- 残留读法豁免核:round_flow 残留 `row.pool` 恰 3 处(:574 种带过滤 row_kind=="battle"、:795 species 名单 ["goblin","bugbear","ogre_chief"] 精确等值不含 goblin_elite、:846 resolver battle 查询结构性不可达)——与卡面 §3.4+决定记录 12 一致,豁免成立。
|
||||
- 改动面越界检查:提交触面=卡面 §7 结构图 12 件+产出物,零越申报面;design/ 零触碰。
|
||||
|
||||
### 3.5 窄复跑读数(审计者本机自跑,2026-11-07)
|
||||
|
||||
`Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://tests/unit/data_config/elite_row_test.gd`:
|
||||
|
||||
- 退出码=**0**;`[PASS]` 计数=**27/27**(V1 合法夹具+①–⑪ 共 12、V2×2、V3×2、V4×3、V5×2、V6×6);`[FAIL]` 与 `SCRIPT ERROR` 扫描=零行。原样尾行:
|
||||
`[elite_row_test] PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过`
|
||||
V6 关键 PASS 行原样:`生产表加载 error 级诊断计数=0(实得 0)〔精英批 V6〕`/`行值 12 字段逐位相等(失配项 无)〔精英批 V6〕`/`有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 { "attack": 9, "lunge": 1, "desperate_guard": 1, "heavy": 2 })〔精英批 V6〕`
|
||||
- 同目录 `config_db_test.gd` 抽跑:退出码=**0**,尾行 `[config_db_test] all green (29 checks)`(fv7 换代样本所在套件全绿)。
|
||||
- (退出时 ObjectDB 泄漏警告为引擎常规退出清场噪音,与本套件断言面无关,headless 直跑既有形态。)
|
||||
|
||||
### 3.6 oracle 出处核对抽查(5 条,均非 oracle 镜像)
|
||||
|
||||
| 断言 | oracle 注记指向 | 独立推导核对 |
|
||||
|---|---|---|
|
||||
| V1 十一违例 | 敌人系统.md:325 拒绝清单 | :325 逐句可独立推出 11 个违例面:缺基础引用/缺附加技/悬空/指向 elite/同名/附加重复/合并池违纪/elite 写池/非 elite 带两键——①–⑦⑨⑩ 与之一一对应;⑪ 权重 0 由 :283「weight ∈ (0, 9999]」推出。**可独立写出,通过** |
|
||||
| V2 合并池 | 敌人系统.md:149 | :149「基础池取自同关、战斗行类、该基础敌种深度带最小的那一行的池模板」——期望值 {attack9,lunge1,heavy2}=band 0 行池+附加,规格句独立可推;夹具反事实(取首行必红)为审计加验。**通过** |
|
||||
| V3 传到精英 | 敌人系统.md:149「继承不复制:基础池调参自动传到精英」 | 改权重 7 期望读 7 直由该句推出。**通过** |
|
||||
| V4 合并池纪律 | 敌人系统.md:150「校验的对象是合并后的池」 | 「附加第二普攻→违规」唯在合并口径下成立(基础行单独合规、附加技单独合法),规格句独立可推。**通过** |
|
||||
| V6 十二字段 | 敌人系统.md:147+卡 3 §2 表 | :147 出「精英=敌人表行类为 elite 的独立一行」的结构面;行值出处=卡面 §2(2026-11-06 用户点头草案 v0,GDD 明示不登记行值)——12 字段值(hp 20/atk 2/1/3/armor_class 2/armor_type light/unit_count 1/base_ref goblin/display_name「精英·地精」等)与卡 §2 表逐值一致,预期值独立于实现存在(需求合同面)。**通过,非镜像** |
|
||||
|
||||
抽查结论:5 条抽查均能由注记指向的规格句(或卡面用户已裁行值表)独立写出预期值,未发现「预期值只在实现里有的」oracle 镜像。
|
||||
|
||||
### 3.7 报告引用瑕疵(不阻塞项)
|
||||
|
||||
实现报告卡 3 T3 摘要引用提交 `e538e2cb`/`f4865ad8`、卡 1 各 T 摘要引用 57f3cc75 等 10 个哈希均于 `iter54-premerge-backup` 分支在案可解;唯卡 2 T2 摘要引用「提交 46287043 区段」全库(含 reflog 与 dangling)不可解——该步实际提交=af127b15(premerge-backup),已折叠进重塑提交 4c424af3。属报告引用笔误,改动本身已由本审计在 diff 与跑批读数双面核实,不影响判定。
|
||||
|
||||
---
|
||||
|
||||
## 总判定:通过(三卡全过)
|
||||
|
||||
| 卡 | 申报档 | 判定 | 关键依据 |
|
||||
|---|---|---|---|
|
||||
| 卡 1 QA 缺陷收口批 | 中 | 通过 | R1–R9 全量核对通过;死接缝双模式 grep 审计自跑双零;削弱检查零删除断言;41 处转换全经 ProbeUiUtil 单点;候选键观测面局限已申报且效力有替代承载 |
|
||||
| 卡 2 营地交互修复批 | 中 | 通过 | R1–R8 全量核对通过;允许改动 0 条既有断言成立(次钮隐藏/三钮计数原样且绿);to_html 判据形态合规;出口形参仅 host;S13 基线修订在预申报通道内 |
|
||||
| 卡 3 精英专属池 | 高 | 通过 | R1–R7 全量核对通过;窄复跑自跑 27/27 PASS+config_db_test all green;4 条申报测试改动与 diff 逐条相符无越面;四条契约全过;oracle 抽查 5 条均非镜像 |
|
||||
|
||||
- 测试削弱总核:三笔提交删除断言行合计恰卡 3 申报 4 条,其余零削弱;禁变异类测试与撤销类取证——三笔 diff 内未发现人为制造红或回退取证的形态。
|
||||
- 最窄复跑省略(中档卡)的执行证据口径=批内逐 T 提交读数(`iter54-premerge-backup` 每步在案)+合回后远端 CI 现查;卡 3 高档窄复跑由审计者自跑实证。
|
||||
- 遗留义务核对:真人目检两项(覆盖面板下钻/顶栏观感)已登记实现报告(登记即了结义务);「开战快照拿到合并后池」半句与精英 EV 重锚的移交去向(待办清单「战斗节点遭遇编排方式」/难度曲线与Boss)在卡 3 §5 与实现报告遗留义务节闭合;反哺点三条(architecture CampHudBand 行/体验验证清单一行/03-内容规模.md:66)落点明确待用户裁决。
|
||||
- 未阻塞观察两条(供后续批次参考,不构成本批缺漏):①上节 3.7 报告哈希引用笔误一处;②卡 1 R3/R4 覆盖登记件对非 BaseButton 控件(地图画布、bcard)无命中键为登记件结构性局限——若日后需要该面命中证据,归「控件级零命中缺口」B5 既有条目处置,非本批义务。
|
||||
@@ -31,7 +31,7 @@ SKILL_REQUIRED = ("skill_id", "display_name", "type", "cooldown_beats")
|
||||
|
||||
# 以下中文名均为表 schema 封闭枚举的展示词(enemy_table.gd / skill_table.gd),非玩法数值
|
||||
LEVEL_CN = {1: "第一关", 2: "第二关", 3: "第三关"}
|
||||
ROW_KIND_CN = {"battle": "战斗行", "boss": "Boss行"}
|
||||
ROW_KIND_CN = {"battle": "战斗行", "boss": "Boss行", "elite": "精英行"}
|
||||
ATTACK_TYPE_CN = {"normal": "普通", "pierce": "穿刺", "magic": "魔法"}
|
||||
ARMOR_TYPE_CN = {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"}
|
||||
SKILL_TYPE_CN = {"basic": "普攻", "damage": "伤害", "buff": "增益", "debuff": "妨害"}
|
||||
@@ -46,6 +46,7 @@ FIELD_CN = {
|
||||
"display_name": "显示名", "unit_count": "单位数", "unit_count_max": "单位数上限",
|
||||
"hp": "生命", "atk": "攻击基线", "atk_min": "攻击下限", "atk_max": "攻击上限",
|
||||
"attack_type": "攻击类型", "armor_type": "护甲类型", "armor_class": "护甲等级",
|
||||
"base_ref": "基础敌种", "pool_extra": "附加技",
|
||||
"skill_id": "技能ID", "type": "类型", "damage_mult": "伤害倍率", "rider_mult": "必中附加分量",
|
||||
"cooldown_beats": "冷却拍数", "trigger": "触发条件", "buff_ref": "增益引用", "buff_duration": "增益持续",
|
||||
}
|
||||
@@ -119,7 +120,7 @@ def render_enemy_card(row: dict, skill_by_id: dict) -> str:
|
||||
f'<span class="badge">{esc(badge)}</span></h4>')
|
||||
out.append('<table class="attrs"><tbody>')
|
||||
for key in row:
|
||||
if key == "pool":
|
||||
if key in ("pool", "pool_extra"):
|
||||
continue
|
||||
out.append(f'<tr><th>{esc(FIELD_CN.get(key, key))} <code>{esc(key)}</code></th>'
|
||||
f'<td>{attr_value(row, key)}</td></tr>')
|
||||
|
||||
@@ -9,6 +9,6 @@ game/core/adventure_runner.gd|quality/max-class-variables = 23
|
||||
game/core/level_runner.gd|quality/max-class-variables = 30
|
||||
game/core/level_runner.gd|quality/max-public-methods = 33
|
||||
game/presentation/main.gd|quality/max-public-methods = 32
|
||||
game/presentation/views/ui_kit.gd|quality/max-public-methods = 23
|
||||
game/presentation/views/ui_kit.gd|quality/max-public-methods = 24
|
||||
game/presentation/views/board_view.gd|quality/max-class-variables = 31
|
||||
game/presentation/views/board_view.gd|quality/max-public-methods = 27
|
||||
Reference in new issue
Block a user