Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
938055df3b | ||
|
|
1d1f111219 | ||
|
|
16d2176576 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 152 B |
Binary file not shown.
|
After Width: | Height: | Size: 193 B |
Binary file not shown.
|
After Width: | Height: | Size: 148 B |
@@ -36,10 +36,10 @@ const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
|
||||
# E1 封装收口:协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
|
||||
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
|
||||
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
|
||||
# 装板域(BoardGenerator+装板六函数族)已拆 BoardInstaller——生成器实例随件自建,宿主零成员。
|
||||
var _board_generator: BoardGenerator
|
||||
var minesweeper: MinesweeperRules
|
||||
var action_economy: ActionEconomy
|
||||
var _enemy_system: UnitContainer
|
||||
@@ -230,12 +230,13 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_board_generator = BoardGenerator.new()
|
||||
action_economy = ActionEconomy.new(db)
|
||||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||||
_enemy_system = UnitContainer.new(db)
|
||||
_status_effects = StatusContainer.new(db)
|
||||
_reward_economy = RewardEconomy.new(db, rng)
|
||||
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
|
||||
_board_generator.fatal_handler = _fatal; _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
|
||||
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
|
||||
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
|
||||
_fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy,
|
||||
@@ -267,7 +268,7 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
return false
|
||||
if not _begin_battle_accept(enc):
|
||||
return false
|
||||
if not _install_and_reset():
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
_begin_battle_fx_state(enc)
|
||||
@@ -410,14 +411,34 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void:
|
||||
action_economy.grant_reserve_ap(voucher_rap)
|
||||
|
||||
|
||||
# ---- 装板受理薄壳(BoardInstaller 拆件后的宿主承接面——begin_battle/中途刷新/轮内刷新三调用点唯一入口)----
|
||||
# 件内装板(生成→全局变异决议→装板受理→资源摇号→变异布置)成功后执行宿主态复位:五处计数清零+
|
||||
# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。
|
||||
func _install_and_reset(during_round := false) -> bool:
|
||||
var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper,
|
||||
"reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal})
|
||||
if not installer.install(during_round):
|
||||
func _install_new_board(during_round := false) -> bool:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
|
||||
var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||||
if board == null:
|
||||
return false
|
||||
# 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant)
|
||||
_resolve_global_variant(board)
|
||||
var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round)
|
||||
if not res.accepted:
|
||||
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
|
||||
return false
|
||||
# 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池)
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var eligible: Array[Dictionary] = []
|
||||
for c in board.safe_cells():
|
||||
if not r0.has(c):
|
||||
eligible.append({"cell": c})
|
||||
for c in board.mines.keys():
|
||||
if not r0.has(c):
|
||||
eligible.append({"cell": c})
|
||||
if not _reward_economy.roll_board(eligible):
|
||||
_fatal("地图战斗资源 roll_board 被拒")
|
||||
return false
|
||||
# 地形变异 布置段(棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 →
|
||||
# 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区
|
||||
_place_variants(board)
|
||||
_seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||||
_seal_miss_seen.clear()
|
||||
_perfect_hit_true = 0 # 整图完美计数随 install 重置
|
||||
@@ -431,6 +452,103 @@ func _install_and_reset(during_round := false) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# ---- 全局作用域变异 决议(装板序内 roll_board 之前)----
|
||||
# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在
|
||||
# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。
|
||||
# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走
|
||||
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
|
||||
func _resolve_global_variant(board: BoardData) -> void:
|
||||
board.board_variants.clear()
|
||||
_reward_economy.board_skew = 0
|
||||
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
|
||||
String(encounter.get("node_type", "")))
|
||||
if row.is_empty():
|
||||
return
|
||||
board.board_variants = [String(row.variant_id)]
|
||||
_reward_economy.board_skew = int(row.get("effect_value", 0))
|
||||
|
||||
|
||||
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
|
||||
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
|
||||
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
|
||||
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
|
||||
# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
|
||||
# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) +
|
||||
# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例,
|
||||
# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。
|
||||
# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。
|
||||
func _place_variants(board: BoardData) -> void:
|
||||
if not board.board_variants.is_empty():
|
||||
return
|
||||
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
|
||||
if pool.is_empty():
|
||||
return
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var candidates: Array[Vector2i] = []
|
||||
for c in board.safe_cells():
|
||||
if not r0.has(c):
|
||||
candidates.append(c)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
|
||||
var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)]
|
||||
var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \
|
||||
+ (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus)
|
||||
total = mini(total, candidates.size())
|
||||
_place_variants_apply(board, candidates, stream, vrow, total)
|
||||
|
||||
|
||||
# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费)
|
||||
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
|
||||
var placed := {}
|
||||
while placed.size() < total:
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
|
||||
var free: Array[Vector2i] = []
|
||||
for c in candidates:
|
||||
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
|
||||
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
|
||||
if placed.has(c) or _adjacent_to_packs(board, placed, c):
|
||||
continue
|
||||
free.append(c)
|
||||
if free.is_empty():
|
||||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||||
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
|
||||
placed[seed_c] = true
|
||||
var pack: Array[Variant] = [seed_c]
|
||||
var pack_set := {seed_c: true}
|
||||
while pack.size() < pack_size:
|
||||
var fringe := _variants_fringe(board, pack, candidates, placed, pack_set)
|
||||
if fringe.is_empty():
|
||||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||||
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
|
||||
placed[nxt] = true
|
||||
pack_set[nxt] = true
|
||||
pack.append(nxt)
|
||||
for c in placed.keys():
|
||||
board.variants[c] = [String(vrow.variant_id)]
|
||||
|
||||
|
||||
# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接)
|
||||
func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]:
|
||||
var fringe: Array[Vector2i] = []
|
||||
for c in pack:
|
||||
for n in board.neighbors8(c):
|
||||
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
|
||||
and not _adjacent_to_packs(board, placed, n, pack_set):
|
||||
fringe.append(n)
|
||||
return fringe
|
||||
|
||||
|
||||
# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max
|
||||
func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool:
|
||||
for m in board.neighbors8(c):
|
||||
if placed.has(m) and not own_pack.has(m):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)----
|
||||
|
||||
func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
@@ -680,7 +798,7 @@ func _exit_distribution() -> void:
|
||||
if mode == "new_board":
|
||||
_reward_economy.retire_mapping()
|
||||
minesweeper.retire_board()
|
||||
if not _install_and_reset():
|
||||
if not _install_new_board():
|
||||
_fatal("换图编排失败")
|
||||
return
|
||||
# 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
|
||||
@@ -714,7 +832,7 @@ func player_request_refresh() -> bool:
|
||||
_refresh_used_this_round += 1
|
||||
_reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
|
||||
minesweeper.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast
|
||||
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
|
||||
|
||||
|
||||
func _battle_over(result: String) -> void:
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
## 棋盘安装与变异布置域件(B1 域·BoardInstaller):生成受理、全局作用域变异决议、装板受理、
|
||||
## 资源摇号资格域组装(roll_board 恰一次——install 后、挖掘前)、region 变异决议与连通片布置。
|
||||
## 棋盘设置顺序 = 生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置(布置在初始展开与
|
||||
## 资源格之后,变异天然不落已开区)。宿主侧装板后状态复位(封印/完美计数清零、板序推进、
|
||||
## board_changed 发射、秘宝叠加层退役)留宿主薄壳——本件零复位职责。零 UI 零 await;
|
||||
## 随机只经注入 RngServiceCore 的 board_gen/variant_place 两封闭流(流名与开流时点不变,
|
||||
## 生成器只消费传入流——同种子同输入逐位复现)。
|
||||
class_name BoardInstaller
|
||||
extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var minesweeper: MinesweeperRules
|
||||
var reward_economy: RewardEconomy
|
||||
var encounter: Dictionary[String, Variant] # 宿主 encounter 同对象引用(assign 原地变异——跨战活引用)
|
||||
var fatal_cb: Callable # 协作件违约→宿主 _fatal 封印(单点接线——BoardGenerator 同款)
|
||||
var generator: BoardGenerator # 件内自建自接线(原宿主 _board_generator 成员随件迁移)
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
for k in ["db", "rng", "minesweeper", "reward_economy", "encounter", "fatal_cb"]:
|
||||
assert(bundle.has(k), "[装板域件] 引用束缺键: %s" % k)
|
||||
db = bundle.db
|
||||
rng = bundle.rng
|
||||
minesweeper = bundle.minesweeper
|
||||
reward_economy = bundle.reward_economy
|
||||
encounter = bundle.encounter
|
||||
fatal_cb = bundle.fatal_cb
|
||||
generator = BoardGenerator.new()
|
||||
generator.fatal_handler = fatal_cb # 协作件违约→宿主 _fatal 封印(单点接线)
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
if fatal_cb.is_valid():
|
||||
fatal_cb.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
|
||||
else:
|
||||
push_error(msg)
|
||||
assert(false, msg)
|
||||
|
||||
|
||||
# 装板单入口(生成 → 全局变异决议 → 装板受理 → 资源摇号 → 变异布置);
|
||||
# 宿主态复位归宿主薄壳(_install_and_reset)——本件返回即装板完成、复位未行。
|
||||
func install(during_round := false) -> bool:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
|
||||
var board: BoardData = generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||||
if board == null:
|
||||
return false
|
||||
# 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant)
|
||||
_resolve_global_variant(board)
|
||||
var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round)
|
||||
if not res.accepted:
|
||||
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
|
||||
return false
|
||||
# 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池)
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var eligible: Array[Dictionary] = []
|
||||
for c in board.safe_cells():
|
||||
if not r0.has(c):
|
||||
eligible.append({"cell": c})
|
||||
for c in board.mines.keys():
|
||||
if not r0.has(c):
|
||||
eligible.append({"cell": c})
|
||||
if not reward_economy.roll_board(eligible):
|
||||
_fatal("地图战斗资源 roll_board 被拒")
|
||||
return false
|
||||
# 地形变异 布置段(棋盘设置顺序第 4 步)——布置在初始展开与资源格之后,变异天然不落已开区
|
||||
_place_variants(board)
|
||||
return true
|
||||
|
||||
|
||||
# ---- 全局作用域变异 决议(装板序内 roll_board 之前)----
|
||||
# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在
|
||||
# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。
|
||||
# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走
|
||||
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
|
||||
func _resolve_global_variant(board: BoardData) -> void:
|
||||
board.board_variants.clear()
|
||||
reward_economy.board_skew = 0
|
||||
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
|
||||
String(encounter.get("node_type", "")))
|
||||
if row.is_empty():
|
||||
return
|
||||
board.board_variants = [String(row.variant_id)]
|
||||
reward_economy.board_skew = int(row.get("effect_value", 0))
|
||||
|
||||
|
||||
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
|
||||
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
|
||||
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
|
||||
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
|
||||
# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
|
||||
# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) +
|
||||
# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例,
|
||||
# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。
|
||||
# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。
|
||||
func _place_variants(board: BoardData) -> void:
|
||||
if not board.board_variants.is_empty():
|
||||
return
|
||||
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
|
||||
if pool.is_empty():
|
||||
return
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var candidates: Array[Vector2i] = []
|
||||
for c in board.safe_cells():
|
||||
if not r0.has(c):
|
||||
candidates.append(c)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
|
||||
var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)]
|
||||
var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \
|
||||
+ (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus)
|
||||
total = mini(total, candidates.size())
|
||||
_place_variants_apply(board, candidates, stream, vrow, total)
|
||||
|
||||
|
||||
# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费)
|
||||
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
|
||||
var placed := {}
|
||||
while placed.size() < total:
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
|
||||
var free: Array[Vector2i] = []
|
||||
for c in candidates:
|
||||
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
|
||||
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
|
||||
if placed.has(c) or _adjacent_to_packs(board, placed, c):
|
||||
continue
|
||||
free.append(c)
|
||||
if free.is_empty():
|
||||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||||
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
|
||||
placed[seed_c] = true
|
||||
var pack: Array[Variant] = [seed_c]
|
||||
var pack_set := {seed_c: true}
|
||||
while pack.size() < pack_size:
|
||||
var fringe := _variants_fringe(board, pack, candidates, placed, pack_set)
|
||||
if fringe.is_empty():
|
||||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||||
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
|
||||
placed[nxt] = true
|
||||
pack_set[nxt] = true
|
||||
pack.append(nxt)
|
||||
for c in placed.keys():
|
||||
board.variants[c] = [String(vrow.variant_id)]
|
||||
|
||||
|
||||
# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接)
|
||||
func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]:
|
||||
var fringe: Array[Vector2i] = []
|
||||
for c in pack:
|
||||
for n in board.neighbors8(c):
|
||||
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
|
||||
and not _adjacent_to_packs(board, placed, n, pack_set):
|
||||
fringe.append(n)
|
||||
return fringe
|
||||
|
||||
|
||||
# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max
|
||||
func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool:
|
||||
for m in board.neighbors8(c):
|
||||
if placed.has(m) and not own_pack.has(m):
|
||||
return true
|
||||
return false
|
||||
@@ -1 +0,0 @@
|
||||
uid://di6u170vqf8ae
|
||||
+94
-14
@@ -41,7 +41,7 @@ var hero_hp: int:
|
||||
var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
|
||||
get: return _wallet.gp
|
||||
set(v): _wallet.gp = v
|
||||
var shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
|
||||
var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
|
||||
# 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
|
||||
var treasure_inventory: TreasureItems = null # 秘宝 关卡域库存(TreasureItems;跨战斗延续,
|
||||
@@ -96,10 +96,10 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v
|
||||
_hero_armor_type = String(ctx.get("hero_armor_type", ""))
|
||||
_voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0)))
|
||||
_voucher_time_sand = maxi(0, int(ctx.get("voucher_time_sand", 0)))
|
||||
shop = ShopSession.new(db, rng)
|
||||
_shop = ShopSession.new(db, rng)
|
||||
var pool: Dictionary[Variant, Variant] = ctx.get("pool_wiring", {}) as Dictionary
|
||||
if String(ctx.get("shop_mode", "")) == "pool" and not pool.is_empty():
|
||||
shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable,
|
||||
_shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable,
|
||||
pool.get("ticket", Callable()) as Callable, pool.get("cap", Callable()) as Callable)
|
||||
_bind_shop()
|
||||
|
||||
@@ -302,8 +302,8 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
"node_id": current_node_id(),
|
||||
"visited_ids": visited_ids(),
|
||||
"aux_items": treasure_snapshot,
|
||||
"shop_shelf": shop.shop_bp_shelf(),
|
||||
"shop_visit_buys": shop.shop_bp_visit_buys(),
|
||||
"shop_shelf": _shop.shop_bp_shelf(),
|
||||
"shop_visit_buys": _shop.shop_bp_visit_buys(),
|
||||
}
|
||||
|
||||
|
||||
@@ -503,11 +503,11 @@ func _enter_node(node: Dictionary) -> bool:
|
||||
"shop":
|
||||
# 商店房兑付:到达即开店(覆盖层归呈现层),购买出账/
|
||||
# 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口
|
||||
var payload := {"gp": gp} # legacy 固定三件(off 对照承载——零 RNG 零池消费)
|
||||
if shop.pool_mode:
|
||||
shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签)
|
||||
payload = {"gp": gp, "shelf": true}
|
||||
node_entered.emit(node, payload)
|
||||
if _shop.pool_mode:
|
||||
_shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签)
|
||||
node_entered.emit(node, {"gp": gp, "shelf": true})
|
||||
return true
|
||||
node_entered.emit(node, {"gp": gp}) # legacy 固定三件(off 对照承载——零 RNG 零池消费)
|
||||
return true
|
||||
push_error("[关卡节点地图] 未知节点类型: %s" % str(node))
|
||||
return false
|
||||
@@ -701,7 +701,7 @@ func continue_after_node() -> bool:
|
||||
if t != "reward" and t != "rest" and t != "shop":
|
||||
return false
|
||||
if t == "shop":
|
||||
shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态)
|
||||
_shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态)
|
||||
return _present_choices()
|
||||
|
||||
|
||||
@@ -721,12 +721,12 @@ func close_wallet() -> void:
|
||||
|
||||
|
||||
# ---- 商店房兑付(GP+商店房实施批:呈现层只读投影 + 购买出账)----
|
||||
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);外部调用面=调用方经宿主公开
|
||||
# 成员 shop 直连(调用直连不留委托壳——2026-10-26 Middle Man 裁定)。
|
||||
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/
|
||||
# 白盒入口——外部调用面与断点取数面零变化。
|
||||
|
||||
# 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用)
|
||||
func _bind_shop() -> void:
|
||||
shop.bind({
|
||||
_shop.bind({
|
||||
"wallet": _wallet,
|
||||
"journal": _journal,
|
||||
"level_row": level_row,
|
||||
@@ -746,3 +746,83 @@ func _emit_wallet_event(event: String, gp_now: int, extra: Dictionary) -> void:
|
||||
# 购血(商店 max_hp 商品——当前生命 +Δ 并封顶新上限,与账本置满/关卡完成回血同式)
|
||||
func _gain_max_hp_flat(flat: int) -> void:
|
||||
_stats.hero_hp = mini(_stats.hero_hp + flat, _stats.effective_max_hp())
|
||||
|
||||
|
||||
func shop_products() -> Array:
|
||||
return _shop.shop_products()
|
||||
|
||||
|
||||
func current_price(prow: Dictionary) -> int:
|
||||
return _shop.current_price(prow)
|
||||
|
||||
|
||||
func purchased_tier(product_id: String) -> int:
|
||||
return _shop.purchased_tier(product_id)
|
||||
|
||||
|
||||
func buy_product(product_id: String) -> bool:
|
||||
return _shop.buy_product(product_id)
|
||||
|
||||
|
||||
func player_request_refresh() -> bool:
|
||||
return _shop.player_request_refresh()
|
||||
|
||||
|
||||
# 散折价单点(测试取数面沿用本类调用名)
|
||||
func _spend_discounted(price: int) -> int:
|
||||
return _shop._spend_discounted(price)
|
||||
|
||||
|
||||
# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 +
|
||||
# 池消费 handler + 专长凭证判定 + cap 检查回调(cap 满 kind 剔出货架候选视图)。
|
||||
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void:
|
||||
_shop.set_shop_pool(deck_ref, consume_handler, ticket_consider, cap_check)
|
||||
|
||||
|
||||
# 货架抽签白盒入口(测试取数面沿用本类调用名)
|
||||
func _shop_draw_shelf() -> void:
|
||||
_shop._draw_shelf()
|
||||
|
||||
|
||||
func pool_stock(kind: String) -> int:
|
||||
return _shop.pool_stock(kind)
|
||||
|
||||
|
||||
func pool_price(kind: String) -> int:
|
||||
return _shop.pool_price(kind)
|
||||
|
||||
|
||||
func shop_shelf() -> Array:
|
||||
return _shop.shop_shelf()
|
||||
|
||||
|
||||
func shop_refresh_free_left() -> int:
|
||||
return _shop.shop_refresh_free_left()
|
||||
|
||||
|
||||
func shop_refresh_cost() -> int:
|
||||
return _shop.shop_refresh_cost()
|
||||
|
||||
|
||||
func shop_refresh() -> bool:
|
||||
return _shop.shop_refresh()
|
||||
|
||||
|
||||
func spend_gp(amount: int) -> bool:
|
||||
return _shop.spend_gp(amount)
|
||||
|
||||
|
||||
func buy_pool_item(kind: String) -> bool:
|
||||
return _shop.buy_pool_item(kind)
|
||||
|
||||
|
||||
func is_shop_pool() -> bool:
|
||||
return _shop.is_shop_pool()
|
||||
|
||||
|
||||
func shop_bp_shelf() -> Array:
|
||||
return _shop.shop_bp_shelf()
|
||||
|
||||
|
||||
func shop_bp_visit_buys() -> int:
|
||||
return _shop.shop_bp_visit_buys()
|
||||
@@ -1,7 +1,7 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。
|
||||
## 钱包/主角账/装配集归宿主(GpWallet/GainJournal 共享引用经 bind 注入——GP 读写走钱包、购买加算走日记单点);
|
||||
## Core 层零 UI 依赖;外部调用面=调用方经宿主 LevelRunner 公开成员 shop 直连(调用直连不留委托壳);
|
||||
## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化);
|
||||
## legacy 模式零 RNG 零池消费(off 对照承载)。
|
||||
class_name ShopSession
|
||||
extends RefCounted
|
||||
|
||||
@@ -170,37 +170,37 @@ func on_shop_buy(product_id: String) -> void:
|
||||
var runner = _runner_get.call()
|
||||
if runner == null:
|
||||
return
|
||||
if runner.shop.is_shop_pool():
|
||||
if runner.is_shop_pool():
|
||||
# pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用)
|
||||
if not runner.shop.buy_pool_item(product_id):
|
||||
if not runner.buy_pool_item(product_id):
|
||||
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
|
||||
toast.show_toast("GP不足——买不起这件商品", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost())
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
return
|
||||
if not runner.shop.buy_product(product_id):
|
||||
if not runner.buy_product(product_id):
|
||||
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||||
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
|
||||
toast.show_toast("GP不足——买不起这件商品", "warn")
|
||||
return
|
||||
shop_overlay.refresh_state(runner.shop.shop_products(), int(runner.gp))
|
||||
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
# MC-1:刷新意图(shop_overlay.refresh_requested——核心 shop_refresh 受理后整架重投喂=重抽反馈)
|
||||
func on_shop_refresh() -> void:
|
||||
var runner = _runner_get.call()
|
||||
if runner == null or not runner.shop.is_shop_pool():
|
||||
if runner == null or not runner.is_shop_pool():
|
||||
return
|
||||
if not runner.shop.shop_refresh():
|
||||
if not runner.shop_refresh():
|
||||
toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost())
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ func on_refresh() -> void:
|
||||
if not refreshed:
|
||||
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
|
||||
var cost: int = adventure.refresh_bank().gp_cost()
|
||||
if runner == null or not runner.shop.spend_gp(cost):
|
||||
if runner == null or not runner.spend_gp(cost):
|
||||
ctx.views.toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
refreshed = adventure.stop().refresh_curio_paid()
|
||||
|
||||
@@ -770,7 +770,7 @@ func _on_refresh_confirmed() -> void:
|
||||
return
|
||||
var ok: bool = false
|
||||
if run_scope.level_active and run_scope.runner != null:
|
||||
ok = run_scope.runner.shop.player_request_refresh()
|
||||
ok = run_scope.runner.player_request_refresh()
|
||||
elif run_scope.settlement != null:
|
||||
ok = run_scope.settlement.player_request_refresh()
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok)
|
||||
|
||||
@@ -78,12 +78,12 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
|
||||
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
|
||||
# pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载)
|
||||
root._open_modal(root.shop_overlay)
|
||||
if root.run_scope.runner.shop.is_shop_pool():
|
||||
root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
|
||||
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
|
||||
if root.run_scope.runner.is_shop_pool():
|
||||
root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
|
||||
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1
|
||||
else:
|
||||
root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
|
||||
root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
|
||||
root.logger.log_node(node, info, root.run_scope.runner.node_index)
|
||||
|
||||
@@ -132,10 +132,10 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic
|
||||
if String(event) == "gain":
|
||||
root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain)
|
||||
if root.shop_overlay.visible and root.run_scope.runner != null:
|
||||
if root.run_scope.runner.shop.is_shop_pool():
|
||||
if root.run_scope.runner.is_shop_pool():
|
||||
root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
|
||||
else:
|
||||
root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.refresh_state(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
|
||||
root._refresh_presentations()
|
||||
|
||||
|
||||
|
||||
@@ -246,30 +246,25 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
for y in _board.height:
|
||||
for x in _board.width:
|
||||
var c := Vector2i(x, y)
|
||||
_cells[c] = _build_cell_unit(c, stride)
|
||||
var u: Node2D = _CELL_UNIT.new()
|
||||
u.st.cell = c
|
||||
u.st.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||||
if _palette.tiles_ok:
|
||||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||||
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
u.st.font = _world.board_font
|
||||
_world.add_child(u)
|
||||
_cells[c] = u
|
||||
set_cell_area(size)
|
||||
_install_entrance_fx()
|
||||
refresh()
|
||||
|
||||
|
||||
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
|
||||
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
|
||||
var u: Node2D = _CELL_UNIT.new()
|
||||
u.st.cell = c
|
||||
u.st.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||||
if _palette.tiles_ok:
|
||||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||||
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
u.st.font = _world.board_font
|
||||
_world.add_child(u)
|
||||
return u
|
||||
|
||||
|
||||
# 逐格重绘(挖掘/结算后调用)
|
||||
func refresh() -> void:
|
||||
if _board == null or _minesweeper == null:
|
||||
@@ -611,6 +606,9 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var removed: bool = _minesweeper.is_removed(c) # 中立移除格(退色降档)
|
||||
var is_hero: bool = c == hero
|
||||
var glyph := ""
|
||||
var bg: Color
|
||||
var digit_col := COL_DIGIT
|
||||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||||
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
@@ -618,23 +616,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||||
seal = false
|
||||
u.st.tex_seal = null
|
||||
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
|
||||
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||||
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
|
||||
_paint_cell_base(c, u, revealed, is_hero, bg)
|
||||
_paint_cell_facade(c, u, revealed)
|
||||
_paint_cell_overlays(c, u, revealed)
|
||||
_paint_cell_fog(c, u, revealed, hero)
|
||||
u.st.removed_dim = removed
|
||||
u.queue_redraw()
|
||||
|
||||
|
||||
# 字形与色牌段:reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌;
|
||||
# 返回基底色 bg(平色回退 sb 构建的唯一消费方在基底段)
|
||||
func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Color:
|
||||
var glyph := ""
|
||||
var bg: Color
|
||||
var digit_col := COL_DIGIT
|
||||
if revealed:
|
||||
var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面
|
||||
if rec.get("kind", "") == "mine":
|
||||
@@ -658,21 +639,6 @@ func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Colo
|
||||
digit_col = COL_SEAL
|
||||
else:
|
||||
bg = COL_BRICK
|
||||
u.glyph = glyph
|
||||
u.glyph_color = digit_col
|
||||
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
|
||||
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
|
||||
var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
|
||||
if plate_digit:
|
||||
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
|
||||
u.glyph_color = digit_col
|
||||
u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit)
|
||||
return bg
|
||||
|
||||
|
||||
# 基底瓦片段:瓦片查表/平色回退 sb+英雄蓝框载体;英雄贴图缺位回退字形「人」在此段落
|
||||
# u.glyph(先于叠加段消费,结果与原函数字形累计局部量逐字同值)
|
||||
func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg: Color) -> void:
|
||||
# 瓦片模式:tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片),
|
||||
# bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上),
|
||||
# 瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三)
|
||||
@@ -692,16 +658,21 @@ func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg:
|
||||
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT,UiKit.RADIUS_OBJECT, 0)
|
||||
sb.set_border_width_all(3)
|
||||
sb.border_color = COL_HERO
|
||||
if u.glyph == "" and _hero_sprite.tex_idle == null:
|
||||
if glyph == "" and _hero_sprite.tex_idle == null:
|
||||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||||
u.glyph = "人"
|
||||
u.glyph_color = COL_HERO
|
||||
glyph = "人"
|
||||
digit_col = COL_HERO
|
||||
u.st.tex_base = tile
|
||||
u.st.bg_sb = sb
|
||||
|
||||
|
||||
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
|
||||
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
u.glyph = glyph
|
||||
u.glyph_color = digit_col
|
||||
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
|
||||
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
|
||||
var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
|
||||
if plate_digit:
|
||||
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
|
||||
u.glyph_color = digit_col
|
||||
u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit)
|
||||
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.st.facade_south = false
|
||||
u.st.proj_band = 0.0
|
||||
@@ -736,15 +707,8 @@ func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
|
||||
u.st.facade_strip = true
|
||||
u.st.tex_facade = _palette.tiles.get("wall_center")
|
||||
|
||||
|
||||
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
|
||||
# 自 u.glyph/u.glyph_color(原函数字形累计局部量的段内重建,预读徽章分支的无效局部写入逐字保留)
|
||||
func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
# ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
|
||||
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
|
||||
var glyph: String = u.glyph
|
||||
var digit_col: Color = u.glyph_color
|
||||
if not revealed:
|
||||
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
if _treasure_preview.has(c) and glyph == "":
|
||||
@@ -761,10 +725,6 @@ func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
u.st.mutation_mark = ""
|
||||
u.st.badge_glyph = _update_badge(c)
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
|
||||
func _paint_cell_fog(c: Vector2i, u: Node2D, revealed: bool, hero: Vector2i) -> void:
|
||||
# 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/
|
||||
# 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。
|
||||
# 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。
|
||||
@@ -774,6 +734,8 @@ func _paint_cell_fog(c: Vector2i, u: Node2D, revealed: bool, hero: Vector2i) ->
|
||||
u.fog_state = (2 if revealed else 1) if fogged else 0
|
||||
else:
|
||||
u.fog_state = 0
|
||||
u.st.removed_dim = removed
|
||||
u.queue_redraw()
|
||||
|
||||
|
||||
# ---- 可挖域接触带呼吸微光(挖掘期唯一在动元素——§3.2 v2.8 画法:光画英雄格周界空间侧,
|
||||
|
||||
@@ -85,7 +85,7 @@ func _xnum(v: float) -> String:
|
||||
return "×%d" % int(round(v)) if absf(v - roundf(v)) < 0.005 else "×%.2f" % v
|
||||
|
||||
|
||||
# 打开商店(products = runner.shop.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}])
|
||||
# 打开商店(products = runner.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}])
|
||||
func open_shop(products: Array, gp: int) -> void:
|
||||
for child in _cards_box.get_children():
|
||||
child.queue_free()
|
||||
@@ -149,7 +149,7 @@ func _emit_buy_requested(p: Dictionary) -> void:
|
||||
|
||||
# ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)----
|
||||
|
||||
# 打开/重刷货架(cards = runner.shop.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。
|
||||
# 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。
|
||||
# 买后组合根重投喂新货架 = 整架重建呈现(三件全重抽反馈——未买不保留);空架 = 池空硬闸合法态。
|
||||
func open_shelf(cards: Array, gp: int) -> void:
|
||||
for child in _cards_box.get_children():
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
extends Node2D
|
||||
|
||||
const CHARGE_SKILL_IDS := ["heavy"]
|
||||
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
|
||||
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。
|
||||
# 护甲类型图标三键(light/ward/heavy)——终稿 = 三枚 24×24 PNG(UiKit.TEX_ARMOR_*,art-bible §5.3
|
||||
# 工单:形状承载身份、色相不承载身份;挂载底色 = 名字行素底 HP_BAR_BACK)。字形占位仅作素材缺位
|
||||
# 回退(与立绘缺位回退链同构——素材不入 git,缺位不炸)。
|
||||
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||||
# 名行/底座纵向预算余量(盒底−1px「声明值刚好=容器值」系高危写法;
|
||||
# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
|
||||
const STANDEE_FOOT_MARGIN := 12.0
|
||||
|
||||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||||
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
|
||||
@@ -97,9 +95,18 @@ func intent_probe() -> Dictionary:
|
||||
return {"visible": st._intent_visible, "text": st._intent_glyph}
|
||||
|
||||
|
||||
# 名字行探针出口
|
||||
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
|
||||
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y)
|
||||
func name_line_probe() -> Dictionary:
|
||||
return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))}
|
||||
var f := _efont()
|
||||
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
|
||||
var plate := _name_plate_rect(f, tex)
|
||||
return {"name": st._display_name,
|
||||
"armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")),
|
||||
"armor_tex_bound": tex != null,
|
||||
"plate": plate, "baseline_y": _name_baseline(plate).y,
|
||||
"plinth_top_y": _pedestal_rect().position.y,
|
||||
"plate_color": UiKit.HP_BAR_BACK}
|
||||
|
||||
|
||||
# ---- setter 面(presenter/组合根零改排) ----
|
||||
@@ -420,6 +427,36 @@ func _pedestal_rect() -> Rect2:
|
||||
return Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0)
|
||||
|
||||
|
||||
# 护甲图标纹理路径(甲档键 → UiKit.TEX_ARMOR_*——显式 match 单点,禁动态拼键取常量;
|
||||
# 未知键回退轻甲路径,由 load_tex exists 守卫接住)
|
||||
func _armor_tex_path(armor: String) -> String:
|
||||
match armor:
|
||||
"ward":
|
||||
return UiKit.TEX_ARMOR_WARD
|
||||
"heavy":
|
||||
return UiKit.TEX_ARMOR_HEAVY
|
||||
_:
|
||||
return UiKit.TEX_ARMOR_LIGHT
|
||||
|
||||
|
||||
# 名字行素底牌矩形(_draw 与 name_line_probe 共用几何——单一来源防两处漂移):
|
||||
# 牌高 26 = 护甲图标 24px(甲类 1× 禁缩放)+ 上下各 1px;牌底沿 = 地台顶(box.y-18)——
|
||||
# 文字禁直接压地台(§6.1/§7.2 素底落法);牌宽 = 行宽(图标+间距+名字)+ 8,水平居中
|
||||
func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2:
|
||||
var icon_w := 24.0
|
||||
if tex == null:
|
||||
icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
var total_w := icon_w + 3.0 + name_w
|
||||
return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0)
|
||||
|
||||
|
||||
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,12px 字形基线=中心+4)
|
||||
func _name_baseline(plate: Rect2) -> Vector2:
|
||||
return plate.position + Vector2(4.0, 17.0)
|
||||
|
||||
|
||||
# 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面——
|
||||
# 最低绘制元素 = 底座下缘)
|
||||
func content_rect() -> Rect2:
|
||||
@@ -438,8 +475,9 @@ func _draw() -> void:
|
||||
|
||||
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
|
||||
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
|
||||
## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
|
||||
## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
|
||||
## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px
|
||||
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔STAND_STONE 族方向稿取色,几何同源
|
||||
## _pedestal_rect——与 panel_skinned 缺位回退同构〕)
|
||||
func _draw_ground(f: Font) -> void:
|
||||
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
|
||||
var sh_rx := (box.x - 36.0) * 0.5
|
||||
@@ -450,26 +488,41 @@ func _draw_ground(f: Font) -> void:
|
||||
pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry))
|
||||
draw_colored_polygon(pts, Color(0, 0, 0, 0.27))
|
||||
var ped := _pedestal_rect()
|
||||
draw_rect(ped, UiKit.STAND_STONE, true)
|
||||
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
|
||||
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
|
||||
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
|
||||
if plaza != null:
|
||||
_draw_plinth_stretched(plaza, ped)
|
||||
else:
|
||||
draw_rect(ped, UiKit.STAND_STONE, true)
|
||||
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
|
||||
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
|
||||
|
||||
|
||||
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8;
|
||||
## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
|
||||
## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
|
||||
## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档,
|
||||
## 顶缘受光线 2px 落纵向不缩放区,工单 §5.2 工艺要点)
|
||||
func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void:
|
||||
draw_texture_rect_region(tex, Rect2(ped.position, Vector2(4.0, 12.0)), Rect2(0, 0, 4, 12))
|
||||
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(4.0, 0.0),
|
||||
Vector2(ped.size.x - 8.0, 12.0)), Rect2(4, 0, 40, 12))
|
||||
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(ped.size.x - 4.0, 0.0),
|
||||
Vector2(4.0, 12.0)), Rect2(44, 0, 4, 12))
|
||||
|
||||
|
||||
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③,字号 FS_HINT——art-bible §7.4 清单外
|
||||
## 11–13px 提档归档);血条几何左 8 / 右 -8;血条填充 = 危险红 HP_BAR_FILL(§4.2 语义物件——
|
||||
## 不因敌我分色);条底 = HP_BAR_TRACK;血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于
|
||||
## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
|
||||
func _draw_hp_bar(f: Font) -> void:
|
||||
var bar_x := 8.0
|
||||
if st._kind == "enemy":
|
||||
var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)])
|
||||
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT)
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT)
|
||||
bar_x += 20.0
|
||||
var bar_w := box.x - 8.0 - bar_x
|
||||
draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移)
|
||||
draw_rect(Rect2(bar_x, 2, bar_w, 10), UiKit.HP_BAR_TRACK) # 条底(值单点收 ui_kit)
|
||||
var fill_w := bar_w * st.hp_value
|
||||
if fill_w > 0.0:
|
||||
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
|
||||
draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句)
|
||||
var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)]
|
||||
var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11)
|
||||
# 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2;
|
||||
@@ -513,19 +566,26 @@ func _draw_intent_badge(f: Font) -> void:
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
|
||||
|
||||
## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
|
||||
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
|
||||
## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 =
|
||||
## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制)
|
||||
func _draw_armor_name_line(f: Font) -> void:
|
||||
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
|
||||
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
|
||||
match st._armor:
|
||||
"ward":
|
||||
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
|
||||
"heavy":
|
||||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
|
||||
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
|
||||
var total_w := armor_w + 3.0 + name_w
|
||||
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
|
||||
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
|
||||
var plate := _name_plate_rect(f, tex)
|
||||
draw_rect(plate, UiKit.HP_BAR_BACK, true)
|
||||
var baseline := _name_baseline(plate)
|
||||
var icon_w := 24.0
|
||||
if tex != null:
|
||||
draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false)
|
||||
else:
|
||||
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
|
||||
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
|
||||
match st._armor:
|
||||
"ward":
|
||||
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
|
||||
"heavy":
|
||||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, col)
|
||||
f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
@@ -190,7 +190,9 @@ const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘
|
||||
const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep)
|
||||
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(兼血数垫牌)
|
||||
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台)
|
||||
const HP_BAR_FILL := Color8(168, 50, 31) # 血条填充·危险红 #A8321F(§4.2 语义物件显式落法——不因敌我分色、不因关卡换族;蓝族 #689CE8 已登记违例退役)
|
||||
const HP_BAR_TRACK := Color(0.10, 0.10, 0.11) # 血条条底(视图原裸值收口单点——非色板新增,现渲染值原样入册)
|
||||
|
||||
# BBCode 十六进制(RichTextLabel 用;与上同值)
|
||||
const HEX_GOLD := "f0a828"
|
||||
@@ -244,6 +246,13 @@ const TEX_ENEMY_DRIDER := "res://assets/art/character/character_enemy_drider.png
|
||||
const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.png" # 灰矮人
|
||||
const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔
|
||||
const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王
|
||||
# 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint)
|
||||
const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png"
|
||||
# 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍;
|
||||
# 缺位 load_tex→null→字形回退,冒烟安全)
|
||||
const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png"
|
||||
const TEX_ARMOR_WARD := "res://assets/ui/icons/ui_armor_ward.png"
|
||||
const TEX_ARMOR_HEAVY := "res://assets/ui/icons/ui_armor_heavy.png"
|
||||
|
||||
# 框材组纹理槽(♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py,
|
||||
# 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全)
|
||||
|
||||
@@ -332,13 +332,13 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr
|
||||
|
||||
# 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的
|
||||
# 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走
|
||||
# runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
|
||||
# runner.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
|
||||
static func _autoplay_shop(r, st: Dictionary) -> void:
|
||||
var guard := 30
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
var affordable: Array = []
|
||||
for p in r.shop.shop_products():
|
||||
for p in r.shop_products():
|
||||
if r.gp >= int(p.get("price", 1 << 30)):
|
||||
affordable.append(p)
|
||||
if affordable.is_empty():
|
||||
@@ -347,20 +347,20 @@ static func _autoplay_shop(r, st: Dictionary) -> void:
|
||||
for p in affordable:
|
||||
if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
|
||||
pick = p
|
||||
if not r.shop.buy_product(String(pick.get("product_id", ""))):
|
||||
if not r.buy_product(String(pick.get("product_id", ""))):
|
||||
return
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
|
||||
# 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买
|
||||
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item
|
||||
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.buy_pool_item
|
||||
# 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。
|
||||
static func _autoplay_shop_pool(r, st: Dictionary) -> void:
|
||||
var guard := 30
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
var affordable: Array = []
|
||||
for c in r.shop.shop_shelf():
|
||||
for c in r.shop_shelf():
|
||||
if r.gp >= int(c.get("price", 1 << 30)):
|
||||
affordable.append(c)
|
||||
if affordable.is_empty():
|
||||
@@ -369,7 +369,7 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void:
|
||||
for c in affordable:
|
||||
if int(c.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
|
||||
pick = c
|
||||
if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))):
|
||||
if not r.buy_pool_item(String(pick.get("kind_id", ""))):
|
||||
return
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
@@ -415,10 +415,10 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int:
|
||||
s = 10.0
|
||||
var cheapest := 1 << 30
|
||||
if String(st.get("shop_mode", "legacy")) == "pool":
|
||||
for c in runner.shop.shop_shelf(): # pool 面:货架现价
|
||||
for c in runner.shop_shelf(): # pool 面:货架现价
|
||||
cheapest = mini(cheapest, int(c.get("price", 1 << 30)))
|
||||
else:
|
||||
for p in runner.shop.shop_products():
|
||||
for p in runner.shop_products():
|
||||
cheapest = mini(cheapest, int(p.get("price", 1 << 30)))
|
||||
if cheapest < (1 << 30) and runner.gp >= cheapest:
|
||||
s = 80.0
|
||||
|
||||
@@ -134,6 +134,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
|
||||
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包)
|
||||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
|
||||
{"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 自动化前置面)
|
||||
|
||||
@@ -308,13 +308,13 @@ fi
|
||||
# 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明
|
||||
# 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。
|
||||
# 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd
|
||||
# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 10 文件,显式清单见 S8_FILES。
|
||||
# + battle_fx_host.gd + reward_grant.gd 两拆件 + settlement/ 五段段类 + damage_controller.gd,共 9 文件,显式清单见 S8_FILES。
|
||||
# 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。
|
||||
# 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/
|
||||
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
|
||||
# 口径仿 S1/S2 的 grep 排除手法。
|
||||
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S8_MISS=0
|
||||
for f in $S8_FILES; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -356,7 +356,7 @@ fi
|
||||
# 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红
|
||||
# (文件被改名=豁免名单失效,宁可红不可静默放行)。
|
||||
if require_scan_face "S7" "$DIR_CORE"; then
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S7_MISS=0
|
||||
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
|
||||
if [ ! -f "$f" ]; then
|
||||
|
||||
@@ -361,7 +361,7 @@ func _smoke_adventure() -> void:
|
||||
if int(pb.get("level_index", -1)) == 2:
|
||||
l2_price_bonus = int(pb.get("bonus", 0))
|
||||
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
|
||||
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.shop.buy_product("hp_up"))
|
||||
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
|
||||
var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
|
||||
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
|
||||
var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获)
|
||||
|
||||
@@ -49,9 +49,9 @@ func _init() -> void:
|
||||
# ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)----
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
lr.gp = 30
|
||||
Kit.check_quiet(lr.shop.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用")
|
||||
Kit.check_quiet(lr.shop.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用")
|
||||
Kit.check_quiet(lr.shop.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)")
|
||||
Kit.check_quiet(lr.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用")
|
||||
Kit.check_quiet(lr.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用")
|
||||
Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)")
|
||||
|
||||
# ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)----
|
||||
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
|
||||
|
||||
@@ -57,13 +57,13 @@ func _init() -> void:
|
||||
|
||||
# ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)----
|
||||
runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例)
|
||||
var products: Array = runner.shop.shop_products()
|
||||
var products: Array = runner.shop_products()
|
||||
var bought := false
|
||||
for p in products:
|
||||
if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "":
|
||||
var attr := String(p.get("attr", ""))
|
||||
var before: int = runner.journal().bonus_total(attr)
|
||||
bought = runner.shop.buy_product(String(p.get("product_id", "")))
|
||||
bought = runner.buy_product(String(p.get("product_id", "")))
|
||||
if bought:
|
||||
Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)),
|
||||
"hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)")
|
||||
|
||||
@@ -251,7 +251,6 @@ func _init() -> void:
|
||||
_smoke_shop_graph()
|
||||
_smoke_multi_terminal_settlement()
|
||||
_smoke_shop_pool()
|
||||
_test_shop_node_entered_payload()
|
||||
_smoke_generated_coverage()
|
||||
quit(Kit.finish("level_shop_economy_test"))
|
||||
|
||||
@@ -1025,17 +1024,17 @@ func _smoke_shop_room() -> void:
|
||||
return
|
||||
Kit.check("到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)", not runner.awaiting_choice() and runner.next_candidate_ids().is_empty())
|
||||
# 现价 = 基价 + price_level_bonus(L2)=2 + 步长 × 已购档数
|
||||
var prods: Array = runner.shop.shop_products()
|
||||
var prods: Array = runner.shop_products()
|
||||
Kit.check("三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)", prods.size() == 3)
|
||||
var by_id := _shop_products_by_id(prods)
|
||||
Kit.check("L2 首购现价 = 基价 + 关级加值 2(atk_up 6 / hp_up 4 / armor_up 6)", int(by_id["atk_up"]["price"]) == 6 and int(by_id["hp_up"]["price"]) == 4 and int(by_id["armor_up"]["price"]) == 6)
|
||||
# 购买 atk_up:钱包 −6 · 加算区 atk +1 · 二购现价 8
|
||||
var gp0: int = runner.gp
|
||||
Kit.check("购买 atk_up 受理(现价 6)", runner.shop.buy_product("atk_up"))
|
||||
Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up"))
|
||||
Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6)
|
||||
# 加算区住加算日记
|
||||
Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0)
|
||||
by_id = _shop_products_by_id(runner.shop.shop_products())
|
||||
by_id = _shop_products_by_id(runner.shop_products())
|
||||
Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1)
|
||||
# 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道)
|
||||
runner.set_hero_override(0, 0)
|
||||
@@ -1043,28 +1042,28 @@ func _smoke_shop_room() -> void:
|
||||
# hp_up:上限与当前生命同步 +3(封顶新上限)
|
||||
var hp_before: int = runner.hero_hp
|
||||
var mhp_before: int = runner.effective_max_hp()
|
||||
Kit.check("购买 hp_up 受理(现价 4)", runner.shop.buy_product("hp_up"))
|
||||
Kit.check("购买 hp_up 受理(现价 4)", runner.buy_product("hp_up"))
|
||||
Kit.check("钱包扣 4(%d)" % runner.gp, runner.gp == gp0 - 6 - 4)
|
||||
Kit.check("生命上限 +3(%d → %d——加算区 max_hp 轴)" % [mhp_before, runner.effective_max_hp()], runner.effective_max_hp() == mhp_before + 3)
|
||||
Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()))
|
||||
# armor_up:AC 10+1
|
||||
Kit.check("购买 armor_up 受理(现价 6)", runner.shop.buy_product("armor_up"))
|
||||
Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up"))
|
||||
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1)
|
||||
# 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝
|
||||
var guard_drain := 20
|
||||
var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环
|
||||
while guard_drain > 0:
|
||||
guard_drain -= 1
|
||||
var hp_price: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"])
|
||||
var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
|
||||
if runner.gp < hp_price:
|
||||
break
|
||||
if not runner.shop.buy_product("hp_up"):
|
||||
if not runner.buy_product("hp_up"):
|
||||
break
|
||||
hp_buys += 1
|
||||
var gp_dry: int = runner.gp
|
||||
var tier_dry: int = runner.shop.purchased_tier("hp_up")
|
||||
var hp_dry: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"])
|
||||
Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.shop.buy_product("hp_up") and runner.gp == gp_dry and runner.shop.purchased_tier("hp_up") == tier_dry)
|
||||
var tier_dry: int = runner.purchased_tier("hp_up")
|
||||
var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
|
||||
Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry)
|
||||
# 离开 → 回选路口(候选 = 该店出边——恰一条,指向终点 Boss)
|
||||
Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs),
|
||||
runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs))
|
||||
@@ -1253,7 +1252,7 @@ func _smoke_shop_pool() -> void:
|
||||
if not _adv_drive_to_shop(a8, _adv_st(), autoplay):
|
||||
continue
|
||||
scen = {"a": a8, "s": s, "r": a8.runner()}
|
||||
first_shelf_a = (a8.runner().shop.shop_shelf() as Array).duplicate(true)
|
||||
first_shelf_a = (a8.runner().shop_shelf() as Array).duplicate(true)
|
||||
break
|
||||
Kit.check("池模式商店房场景可达(seed=%s)" % str(scen.get("s", -1)), not scen.is_empty())
|
||||
if not scen.is_empty():
|
||||
@@ -1262,19 +1261,19 @@ func _smoke_shop_pool() -> void:
|
||||
a8b.set_hero_override(900, 400)
|
||||
if a8b.start_adventure(int(scen["s"]), {"shop_mode": "pool"}) \
|
||||
and _adv_drive_to_shop(a8b, _adv_st(), autoplay):
|
||||
first_shelf_b = (a8b.runner().shop.shop_shelf() as Array).duplicate(true)
|
||||
first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true)
|
||||
Kit.check("进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size(), first_shelf_a.size() == 3)
|
||||
# MC-1 商店独立刷新(P-C2+Q2 裁——基础免费 0;GP 刷新 ceil(20×1.8^n) provisional;重抽同源)
|
||||
if not scen.is_empty():
|
||||
var r8 = scen["r"]
|
||||
var gp0: int = int(r8.gp)
|
||||
var cost0: int = r8.shop.shop_refresh_cost()
|
||||
var cost0: int = r8.shop_refresh_cost()
|
||||
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base))
|
||||
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop.shop_refresh_free_left() == 0)
|
||||
var shelf_pre: Array = (r8.shop.shop_shelf() as Array).duplicate(true)
|
||||
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop.shop_refresh())
|
||||
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0)
|
||||
var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true)
|
||||
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh())
|
||||
Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0)
|
||||
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop.shop_refresh_cost()), int(r8.shop.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
|
||||
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
|
||||
var same_shelf := first_shelf_a.size() == first_shelf_b.size()
|
||||
if same_shelf:
|
||||
for i in first_shelf_a.size():
|
||||
@@ -1290,10 +1289,10 @@ func _smoke_shop_pool() -> void:
|
||||
# ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算)
|
||||
var l2 := int(r8.level_row.level_index)
|
||||
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
|
||||
var relic_price_now: int = r8.shop.pool_price("relic")
|
||||
var relic_price_now: int = r8.pool_price("relic")
|
||||
var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
|
||||
Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre)
|
||||
for c in r8.shop.shop_shelf():
|
||||
for c in r8.shop_shelf():
|
||||
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
|
||||
# (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价)
|
||||
var ckind := String(c.get("kind_id", ""))
|
||||
@@ -1308,7 +1307,7 @@ func _smoke_shop_pool() -> void:
|
||||
Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p)
|
||||
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
|
||||
var buy_c: Dictionary = {}
|
||||
for c in r8.shop.shop_shelf():
|
||||
for c in r8.shop_shelf():
|
||||
if int(c.get("price", 1 << 30)) <= int(r8.gp):
|
||||
buy_c = c
|
||||
break
|
||||
@@ -1320,7 +1319,7 @@ func _smoke_shop_pool() -> void:
|
||||
var gp0: int = int(r8.gp)
|
||||
var taken0: int = int(a8._curio.taken.get(kind, 0))
|
||||
var val0: int = a8.journal().bonus_total(String(val_map[kind]))
|
||||
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.shop.buy_pool_item(kind))
|
||||
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
|
||||
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier)))
|
||||
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1)
|
||||
@@ -1328,7 +1327,7 @@ func _smoke_shop_pool() -> void:
|
||||
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
|
||||
r8.gp = int(r8.gp) + 64
|
||||
var second: Dictionary = {}
|
||||
for c in r8.shop.shop_shelf():
|
||||
for c in r8.shop_shelf():
|
||||
if int(c.get("price", 1 << 30)) <= int(r8.gp):
|
||||
second = c
|
||||
break
|
||||
@@ -1336,14 +1335,14 @@ func _smoke_shop_pool() -> void:
|
||||
var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", "")))
|
||||
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a))
|
||||
Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2)
|
||||
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.shop.buy_pool_item(String(second.get("kind_id", ""))))
|
||||
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", ""))))
|
||||
else:
|
||||
Kit.check("A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败", false)
|
||||
# ④ 切边零入店(白-box 重抽 40 次——GP 袋/规则改写型特性永不在架;重抽消费 shop_draw 签,场景尾不回写主时间线)
|
||||
var cut_seen := false
|
||||
for i in 40:
|
||||
r8.shop._draw_shelf()
|
||||
for c in r8.shop.shop_shelf():
|
||||
r8._shop_draw_shelf()
|
||||
for c in r8.shop_shelf():
|
||||
if String(c.get("kind_id", "")) == "gp" or String(c.get("kind_id", "")) == "treasure_item":
|
||||
cut_seen = true
|
||||
Kit.check("切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)", not cut_seen)
|
||||
@@ -1353,8 +1352,8 @@ func _smoke_shop_pool() -> void:
|
||||
a8._curio.deck.remove_at(di8)
|
||||
var attack_seen := false
|
||||
for i in 40:
|
||||
r8.shop._draw_shelf()
|
||||
if (r8.shop.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"):
|
||||
r8._shop_draw_shelf()
|
||||
if (r8.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"):
|
||||
attack_seen = true
|
||||
break
|
||||
Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen)
|
||||
@@ -1392,74 +1391,6 @@ func _smoke_shop_pool() -> void:
|
||||
Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok)
|
||||
|
||||
|
||||
# ---- 商店房 node_entered 载荷(尾波 V1——_enter_node pool/legacy 双发射路径归一后的行为保持断言)----
|
||||
|
||||
func _test_shop_node_entered_payload() -> void:
|
||||
print("--- 商店房 node_entered 载荷(pool 含 shelf:true+gp 现值/legacy 无 shelf 键)---")
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
# pool 态:载荷字典经信号闭包当场取样(字典为引用累积器——lambda 按值捕获标量的绕行惯例)
|
||||
var cap_pool := {"payload": {}, "gp": -1}
|
||||
var pool_ok := false
|
||||
for s in range(42, 92):
|
||||
var pr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
pr.set_hero_override(900, 400)
|
||||
if not pr.start_adventure(s, {"shop_mode": "pool"}):
|
||||
continue
|
||||
var rp = pr.runner()
|
||||
rp.node_entered.connect(func(node, payload):
|
||||
if String(node.get("type", "")) == "shop":
|
||||
cap_pool["payload"] = payload
|
||||
cap_pool["gp"] = int(rp.gp))
|
||||
if not _adv_drive_to_shop(pr, _adv_st(), autoplay):
|
||||
continue
|
||||
pool_ok = not (cap_pool["payload"] as Dictionary).is_empty()
|
||||
if pool_ok:
|
||||
break
|
||||
Kit.check("pool 态进店信号可达(seed 扫描 42–91)", pool_ok)
|
||||
Kit.check("pool 态 node_entered 载荷含 shelf:true〔尾波 V1〕", pool_ok and bool((cap_pool["payload"] as Dictionary).get("shelf", false)))
|
||||
Kit.check("pool 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_pool["gp"]), pool_ok and int((cap_pool["payload"] as Dictionary).get("gp", -1)) == int(cap_pool["gp"]))
|
||||
# legacy 态:载荷无 shelf 键(两 payload 字典逐字保持的另一半——直构 LevelRunner 链)
|
||||
var cap_leg := {"payload": {}, "gp": -1}
|
||||
var leg_ok := false
|
||||
for s in range(42, 92):
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(s))
|
||||
lr.set_hero_override(900, 400)
|
||||
lr.start_level(2, s)
|
||||
lr.node_entered.connect(func(node, payload):
|
||||
if String(node.get("type", "")) == "shop":
|
||||
cap_leg["payload"] = payload
|
||||
cap_leg["gp"] = int(lr.gp))
|
||||
var st := _battle_st("normal")
|
||||
var guard := 60
|
||||
while String(lr.level_result) == "" and String(lr.current_node().get("type", "")) != "shop" and guard > 0:
|
||||
guard -= 1
|
||||
if lr.awaiting_choice():
|
||||
var ids: Array = lr.next_candidate_ids()
|
||||
ids.sort()
|
||||
lr.pick_next_node(int(ids[0]))
|
||||
continue
|
||||
var t := String(lr.current_node().get("type", ""))
|
||||
if t == "reward" or t == "rest":
|
||||
lr.continue_after_node()
|
||||
continue
|
||||
var b = lr.current_battle()
|
||||
if b == null:
|
||||
break
|
||||
var fin := {}
|
||||
b.battle_finished.connect(func(p): fin["p"] = p)
|
||||
var g2 := 80
|
||||
while lr.current_battle() == b and not fin.has("p") and g2 > 0:
|
||||
g2 -= 1
|
||||
autoplay._autoplay_battle(lr, b, st, null)
|
||||
if String(lr.level_result) == "" and String(lr.current_node().get("type", "")) == "shop":
|
||||
leg_ok = not (cap_leg["payload"] as Dictionary).is_empty()
|
||||
if leg_ok:
|
||||
break
|
||||
Kit.check("legacy 态进店信号可达(seed 扫描 42–91)", leg_ok)
|
||||
Kit.check("legacy 态 node_entered 载荷无 shelf 键〔尾波 V1〕", leg_ok and not (cap_leg["payload"] as Dictionary).has("shelf"))
|
||||
Kit.check("legacy 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_leg["gp"]), leg_ok and int((cap_leg["payload"] as Dictionary).get("gp", -1)) == int(cap_leg["gp"]))
|
||||
|
||||
|
||||
# 入口→Boss 全路径枚举(DFS 迭代栈——图合法性校验保证单入口无环,路径有限)
|
||||
|
||||
|
||||
|
||||
@@ -732,12 +732,12 @@ func _smoke_tree_rules() -> void:
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
|
||||
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散未激活:商品标价原价(无 economy_spend 键)",
|
||||
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
|
||||
int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
|
||||
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
|
||||
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
|
||||
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
|
||||
and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
|
||||
int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
|
||||
and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
|
||||
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
|
||||
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
|
||||
Kit.check("聚 rig:基线编排启动(无增益侧)",
|
||||
|
||||
@@ -42,9 +42,9 @@ func _init() -> void:
|
||||
Kit.check_quiet(not e.stop().skip_empty_curio(),
|
||||
"空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕")
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
lr.shop.set_shop_pool([], Callable(), Callable(), Callable())
|
||||
lr.shop._draw_shelf()
|
||||
Kit.check_quiet((lr.shop.shop_bp_shelf() as Array).is_empty(),
|
||||
lr.set_shop_pool([], Callable(), Callable(), Callable())
|
||||
lr._shop_draw_shelf()
|
||||
Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(),
|
||||
"空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕")
|
||||
|
||||
quit(Kit.finish("pool_case_d"))
|
||||
|
||||
@@ -14,7 +14,6 @@ const RngScript := preload("res://core/rng_service.gd")
|
||||
const BoardGeneratorScript := preload("res://core/board_generator.gd")
|
||||
const RewardEconomyScript := preload("res://core/reward_economy.gd")
|
||||
const OrchestratorScript := preload("res://core/battle_orchestrator.gd")
|
||||
const BoardInstallerScript := preload("res://core/board_installer.gd")
|
||||
|
||||
const TAG_OK := "board_gen" # 在册合法标签(PURPOSE_TAGS 含;wr7 必须用合法标签——未知标签检查排在播种检查之前)
|
||||
|
||||
@@ -65,15 +64,11 @@ func _default_form(db) -> bool:
|
||||
|
||||
|
||||
# ①wr5(p5):两协作件违约经组装点接线传导 → orchestrator.battle_result 落 "fatal_error"
|
||||
# (棋盘生成半边:装板域拆件后生成器随 BoardInstaller 件内自建——组装点=引用束 fatal_cb 接线,
|
||||
# 经新组装点构建后触发生成器违约,封印传导断言不变)
|
||||
func _seal_propagation(db) -> bool:
|
||||
var rng_a = RngScript.new()
|
||||
rng_a.set_master_seed(5)
|
||||
var oc_bg = OrchestratorScript.new(db, rng_a)
|
||||
var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper,
|
||||
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg.encounter, "fatal_cb": oc_bg._fatal})
|
||||
bi_bg.generator._fatal("测试违约①棋盘生成")
|
||||
oc_bg._board_generator._fatal("测试违约①棋盘生成")
|
||||
if oc_bg.battle_result != "fatal_error":
|
||||
push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result))
|
||||
return false
|
||||
|
||||
@@ -226,11 +226,11 @@ func _init() -> void:
|
||||
|
||||
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
|
||||
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
|
||||
"刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free))
|
||||
lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(),
|
||||
{"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2,
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2,
|
||||
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
|
||||
|
||||
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/封印格跳过)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# 在册词表组合;本测试直构 rebuild 同构合成行(与 effect_engine.gd rebuild 树规则段逐字段同源)
|
||||
# 调 validate_trigger_row 断零违例。实现侧扩闭集未呈报时,本断言以在册词表为准转红。
|
||||
# 聚散两键(economy_hoard/economy_spend)不入事件面:消费点=tree_rules 成员判定
|
||||
# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / shop_session._spend_discounted 商品折价),
|
||||
# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / level_runner._spend_discounted 商品折价),
|
||||
# rebuild 树规则段对两键零合成行——此状态在此被显式断言(旧版对两键取空夹具静默跳过=假绿):
|
||||
# rebuild 实装面行数对照——四键逐键与夹具同行数(rebuild 分支被删/漂移即转红),
|
||||
# 两键断零行(将来被加合成行即转红,提示先复核效果闭集再同步夹具与实现)。
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 战斗一屏样张批 V5–V9 断言面(2026-10-29 迭代 43)——五项实装定值的真实入口断言:
|
||||
# V5 血条填充 = 危险红(UiKit.HP_BAR_FILL 值单点 + 绘制点源码锚:全仓零 #689CE8 与零裸
|
||||
# Color(0.41,0.61,0.91);条底收 HP_BAR_TRACK 常量);
|
||||
# V6 站桩名字行垫 HP_BAR_BACK 素底且不压地台(名字行基线 y < 地台顶 y——name_line_probe 几何出口);
|
||||
# V7 顺位徽标提档 UiKit.FS_HINT(值断言 + 绘制点源码锚,§7.4 清单外 11–13px 提档);
|
||||
# V8 护甲图标三键(light/ward/heavy)绑对应 24×24 PNG(ResourceLoader 装载结果 + name_line_probe
|
||||
# 绑定面);
|
||||
# V9 地台件 scene_stage_plaza.png + 护甲三枚入包可加载且尺寸 48×12 / 24×24(§5.2/§5.3 工单)。
|
||||
# headless 无渲染面:像素采样以常量单点 + 源码锚 + 探针几何承载(观测面注记,验收对照表 V5 行)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const StandeeBodyScript := preload("res://presentation/views/standee_body.gd")
|
||||
|
||||
|
||||
# 源码锚读面(静态判据——源文件即提交面)
|
||||
func _read_source(rel_path: String) -> String:
|
||||
var f := FileAccess.open(rel_path, FileAccess.READ)
|
||||
if f == null:
|
||||
return ""
|
||||
return f.get_as_text()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# ---- V5 血条填充 = 危险红 #A8321F(§4.2 语义物件显式落法) ----
|
||||
Kit.check("血条填充常量 = 危险红 #A8321F〔样张批 V5〕",
|
||||
UiKit.HP_BAR_FILL.to_html(false) == "a8321f")
|
||||
Kit.check("血条条底收常量 HP_BAR_TRACK(视图原裸值原样入册)〔样张批 V5〕",
|
||||
UiKit.HP_BAR_TRACK.is_equal_approx(Color(0.10, 0.10, 0.11)))
|
||||
var body_src := _read_source("res://presentation/views/standee_body.gd")
|
||||
Kit.check("血条绘制点引用 UiKit.HP_BAR_FILL(源码锚)〔样张批 V5〕",
|
||||
body_src.contains("UiKit.HP_BAR_FILL"))
|
||||
Kit.check("全源零违例值 #689CE8 与零裸 Color(0.41,0.61,0.91)(源码锚)〔样张批 V5〕",
|
||||
not body_src.contains("689CE8") and not body_src.contains("689ce8")
|
||||
and not body_src.contains("0.41, 0.61, 0.91"))
|
||||
|
||||
# ---- V6/V8 站桩实例面(名字行几何 + 护甲绑定) ----
|
||||
var standee: Node2D = StandeeBodyScript.new("enemy")
|
||||
root.add_child(standee)
|
||||
standee.set_data({"display_name": "地精", "enemy_id": "goblin", "hp": 3, "max_hp": 3,
|
||||
"order": 1, "armor_type": "light"})
|
||||
var nl: Dictionary = standee.name_line_probe()
|
||||
Kit.check("名字行基线 y < 地台顶 y(文字禁压地台——V6 判据坐标面)〔样张批 V6〕",
|
||||
float(nl.baseline_y) < float(nl.plinth_top_y))
|
||||
Kit.check("素底牌底沿 ≤ 地台顶(整行不越地台)〔样张批 V6〕",
|
||||
Rect2(nl.plate).end.y <= float(nl.plinth_top_y) + 0.01)
|
||||
Kit.check("名字行素底 = HP_BAR_BACK 深档 #282220(挂载底色口径)〔样张批 V6〕",
|
||||
Color(nl.plate_color).to_html(false) == "282220")
|
||||
Kit.check("基线读数与实装几何同源(基线 = 地台顶上方 9px 档:149 < 158)〔样张批 V6〕",
|
||||
absf(float(nl.baseline_y) - 149.0) < 0.51 and absf(float(nl.plinth_top_y) - 158.0) < 0.51)
|
||||
|
||||
# ---- V7 顺位徽标提档 FS_HINT ----
|
||||
Kit.check("FS_HINT = 14(§7.4 六档)〔样张批 V7〕", UiKit.FS_HINT == 14)
|
||||
Kit.check("顺位徽标绘制点取 UiKit.FS_HINT(源码锚)〔样张批 V7〕",
|
||||
body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT"))
|
||||
|
||||
# ---- V8 护甲图标三键绑 24×24 PNG ----
|
||||
var armor_paths := {"light": UiKit.TEX_ARMOR_LIGHT, "ward": UiKit.TEX_ARMOR_WARD,
|
||||
"heavy": UiKit.TEX_ARMOR_HEAVY}
|
||||
for key: String in armor_paths:
|
||||
var tex := UiKit.load_tex(armor_paths[key])
|
||||
Kit.check("护甲 %s 键绑对应 PNG 且 24×24(§5.3 工单)〔样张批 V8〕" % key,
|
||||
tex != null and tex.get_width() == 24 and tex.get_height() == 24)
|
||||
var ward_standee: Node2D = StandeeBodyScript.new("enemy")
|
||||
root.add_child(ward_standee)
|
||||
ward_standee.set_data({"display_name": "蛛化卓尔精灵", "enemy_id": "drider", "hp": 5, "max_hp": 5,
|
||||
"order": 1, "armor_type": "ward"})
|
||||
var nl_ward: Dictionary = ward_standee.name_line_probe()
|
||||
Kit.check("甲档键 → 纹理路径绑定(name_line_probe 出 armor_icon 路径)〔样张批 V8〕",
|
||||
bool(nl_ward.armor_tex_bound) and String(nl_ward.armor_icon) == UiKit.TEX_ARMOR_WARD)
|
||||
|
||||
# ---- V9 地台件与护甲三枚入包可载且尺寸对 ----
|
||||
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
|
||||
Kit.check("地台件 scene_stage_plaza.png 可加载且 48×12(§5.2 工单)〔样张批 V9〕",
|
||||
plaza != null and plaza.get_width() == 48 and plaza.get_height() == 12)
|
||||
for key: String in armor_paths:
|
||||
var tex9 := UiKit.load_tex(armor_paths[key])
|
||||
Kit.check("护甲三枚入包(24×24)且路径常量在 UiKit 单点〔样张批 V9〕" if key == "light"
|
||||
else "护甲三枚入包(24×24)〔样张批 V9〕",
|
||||
tex9 != null and tex9.get_width() == 24 and tex9.get_height() == 24)
|
||||
|
||||
quit(Kit.finish("standee_present_values"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7a6acc5aici0
|
||||
@@ -1,24 +0,0 @@
|
||||
# 迭代 42 反哺点清单(2026-10-29)
|
||||
|
||||
要写进 `design/` 的点逐条列全:哪个文件哪一节、改成什么、依据是什么。用户在会话中裁决后归文档批落文,落文后本条清掉(不登记待办清单)。
|
||||
|
||||
## 反哺点 1:architecture.md:109 ShopSession 行——委托承载句失效+公开面格重写(卡② §6)
|
||||
|
||||
- **位置**:`design/engineering/architecture/architecture.md` 功能层表 ShopSession 行(:109)「对外暴露」格。
|
||||
- **失效陈述**:「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」——委托壳已随迭代 42 卡② 全量拆除(19 委托+2 死壳归零)。
|
||||
- **改写内容全文(建议落文)**:该格末句改为——「外部调用面=调用方经宿主 LevelRunner 公开成员 `shop` 直连(调用直连不留委托壳,2026-10-26 Middle Man 裁定;迭代 42 拆除);直连消费的方法=shop_products/current_price/purchased_tier/buy_product/player_request_refresh/set_shop_pool/pool_price/shop_shelf/shop_refresh_free_left/shop_refresh_cost/shop_refresh/spend_gp/buy_pool_item/is_shop_pool/shop_bp_shelf/shop_bp_visit_buys(+白盒两入口仅测试侧:_draw_shelf/_spend_discounted)」。
|
||||
- **set_shop_pool 调用面改述**:pool 注入经 LevelRunner 构造 ctx 供给(level_runner.gd:101–102 内部直调 ShopSession.set_shop_pool);原委托块注释「AdventureRunner._start_current_level 唯一调用方」系陈旧表述(全仓 grep 证实产品码零其他调用方),随壳删除作废;测试面直连 `shop.set_shop_pool`。
|
||||
- **依据**:迭代 42 卡② v1.1 §2–§6(拆除形态权威=architecture.md:154「调用直连:不留中间人委托壳」);实测残留扫描零真残留(实现报告 §7)。
|
||||
|
||||
## 反哺点 2:architecture.md「测试与质量域」S13 豁免登记段——本批 5 键+既有讨论 5 键按实测值落文(卡① D5/§14.5)
|
||||
|
||||
- **位置**:`design/engineering/architecture/architecture.md`「测试与质量域」S13 豁免登记段(先例=battle_orchestrator 第三条形态:键永久基线内、禁再恶化、豁免不等于了结)。
|
||||
- **落文内容(批末实测值,2026-10-29 全量扫描)**:
|
||||
- board_view.gd max-class-variables = 32(D2 裁悬停域不拆——类变量无拆分动作可归绿,残留面全为契约承载)
|
||||
- board_view.gd max-public-methods = 27(同 D2——27 个逐类核对全为探针契约面与依赖单向纪律承载面)
|
||||
- level_runner.gd max-public-methods = 33(D3/D6——委托壳拆除后终值,直连形态下无再降空间)
|
||||
- level_runner.gd max-class-variables = 30(D3——`shop` 公开成员留宿主,非无效成员)
|
||||
- adventure_runner.gd max-class-variables = 28(D4 裁断点域不拆——断点协议住冒险生命周期 owner 内本身内聚)
|
||||
- 既有讨论 5 键同段合并落文:main.gd max-class-variables = 49/tune_sim_controller.gd await-in-loop/board_cell_unit.gd max-class-variables = 30/standee_body.gd max-class-variables = 19/battle_orchestrator.gd max-class-variables = 37(随装板域拆件 38→37 只降,替代原登记值 38)。
|
||||
- **依据**:卡① §6 D5 裁决(2026-10-29 用户裁)+§14.5;实测值见实现报告 §3 键账对账表。
|
||||
- **配套**:S13 基线重锚由用户发令 `python tools/gdstyle_gate.py --refresh --force`(16→10 键,降幅逾 10%)——重锚后归绿 6 键自然消失,豁免 10 键以新登记值留基线。
|
||||
@@ -1,92 +0,0 @@
|
||||
# 迭代 42 实现报告(2026-10-29)
|
||||
|
||||
批次=gdstyle 尾波清偿批(卡① v2.0)+ level_runner 商店委托层拆除(卡② v1.1)并排编队。执行形态=整迭代单工作区 `g-plan2-iter42`(两卡均未声明并行工作区,D6 裁「同迭代并排编队、先后皆可」=串行任一序),卡序 gdstyle 先(编排既定)、level_runner 后;卡① T6 批末对账与卡② T2 收口合并为迭代总收口一次执行。定义面(roadmap/待办清单/两卡)已随编排提交 14a10e5e 入库,本批无定义面搬运提交。
|
||||
|
||||
## 1. 各卡状态
|
||||
|
||||
| 卡 | 任务 | 状态 | 分支提交 |
|
||||
|---|---|---|---|
|
||||
| 卡① gdstyle 尾波清偿 | T0 读数复核与对账 | 完成(16 键现值与基线登记值逐键全数对上,零漂移;行位订正 7 处就地落卡) | 509f41f5(随 T1 同笔) |
|
||||
| 卡① | T1 board_view 拆分 | 完成(键 1–4 归绿) | 509f41f5 |
|
||||
| 卡① | T2 _enter_node 载荷合并 | 完成(键 9 归绿) | c47a032d |
|
||||
| 卡① | T3 装板域拆件 BoardInstaller | 完成(键 10 归绿,类变量 38→37) | adff0772 |
|
||||
| 卡② level_runner 委托拆除 | T0 调用面直连(中间态) | 完成(改址六件产品码+sim+七件测试,壳在位全绿) | f7e898be |
|
||||
| 卡② | T1 壳拆除与完整性证明 | 完成(键 7 实测 50→33,残留扫描零真残留) | ae3bd5fc |
|
||||
| 两卡 | T6/T2 迭代总收口 | 本报告+反哺点清单+全量域闸+独立验收审计+合回呈报 | 见 git log |
|
||||
|
||||
## 2. 实际改动—验收对照表(承两卡 §13/§11 需求侧对照表;结果列为本批实测)
|
||||
|
||||
| 行 | 需求点 | 改动(文件:行) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| V1 | 键 1/2/3/4——_paint_cell 拆五段+rebuild 构建器 | board_view.gd `_paint_cell` 拆 `_paint_cell_ink/_paint_cell_base/_paint_cell_facade/_paint_cell_overlays/_paint_cell_fog` 五段+`_build_cell_unit` 出函 | 复用 type_glow_test(48 checks 绿)/theme_projection_test._smoke_type_glow(163 checks 绿)/player_copy_sweep_test(22 checks 绿);render_gate 棋盘 golden 三张 | 单元+探针+golden | BoardView.rebuild()→refresh()→type_glow_probe();board_shot_probe 真实场景帧 | 既有断言面+golden 逐像素 | 绿——三套件全绿(theme_projection 的 163 checks 全过;该套件 FAIL=存量红,见 §6-①,与本改动无涉);golden 有 diff 与 stash 对照逐位同值(L2/L3 0.00%、L1 2.74% 双侧同值——见 §6-③) |
|
||||
| V3 | 键 9——_enter_node 载荷合并 | level_runner.gd shop 分支双 return 并单 return(payload 变量,两字典逐字保持) | 新增 level_shop_economy_test.gd:_test_shop_node_entered_payload〔尾波 V1〕四断言 | 单元 | LevelRunner.start_level(_external)→pick_next_node 进 shop 节点 | node_entered 信号载荷(pool 含 shelf:true 且 gp=进店钱包现值 27;legacy 无 shelf 键且 gp=现值 30) | 绿——套件 192 checks 全绿(exit 0) |
|
||||
| V4 | 键 10——装板域拆件 | board_installer.gd 新建;battle_orchestrator 六函数迁出、复位块留薄壳 `_install_and_reset`、三调用点改址、`_board_generator` 成员退役 | 复用 variant_placement_test(49 checks)/round_flow_skill_pool_test(129 checks)/seal_polarity_test(70 checks)/run_time_fatal_test(负套件 exit=1,wr5 棋盘生成半边随拆件改指 BoardInstaller 新组装点——见 §5-7)+run_smoke(15 项) | 单元+集成 | BattleOrchestrator.begin_battle()/player_request_refresh() | 同种子变异布置逐位断言面+全战斗步骤流逐行回放断言(board_gen/variant_place 取号序零漂移由同种子断言钉住)+违约封印传导链(wr5 双半边) | 绿——三套件+负套件+冒烟全绿;键 10 归绿(文件长 916≤1000 键消失)、类变量实测 37 |
|
||||
| R1 | 调用面直连改址(非行为改动) | 产品码 6 件+sim 1 件+测试 7 件全部调用点 `<表达式>.shop.<方法>` | 既有套件回归两轮全绿:T0 中间态轮(壳在位)+T1 壳删后轮(level_shop_economy 192/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全套+冒烟) | 单元+接口 | 各套件既有真实入口 | 既有断言逐位不变+壳删后编译通过 | 绿——两轮全绿(全量 65 套件两轮失败清单逐一同值=10 存量红,见 §6-②;七件相关套件两轮全绿) |
|
||||
| R2 | 壳块拆除(非行为改动) | level_runner.gd :723–725 区块涉委托两句+:751–828 十九委托全量删除(含死壳 2 枚) | 残留扫描零命中(19 方法名全仓 grep,输出留 §7)+T1 套件绿 | 静态+单元 | grep 全仓+套件真实入口 | 扫描面>0(19 名单命中 34 行逐一判类归属)+壳删后零引用可编译 | 绿——34 行全数为他类定义与调用/字典键名/宿主内部 shop. 直连点,零真残留 |
|
||||
| R3 | 宿主成员公开化(非行为改动) | level_runner.gd:44 `_shop`→`shop`+内部引用 28 处随更名 | 随 R1 两轮套件绿 | 单元 | 套件真实入口 | 内部使用点行为不变 | 绿——词级 `_shop` 全仓 .gd 零残留(件核) |
|
||||
| R4 | 注释同步六处(非行为改动) | shop_session.gd:4/level_runner.gd 区块句/level_autoplay.gd:335/:356/shop_overlay_view.gd:88/152/tree_rules_v11_test.gd:6 | 随批代码域闸绿(静态扫描覆盖文件头纪律面) | 静态 | —(静态判据) | 静态扫描+评审注记(观测面局限申报:注释无运行时提交面) | 绿——六处已同步,闸 09 见全量域闸回执 |
|
||||
|
||||
反向核对:diff 中行为改动仅 V3 一处(载荷合并,行为保持——载荷字典逐字);其余均为非行为改动(结构整改,判据=零漂移证明如上)。孤儿断言:无(新增断言仅 V3 行四条〔尾波 V1〕,已挂行)。
|
||||
|
||||
## 3. 键账对账(S13——11 键终值对照卡① §7 预期)
|
||||
|
||||
| 键 | 预期(卡① §7) | 批末实测 | 判定 |
|
||||
|---|---|---|---|
|
||||
| 1 board_view _paint_cell 长度 | ≤50 归绿 | 键消失(函数已拆五段,新函数无超标键) | 归绿 ✓ |
|
||||
| 2 board_view _paint_cell 分支 | ≤8 归绿 | 键消失 | 归绿 ✓ |
|
||||
| 3 board_view _paint_cell 局部变量 | ≤10 归绿 | 键消失 | 归绿 ✓ |
|
||||
| 4 board_view allocation-in-loop | 消除归绿 | 键消失(.new() 迁入 _build_cell_unit) | 归绿 ✓ |
|
||||
| 5 board_view 类变量 | 32 维持豁免终局 | 32 持平 | 豁免终局 ✓ |
|
||||
| 6 board_view 公开方法 | 27 维持豁免终局 | 27 持平 | 豁免终局 ✓ |
|
||||
| 7 level_runner 公开方法 | 约 33 降值后豁免终局 | 33(50→33,并排卡兑现,与卡面预测精确一致) | 豁免终局 ✓ |
|
||||
| 8 level_runner 类变量 | 30 维持豁免终局 | 30 持平 | 豁免终局 ✓ |
|
||||
| 9 _enter_node 返回数 | 6 归绿 | 键消失 | 归绿 ✓ |
|
||||
| 10 battle_orchestrator 文件长 | 约 910–920 归绿 | 键消失(实测 916 行) | 归绿 ✓ |
|
||||
| 11 adventure_runner 类变量 | 28 维持豁免终局 | 28 持平(本批零触碰) | 豁免终局 ✓ |
|
||||
|
||||
豁免通道既有键随批只降核对:battle_orchestrator 类变量 38→37(D1 修正二兑现,登记值不抬)。**零新增键**(全扫描现值 10 键=基线 16−归绿 6,EXTRA 清单为空)。基线重锚=用户发令人工 `python tools/gdstyle_gate.py --refresh --force`(降幅 6/16≈37.5% 逾 10% 须 --force,卡① §14.4)。
|
||||
|
||||
## 4. 逐拆出单元职责语句(拆分边界纪律①,逐件一行)
|
||||
|
||||
- **BoardInstaller**(新件,board_installer.gd):棋盘安装与变异布置域唯一承载——生成受理→全局作用域变异决议→装板受理→资源摇号资格域组装(roll_board 恰一次)→region 变异决议与连通片布置;单一变化原因=棋盘设置顺序与变异布置算法。
|
||||
- **_paint_cell_ink**(board_view 私有段):字形与色牌层——reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌。
|
||||
- **_paint_cell_base**:基底瓦片层——瓦片查表/平色回退 sb+英雄蓝框载体与贴图缺位回退字形。
|
||||
- **_paint_cell_facade**:立面投影层——未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面。
|
||||
- **_paint_cell_overlays**:秘宝/变异叠加层——真视红框/预读徽章/变异角标/奖励徽章/类型微光组号。
|
||||
- **_paint_cell_fog**:迷雾层——灯光区外未开格全黑/已开格记忆态灰显。
|
||||
- **_build_cell_unit**:逐格构建器——CellUnit 实例化与属性装配(allocation 出循环唯一出函面)。
|
||||
- **_install_and_reset**(battle_orchestrator 宿主薄壳):装板受理承接+宿主态复位单点(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)。
|
||||
- **LevelRunner.shop**(公开成员,非新件):商店件取件唯一形态(委托壳拆除后的直连承载,零新增职责)。
|
||||
|
||||
## 5. 实现决定记录(代拍板白名单外事项备案)
|
||||
|
||||
1. **run_all.gd `--only` 逗号形态工具缺陷**(卡① T1/T3、卡② T0 验证命令形态适配):`game/tests/run_all.gd` 的 `--only` 解析自 6caf56b5 起把整串 `--only=a,b,c` 当单个词(不按逗号拆分),与文件头注「词[,词…]」文档形态及 `tools/run_all_gates.sh:331` 的逗号拼接传参形态均不符——本批改用逐词重复 `--only=` 形态(语义等价)。批末全量闸不受影响(本批触 battle_orchestrator=PARITY 触面表→横切→02 回落全部套件,不经选跑分支);**建议登记工程改动**:run_all.gd 补逗号拆分(或闸脚本改多旗标传参)。
|
||||
2. **BoardInstaller 宿主持有形态**:薄壳 `_install_and_reset` 内逐调用构建 installer(非宿主成员)——T3 验收项「类变量实测 37」要求宿主不新增成员;BoardGenerator 唯一成员 fatal_handler、零跨 generate 调用状态,逐调用构建观察等价(装板取号序由同种子断言钉住,实测全绿)。
|
||||
3. **ink→base 段间 bg 传值**:卡① §7 键 1 限「段间只经 u.st/glyph/digit_col 传值」;bg(基底色)无单元场域可承载、其唯一消费方在 base 段回退分支——经 ink 段返回值交 base 段(唯一例外,函数体逐字搬运不受影响)。
|
||||
4. **两卡里程碑全量闸并入迭代总收口**:卡① T6 与卡② T1 的「里程碑出口 run_all_gates --code」按 iter 技能闸序列「全量域闸每迭代一次」铁律合并为收口单次跑闸(重塑后)——卡面两处闸义务由同一次跑闸兑付。
|
||||
5. **worktree 冷检出 `--editor --quit` 首跑 `--path` 误指仓库根**(未建 .godot 类缓存致首跑解析红)——改指 game 切片目录重刷后正常(106 类)。坑速查条目候选,登记建议随工程批。
|
||||
6. **信号契约注释陈旧发现**(不属本批改动面):level_runner.gd:14 node_entered 信号注释 `shop={gp}` 与 pool 态实际载荷 `{gp, shelf:true}` 不符——改动前即如此(本批未触载荷),随本报告呈报,归文档批/后续批处置。
|
||||
7. **run_time_fatal_test wr5 改指新组装点(L1 就地修复)**:T3 删宿主 `_board_generator` 成员后,该负面套件 :71 的白盒取数 `oc._board_generator._fatal(...)` 断链(脚本错误中断断言函数→零断言退出 2)=**本批新增红**。卡面 §3 现状考证只查了 battle_orchestrator.gd 全文件(四处使用属实)漏查跨文件测试取数点;§16 L1「探针断言调用面改址漏改——就地修复重跑最窄确认」通道处置:wr5 棋盘生成半边改经新组装点(BoardInstaller 引用束 fatal_cb 接线)构建后触发,断言目标(违约→封印传导)不变、断言零增删(§17 四不违);卡 §9 文件结构图就地补行;复跑 exit=1(负套件期望码)绿。**勘误**:本报告先前稿曾将该套件误列存量红——彼次 stash 对照时工作树处于卡①完成态(HEAD 已含 T3)而非 master 原态,对照被污染;更正后 02 存量红=9 套件与基线登记清单逐项一致。
|
||||
|
||||
## 6. 存量红清单(master 原状同值复现,非本批新增——stash 对照法逐组取证)
|
||||
|
||||
1. **theme_projection_test**:`_recent_list` 断链(test:805 访问 `camp._recent_list`——成员已随迭代 41 卡② 拆入内嵌类 `_SideState`;iter41 特许移交合回未跑遗留)。该套件 163 checks 本体全绿,FAIL=脚本错误扫描命中。
|
||||
2. **全量轮 9 套件红**(T0/T1 两轮与 master 原状逐一同值,与基线登记清单及待办清单「迭代 41 测试债修复」条目逐项一致):config_db_test(cattr 解析)/config_db_fatal_oob_test(退出码 2≠1)/table_structure_guard_test(meta_state)/treasure_items_test(crossrun)/standee_fx_sweep_test(set_trans)/forge_workbench_test(120s 挂死)/tree_requires_level_test(row_by_id Nil)/save_manager_domain_test(_migrate_legacy_save)/theme_projection_test(同①)——形态与迭代 41 卡①(核心数据面结构收敛)卡②(呈现层结构收敛)改动特征吻合;已随基线 80b49c0f 记账(用户 2026-10-29 裁带红合并承接)。初稿曾多列 run_time_fatal_test 一项,经 §5-7 勘误更正为本批新增红(已修)。
|
||||
3. **probe_gate --click 三红**:ui_input_probe 超时挂死(exit 124)/ui_struct_probe resume 断言/ui_click_probe 16 行 set_trans 空值(battle_end_overlay 等未触文件)——master 原状同值复现(连错误行数都一致)。
|
||||
4. **render_gate golden L1_dungeon 2.74% 超容差**:master 原状同值(L2/L3 双侧 0.00%)——golden 锚落后于 master 现状,重锚须用户批准(本批禁自行换锚)。
|
||||
|
||||
## 7. 残留扫描输出留痕(卡② T1/R2 判据)
|
||||
|
||||
19 方法名全仓 grep(game/ 下 .gd)命中 34 行,逐行判接收方类归属**零真残留**:字典键名(adventure_runner:505/546/591/600、breakpoint_resume_test:196、level_shop_economy_test:1378/1383、sim 两文件共 4 行);他类定义与调用(battle_orchestrator.player_request_refresh 定义 :709+settlement 侧 8 处调用、CurioDeck.pool_stock 经 deck() 9 处、GpWallet.spend_gp 定义 :15);宿主内部直连使用点(level_runner.gd:102/305/306/704 shop. 形态)。产品码与测试面零旧委托调用。
|
||||
|
||||
## 8. 实机目检总清单
|
||||
|
||||
本批零玩家可观测改动(结构整改+调用面直连)——**无新增目检项**。既有在册提醒(不阻塞):迭代 41 移交的四项实机目检在体验验证清单㊵。
|
||||
|
||||
## 9. 遗留读数义务与登记建议
|
||||
|
||||
1. S13 基线重锚:用户发令 `python tools/gdstyle_gate.py --refresh --force`(§3)。
|
||||
2. MC 对拍:不补跑(零数值零数据表零 RNG 流词表改动,卡① §15)。
|
||||
3. PARITY gate 11(回放确定性对拍):本批触 PARITY_TOUCH_FACES(battle_orchestrator.gd)——闸记 NEED-APPROVAL,实跑或跳过由用户裁决(卡① §15,卡内不定案);批准实跑=`PARITY_APPROVED=1` 重跑闸。
|
||||
4. 工程改动登记建议:§5-1(--only 逗号拆分)+§5-5(坑速查补条)。
|
||||
5. 独立验收审计:报告落 `reports/iter42-验收审计-2026-10-29.md`(未参与实现的子代理执行,test-conventions §8.7 四事),结论见该件。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 迭代 43 代拍板追认单——战斗一屏样张批(2026-10-29)
|
||||
|
||||
> 接收方:待办清单条目「战斗一屏样张 P0 未开工」(随实现报告一并呈报,用户追认后随反哺点一并处置)。
|
||||
> 卡片 §10 代拍板白名单四项内行使的记录+白名单边缘判断的追认请求。超白名单事项零(无越权裁断)。
|
||||
|
||||
## 白名单内行使记录
|
||||
|
||||
1. **样张脚本内临时变量命名、辅助函数拆分方式**:`draw_base()`/`_grad_top_cells()`/`_region_wcag()`/`_mask_maxch_dist()`/`_resample_mask()`/`_mask_iou()`/`_outline_underlay()`/`_ink_outline_polygon()` 等拆分;实装侧 `_armor_tex_path()`/`_name_plate_rect()`/`_name_baseline()`/`_draw_plinth_stretched()`。
|
||||
2. **断言在 CHECKS 内的 id 命名与登记顺序**:S1–S4(整屏四项)、P1–P5(战斗表现五项)、A7(护甲对比锚)、V1(两版出口)、V10(基线并排)、A5b(成稿净空)。
|
||||
3. **程序化产出素材的绘制实现细节**(颜色全取 v3.22 在册值、零新增色):地台件按工单 §5.2 工艺逐行(1px 上描边 #4C4038+2px 顶缘受光线 #C9AF83+8px 板身 #968264+1px 下描边,全周闭合);护甲三枚按 §5.3.1–5.3.3 形状(轻甲=斜跨皮带+扣件、魔甲=符纹带+两枚 3×3 符点、重甲=肩两块+胸一块甲板,深墨 #33261B 1px 描边)。
|
||||
4. **两版样张输出文件名与落盘目录**:`artwork/mockups/sample_battle_screen_{1280x800,1920x1080}.png`;附属:基线 `sample_battle_screen_baseline_1280.png`、并排 `sample_battle_screen_before_after.png`、装配稿沿用 `sample-batch-v3-final-2026-09-30.png`。
|
||||
|
||||
## 追认请求(白名单边缘判断,已按实现决定记录备案)
|
||||
|
||||
5. **S3 首焦唯一的机器代理换型**:首版「最高对比点簇 ∈ 单区 ≥85%」被文字字形边打穿(红),按卡片 §9 L1(断言红修实现不修判据原则的反向面——判据为本批新建,代理形态属白名单 2「断言实现细节」)改为「高对比能量占比 winner ≥55% 且 ∈{数字区,B 区}」。语义等价(无第二焦点竞争),实测数字区 59%。
|
||||
6. **P2 剪影灰度 IoU 阈值 0.35**:新建断言的初始阈值,按三语境实测(0.36/0.37/0.61)与「同角色体块计划过半重合」的设计判断设定。
|
||||
7. **mockup C 区次级文换墨**:原裸值 (140,90,30)/(120,110,98) 对 C 区板 4.06:1/3.5:1 违整屏①——换在册墨 GOLD_INK(5.34:1)/STONE_DEEP(6.13:1),值单点不新增色。
|
||||
8. **mockup 意图徽标补 PANEL_DATA 垫牌**:实装已有素牌而 mockup 漏镜像(裸字形对舞台底 2.36:1 违①),补牌对齐实装。
|
||||
9. **A5b 补充断言**:对装配后成稿补跑画布净空(既有 A5 历史形态跑在空白底板上)——首跑即抓出注记长行溢出 6px,已拆行修复。
|
||||
10. **单测承载选型**:V5–V9 落新套件 `standee_present_values_test.gd`(manifest 登记一行)而非并入 standee_fx_sweep——域命名与 grep 锚更干净,套件族映射自动覆盖。
|
||||
11. **数据保真从紧**:三敌甲档按数据面全 light,护甲三枚展示移 S6 附录——不为展示效果改 mockup 数据面。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 迭代 43 反哺点清单——战斗一屏样张批(2026-10-29)
|
||||
|
||||
> 接收方:待办清单(文档批落文裁决用)。用户在会话中逐条裁决后由文档批写入 `design/` 与根流程件,并把该条清掉;实现批对 `design/` 与根规则文件只读。
|
||||
|
||||
## 卡片 §6 预登记的关联文档同步面
|
||||
|
||||
1. **`design/gdd/ux/battle.md`**——B 区高度:文 270 vs 代码 252 的 18px 差异。本批样张按代码 252 出(样张反映实机),实装对齐与否待用户裁决(对齐=需求改动走写卡通道)。
|
||||
2. **`design/art/art-bible.md`(v3.22)**——§1.4 整屏四项闸的通过事实与抽样读数(文字对比最弱 5.34:1;舞台底 L0.036/格底 L0.089 落两子带间隔 0.053;首焦=数字区能量占比 59%;名字行素底牌平底零地台色);§6.1/§7.2 站桩素底深档 `#282220` 实装落地事实(名字行素底牌+基线上移地台顶上方)。
|
||||
3. **`design/art/art-prompts.md` §5**——组二地台件、组三护甲三枚已「程序化产出即终稿」并入包(`scene_stage_plaza.png` 48×12、`ui_armor_{light,ward,heavy}.png` 24×24);组一背景插画、组四立绘重产仍待出图(状态不变)。
|
||||
4. **`design/gdd/systems/战斗循环/战斗表现.md`**——「一屏样张的验收标准」五项过闸结果(P1–P5 断言全绿;判据机器代理口径见实现报告 §3.2–3.3)。注意:本批样张为**方向稿**,目检先行——气质终审通过后才算样张整体过闸(§1.4 闸形)。
|
||||
5. **`design/engineering/test-conventions.md`**——§8.4 撤销证明条文与 2026-10-28 新验证口径(禁变异/禁撤销取证)冲突,旧句待清。
|
||||
6. **`AGENTS.md`**——「验收闭合」红线句仍保留「撤销该改动能让对应断言变红」旧措辞,与新口径冲突。
|
||||
|
||||
## 本批新发现(实现期产出,非卡片预登记)
|
||||
|
||||
7. **血条递减 tween 静默空转(既有缺陷,非本批引入)**——`game/presentation/views/standee_body.gd` `_set_hp()` 内 `tween_property(self, "st.hp_value", ...)` 用点号子属性路径,Tween 子属性语法应为冒号(`st:hp_value`);现路径无 Tweener,血条递减呈瞬时直落非 0.15s 递减(运行日志 ERROR 可复现:单跑 standee_present_values 套件可见)。修一行;归用户裁决走 mini-loop 或并入后续批。
|
||||
8. **站桩名字行 12px 的字号档位归属**——`standee_body.gd` 名字行绘制取 `UiKit.FS_CAPTION`(12,本批仅收常量不改值);art-bible §7.4 字号档位表下该位既不在①条带内读数豁免、也不在②装饰 caption 在册面,属「清单外 11–13px」。处置二选一由用户裁:提档(FS_HINT 14,与顺位徽标同款)或增补 caption 在册面。
|
||||
9. **能力清单缺本批预留行**——`game/tests/README.md` 能力清单无本批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿)。归写卡段按 test-conventions §8.3 补行(代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`)。
|
||||
10. **art-bible §4.1 换色合并三处的执行状态**——地台绘制点半边已由地台件纹理执行(顶缘受光线 `#C9AF83`、描边 `#4C4038` 落 `scene_stage_plaza.png`);残余:① `STAGE_BG`(#3E342A→#3E362C)未执行(本批不触 `battle_stage_view.gd` 底色);② `STAND_STONE_RIM`(#C4AC84)仍被 `board_cell_unit.gd:359` 口沿高光在役引用;③ `STAND_STONE_EDGE`(#5A4C3A)由地台缺位回退路径在役引用。常量彻底退役与否归文档批裁决。
|
||||
11. **体验验证清单登记**——「战斗一屏样张方向稿目检」项(判据与材料见实现报告 §5;真人检测只登记不阻塞)。
|
||||
12. **根流程件换代**——待办清单条目「战斗一屏样张 P0 未开工」状态推进(本批实现完成后按 doc-conventions §12.1 整条删除);roadmap 迭代 43 行状态与实现报告列、「当前位」三行换代;样张产物归档位=`artwork/archive/战斗一屏样张批-2026-10-29/`(batch-closure 执行)。
|
||||
@@ -0,0 +1,83 @@
|
||||
# 迭代 43 实现报告——战斗一屏样张批(2026-10-29)
|
||||
|
||||
> 接收方:待办清单条目「战斗一屏样张 P0 未开工」;roadmap 迭代 43 行。
|
||||
> 分支 iter43(基准 ee4efad5)· 单工作区单卡串行 · 实现报告/追认单/反哺点清单三件同日落 `reports/`。
|
||||
|
||||
## 1. 各卡状态
|
||||
|
||||
| 卡 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| T0 | 开工准备(worktree+素材导入+user 目录+v3 脚本基线跑) | 完成(基线 17/17 全绿存档 `artwork/mockups/sample_battle_screen_baseline_1280.png`) |
|
||||
| T1 | 样张底板两版画布 | 完成(`draw_base()`+两版出口,机检 V1 绿) |
|
||||
| T2 | 内容清单铺画 | 完成(色板变体第二套 S3b/三组关键帧条 S5/剪影三语境+描边对照+遮挡对照组 S6/mockup 镜像实装定值) |
|
||||
| T3 | 整屏四项断言进 CHECKS | 完成(S1–S4 全绿) |
|
||||
| T4 | 战斗表现五项断言进 CHECKS | 完成(P1–P5 全绿+A7 护甲对比锚) |
|
||||
| T5 | 血条填充换 #A8321F(经 UiKit 常量)、条底收常量 | 完成 |
|
||||
| T6 | 名字行垫 HP_BAR_BACK 素底、不压地台 | 完成 |
|
||||
| T7 | 顺位徽标提档 UiKit.FS_HINT | 完成 |
|
||||
| T8 | 护甲图标由字形换三枚 24×24 PNG | 完成 |
|
||||
| T9 | 地台件与护甲三枚程序化产出并入包 | 完成(`--gen-assets` 四件,工单 §5.2/§5.3 口径) |
|
||||
| T10 | 三份批件+改前/改后并排对拍图 | 完成(并排图机检 V10 绿) |
|
||||
|
||||
**样张机检:30/30 全绿**(17 条既有 F/A 断言+S1–S4+P1–P5+A7+V1+V10+A5b)。
|
||||
**单元套件:standee_present_values 18/18 全绿**(exit 0)。
|
||||
|
||||
## 2. 实际改动—验收对照表
|
||||
|
||||
> 卡片成文(2026-10-28)早于迭代 41 呈现层结构收敛合入(2026-10-29),卡片 §3/§8 的 `standee_body.gd` 行号普遍漂移;实质判据 15 处实查零冲突(开工检查记录见 git 提交史)。本表按**实际行号**记载。
|
||||
|
||||
| # | 需求点 | 改动(实际 file:line) | 验收测试(文件:行+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| V1 | 样张两版产出 | `tools/render_sample_sheet_v3.py` main() 两版出口(1280×800 基座;1920×1080=1.35×+黑边各 96px) | `tools/render_sample_sheet_v3.py` CHECKS 条目 V1 | 探针 | `python tools/render_sample_sheet_v3.py` 真实跑批 | 两版位图尺寸 1280×800 / 1920×1080 | 绿(ed1 (1280,800)/ed2 (1920,1080)) |
|
||||
| V2 | 文字对比 ≥4.5:1 | 同上(S1 断言+mockup C 区次级文换在册墨 STONE_DEEP/GOLD_INK) | CHECKS 条目 S1(最弱对 5.34:1) | 探针 | 同上 | 八组文字面实际用色 WCAG 对表 | 绿 |
|
||||
| V3 | 明度三档带成立(含两子带) | 同上(S2 断言) | CHECKS 条目 S2 | 探针 | 同上 | 舞台底 L0.036/已开格底 L0.089 落两子带、间隔 0.053≥0.02;信息 0.78>中层 0.23 | 绿 |
|
||||
| V4 | 首焦唯一 | 同上(S3 断言) | CHECKS 条目 S3 | 探针 | 同上 | 高对比能量占比:数字区 59%(≥55% 且 winner∈候选区) | 绿 |
|
||||
| V5 | 血条=危险红 #A8321F | `standee_body.gd:525` 填充引 `UiKit.HP_BAR_FILL`;`:522` 条底引 `HP_BAR_TRACK`;`ui_kit.gd:194-195` 两常量 | `standee_present_values_test.gd:28-35`〔样张批 V5〕 | 单元 | 冒烟套真实跑批(run_all manifest) | 常量值 to_html=="a8321f"+源码锚(绘制点引用+全源零 #689CE8/零裸 Color(0.41,0.61,0.91)) | 绿(headless 无渲染面,像素采样以常量单点+源码锚承载——观测面注记) |
|
||||
| V6 | 名字行垫素底、不压地台 | `standee_body.gd:445` `_name_plate_rect()` 几何单点(牌底沿=地台顶)、`:449` `_name_baseline()`、`:570-588` 绘制域;`name_line_probe()`(`:100-109`)扩几何出口 | `standee_present_values_test.gd:45-51`〔样张批 V6〕 | 单元 | 同上 | probe 几何:基线 y=149 < 地台顶 y=158;牌色 #282220;样张侧 S4 断言(平底 100%+零地台板身色) | 绿 |
|
||||
| V7 | 顺位徽标提档 14 | `standee_body.gd:519` 裸 13 → `UiKit.FS_HINT` | `standee_present_values_test.gd:55-56`〔样张批 V7〕 | 单元 | 同上 | FS_HINT==14+绘制点源码锚(grep 该处零裸字号) | 绿 |
|
||||
| V8 | 护甲图标换三枚 PNG | `standee_body.gd:432` `_armor_tex_path()`(三键→`UiKit.TEX_ARMOR_*`)、`:570-588` draw_texture_rect 24×24(缺位回退字形);`ui_kit.gd:253-255` 路径单点 | `standee_present_values_test.gd:64-71`〔样张批 V8〕 | 单元 | 同上 | 三键 ResourceLoader 装载 24×24+probe armor_icon 出绑定路径 | 绿 |
|
||||
| V9 | 地台件与护甲三枚入包 | `game/assets/art/scene/scene_stage_plaza.png`(48×12,不入 git)+`game/assets/ui/icons/ui_armor_{light,ward,heavy}.png`(24×24,入 git);`standee_body.gd:493`/`:502` 九宫格横向拉伸接装 | `standee_present_values_test.gd:76-81`〔样张批 V9〕 | 接口 | 同上 | 资源可加载且尺寸 48×12 / 24×24 | 绿 |
|
||||
| V10 | 改前/改后对拍图产出 | T0 存基线(1280×800);main() 生成并排图 | CHECKS 条目 V10 | 探针 | 样张脚本真实跑批 | 基线版与改后版两源文件尺寸一致+并排图落盘 | 绿(`sample_battle_screen_before_after.png` 2570×810) |
|
||||
|
||||
**反向覆盖**:diff 内全部行为改动均上表有行;非行为改动=`run_all.gd` manifest 一行登记(套件注册,非游戏行为)、测试 `.uid` 边车、样张脚本注释与文档头扩展。孤儿断言:无(样张 CHECKS 全部挂 V1–V4/V10/S/P/A7;单测 18 条全挂 V5–V9)。
|
||||
|
||||
## 3. 实现决定记录(代拍板白名单内行使+边界判断备案)
|
||||
|
||||
1. **断言 id 命名与登记顺序**(白名单 2):整屏四项=S1–S4、战斗表现五项=P1–P5、护甲对比锚=A7、两版出口=V1、基线并排=V10、成稿净空=A5b。
|
||||
2. **S3 首焦唯一的机器代理**:首版「最高对比点簇 ∈ 单区 ≥85%」被文字字形边打穿(各文字面对比同档,簇分裂 23%/25%)。改用**高对比能量占比**(top10% 梯度格按区归堆,winner ≥55% 且 ∈{数字区,B 区立绘})——语义=「对比内容量集中单区,无第二焦点竞争」,与 battle.md「挖掘期视线锚棋盘区」同轴。实测:数字区 59%/B 区 20%/C 区 17%。
|
||||
3. **P2 IoU 阈值 0.35 校准依据**:三语境剪影两两 IoU 实测 0.36/0.37/0.61(棋盘×明细同源资产最高;舞台立绘×棋盘小人跨源档最低)。0.35=「过半体块计划重合」档,按同角色 chibi 渲染的设计判断设定,非放宽既有断言(本断言为新建)。
|
||||
4. **HP_BAR_TRACK 新常量**=视图原裸值 `Color(0.10,0.10,0.11)` 原样收口(值不属色板新增——现渲染值单点化,卡片 §7.1「条底收常量」的直接执行)。
|
||||
5. **STAND_STONE_RIM/EDGE 两常量保留**:RIM 另有 `board_cell_unit.gd:359` 口沿高光在役引用(本批不触该文件);EDGE 由地台缺位回退路径在役引用。art-bible §4.1 换色合并三处的**地台绘制点半边**已由地台件纹理(#C9AF83/#4C4038)执行;常量彻底退役与 STAGE_BG 合并半边归文档批裁决(反哺点 10)。
|
||||
6. **意图徽标垫牌镜像补齐**:实装 `standee_body.gd` 意图徽标有 PANEL_DATA 素牌(四色墨对牌 4.7–5.8:1),mockup 原裸画字形(对舞台底 2.36:1 违 S1)——mockup 补牌对齐实装。
|
||||
7. **护甲三枚数据保真**:三敌甲档按数据面全 light(goblin/bugbear/ogre_chief),不为展示图标改甲档;三枚图标的展示落 S6 附录(挂 HP_BAR_BACK 素底,与实装挂载底同口径)。
|
||||
8. **观测面注记(test-conventions §8.4 出口)**:headless 无渲染面,V5/V7 的「像素采样色值」以常量值断言+绘制点源码锚+探针几何承载;V6 判据「基线 y < 地台顶 y」绑 `name_line_probe()` 几何出口(公开方法,非内部字段直查)。
|
||||
9. **A5b 补充断言**:既有 A5(画布四缘净空)跑在空白底板上(v3.5 冻结形态),本批补 A5b 对**装配后成稿**跑同一判据——首跑抓出 S4 注记长行溢出右缘 6px(已拆行修复),确证其价值。
|
||||
10. **样张输出文件名与落盘目录**(白名单 4):两版=`artwork/mockups/sample_battle_screen_{1280x800,1920x1080}.png`;基线=`sample_battle_screen_baseline_1280.png`;并排=`sample_battle_screen_before_after.png`;装配稿=原 `sample-batch-v3-final-2026-09-30.png`(1560×2080)。
|
||||
11. **单测承载选型**:V5–V9 落新套件 `standee_present_values_test.gd`(单元面按 §8.2 从领域类对外方法进出)而非既有 standee_fx_sweep(其 _init 为缺陷扫掠专用段)——manifest 登记一行;闸映射 `game/presentation/*`→presentation 族自动覆盖。
|
||||
12. **能力清单缺口**:本批 V5–V9 代表用例行在 `game/tests/README.md` 能力清单**无预留行**(卡片成文早于清单改版定稿)——实现批禁写 README,归写卡段补行(反哺点 9);测试函数名以卡片 V1–V10 为唯一锚。
|
||||
|
||||
## 4. 存量红判别(闸前自检)
|
||||
|
||||
- S10 文件头 4 处:`game/core/config_db.gd` / `config_domain_{combat,crossrun,run}.gd`——基准提交 ee4efad5 已在,本批未触碰;存量按白名单清偿通道。
|
||||
- S11 禁用词 1 处:`game/data/schemas/adventure_table.gd:55`「罕见」——`git show ee4efad5` 实证基准已在其行;存量。
|
||||
- gdstyle S13:基线外新违规 0、较登记值恶化 0(`standee_body.gd` 类变量计数未越基线 19——纹理装载走 `UiKit.load_tex` 即取即绘,零新增成员)。
|
||||
- **本批零新增红。**
|
||||
|
||||
## 5. 实机目检总清单(真人检测只登记不阻塞)
|
||||
|
||||
| 项 | 判据 | 材料 |
|
||||
|---|---|---|
|
||||
| 方向稿目检(卡片 §4(3)——目检先行,机检冻结归目检通过后) | 气质=明亮地窟看台、立绘最抢眼、无压抑;构图/分区/明度三档带/首焦落点与设计意图一致 | `artwork/mockups/sample_battle_screen_1280x800.png`、`sample_battle_screen_1920x1080.png`(批末归档 `artwork/archive/战斗一屏样张批-2026-10-29/`) |
|
||||
| 改前/改后对照 | 五项定值前后差异可辨(血条色/素底牌/徽标/护甲图标/地台件) | `sample_battle_screen_before_after.png` |
|
||||
|
||||
登记义务同步进反哺点清单(体验验证清单归文档批登记——实现批禁写根流程清单)。
|
||||
|
||||
## 6. 遗留读数义务与观测记录
|
||||
|
||||
- MC/verifier:**零跑**(卡片未要求 MC 对拍;本批无玩法数值改动)。
|
||||
- 全量回归:合回后由用户自发(2026-10-29 关联档选测口径,无自动兜底)。
|
||||
- 既有潜在缺陷(随测发现,本批未触碰该行):`standee_body.gd` 血条 tween `tween_property(self, "st.hp_value", ...)` 用点号子属性路径——Tween 子属性语法为冒号(`st:hp_value`),现路径静默无 Tweener、血条递减呈瞬时直落非 0.15s 递减。已记反哺点 7,处置归用户裁决(修一行=工程改动的 mini-loop 或并入后续批)。
|
||||
|
||||
## 7. 独立验收审计结论
|
||||
|
||||
(合并前由未参与实现的子代理执行,结论见 `reports/iter43-验收审计-2026-10-29.md`;未通过不得合并。)
|
||||
@@ -34,9 +34,9 @@
|
||||
| 2 | `board_view.gd\|quality/max-branches\|_paint_cell = 24` | 24/8 | 同上 |
|
||||
| 3 | `board_view.gd\|quality/max-local-variables\|_paint_cell = 22` | 22/10 | 同上 |
|
||||
| 4 | `board_view.gd\|quality/allocation-in-loop`(裸键) | 命中 | `board_view.gd:249`——rebuild() 建格循环内 `_CELL_UNIT.new()` |
|
||||
| 5 | `board_view.gd\|quality/max-class-variables = 32` | 32/15 | `board_view.gd:49-85`(var 成员 31)+:93(`cell_size` 只读属性;计数以闸实测为准) |
|
||||
| 5 | `board_view.gd\|quality/max-class-variables = 32` | 32/15 | `board_view.gd:49-85`(var 成员 31 + `cell_size` 只读属性 1,以闸实测为准) |
|
||||
| 6 | `board_view.gd\|quality/max-public-methods = 27` | 27/20 | 全文件:11 setter+6 探针出口+4 几何契约出口+3 入站事件+2 生命周期(rebuild/refresh)+1 访问器(board()) |
|
||||
| 7 | `level_runner.gd\|quality/max-public-methods = 50` | 50/20 | `game/core/level_runner.gd`——其中商店同签名委托 17 个公开方法(751-828 行)+3 个私有转发(`_bind_shop`:728、`_spend_discounted`:772、`_shop_draw_shelf`:783)+`_shop` 成员(:44) |
|
||||
| 7 | `level_runner.gd\|quality/max-public-methods = 50` | 50/20 | `game/core/level_runner.gd`——其中商店同签名委托 17 个公开方法(751-829 行)+3 个私有转发(`_bind_shop`:728、`_spend_discounted`:772、`_shop_draw_shelf`:783)+`_shop` 成员(:44) |
|
||||
| 8 | `level_runner.gd\|quality/max-class-variables = 30` | 30/15 | `level_runner.gd` 类成员区(含 `hero_hp`/`gp` 两属性) |
|
||||
| 9 | `level_runner.gd\|quality/max-returns\|_enter_node = 7` | 7/6 | `level_runner.gd:486-513`——shop 分支 pool/legacy 双 return(:509、:511) |
|
||||
| 10 | `battle_orchestrator.gd\|quality/max-file-length = 1035` | 1035/1000 | `game/core/battle_orchestrator.gd`——装板与变异布置域六函数合计约 140 行(:414-549) |
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
- board_view 公开方法 27 个全是契约面:setter 是组合根注入面(依赖单向纪律),探针出口(cells_probe/diggable_probe/hover_probe/fog_params_probe/type_glow_probe/cell_rect_probe)是探针断言面,几何出口与入站事件是演出与输入契约。可降空间极小。
|
||||
- battle_orchestrator 装板域六函数全仓零外部调用,仅本文件三处调用(:271 begin_battle、:801 _exit_distribution、:835 player_request_refresh)——拆件不触任何外部调用面。
|
||||
- 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 体内 443-452 行为宿主态复位块(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)——按第 6 节 D1 裁决留宿主薄壳。
|
||||
- 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 体内 442-451 行为宿主态复位块(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)——按第 6 节 D1 裁决留宿主薄壳。
|
||||
- `_board_generator` 全文件仅四处使用(:42 声明、:233 创建、:239 fatal 接线、:416 装板消费)——装板族独占,随件迁移后宿主成员退役(38→37)。
|
||||
- level_runner 商店委托面被呈现层 5 文件(camp_shop_presenter/main_adventure_flow/main_overlay_flow/main_ui_build/shop_overlay_view)、`game/sim/level_autoplay.gd`、测试 4 套件(level_shop_economy/pool_case_d/tree_rules_v11/hero_ledger)引用——shop_overlay_view 三处命中均为注释投影引用(:15/:88/:152 实查),代码调用面在 presenter/组合根拆件/sim 侧。
|
||||
- `tools/run_all_gates.sh` PARITY_TOUCH_FACES 含 `game/core/battle_orchestrator.gd`——本批必触面。
|
||||
@@ -122,7 +122,6 @@
|
||||
| game/core/battle_orchestrator.gd | 战斗结算编排中枢 | 修改(非行为改动——装板域六函数迁出、宿主薄壳承接复位块、`_board_generator` 成员退役、三调用点改址;验收=对照表行 V4) |
|
||||
| game/core/board_installer.gd | 棋盘安装与变异布置域件(新增,architecture v1.24 功能层在册) | 新增(非行为改动;验收=对照表行 V4) |
|
||||
| game/tests/unit/core/level_shop_economy_test.gd | 关卡图与商店宿主套件 | 修改(仅新增断言 _test_shop_node_entered_payload〔尾波 V1〕;委托面调用点改址归并排卡) |
|
||||
| game/tests/unit/core/run_time_fatal_test.gd | 运行期违约通道负面套件 | 修改(wr5 棋盘生成半边改指 BoardInstaller 新组装点——T0 复核补查项:卡面 §3 漏查的该测试白盒取数点 `_board_generator` 随成员退役断链,wr5 断言目标违约→封印传导不变) |
|
||||
| game/tests/static_scan.sh | 静态扫描域承载 | 修改(S7 B1 域文件集与 S8 await 文件集增行 board_installer——工程随动,architecture 拆件定稿节 S7/S8 随动条在册) |
|
||||
|
||||
新增件「为什么不能复用」理由与唯一落点核查见第 10 节;对不上即卡片不成立。
|
||||
@@ -164,7 +163,7 @@
|
||||
|
||||
**T3 battle_orchestrator 装板域拆件——键 10 归绿(1–1.5 工作日)**(前置:architecture v1.24 轴丙在册)
|
||||
|
||||
- [ ] 建 `game/core/board_installer.gd`(引用束注入,第 10 节落位预检),装板逻辑六函数迁件;`_install_new_board` 拆件体=生成→决议→装板→摇号→布置(415-441 行段),宿主薄壳 `_install_and_reset` 承载复位块(443-452 行段:五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役),时序逐字
|
||||
- [ ] 建 `game/core/board_installer.gd`(引用束注入,第 10 节落位预检),装板逻辑六函数迁件;`_install_new_board` 拆件体=生成→决议→装板→摇号→布置(415-441 行段),宿主薄壳 `_install_and_reset` 承载复位块(442-451 行段:五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役),时序逐字
|
||||
- [ ] 宿主三调用点(:271/:801/:835)改调薄壳;删宿主 `_board_generator` 成员(38→37,禁无效成员)
|
||||
- [ ] static_scan.sh S7 B1 域文件集与 S8 await 文件集增行
|
||||
- [ ] 孤立验证:`-- --only=variant_placement,round_flow,seal_polarity` 全绿+`python tools/run_smoke.py`
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
跳过设计线的证据指认(需求性工程条目,第〇步核验放行 2026-10-29):
|
||||
|
||||
- 待办清单条目实况:委托壳=迭代 37 卡④ ShopSession 拆分时立的「冻结公开面」契约过渡件;架构登记=`design/engineering/architecture/architecture.md:109`「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」。
|
||||
- 拆除形态权威=`architecture.md:154`「调用直连:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定)」。
|
||||
- 拆除形态权威=`architecture.md:152`「调用直连:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定)」。
|
||||
- 2026-10-29 写卡会话裁决:改址定式四条(公开成员取件/零调用死壳删/测试直连私有面/契约注释随壳消失)+ spend_gp 归属确认非问题(共享冒险钱包的校验扣款原语,宿主 ShopSession 语义自洽、不迁移)。
|
||||
|
||||
## 3. 现状事实(代码级,开工自行复核)
|
||||
|
||||
委托壳块:
|
||||
|
||||
1. level_runner.gd:723–725 区块注释(:723「商店房兑付」节头保留;:724「本类保留同签名委托」句+:725「白盒入口——外部调用面与断点取数面零变化」两句随壳删除)+:751–828 共 19 个同签名转传方法:shop_products:751/current_price:755/purchased_tier:759/buy_product:763/player_request_refresh:767/_spend_discounted:772/set_shop_pool:778/_shop_draw_shelf:783/pool_stock:787/pool_price:791/shop_shelf:795/shop_refresh_free_left:799/shop_refresh_cost:803/shop_refresh:807/spend_gp:811/buy_pool_item:815/is_shop_pool:819/shop_bp_shelf:823/shop_bp_visit_buys:827。
|
||||
1. level_runner.gd:722–724 区块注释(「本类保留同签名委托」句)+:725(「白盒入口——外部调用面与断点取数面零变化」)+:751–828 共 19 个同签名转传方法:shop_products:751/current_price:755/purchased_tier:759/buy_product:763/player_request_refresh:767/_spend_discounted:772/set_shop_pool:778/_shop_draw_shelf:783/pool_stock:787/pool_price:791/shop_shelf:795/shop_refresh_free_left:799/shop_refresh_cost:803/shop_refresh:807/spend_gp:811/buy_pool_item:815/is_shop_pool:819/shop_bp_shelf:823/shop_bp_visit_buys:827。
|
||||
2. 零外部调用方死壳 2 枚:`_spend_discounted`(全库仅 tree_rules_v11_test.gd:6 注释提及)、`pool_stock`(外部同名调用全为 CurioDeck.pool_stock,如 adventure_feats_test.gd:545——同名不同件)。
|
||||
|
||||
外部调用面(产品码与 sim):
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
登记与账面:
|
||||
|
||||
9. 架构:architecture.md:109(委托承载句+公开面格)/:154(直连原则);`design/` 全目录仅此一处委托登记(grep 考证 2026-10-29)。
|
||||
9. 架构:architecture.md:109(委托承载句+公开面格)/:152(直连原则);`design/` 全目录仅此一处委托登记(grep 考证 2026-10-29)。
|
||||
10. gdstyle 基线:tools/gdstyle_baseline.txt:11–13——level_runner 三键 max-class-variables=30/max-public-methods=50/max-returns|_enter_node=7。
|
||||
11. 能力清单:game/tests/README.md 旅程段四「GP 钱包/escrow/商店收口/共享池」行真实入口列引用委托名——已随本卡写作更新为直连形态(写卡段义务,实现批禁写该文件)。
|
||||
12. shop_session.gd:4 头注「公开面由 LevelRunner 同签名委托承载」句随拆除失效(商店件唯一注释级 diff,见契约 3)。
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
## 5. 范围边界
|
||||
|
||||
做:壳块与死壳删除;`_shop`→`shop` 公开化(含内部引用点);产品码/sim/测试调用点改址;注释同步六处(shop_session.gd:4、level_runner.gd:723–725 区块涉委托两句〔删 :724–725、留 :723 节头〕、level_autoplay.gd:335、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6);level_runner 头注涉委托表述随批核查同步。
|
||||
做:壳块与死壳删除;`_shop`→`shop` 公开化(含内部引用点);产品码/sim/测试调用点改址;注释同步六处(shop_session.gd:4、level_runner.gd:722–724 区块句、level_autoplay.gd:335、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6);level_runner 头注涉委托表述随批核查同步。
|
||||
|
||||
不做(去向):
|
||||
|
||||
@@ -136,7 +136,7 @@ game/tests/unit/core/tree_rules_v11_test.gd 头注同步 1 处【非行
|
||||
|
||||
- 取用:T0 全调用点直连形态在位。
|
||||
- 产出:终态 level_runner(壳归零);T2 的闸与审计对象。
|
||||
- [ ] 删除 level_runner.gd:723–725 区块注释中涉委托与白盒两句(:724–725;保留 :723「商店房兑付」节头与 :727 件注入说明)+:751–828 十九个委托全量(含死壳 `_spend_discounted`:772/`pool_stock`:787)
|
||||
- [ ] 删除 level_runner.gd:722–724 区块注释涉委托句+:725 契约注释(保留「商店房兑付」节头与件注入说明)+:751–828 十九个委托全量(含死壳 `_spend_discounted`:772/`pool_stock`:787)
|
||||
- [ ] shop_session.gd:4 头注委托承载句更新(「外部调用面=调用方经宿主公开成员 `shop` 直连」表述)
|
||||
- [ ] 验证:`python tools/run_smoke.py` 绿+全单元套件绿(壳删后绿=T0 改址无遗漏的编译级证明)
|
||||
- [ ] 残留扫描:grep 19 个方法名于 `game/`(排除 shop_session.gd 本体定义、level_runner.gd 内部 `shop.` 使用点、跨类同名方法——BattleOrchestrator.player_request_refresh 与 CurioDeck.pool_stock 按现状事实 5 清单判接收方类归属)——产品码与测试面零命中,输出留实现报告
|
||||
@@ -160,7 +160,7 @@ game/tests/unit/core/tree_rules_v11_test.gd 头注同步 1 处【非行
|
||||
| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 调用面直连改址(非行为改动——调用目标/实参/返回/副作用逐项同前,仅取件路径文本变化) | §9 结构图产品码 6 件+sim 1 件+测试 7 件全部调用点 | 既有套件回归两轮全绿:T0 中间态轮+T1 壳删后轮(level_shop_economy/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全套+冒烟) | 单元+接口 | 各套件既有真实入口(直构 LevelRunner/AdventureRunner 链) | 既有断言逐位不变(行为零改动正向证据)+壳删后编译通过(改址无遗漏) | 既有:GP 钱包/escrow/商店收口/共享池;既有:奖励三选一候选/刷新付费/呈现 |
|
||||
| R2 壳块拆除(非行为改动——纯转传方法与契约注释删除,行为承载在 ShopSession 不变) | level_runner.gd:723–725 区块涉委托两句(:724–725)+:751–828(含死壳 `_spend_discounted`:772/`pool_stock`:787) | 残留扫描零命中(T1 静态步)+T1 套件绿 | 静态+单元 | grep 全仓(19 方法名清单固定)+套件真实入口 | 扫描面>0 前置(grep 命令对 19 名单非空可执行)+壳删后零引用可编译;命中行按接收方类归属判别(现状事实 5 跨类同名清单) | 既有:GP 钱包/escrow/商店收口/共享池 |
|
||||
| R2 壳块拆除(非行为改动——纯转传方法与契约注释删除,行为承载在 ShopSession 不变) | level_runner.gd:722–724 句+:725+:751–828(含死壳 `_spend_discounted`:772/`pool_stock`:787) | 残留扫描零命中(T1 静态步)+T1 套件绿 | 静态+单元 | grep 全仓(19 方法名清单固定)+套件真实入口 | 扫描面>0 前置(grep 命令对 19 名单非空可执行)+壳删后零引用可编译;命中行按接收方类归属判别(现状事实 5 跨类同名清单) | 既有:GP 钱包/escrow/商店收口/共享池 |
|
||||
| R3 宿主成员公开化(非行为改动——私有 `_shop` 更名公开 `shop`,装配与读写路径不变) | level_runner.gd:44+内部引用 :99/:102/:104/:305–306/:506–507/:704/:729 | 随 R1 两轮套件绿 | 单元 | 套件真实入口 | 同 R1(内部使用点行为不变) | 既有:GP 钱包/escrow/商店收口/共享池 |
|
||||
| R4 注释同步六处(非行为改动——注释文本,无运行时可观测面) | shop_session.gd:4/level_runner.gd:722–724/level_autoplay.gd:335/shop_overlay_view.gd:88/152/tree_rules_v11_test.gd:6 | 随批代码域闸绿(静态扫描覆盖文件头纪律面) | 静态 | —(静态判据) | 观测面局限申报:注释无运行时提交面,判据=静态扫描+评审注记 | 既有:GP 钱包/escrow/商店收口/共享池 |
|
||||
|
||||
|
||||
+649
-76
@@ -34,6 +34,13 @@ v3.0 口径(方向稿=目检先行闸,冻结归机检):
|
||||
输出 = artwork/mockups/sample-batch-v3-final-2026-09-30.png(v3.5 冻结口径的正式产物)
|
||||
+ 机检报告(中间产物,吸收即死不入 git;断言定值=A3b Δ≥30/A4 尺寸基线)。
|
||||
用法:python tools/render_sample_sheet_v3.py
|
||||
python tools/render_sample_sheet_v3.py --gen-assets (本批追加:产出 §5.2/§5.3 四件素材入包)
|
||||
python tools/render_sample_sheet_v3.py --save-baseline(本批追加:把当前整屏 mockup 存为改前基线)
|
||||
|
||||
迭代 43 扩展(2026-10-29 战斗一屏样张批):整屏样张两版出口(1280×800 基座 + 1920×1080 全局等比
|
||||
1.35×+左右黑边,§8.2 1920 档策略)+ art-bible §1.4 整屏四项断言(S1–S4)+ 战斗表现五项断言(P1–P5)
|
||||
+ S3b 图类型色板变体第二套 + S5 三组关键帧条 + S6 描边同屏对照与遮挡对照组 + V1 两版尺寸断言
|
||||
+ V10 改前/改后基线并排对拍;B 区高按代码现状 252 出样(art-bible §8.2 文 270 差异只登记不修)。
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont
|
||||
import os
|
||||
@@ -60,6 +67,10 @@ BOARD_DIR = ("game", "assets", "art", "board")
|
||||
CHAR_DIR = ("game", "assets", "art", "character")
|
||||
ICON_DIR = ("game", "assets", "ui", "icons")
|
||||
OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "sample-batch-v3-final-2026-09-30.png")
|
||||
OUT_ED1 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1280x800.png")
|
||||
OUT_ED2 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1920x1080.png")
|
||||
BASELINE_PNG = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_baseline_1280.png")
|
||||
OUT_BEFORE_AFTER = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_before_after.png")
|
||||
OUT_REPORT = os.path.join(ROOT, "reports", "sample-v3-final-机检报告-2026-09-30.md")
|
||||
T_FONT = asset_root("artwork", "ui-reuse-2026-09-23",
|
||||
"4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf")
|
||||
@@ -107,7 +118,7 @@ F_LBL2 = ImageFont.truetype(ZH_FONT, 12)
|
||||
F_TITLE = ImageFont.truetype(ZH_FONT, 19)
|
||||
F_STANDEE = ImageFont.truetype(ZH_FONT, 42) # 敌像字形占位
|
||||
F_NAME = ImageFont.truetype(ZH_FONT, 12)
|
||||
F_ORDER = ImageFont.truetype(ZH_FONT, 13)
|
||||
F_ORDER = ImageFont.truetype(ZH_FONT, 14) # 顺位徽标=UiKit.FS_HINT 14(§7.4 清单外 11–13px 提档;镜像实装)
|
||||
F_INTENT = ImageFont.truetype(ZH_FONT, 15)
|
||||
|
||||
CHECKS = [] # (id, name, ok, detail)
|
||||
@@ -729,21 +740,47 @@ def render_main_board():
|
||||
return board, placement, records, band, gmap
|
||||
|
||||
|
||||
# ══ B 区站桩(mockup 1:1) ══
|
||||
# ══ B 区站桩(mockup 1:1——镜像 standee_body.gd 实装定值:血条危险红/条底收口/徽标 FS_HINT/
|
||||
# 护甲 PNG/名字行素底不压地台/地台件九宫格拉伸;缺位回退与实装同构) ══
|
||||
_PLAZA_CACHE = {}
|
||||
_ARMOR_CACHE = {}
|
||||
|
||||
|
||||
def _plaza_tex():
|
||||
"""地台件 48×12(缺位回退 None——程序化平色镜像实装 fallback)。"""
|
||||
if "tex" not in _PLAZA_CACHE:
|
||||
try:
|
||||
_PLAZA_CACHE["tex"] = Image.open(asset_root(
|
||||
"game", "assets", "art", "scene", "scene_stage_plaza.png")).convert("RGBA")
|
||||
except FileNotFoundError:
|
||||
_PLAZA_CACHE["tex"] = None
|
||||
return _PLAZA_CACHE["tex"]
|
||||
|
||||
|
||||
def _armor_tex(armor):
|
||||
"""护甲图标 24×24(缺位回退 None——mockup 画字形,实装画字形占位同构)。"""
|
||||
if armor not in _ARMOR_CACHE:
|
||||
try:
|
||||
_ARMOR_CACHE[armor] = Image.open(asset_root(
|
||||
"game", "assets", "ui", "icons", "ui_armor_%s.png" % armor)).convert("RGBA")
|
||||
except FileNotFoundError:
|
||||
_ARMOR_CACHE[armor] = None
|
||||
return _ARMOR_CACHE[armor]
|
||||
|
||||
|
||||
def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light",
|
||||
hp=1.0, intent="", statuses=(), dead=False):
|
||||
"""站桩 150×176(standee_view.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。"""
|
||||
"""站桩 150×176(standee_body.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。"""
|
||||
d = ImageDraw.Draw(canvas)
|
||||
W, H = 150, 176
|
||||
# 头顶:顺位徽标(敌)+血条
|
||||
# 头顶:顺位徽标(敌,FS_HINT 14)+血条(填充=危险红 #A8321F 不因敌我分色;条底=HP_BAR_TRACK)
|
||||
tx = ox + 8
|
||||
if kind == "enemy":
|
||||
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20))
|
||||
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(224, 184, 96))
|
||||
tx += 18
|
||||
bw = W - 16 - (18 if kind == "enemy" else 0)
|
||||
d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(60, 52, 42))
|
||||
d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14],
|
||||
fill=(124, 196, 124) if kind == "hero" else (150, 60, 48))
|
||||
d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(26, 26, 28))
|
||||
d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14], fill=(168, 50, 31))
|
||||
# 状态图标行(血条正下方,16px 定稿 PNG)
|
||||
sx = ox + (W - 17 * max(1, len(statuses))) // 2
|
||||
for i, ic in enumerate(statuses):
|
||||
@@ -758,39 +795,103 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light",
|
||||
glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌")
|
||||
col = (150, 60, 48) if not dead else (110, 104, 98)
|
||||
d.text((ox + W // 2, pbox_y + 48), glyph, font=F_STANDEE, fill=col, anchor="mm")
|
||||
# 意图徽标(立绘上半部居中悬浮)
|
||||
# 意图徽标(立绘上半部居中悬浮;镜像实装:PANEL_DATA 素牌垫底——四色墨对牌 4.7–5.8:1)
|
||||
if intent:
|
||||
glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)),
|
||||
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent]
|
||||
glyph, col = {"damage": ("⚔", (150, 44, 36)), "buff": ("✦", (30, 90, 150)),
|
||||
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (116, 80, 11))}[intent]
|
||||
ts = d.textbbox((0, 0), glyph, font=F_INTENT)
|
||||
tw_, th_ = ts[2] - ts[0], ts[3] - ts[1]
|
||||
d.rectangle([ox + W // 2 - tw_ / 2 - 4, pbox_y + 30 - th_ * 0.75,
|
||||
ox + W // 2 + tw_ / 2 + 4, pbox_y + 30 + th_ * 0.35], fill=(233, 217, 184))
|
||||
d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm")
|
||||
# v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚)
|
||||
# v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚;地台=独立件九宫格横向拉伸)
|
||||
sh = Image.new("RGBA", (W, 30), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(sh).ellipse([28, 6, W - 28, 24], fill=(0, 0, 0, 70))
|
||||
canvas.alpha_composite(sh, (ox, oy + 146))
|
||||
d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 170], fill=(150, 130, 100), outline=(90, 76, 58))
|
||||
d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 160], fill=(196, 172, 132))
|
||||
# 敌名行:护甲类型图标+纯名字(AC 数值不展示)
|
||||
ped_w = W - 36
|
||||
ped_x, ped_y = ox + 18, oy + 158
|
||||
tex = _plaza_tex()
|
||||
if tex is not None:
|
||||
canvas.paste(tex.crop((0, 0, 4, 12)).resize((4, 12), Image.NEAREST), (ped_x, ped_y))
|
||||
canvas.paste(tex.crop((4, 0, 44, 12)).resize((ped_w - 8, 12), Image.NEAREST), (ped_x + 4, ped_y))
|
||||
canvas.paste(tex.crop((44, 0, 48, 12)).resize((4, 12), Image.NEAREST), (ped_x + ped_w - 4, ped_y))
|
||||
else:
|
||||
d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 12], fill=PLAZA_BODY, outline=PLAZA_EDGE)
|
||||
d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 2], fill=PLAZA_RIM)
|
||||
# 名字行:整行垫 HP_BAR_BACK 素底(牌底沿=地台顶——文字禁直接压地台 §6.1/§7.2;
|
||||
# 护甲图标=24×24 PNG 缺位回退字形;几何镜像 standee_body._name_plate_rect)
|
||||
if kind == "enemy":
|
||||
d.text((ox + 16, oy + 172), ARMOR_ICONS[armor], font=F_NAME, fill=ARMOR_COLORS[armor], anchor="lm")
|
||||
d.text((ox + 30, oy + 172), name, font=F_NAME, fill=(240, 226, 200), anchor="lm")
|
||||
itex = _armor_tex(armor)
|
||||
if itex is not None:
|
||||
icon_w, icon_draw = 24.0, ("tex", itex)
|
||||
else:
|
||||
gtxt = ARMOR_ICONS[armor]
|
||||
icon_w = d.textbbox((0, 0), gtxt, font=F_NAME)[2]
|
||||
icon_draw = ("glyph", gtxt, ARMOR_COLORS[armor])
|
||||
name_w = d.textbbox((0, 0), name, font=F_NAME)[2]
|
||||
plate_w = icon_w + 3.0 + name_w + 8.0
|
||||
px0 = ox + (W - plate_w) / 2
|
||||
py0 = oy + 158 - 26
|
||||
d.rectangle([px0, py0, px0 + plate_w, py0 + 26], fill=(40, 34, 32))
|
||||
base_x, base_y = px0 + 4, py0 + 17 # 基线=牌顶+17(镜像 _name_baseline;box.y-27 < 地台顶)
|
||||
if icon_draw[0] == "tex":
|
||||
canvas.paste(icon_draw[1].resize((24, 24), Image.NEAREST),
|
||||
(int(base_x), py0 + 1), icon_draw[1].resize((24, 24), Image.NEAREST))
|
||||
else:
|
||||
d.text((base_x, base_y), icon_draw[1], font=F_NAME, fill=icon_draw[2], anchor="ls")
|
||||
d.text((base_x + icon_w + 3.0, base_y), name, font=F_NAME, fill=(240, 226, 200), anchor="ls")
|
||||
if dead:
|
||||
gray = Image.new("RGBA", (W, H), (100, 100, 100, 120))
|
||||
canvas.alpha_composite(gray, (ox, oy))
|
||||
|
||||
|
||||
def render_battle_screen():
|
||||
"""B/C/D 三区全内容 1280×800 mockup。"""
|
||||
scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255))
|
||||
d = ImageDraw.Draw(scr)
|
||||
# ── B 区 252px 全宽(v3 舞台:世界底层暗背景+顶光柱氛围;文字=信息面层暖白) ──
|
||||
d.rectangle([0, 0, 1280, 252], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"])
|
||||
beam = Image.new("RGBA", (1280, 252), (0, 0, 0, 0))
|
||||
B_H = 252 # B 区高=代码现状(battle_stage_view custom_minimum_size 252;§8.2 文 270 差异只登记不修)
|
||||
CANVAS_W = 1280 # 基座档(art-bible §8.1)
|
||||
|
||||
|
||||
def draw_base(canvas_w=CANVAS_W, canvas_h=800, b_h=B_H):
|
||||
"""样张底板(T1):B/C/D 三区骨架——分区数值按 battle.md 指路、B 区按代码 252;格边长=CELL_D 45
|
||||
(D 区 10×10 板实比,§8.1)。返回 (img, zones)——zones 供 S/P 断言取样:
|
||||
b=(0,0,w,b_h) 含明细带;band=明细带 24px;c=(0,b_h+4,240,h);d=(244,b_h+4,w,h)。"""
|
||||
img = Image.new("RGBA", (canvas_w, canvas_h), (24, 20, 17, 255))
|
||||
d = ImageDraw.Draw(img)
|
||||
c_y = b_h + 4
|
||||
# B 区(舞台底=世界底层下沿子带 L 0.014–0.06;文字=信息面层暖白)
|
||||
d.rectangle([0, 0, canvas_w, b_h], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"])
|
||||
beam = Image.new("RGBA", (canvas_w, b_h), (0, 0, 0, 0))
|
||||
db = ImageDraw.Draw(beam)
|
||||
for bx0 in (110, 900, 1055, 1210):
|
||||
for i in range(60):
|
||||
a = int(26 * (1 - i / 60.0))
|
||||
db.rectangle([bx0 - 46 + i // 3, 0, bx0 + 46 - i // 3, 40 + i * 3], fill=(255, 236, 200, a))
|
||||
scr.alpha_composite(beam, (0, 0))
|
||||
img.alpha_composite(beam, (0, 0))
|
||||
# 明细带(挖掘期收起 24px)
|
||||
d.rectangle([0, b_h - 24, canvas_w, b_h], fill=(46, 39, 32))
|
||||
# C 区
|
||||
d.rectangle([0, c_y, 240, canvas_h], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
|
||||
# D 区
|
||||
d.rectangle([244, c_y, canvas_w, canvas_h], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"])
|
||||
# D 区顶行工具件(停手/图例/刷新+AP 点阵+RAP)
|
||||
d.rectangle([252, c_y + 6, 304, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((278, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([310, c_y + 6, 354, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((332, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([360, c_y + 6, 428, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((394, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
for i in range(6):
|
||||
col = (232, 200, 120) if i < 4 else (120, 106, 88)
|
||||
d.ellipse([440 + i * 18, c_y + 10, 452 + i * 18, c_y + 22], fill=col)
|
||||
d.text((552, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm")
|
||||
zones = {"b": (0, 0, canvas_w, b_h), "band": (0, b_h - 24, canvas_w, b_h),
|
||||
"c": (0, c_y, 240, canvas_h), "d": (244, c_y, canvas_w, canvas_h)}
|
||||
return img, zones
|
||||
|
||||
|
||||
def render_battle_screen(board_img=None):
|
||||
"""B/C/D 三区全内容 1280×800 mockup(T1 底板+T2 内容)。返回 (scr, zones, board_origin)。"""
|
||||
scr, zones = draw_base()
|
||||
d = ImageDraw.Draw(scr)
|
||||
b_h = B_H
|
||||
d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(240, 226, 200), anchor="mm")
|
||||
draw_standee(scr, 40, 34, "hero", "主角", "", hp=0.7, statuses=("attack_up", "ac_up"))
|
||||
d.text((560, 120), "兑现演出区\n(结算期:光带 → 弹道 → 受击)", font=ImageFont.truetype(ZH_FONT, 13),
|
||||
@@ -799,34 +900,28 @@ def render_battle_screen():
|
||||
draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge",
|
||||
statuses=("alert",))
|
||||
draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff")
|
||||
# 明细带(挖掘期收起 24px)
|
||||
d.rectangle([0, 228, 1280, 252], fill=(46, 39, 32))
|
||||
# 明细带摘要行 + 结算语境 hero 剪影 mini(P2 三语境之一;右端贴装不越缘)
|
||||
d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2,
|
||||
fill=(240, 226, 200), anchor="lm")
|
||||
# ── C 区 240×548 ──
|
||||
d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
|
||||
mini = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
|
||||
scr.alpha_composite(mini, (1236, b_h - 24))
|
||||
# ── C 区内容(次级文对齐 §7.2 ≥4.5:1——STONE_DEEP/GOLD_INK 在册墨) ──
|
||||
d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线)
|
||||
d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
|
||||
d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30))
|
||||
d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(116, 80, 11))
|
||||
d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
|
||||
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98))
|
||||
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(85, 77, 65))
|
||||
for i in range(4):
|
||||
d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1)
|
||||
d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(120, 110, 98))
|
||||
# ── D 区 ──
|
||||
d.rectangle([244, 256, 1280, 800], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"])
|
||||
# 顶行:停手/图例/刷新+AP 点阵+RAP
|
||||
d.rectangle([252, 262, 304, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((278, 273), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([310, 262, 354, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((332, 273), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([360, 262, 428, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((394, 273), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
for i in range(6):
|
||||
col = (232, 200, 120) if i < 4 else (120, 106, 88)
|
||||
d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col)
|
||||
d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm")
|
||||
return scr
|
||||
d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(85, 77, 65))
|
||||
# ── D 区主样张板(10×10@45px=450×450+悬崖 8)嵌进工具行之下居中 ──
|
||||
board_origin = None
|
||||
if board_img is not None:
|
||||
bx = 244 + (1036 - board_img.size[0]) // 2
|
||||
by = b_h + 4 + 34 + (800 - b_h - 4 - 34 - board_img.size[1]) // 2
|
||||
scr.paste(board_img.convert("RGB"), (bx, by), board_img)
|
||||
board_origin = (bx, by)
|
||||
return scr, zones, board_origin
|
||||
|
||||
|
||||
# ══ 机检 ══
|
||||
@@ -1033,6 +1128,211 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None):
|
||||
return all(c[2] for c in CHECKS)
|
||||
|
||||
|
||||
# ══ 整屏四项(S1–S4,art-bible §1.4 整屏样张闸)+战斗表现五项(P1–P5)+护甲对比锚(A7) ══
|
||||
def _grad_top_cells(scr, frac=100):
|
||||
"""整屏横向梯度图(步长 2)——首焦唯一性取样用,返回 [(ΔL, x, y)] 降序。"""
|
||||
rgbp = scr.convert("RGB").load()
|
||||
w, h = scr.size
|
||||
grad = []
|
||||
for y in range(0, h, 2):
|
||||
for x in range(0, w - 2, 2):
|
||||
c1, c2 = rgbp[x, y], rgbp[x + 2, y]
|
||||
l1 = 0.2126 * c1[0] + 0.7152 * c1[1] + 0.0722 * c1[2]
|
||||
l2 = 0.2126 * c2[0] + 0.7152 * c2[1] + 0.0722 * c2[2]
|
||||
grad.append((abs(l1 - l2), x, y))
|
||||
grad.sort(reverse=True)
|
||||
return grad[:max(1, len(grad) // frac)]
|
||||
|
||||
|
||||
def _mask_maxch_dist(img, box, tol=40):
|
||||
"""隔离剪影掩码:四角中值底色 + 三通道最大距离阈值(蓝夹克对暖底 maxΔ>40 可掩)。"""
|
||||
crop = img.crop(box).convert("RGB")
|
||||
w, h = crop.size
|
||||
px = crop.load()
|
||||
corners = [px[1, 1], px[w - 2, 1], px[1, h - 2], px[w - 2, h - 2]]
|
||||
bg = tuple(sorted(c[i] for c in corners)[1] for i in range(3))
|
||||
mask = []
|
||||
for yy in range(h):
|
||||
for xx in range(w):
|
||||
c = px[xx, yy]
|
||||
mask.append(1 if max(abs(c[i] - bg[i]) for i in range(3)) > tol else 0)
|
||||
return mask, (w, h)
|
||||
|
||||
|
||||
def _resample_mask(mask, size, tw=24, th=24):
|
||||
w, h = size
|
||||
return [mask[(ty * h // th) * w + (tx * w // tw)] for ty in range(th) for tx in range(tw)]
|
||||
|
||||
|
||||
def _mask_iou(a, b):
|
||||
inter = sum(1 for x, y in zip(a, b) if x and y)
|
||||
uni = sum(1 for x, y in zip(a, b) if x or y)
|
||||
return (inter / uni) if uni else 0.0
|
||||
|
||||
|
||||
def _region_wcag(img, box):
|
||||
"""区域 WCAG 相对亮度均值(0–1 线性域——§2 明度带口径;region_luma 是 0–255 加权域不可混用)。"""
|
||||
crop = img.crop(box).convert("RGB")
|
||||
px = crop.load()
|
||||
tot = n = 0
|
||||
for yy in range(0, crop.size[1], 2):
|
||||
for xx in range(0, crop.size[0], 2):
|
||||
tot += wcag_lum(px[xx, yy])
|
||||
n += 1
|
||||
return tot / max(1, n)
|
||||
|
||||
|
||||
def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta):
|
||||
"""整屏样张闸四项 + 战斗表现五项(战斗表现「一屏样张的验收标准」)+护甲图标挂载对比锚。"""
|
||||
# S1 整屏①全部文字对直接背景 ≥4.5:1(静态对表——对值即图内实际用色;大字档 3:1 由 A3a 承载)
|
||||
pairs = [("暖白×舞台底", (240, 226, 200), (62, 52, 42)), ("金亮徽标×舞台底", (224, 184, 96), (62, 52, 42)),
|
||||
("暖白×明细带底", (240, 226, 200), (46, 39, 32)), ("标题墨×C区板", (30, 26, 22), (232, 220, 196)),
|
||||
("暖白×名字行素底", (240, 226, 200), (40, 34, 32)), ("金深墨×C区板", (116, 80, 11), (232, 220, 196)),
|
||||
("深化石×C区板", (85, 77, 65), (232, 220, 196)), ("暖白×工具钮", (232, 222, 200), (90, 80, 66))]
|
||||
ratios = [(n, round((max(wcag_lum(a), wcag_lum(b)) + 0.05) / (min(wcag_lum(a), wcag_lum(b)) + 0.05), 2))
|
||||
for n, a, b in pairs]
|
||||
weakest = min(ratios, key=lambda r: r[1])
|
||||
check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios),
|
||||
"最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios))
|
||||
# S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域)
|
||||
stage_l = _region_wcag(scr, (300, 190, 600, 220))
|
||||
bx, by = board_origin
|
||||
board_span = 10 * CELL_D + 8
|
||||
floor_l = _region_wcag(scr, (bx + 2 * CELL_D, by + 3 * CELL_D + 10, bx + 4 * CELL_D, by + 4 * CELL_D - 10))
|
||||
info_l = wcag_lum(INK_L_GOLD)
|
||||
mid_l = wcag_lum(PLAZA_BODY)
|
||||
ok_s2 = (info_l > mid_l > stage_l and 0.08 <= floor_l <= 0.14 and 0.014 <= stage_l <= 0.06
|
||||
and 0.22 <= mid_l <= 0.45 and (floor_l - stage_l) >= 0.02)
|
||||
check("S2", "整屏②明度三档带成立(信息>世界中层>世界底层)+世界底层两子带(0.014–0.06/0.08–0.14,间隔≥0.02)",
|
||||
ok_s2, "信息 %.2f/中(地台) %.2f/底层 舞台 %.3f+格底 %.3f/子带间隔 %.3f"
|
||||
% (info_l, mid_l, stage_l, floor_l, floor_l - stage_l))
|
||||
# S3 整屏③首焦唯一(§3.4 整屏延伸:高对比内容量集中单区 ≥55%——挖掘期第一注视点=棋盘区
|
||||
# 〔battle.md 信息层级〕;候选区=数字区/B 区立绘,C 区规划面与工具行抢焦即违)
|
||||
top = _grad_top_cells(scr, 10)
|
||||
bx, by = board_origin
|
||||
board_span = 10 * CELL_D + 8
|
||||
b_rect = zones["b"]
|
||||
c_rect = zones["c"]
|
||||
e_board = e_b = e_c = e_tool = 0
|
||||
for g, x, y in top:
|
||||
if bx <= x < bx + board_span and by <= y < by + board_span:
|
||||
e_board += g
|
||||
elif b_rect[0] <= x < b_rect[2] and b_rect[1] <= y < b_rect[3]:
|
||||
e_b += g
|
||||
elif c_rect[0] <= x < c_rect[2] and c_rect[1] <= y < c_rect[3]:
|
||||
e_c += g
|
||||
elif zones["d"][1] <= y < zones["d"][1] + 30:
|
||||
e_tool += g
|
||||
total = e_board + e_b + e_c + e_tool
|
||||
shares = {"数字区": e_board / max(1, total), "B 区": e_b / max(1, total),
|
||||
"C 区": e_c / max(1, total), "工具行": e_tool / max(1, total)}
|
||||
winner = max(shares, key=shares.get)
|
||||
check("S3", "整屏③首焦唯一(高对比内容量集中单区 ≥55%,无第二焦点竞争;候选=数字区/B 区立绘)",
|
||||
shares[winner] >= 0.55 and winner in ("数字区", "B 区"),
|
||||
"能量占比 %s(首焦=%s)" % ({k: round(v, 2) for k, v in shares.items()}, winner))
|
||||
# S4 整屏④文字禁直接压纹理底(垫容器/罩层):素底牌顶右上角无字形区平底 + 全牌主色占比
|
||||
# + 牌区零地台板身色(§6.1/§7.2 同文断言的样张侧)+明细带平底
|
||||
d_ = ImageDraw.Draw(Image.new("RGB", (1, 1)))
|
||||
name_w = d_.textbbox((0, 0), "地精", font=F_NAME)[2]
|
||||
plate_w = 24 + 3 + name_w + 8
|
||||
px0 = 818 + (150 - plate_w) / 2
|
||||
py0 = 34 + 132
|
||||
plate_crop = scr.convert("RGB").crop((int(px0), int(py0), int(px0 + plate_w), int(py0) + 26))
|
||||
all_px = list(plate_crop.getdata())
|
||||
from collections import Counter as _Counter
|
||||
main_share = _Counter(all_px).most_common(1)[0]
|
||||
flat_crop = plate_crop.crop((plate_crop.size[0] - 10, 1, plate_crop.size[0] - 2, 7))
|
||||
flat_px = list(flat_crop.getdata())
|
||||
flat_plate = sum(1 for c in flat_px if c == (40, 34, 32)) / max(1, len(flat_px))
|
||||
rgbp = scr.convert("RGB").load()
|
||||
plinth_hit = sum(1 for yy in range(int(py0), int(py0) + 26) for xx in range(int(px0), int(px0 + plate_w))
|
||||
if rgbp[xx, yy][:3] == PLAZA_BODY)
|
||||
band_flat = region_luma(scr, (700, 232, 1100, 246))
|
||||
band_expect = sum(w * c for w, c in zip((0.2126, 0.7152, 0.0722), (46, 39, 32)))
|
||||
check("S4", "整屏④文字禁直接压纹理底(名字行=素底牌平底+牌区零地台板身色+明细带平底)",
|
||||
flat_plate >= 0.95 and main_share[0] == (40, 34, 32) and main_share[1] >= len(all_px) * 0.6
|
||||
and plinth_hit == 0 and abs(band_flat - band_expect) < 3,
|
||||
"牌顶角平底 %.0f%%;主色 %s 占 %.0f%%;牌区地台色命中 %d;明细带 L %.2f"
|
||||
% (flat_plate * 100, main_share[0], main_share[1] * 100.0 / len(all_px), plinth_hit, band_flat))
|
||||
# P1 战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;数字对比硬指标由 A3a 承载。
|
||||
# 签名表不含 CONTACT_GLOW——其色与数字冷亮墨 INK_L_COLD 差 <24 不可色分,空间约束由 F5a 承载)
|
||||
sigs = list(GLOW_GROUPS.values()) + [DANGER_RED]
|
||||
viol = []
|
||||
for (dx_, dy_), dg in DIGITS.items():
|
||||
if dg <= 0:
|
||||
continue
|
||||
x0, y0 = bx + dx_ * CELL_D, by + dy_ * CELL_D
|
||||
m = int(CELL_D * 0.2)
|
||||
for yy in range(y0 + m, y0 + CELL_D - m, 2):
|
||||
for xx in range(x0 + m, x0 + CELL_D - m, 2):
|
||||
c = rgbp[xx, yy][:3]
|
||||
if any(all(abs(c[i] - s_[i]) <= 24 for i in range(3)) for s_ in sigs):
|
||||
viol.append(((dx_, dy_), c))
|
||||
break
|
||||
check("P1", "战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;挖掘期抢焦禁令由 S3 首焦承载)",
|
||||
not viol, "违例 %s" % viol if viol else "%d 个数字格砖心全净" % len(DIGITS))
|
||||
# P2 战斗表现②剪影跨舞台通用(灰度轮廓通道——S6 三语境隔离剪影两两 IoU ≥0.35)
|
||||
masks = [_mask_maxch_dist(s6_img, box) for box in s6_meta["hero_boxes"]]
|
||||
rm = [_resample_mask(m, s) for m, s in masks]
|
||||
ious = [round(_mask_iou(rm[0], rm[1]), 2), round(_mask_iou(rm[0], rm[2]), 2), round(_mask_iou(rm[1], rm[2]), 2)]
|
||||
check("P2", "战斗表现②剪影跨舞台通用(灰度三语境两两 IoU ≥0.35——识别只靠轮廓通道)",
|
||||
min(ious) >= 0.35,
|
||||
"舞台×棋盘 %s/舞台×明细 %s/棋盘×明细 %s" % tuple(ious))
|
||||
# P3 战斗表现③动画方案可读(三组关键帧 ≥3 帧、相邻帧互异、退场末帧=灰化残位非瞬消)
|
||||
fw, fh = frames_meta["frame_size"]
|
||||
distinct, death_ok, death_note = True, True, ""
|
||||
for key, (xs, y0) in frames_meta["strips"].items():
|
||||
crops = [frames_strip.crop((x, y0, x + fw, y0 + fh)).convert("RGB") for x in xs]
|
||||
for c1, c2 in zip(crops, crops[1:]):
|
||||
if list(c1.getdata()) == list(c2.getdata()):
|
||||
distinct = False
|
||||
if key == "death":
|
||||
sat = list(crops[2].convert("HSV").split()[1].getdata())
|
||||
ink = sum(1 for p in crops[2].getdata() if p != (34, 29, 24))
|
||||
death_ok = max(sat) <= 90 and ink > 200
|
||||
death_note = "末帧最大饱和 %d/残位像素 %d" % (max(sat), ink)
|
||||
check("P3", "战斗表现③动画方案可读(三组关键帧条各 3 帧+相邻帧互异+退场末帧灰化残位禁瞬消)",
|
||||
distinct and death_ok, death_note or "三组帧互异")
|
||||
# P4 战斗表现④战斗空间成立(双舞台结构边界在位:C/D 顶缘描边连续+区间净缝+明细带在位)
|
||||
c_y = zones["c"][1]
|
||||
|
||||
def line_hit(y, x0, x1, col, tol=30):
|
||||
n = hits = 0
|
||||
for x in range(x0, x1, 4):
|
||||
n += 1
|
||||
if all(abs(rgbp[x, y][i] - col[i]) <= tol for i in range(3)):
|
||||
hits += 1
|
||||
return hits / max(1, n)
|
||||
|
||||
ok_p4 = (line_hit(c_y, 246, 1276, UIKIT["PANEL_BORDER"]) >= 0.9
|
||||
and line_hit(c_y, 2, 238, UIKIT["PANEL_BORDER"]) >= 0.9
|
||||
and all(rgbp[x, B_H + 2][:3] == (24, 20, 17) for x in range(250, 1270, 60))
|
||||
and line_hit(B_H - 2, 250, 1270, (46, 39, 32)) >= 0.9)
|
||||
check("P4", "战斗表现④战斗空间成立(双舞台边界线在位+区间净缝;视线锚归 S3 首焦唯一)", ok_p4,
|
||||
"C/D 顶缘描边与净缝读数见 detail")
|
||||
# P5 战斗表现⑤敌层插入位置可裁(遮挡对照:顶层 数字/印记/小人 可辨、敌像垫底可辨)
|
||||
ox0, oy0, ox1, oy1 = s6_meta["overlap"]
|
||||
crop = s6_img.crop((ox0, oy0, ox1, oy1)).convert("RGB")
|
||||
cpx = crop.load()
|
||||
|
||||
def count_near(col, tol=28):
|
||||
return sum(1 for yy in range(crop.size[1]) for xx in range(crop.size[0])
|
||||
if all(abs(cpx[xx, yy][i] - col[i]) <= tol for i in range(3)))
|
||||
|
||||
cnt = {"数字墨": count_near(INK_L_WARM), "封印黑": count_near(SEAL_BLACK, 12),
|
||||
"小人蓝": count_near(HERO_BLUE), "敌像灰": count_near((110, 104, 98), 14)}
|
||||
ok_p5 = cnt["数字墨"] >= 25 and cnt["封印黑"] >= 8 and cnt["小人蓝"] >= 20 and cnt["敌像灰"] >= 10
|
||||
check("P5", "战斗表现⑤敌层插入位置可裁(遮挡对照组:数字>小人>旗>敌,顶层三件可辨计数达标)", ok_p5,
|
||||
"计数 %s" % cnt)
|
||||
# A7 护甲三枚对挂载素底 ≥3:1(§5.3 共用验收——色值=产出件常量,尺寸/装载由 V8/V9 单测承载)
|
||||
a_mount = wcag_lum((40, 34, 32))
|
||||
a_ratios = [(k, round((max(wcag_lum(c), a_mount) + 0.05) / (min(wcag_lum(c), a_mount) + 0.05), 2))
|
||||
for k, c in [("轻甲", ARMOR_LIGHT_COL), ("魔甲", ARMOR_WARD_COL), ("重甲", ARMOR_HEAVY_COL)]]
|
||||
check("A7", "护甲三枚对挂载底色(名字行素底 #282220)图形对比 ≥3:1(§5.3 共用验收)",
|
||||
all(r[1] >= 3.0 for r in a_ratios), " %s" % a_ratios)
|
||||
return all(c[2] for c in CHECKS)
|
||||
|
||||
|
||||
def cg_board_region(board, x0, y0, c):
|
||||
return board.crop((x0, y0, x0 + c, y0 + c)).convert("RGB")
|
||||
|
||||
@@ -1090,26 +1390,263 @@ def render_s3(cell=64):
|
||||
return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
|
||||
|
||||
|
||||
def render_s3b(cell=64):
|
||||
"""S3b 图类型色板变体第二套(§1.4 内容清单「至少两套」的第三套槽位;§4.3 四槽换色示意——
|
||||
deep_underdark 方向候选:灰黑主调+矿脉金属,§6.3 终值未冻结本套为方向示意)。信息层恒定:
|
||||
数字亮墨在换色后重画;微光取与 S3 不同组(blast 覆盖另一组互辨)。"""
|
||||
rows = ["WWWW", "WFFW", "WFWF"]
|
||||
gmap = GMap(rows)
|
||||
board, placement, glow = render_board(gmap, cell, overlays=False)
|
||||
gd = Image.new("RGBA", board.size, (0, 0, 0, 0))
|
||||
draw_type_glow(gd, 1 * cell, 2 * cell, cell, "blast")
|
||||
board.alpha_composite(gd)
|
||||
board = draw_slab_cliff(board, cols=gmap.w)
|
||||
h, s, v = board.convert("HSV").split()
|
||||
h = h.point(lambda _: 12)
|
||||
s = s.point(lambda x: min(255, int(10 + x * 0.30)))
|
||||
v = v.point(lambda x: int(x * 0.90))
|
||||
out = Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
|
||||
draw_digit_direct(out, 2 * cell, 1 * cell, cell, 5, F_DIGIT) # 信息层恒定——换色后重画
|
||||
return out
|
||||
|
||||
|
||||
def render_keyframes():
|
||||
"""S5 三组关键帧条(战斗表现「一屏样张的验收标准」内容清单:受击/退场/高光兑现,
|
||||
每组至少起中末三帧;退场末帧=灰化残位——「死亡禁止瞬消」机器代理;每组标注减动效简化形态)。
|
||||
返回 (strip, meta)——meta[key]=(行 x 列表, 行 y0),供 P3 断言按 bbox 取样。"""
|
||||
fw, fh, gap = 110, 116, 16
|
||||
labels = [("hit", "受击:常态→闪白+火花→回位(减动效=时长减半)"),
|
||||
("death", "退场:常态→灰化+碎块→塌陷残位(减动效=直达残位;禁瞬消)"),
|
||||
("highlight", "高光兑现:行→金框强调→定格(减动效=直达终态)")]
|
||||
row_h = fh + 30
|
||||
strip = Image.new("RGBA", ((fw + gap) * 3 + gap, row_h * 3 + 6), (24, 20, 17, 255))
|
||||
d = ImageDraw.Draw(strip)
|
||||
meta = {"frame_size": (fw, fh), "strips": {}}
|
||||
def base_frame(sx, sy, col_body, flash=0, sparks=0, shards=(), sink=0, alpha=255, gold=0):
|
||||
x0, y0 = sx, sy + 4
|
||||
d.rectangle([x0, y0, x0 + fw, y0 + fh], fill=(34, 29, 24), outline=(120, 106, 88))
|
||||
cy = y0 + 58 + sink
|
||||
if gold == 2:
|
||||
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], fill=(240, 168, 40), outline=(224, 184, 96), width=2)
|
||||
elif gold == 1:
|
||||
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(224, 184, 96), width=2)
|
||||
else:
|
||||
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(120, 106, 88), width=1)
|
||||
d.text((x0 + 10, y0 + fh - 20), "伤害 4 → 地精", font=F_LBL2, fill=(240, 226, 200))
|
||||
col = (col_body[0], col_body[1], col_body[2], alpha)
|
||||
d.text((x0 + fw // 2, cy), "▆", font=F_STANDEE, fill=col, anchor="mm")
|
||||
for k, (ox_, oy_) in enumerate(shards):
|
||||
d.rectangle([x0 + fw // 2 + ox_, cy + oy_, x0 + fw // 2 + ox_ + 6, cy + oy_ + 4],
|
||||
fill=(110, 104, 98, alpha))
|
||||
if sparks:
|
||||
for k in range(sparks):
|
||||
ang = 6.283 * k / sparks
|
||||
d.ellipse([x0 + fw // 2 + 26 * math.cos(ang) - 2, cy - 14 * math.sin(ang) - 2,
|
||||
x0 + fw // 2 + 26 * math.cos(ang) + 2, cy - 14 * math.sin(ang) + 2],
|
||||
fill=(230, 190, 90))
|
||||
if flash:
|
||||
d.rectangle([x0 + 1, y0 + 1, x0 + fw - 1, y0 + fh - 1], fill=(255, 255, 255, flash))
|
||||
xs = [gap + i * (fw + gap) for i in range(3)]
|
||||
for gi, (key, label) in enumerate(labels):
|
||||
y0 = 4 + gi * row_h
|
||||
meta["strips"][key] = (list(xs), y0)
|
||||
if key == "hit":
|
||||
base_frame(xs[0], y0, (150, 60, 48))
|
||||
base_frame(xs[1], y0, (150, 60, 48), flash=120, sparks=6)
|
||||
base_frame(xs[2], y0, (150, 60, 48))
|
||||
elif key == "death":
|
||||
base_frame(xs[0], y0, (150, 60, 48))
|
||||
base_frame(xs[1], y0, (110, 104, 98), shards=[(-18, 6), (14, 10), (-6, 14)], alpha=200)
|
||||
base_frame(xs[2], y0, (110, 104, 98), sink=6,
|
||||
shards=[(-26, 18), (20, 22), (-10, 26), (8, 20), (-2, 30)], alpha=150)
|
||||
else:
|
||||
base_frame(xs[0], y0, (150, 60, 48), gold=0)
|
||||
base_frame(xs[1], y0, (150, 60, 48), gold=1)
|
||||
base_frame(xs[2], y0, (240, 168, 40), gold=2)
|
||||
d.text((xs[2] + fw + gap + 4, y0 + 46), label, font=F_LBL2, fill=(225, 215, 195))
|
||||
return strip, meta
|
||||
|
||||
|
||||
def _outline_underlay(src, outline_rgb):
|
||||
"""alpha 边缘 1px 外扩描边垫层(主角深墨描边/敌方暖白描边共用)。"""
|
||||
a = src.split()[3]
|
||||
dil = a.filter(ImageFilter.MaxFilter(3))
|
||||
edge = ImageChops.subtract(dil, a)
|
||||
under = Image.new("RGBA", src.size, outline_rgb + (255,))
|
||||
under.putalpha(edge)
|
||||
return under
|
||||
|
||||
|
||||
def render_outline_overlap():
|
||||
"""S6 剪影跨舞台三语境(P2 灰度取样,隔离平底免污染)+描边同屏对照(主角深墨 (40,19,13)/
|
||||
敌方暖白族——art-prompts §5.4 描边条款)+护甲三枚展示+遮挡对照组(P5——敌人姿态精灵与
|
||||
数字/封印印记/小人同格叠放,绘制优先级 数字>小人>旗>敌)。返回 (img, meta)。"""
|
||||
W, H = 940, 190
|
||||
img = Image.new("RGBA", (W, H), (24, 20, 17, 255))
|
||||
d = ImageDraw.Draw(img)
|
||||
meta = {"hero_boxes": [], "overlap": None, "armor": {}}
|
||||
# ① 战斗舞台语境:主角立绘 96px + 深墨描边
|
||||
por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST)
|
||||
img.alpha_composite(_outline_underlay(por, (40, 19, 13)), (18, 14))
|
||||
img.alpha_composite(por, (18, 14))
|
||||
meta["hero_boxes"].append((18, 14, 18 + 96, 14 + 96))
|
||||
d.text((18, 116), "主角·战斗舞台语境\n描边=深墨 (40,19,13)", font=F_LBL2, fill=(225, 215, 195))
|
||||
# ② 棋盘格语境:小人 48px 压已开格底
|
||||
d.rectangle([150, 14, 214, 78], fill=(24, 20, 16))
|
||||
fl = FLOORS[0].resize((64, 64), Image.NEAREST)
|
||||
img.paste(fl.convert("RGB"), (150, 14))
|
||||
h48 = HERO.resize((48, int(48 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
|
||||
img.alpha_composite(h48, (150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8))
|
||||
meta["hero_boxes"].append((150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8,
|
||||
150 + (64 - h48.size[0]) // 2 + h48.size[0], 14 + 64 - 8))
|
||||
d.text((150, 116), "棋盘格语境\n(隔离已开格底)", font=F_LBL2, fill=(225, 215, 195))
|
||||
# ③ 结算明细带语境:小人 24px 压明细带底
|
||||
d.rectangle([238, 14, 302, 78], fill=(46, 39, 32))
|
||||
h24 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
|
||||
img.alpha_composite(h24, (238 + 8, 14 + 52 - h24.size[1]))
|
||||
meta["hero_boxes"].append((238 + 8, 14 + 52 - h24.size[1], 238 + 8 + h24.size[0], 14 + 52))
|
||||
d.text((238, 116), "结算明细带语境\n(同剪影 24px)", font=F_LBL2, fill=(225, 215, 195))
|
||||
# ④ 敌方描边对照:字形多向偏移暖白垫底+本体深朱
|
||||
for ddx, ddy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||
d.text((350 + 32 + ddx, 14 + 32 + ddy), "█", font=F_STANDEE, fill=(240, 226, 200), anchor="mm")
|
||||
d.text((350 + 32, 14 + 32), "█", font=F_STANDEE, fill=(150, 60, 48), anchor="mm")
|
||||
d.text((350, 116), "敌方描边=暖白族\n(非黑非深褐)", font=F_LBL2, fill=(225, 215, 195))
|
||||
# ⑤ 护甲三枚展示(源 24×24 挂 HP_BAR_BACK 素底——工单 §5.3 挂载底色口径)
|
||||
for i, key in enumerate(["light", "ward", "heavy"]):
|
||||
ax = 470 + i * 60
|
||||
d.rectangle([ax, 14, ax + 40, 54], fill=(40, 34, 32))
|
||||
tex = _armor_tex(key)
|
||||
if tex is not None:
|
||||
img.paste(tex, (ax + 8, 22), tex)
|
||||
meta["armor"][key] = (ax + 8, 22, ax + 8 + 24, 22 + 24)
|
||||
d.text((ax + 4, 58), {"light": "轻甲", "ward": "魔甲", "heavy": "重甲"}[key], font=F_LBL2, fill=(225, 215, 195))
|
||||
d.text((470, 116), "护甲三枚 24×24\n挂载底=名字行素底", font=F_LBL2, fill=(225, 215, 195))
|
||||
# ⑥ 遮挡对照组:敌像(最底)→旗→小人+蓝框→数字(最顶)——P5 断言:顶层三件仍可辨
|
||||
cx, cy0 = 700, 10
|
||||
d.rectangle([cx, cy0, cx + 68, cy0 + 68], fill=(24, 20, 16))
|
||||
img.paste(FLOORS[1].resize((64, 64), Image.NEAREST).convert("RGB"), (cx + 2, cy0 + 2))
|
||||
d.text((cx + 34, cy0 + 30), "▆", font=ImageFont.truetype(ZH_FONT, 30), fill=(110, 104, 98), anchor="mm")
|
||||
px_, py_ = cx + 30, cy0 + 12
|
||||
d.line([(px_, py_), (px_, cy0 + 52)], fill=(222, 210, 186), width=2)
|
||||
d.polygon([(px_, py_), (px_ + 14, py_ + 4), (px_, py_ + 9)], fill=SEAL_BLACK)
|
||||
h28 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
|
||||
img.alpha_composite(h28, (cx + 36, cy0 + 40 - h28.size[1]))
|
||||
d.rectangle([cx + 3, cy0 + 3, cx + 65, cy0 + 65], outline=HERO_BLUE, width=2)
|
||||
d.text((cx + 14, cy0 + 46), "6", font=ImageFont.truetype(ZH_FONT, 24), fill=INK_L_WARM)
|
||||
meta["overlap"] = (cx, cy0, cx + 68, cy0 + 68)
|
||||
d.text((cx, cy0 + 76), "遮挡对照组:数字>小人>旗>敌\n(敌像最底,顶层三件可辨)", font=F_LBL2, fill=(225, 215, 195))
|
||||
return img, meta
|
||||
|
||||
|
||||
# ══ 素材程序化产出(--gen-assets;art-prompts §5.2/§5.3 工单,色值全取 v3.22 在册值零新增) ══
|
||||
PLAZA_EDGE = (76, 64, 56) # 描边 #4C4038(§4.1 换色合并登记目标值)
|
||||
PLAZA_RIM = (201, 175, 131) # 顶缘受光线 #C9AF83(掩尘同枚——合并登记)
|
||||
PLAZA_BODY = (150, 130, 100) # 板身 #968264(STAND_STONE)
|
||||
ARMOR_INK = (51, 38, 27) # 图标深墨描边 #33261B(主墨)
|
||||
ARMOR_LIGHT_COL = (205, 184, 140) # 轻甲 #CDB88C(WARM_FAINT——换色合并⑤)
|
||||
ARMOR_WARD_COL = (240, 226, 200) # 魔法护甲 #F0E2C8(暖白)
|
||||
ARMOR_HEAVY_COL = (134, 140, 154) # 重甲 #868C9A(ARMOR_PLATE_COLD)
|
||||
|
||||
|
||||
def _ink_outline_polygon(d, pts, fill, width=3):
|
||||
"""深墨描边画法:先画 width+2 的墨线再画 width 的面色(=1px 墨缘,工单「1px 深墨描边」)。"""
|
||||
d.line(pts, fill=ARMOR_INK, width=width + 2)
|
||||
d.line(pts, fill=fill, width=width)
|
||||
|
||||
|
||||
def gen_scene_plaza():
|
||||
"""地台件 48×12(§5.2 工艺要点:1px 上描边+2px 顶缘受光线+8px 板身+1px 下描边;左右 1px 全周闭合)。"""
|
||||
im = Image.new("RGBA", (48, 12), PLAZA_BODY + (255,))
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle([0, 0, 47, 11], outline=PLAZA_EDGE + (255,), width=1) # 全周闭合描边
|
||||
d.rectangle([1, 1, 46, 2], fill=PLAZA_RIM + (255,)) # 顶缘受光线 2px(纵向不缩放区)
|
||||
return im
|
||||
|
||||
|
||||
def _armor_canvas():
|
||||
return Image.new("RGBA", (24, 24), (0, 0, 0, 0))
|
||||
|
||||
|
||||
def gen_armor_light():
|
||||
"""轻甲 = 皮条/布带记号(斜跨皮带+扣件,甲面覆盖率≈0,主体留空——§5.3.1;3px 带宽 1px 墨描边)。"""
|
||||
im = _armor_canvas()
|
||||
d = ImageDraw.Draw(im)
|
||||
_ink_outline_polygon(d, [(5, 18), (18, 6)], ARMOR_LIGHT_COL, width=3) # 斜跨皮带
|
||||
d.rectangle([9, 9, 14, 14], outline=ARMOR_INK + (255,), width=1) # 皮带扣
|
||||
d.rectangle([10, 10, 13, 13], fill=ARMOR_LIGHT_COL + (255,))
|
||||
return im
|
||||
|
||||
|
||||
def gen_armor_ward():
|
||||
"""魔法护甲 = 符纹饰带(横向符纹带+两枚 ≤3×3 符点,零金属甲板——§5.3.2)。"""
|
||||
im = _armor_canvas()
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle([4, 10, 19, 14], fill=ARMOR_INK + (255,))
|
||||
d.rectangle([4, 11, 19, 13], fill=ARMOR_WARD_COL + (255,)) # 符纹带
|
||||
d.rectangle([8, 17, 10, 19], fill=ARMOR_WARD_COL + (255,)) # 符点一(3×3)
|
||||
d.rectangle([13, 4, 15, 6], fill=ARMOR_WARD_COL + (255,)) # 符点二(3×3)
|
||||
return im
|
||||
|
||||
|
||||
def gen_armor_heavy():
|
||||
"""重甲 = 金属甲板覆盖肩与胸(肩两块+胸一块,覆盖率三档最大——§5.3.3)。"""
|
||||
im = _armor_canvas()
|
||||
d = ImageDraw.Draw(im)
|
||||
for box in [(3, 3, 10, 9), (13, 3, 20, 9), (7, 12, 16, 21)]: # 肩×2+胸×1
|
||||
d.rectangle(list(box), fill=ARMOR_INK + (255,))
|
||||
d.rectangle([box[0] + 1, box[1] + 1, box[2] - 1, box[3] - 1], fill=ARMOR_HEAVY_COL + (255,))
|
||||
return im
|
||||
|
||||
|
||||
def gen_batch_assets():
|
||||
"""产出四件素材并入包:地台件 scene/ + 护甲三枚 ui/icons/(§5.2/§5.3——程序化直出即终稿)。"""
|
||||
out = []
|
||||
plaza_path = os.path.join(ROOT, "game", "assets", "art", "scene", "scene_stage_plaza.png")
|
||||
os.makedirs(os.path.dirname(plaza_path), exist_ok=True)
|
||||
im = gen_scene_plaza()
|
||||
im.save(plaza_path)
|
||||
out.append((plaza_path, im.size, 3))
|
||||
for key, fn in [("light", gen_armor_light), ("ward", gen_armor_ward), ("heavy", gen_armor_heavy)]:
|
||||
p = os.path.join(ROOT, "game", "assets", "ui", "icons", "ui_armor_%s.png" % key)
|
||||
im = fn()
|
||||
im.save(p)
|
||||
out.append((p, im.size, 2))
|
||||
for p, size, ncol in out:
|
||||
colors = len({c for c in Image.open(p).convert("RGBA").getdata() if c[3] > 0})
|
||||
print("[gen] %s size=%s 不透明像素域色数=%d" % (p, size, colors))
|
||||
if size not in ((48, 12), (24, 24)) or colors > 32:
|
||||
raise RuntimeError("素材产出校验失败: %s" % p)
|
||||
print("[gen] 四件素材产出完成(§5.2/§5.3 工单口径)")
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
if "--gen-assets" in sys.argv:
|
||||
gen_batch_assets()
|
||||
return 0
|
||||
print("=== 样张 v3 方向稿生成 + 机检(参考) ===")
|
||||
board, placement, records, band, gmap = render_main_board()
|
||||
canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255))
|
||||
canvas = Image.new("RGBA", (1560, 2080), (24, 20, 17, 255))
|
||||
ok = run_checks(board, placement, records, band, gmap, canvas)
|
||||
|
||||
d = ImageDraw.Draw(canvas)
|
||||
d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0)——格底分档+亮墨直画+色牌后备+B区看台+E格高墙立面(v3.1);机检 %s(方向稿参考,冻结归目检后机检批);巢穴纹=程序占位" %
|
||||
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
|
||||
# 三区 mockup(T1 底板 + T2 内容;板面在 render_battle_screen 内嵌装)
|
||||
scr, zones, board_origin = render_battle_screen(board)
|
||||
if "--save-baseline" in sys.argv:
|
||||
os.makedirs(os.path.dirname(BASELINE_PNG), exist_ok=True)
|
||||
scr.convert("RGB").save(BASELINE_PNG)
|
||||
print("saved(baseline):", BASELINE_PNG)
|
||||
return 0
|
||||
frames_strip, frames_meta = render_keyframes()
|
||||
s6_img, s6_meta = render_outline_overlap()
|
||||
run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta)
|
||||
|
||||
# 三区 mockup
|
||||
scr = render_battle_screen()
|
||||
# D 区主样张板(10×10@45px=450×450)嵌进 mockup D 区(工具行之下居中)
|
||||
bx = 244 + (1036 - board.size[0]) // 2
|
||||
by = 256 + 34 + (800 - 256 - 34 - board.size[1]) // 2
|
||||
scr.paste(board.convert("RGB"), (bx, by), board)
|
||||
d = ImageDraw.Draw(canvas)
|
||||
d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0;迭代 43 扩展:整屏两版+S1–S4/P1–P5 断言)——机检 %s(方向稿参考,冻结归目检后机检批)" %
|
||||
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
|
||||
canvas.paste(scr, (20, 52))
|
||||
d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)",
|
||||
d.text((20, 866), "三区 mockup(1280×800 实比,B 区按代码 252):B 区站桩(主角立绘 96 朝右/敌字形占位/顺位 FS_HINT/意图素牌/血条=危险红/名行=素底牌+护甲 PNG)",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/封印印记/危险格巢穴占位)",
|
||||
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光/悬停金框/封印印记/危险格巢穴占位)",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
|
||||
# S2 数字全档
|
||||
@@ -1121,11 +1658,15 @@ def main():
|
||||
s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST)
|
||||
canvas.paste(s2h, (20, oy2 + 76))
|
||||
|
||||
# S3 underdark
|
||||
# S3 / S3b 图类型色板变体两套(§1.4 内容清单:至少两套)
|
||||
oy3 = oy2 + 76 + 60
|
||||
s3 = render_s3()
|
||||
canvas.paste(s3, (20, oy3))
|
||||
d.text((20 + s3.size[0] + 14, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2",
|
||||
s3b = render_s3b()
|
||||
canvas.paste(s3b, (320, oy3))
|
||||
d.text((630, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
d.text((630, oy3 + 78), "S3b 变体第二套(deep_underdark 方向候选:灰黑+矿脉金属;\n§6.3 终值未冻结,方向示意)——两套并陈满足 §1.4「至少两套」",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
|
||||
# S4 图例
|
||||
@@ -1134,40 +1675,72 @@ def main():
|
||||
for name, rgb in [("封印黑", SEAL_BLACK), ("危险红", DANGER_RED), ("奖励金", REWARD_GOLD),
|
||||
("小人蓝(候选)", HERO_BLUE), ("接触带微光(候选)", CONTACT_GLOW)]:
|
||||
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
|
||||
d.text((x - 4, oy4 + 38), name, col=(225, 215, 195), font=F_LBL2) if False else d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195))
|
||||
d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195))
|
||||
x += 150
|
||||
for key in ["attack", "heal", "pierce", "magic", "blast"]:
|
||||
rgb = GLOW_GROUPS[key]
|
||||
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
|
||||
d.text((x - 4, oy4 + 38), GLOW_FAMILY[key] + "微光", font=F_LBL2, fill=(225, 215, 195))
|
||||
x += 150
|
||||
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源 + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度——详见机检报告;全过=四冻结,气质终审=批末用户目检",
|
||||
col=(225, 215, 195), font=F_LBL2)
|
||||
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源"
|
||||
" + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度", col=(225, 215, 195), font=F_LBL2)
|
||||
d.text((20, oy4 + 82), "整屏四项 S1–S4 + 战斗表现五项 P1–P5 + A7 护甲对比 + V1 两版出口/V10 基线并排——详见机检报告;"
|
||||
"全过=机检冻结,气质终审=用户目检(方向稿目检先行)", col=(225, 215, 195), font=F_LBL2)
|
||||
|
||||
os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True)
|
||||
# S5 三组关键帧条 / S6 剪影三语境+描边对照+遮挡对照组
|
||||
oy5 = oy4 + 104
|
||||
canvas.paste(frames_strip, (20, oy5))
|
||||
d.text((430, oy5 + 8), "S5 三组关键帧条(受击/退场/高光兑现,各起中末三帧;\n退场末帧=灰化残位——禁瞬消;每组标注减动效简化形态)", col=(225, 215, 195), font=F_LBL)
|
||||
oy6 = oy5 + frames_strip.size[1] + 26
|
||||
canvas.paste(s6_img, (20, oy6))
|
||||
d.text((970, oy6 + 8), "S6 剪影跨舞台三语境(P2 灰度取样)+描边同屏对照\n(主角深墨/敌方暖白族)+护甲三枚展示+遮挡对照组(P5)", col=(225, 215, 195), font=F_LBL)
|
||||
|
||||
# 两版样张出口(V1)+改前/改后并排(V10)
|
||||
os.makedirs(os.path.dirname(OUT_ED1), exist_ok=True)
|
||||
canvas.convert("RGB").save(OUT_PNG)
|
||||
print("saved:", OUT_PNG)
|
||||
scr.convert("RGB").save(OUT_ED1)
|
||||
print("saved:", OUT_ED1)
|
||||
scale = 1080.0 / scr.size[1]
|
||||
w2 = int(round(scr.size[0] * scale))
|
||||
ed2 = Image.new("RGB", (1920, 1080), (0, 0, 0))
|
||||
ed2.paste(scr.convert("RGB").resize((w2, 1080), Image.NEAREST), ((1920 - w2) // 2, 0))
|
||||
ed2.save(OUT_ED2)
|
||||
print("saved:", OUT_ED2)
|
||||
check("V1", "样张两版产出(1280×800 基座 + 1920×1080=全局等比 1.35×+左右黑边,§8.2 1920 档策略)",
|
||||
os.path.exists(OUT_ED1) and os.path.exists(OUT_ED2)
|
||||
and Image.open(OUT_ED1).size == (1280, 800) and Image.open(OUT_ED2).size == (1920, 1080),
|
||||
"ed1 %s / ed2 %s / 黑边各 %dpx" % (Image.open(OUT_ED1).size, Image.open(OUT_ED2).size, (1920 - w2) // 2))
|
||||
has_base = os.path.exists(BASELINE_PNG)
|
||||
if has_base:
|
||||
ba = Image.new("RGB", (2570, 810), (24, 20, 17))
|
||||
base_im = Image.open(BASELINE_PNG).convert("RGB")
|
||||
ba.paste(base_im, (5, 5))
|
||||
ba.paste(Image.open(OUT_ED1).convert("RGB"), (1285 + 5, 5))
|
||||
ba.save(OUT_BEFORE_AFTER)
|
||||
print("saved:", OUT_BEFORE_AFTER)
|
||||
check("V10", "改前/改后并排对拍图产出(基线版与改后版两源样张尺寸一致+并排图生成)",
|
||||
has_base and Image.open(BASELINE_PNG).size == (1280, 800) and os.path.exists(OUT_BEFORE_AFTER),
|
||||
"baseline %s / ed1 1280×800 / 并排 %s" % (Image.open(BASELINE_PNG).size if has_base else "缺(先 --save-baseline)", OUT_BEFORE_AFTER))
|
||||
# A5b 终稿净空(装配后画布——A5 原断言跑在空白底板上,此处补实际成图净空)
|
||||
edge_hits_final = edge_content(canvas)
|
||||
check("A5b", "装配成稿四缘 2px 净空(实际画幅,零溢出)", edge_hits_final == 0, "缘带内容像素 %d" % edge_hits_final)
|
||||
|
||||
ok_all = all(c[2] for c in CHECKS)
|
||||
# 机检报告
|
||||
os.makedirs(os.path.dirname(OUT_REPORT), exist_ok=True)
|
||||
with open(OUT_REPORT, "w", encoding="utf-8") as f:
|
||||
f.write("# 样张 v3 机检报告【冻结】(2026-09-30 批 A——v3.5 用户验收态断言定值落锁;气质终审已由 2026-09-29 用户验收豁免,台账 281)\n\n")
|
||||
f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档(世界底层档 L≈0.10,带 0.08–0.14)/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;断言定值=A3b 图底分离 Δ≥30(实测 38)+A4 尺寸基线(立绘 46×48/小人源 32×32 硬等);F6 沿用 v3.4 双明度带口径 Δ≥18。\n\n")
|
||||
f.write("## v1 否决原因 → 断言映射\n\n")
|
||||
f.write("| 否决原因(2026-09-28 用户) | 断言承载 |\n|---|---|\n")
|
||||
f.write("| ①显示太丑(总纲) | A6 观感密度(整板明度 std ≥50.9=v1 实测 44.31×1.15)+既有装饰变体 ≥2 种 |\n")
|
||||
f.write("| ②封印色冲突 | 封印黑 #000000(9b6fcf6 已落,A1a 对表自动覆盖) |\n")
|
||||
f.write("| ③立体感不够 | A3 阈值 40→55 + F6 顶面/立面双明度带 Δ≥18 + F7 整板悬崖 DEPTH_PX 同源 |\n")
|
||||
f.write("| ④微光格只留微光 | F5c 微光段零 draw_inlay(d1dea82 已落) |\n\n")
|
||||
f.write("# 样张 v3 机检报告(2026-09-30 批 A 断言定值 + 迭代 43 整屏扩展 S1–S4/P1–P5/V1/V10/A7/A5b)\n\n")
|
||||
f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;A3b 图底分离 Δ≥30;A4 尺寸基线(立绘 46×48/小人源 32×32 硬等)。"
|
||||
"迭代 43 扩展:B 区按代码 252 出样;整屏四项与战斗表现五项断言落 CHECKS;两版样张出口与基线并排对拍。方向稿目检先行,机检冻结归目检通过后。\n\n")
|
||||
f.write("## 断言明细\n\n")
|
||||
f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n")
|
||||
for cid, name, okk, detail in CHECKS:
|
||||
f.write("| %s | %s | %s | %s |\n" % (cid, name, "PASS" if okk else "**FAIL**", detail.replace("|", "/")))
|
||||
f.write("\n结论:**%s**(%d/%d PASS)。v3 方向稿机检=可行性参考(A3 亮墨对比为硬指标);冻结 = 用户目检方向稿后机检批(v3.0 闸形:目检先行→机检冻结)。\n" %
|
||||
("全绿" if ok else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
f.write("\n结论:**%s**(%d/%d PASS)。\n" % ("全绿" if ok_all else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
print("saved:", OUT_REPORT)
|
||||
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
return 0 if ok else 1
|
||||
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok_all else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
return 0 if ok_all else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user