卡①T7(核心数据面结构收敛批): config_db归位+域容器——三容器建件(ConfigDomainCombat 7表/Run 7表/Crossrun 5表,类型化纯持有)/config_db瘦身44→8公开(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)+类变量9(容器3+is_ready+诊断/resolver/validator/reload_gate/fatal_flag)/溶解16+迁移14+委托3访问器归位表类(shop_table 7方法/meta_table 2/forge_table 3[route_rows避routes属性]/variation_table 2/level_table level_count)+resolver装配段注入三表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers委托,resolver文件零搬移)/删2(meta_conversion_row/forge_weapon_route_by_type生产零消费)/私1(validate_skill_pool_links→_);全域改址86文件(组①db.<表>→db.<容器>.<表>组②33访问器归位新名+删2消费面直扫同判据+桩件三处适配FakeDB/_BoardRowStub嵌套holder+treasure_items has_method改crossrun探测);首跑清扫漏表名bug已整树回滚重跑修正;gdstyle 168→92键NEW=0;[测试动作移交用户:套件停跑]

This commit is contained in:
DKChan committed 2026-10-28 08:37:22 +08:00
1 parent ced64d9cb3
commit a9a163f924
97 files changed
+1015 -1072

No files matched your search

+3 -3
View File
@@ -204,9 +204,9 @@ func request_refresh() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if _rap < db.action_economy.rap_refresh_cost:
if _rap < db.run.action_economy.rap_refresh_cost:
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
_rap -= db.action_economy.rap_refresh_cost
_rap -= db.run.action_economy.rap_refresh_cost
return true
@@ -249,7 +249,7 @@ func notify_round_start() -> bool:
else:
_reject("phase")
return false
_ap = maxi(0, db.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
_ap = maxi(0, db.run.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
_pending_ap_delta = 0
_phase = Phase.ACTIVE
_settlement_consumed = false
+6 -6
View File
@@ -105,7 +105,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_stats.armor_type = String(params.get("armor_type", ""))
summary = {}
_reset_curio_state()
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = 1
_pending_next_level = false
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
@@ -120,7 +120,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
for iid_v2 in _attunement:
var rp_row: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v2))
var rp_row: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v2))
if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"):
_refresh_bank.add_refresh_stock(int(rp_row.get("magnitude", 0)))
# 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
@@ -184,7 +184,7 @@ func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
_wallet.restore_fragment(bp, params)
_journal.restore_fragment(bp, params)
summary = {}
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = int(bp.get("level_index", 1))
_pending_next_level = false
@@ -223,7 +223,7 @@ func _resume_params(bp: Dictionary, params: Dictionary) -> void:
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
_stats.clamp_hp_to_resume_cap(int(db.hero.max_hp))
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
@@ -330,7 +330,7 @@ func present_node_curio(candidates: Array) -> void:
func _attune_refresh_x_total() -> int:
var total := 0
for iid_v in _attunement:
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"):
total += int(arow.get("magnitude", 0))
return total
@@ -370,7 +370,7 @@ func _boss_keystone_drop(li: int) -> void:
var first := true
if _save_manager != null:
first = not _save_manager.meta_state.career_state().boss_first_kill.has(str(li))
var row: Dictionary[Variant, Variant] = db.meta_boss_keystone_row(li)
var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li)
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
+3 -3
View File
@@ -33,7 +33,7 @@ func item_ids() -> Array:
func seed_for_adventure(inventory: TreasureItems) -> void:
for iid_v in _ids:
var iid := String(iid_v)
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
if String(arow.get("form", "")) != "active_charged":
continue
_ensure_rows(iid, maxi(0, int(arow.get("charges_per_adventure", 0))), inventory)
@@ -44,7 +44,7 @@ func seed_for_adventure(inventory: TreasureItems) -> void:
func reset_cycle(cycle: String, inventory: TreasureItems) -> void:
for iid_v in _ids:
var iid := String(iid_v)
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
if String(arow.get("form", "")) != "active_charged":
continue
if String(arow.get("charge_reset", "adventure")) != cycle:
@@ -94,7 +94,7 @@ func breakpoint_fragment(inventory: TreasureItems) -> Dictionary[Variant, Varian
var charges: Dictionary[Variant, Variant] = {}
for iid_v in _ids:
var iid := String(iid_v)
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid)
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(iid)
if String(arow.get("form", "")) == "active_charged":
charges[iid] = charges_left(iid, inventory)
return {"attunement_charges": charges}
+1 -1
View File
@@ -175,5 +175,5 @@ func _fx_fact(fact: String) -> Variant:
# 基线 = enc 口径,-1(表值兜底态)时解析为表值后叠加,守恒口径与 begin_battle 注入一致)
func _sync_hero_ac() -> void:
if int(ac.base) < 0:
ac.base = int(db.hero.armor_class)
ac.base = int(db.combat.hero.armor_class)
damage.set_hero_ac_override(int(ac.base) + int(ac.round) + int(ac.battle))
+19 -19
View File
@@ -180,7 +180,7 @@ func attune_has(item_id: String) -> bool:
func attune_param_sum(kind: String) -> int:
var total := 0
for iid_v in attunement:
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has(kind):
total += int(arow.get("magnitude", 0))
return total
@@ -190,7 +190,7 @@ func attune_param_sum(kind: String) -> int:
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
func _attune_charge_safe_row() -> Dictionary:
for iid_v in attunement:
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
return arow
return {}
@@ -301,7 +301,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
return false
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
_hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk)))
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), dice[0],
_hero_atk, int(enc.get("hero_max_hp", -1)), dice[1],
String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), String(enc.get("node_type", "battle"))): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
@@ -317,7 +317,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。
func _begin_battle_dice(enc: Dictionary) -> Array:
# 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type)
var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle")))
var erowset_pre: Array[Variant] = db.combat.enemy.resolve_rowset(int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle")))
if erowset_pre.is_empty():
return []
var needs_count := false
@@ -462,7 +462,7 @@ func _install_new_board(during_round := false) -> bool:
func _resolve_global_variant(board: BoardData) -> void:
board.board_variants.clear()
_reward_economy.board_skew = 0
var row: Dictionary[Variant, Variant] = db.global_variant_row(int(encounter.level_index), String(board.board_type),
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
@@ -482,7 +482,7 @@ func _resolve_global_variant(board: BoardData) -> void:
func _place_variants(board: BoardData) -> void:
if not board.board_variants.is_empty():
return
var pool: Array[Variant] = db.variant_pool(int(encounter.level_index), String(board.board_type))
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
if pool.is_empty():
return
var r0 := {}
@@ -496,8 +496,8 @@ func _place_variants(board: BoardData) -> void:
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.variation.percent_of_cells) / 100.0)) \
+ (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus)
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)
@@ -506,7 +506,7 @@ func _place_variants(board: BoardData) -> void:
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.variation.pack_max)))
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 的连通分量
@@ -638,7 +638,7 @@ func _harmful_variants(vids: Array) -> Array:
var out: Array[Variant] = []
for vid_v in vids:
var vid := String(vid_v)
var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(vid)
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
if String(vrow.get("effect", "")) == "flat_damage":
out.append(vid)
return out
@@ -677,7 +677,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。
func _roll_round_actions() -> void:
var pity_m: int = int(db.skill.pity_threshold)
var pity_m: int = int(db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
@@ -692,7 +692,7 @@ func _roll_round_actions() -> void:
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in _enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = db.skill_row(sid)
var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
_emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid})
@@ -733,7 +733,7 @@ func _defender_ac(unit: Dictionary) -> int:
func _pool_effective(effect_id: String) -> int:
var side := "hero"
var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id)
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
if not row.is_empty():
side = String(row.target_side)
for eff in _status_effects.query_active(side):
@@ -818,9 +818,9 @@ func _exit_distribution() -> void:
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
func can_refresh() -> bool:
return battle_result == "" \
and _refresh_used_this_round < int(db.action_economy.refresh_cap_per_round) \
and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \
and action_economy.phase == action_economy.PHASE_ACTIVE \
and action_economy.rap >= int(db.action_economy.rap_refresh_cost)
and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost)
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
@@ -870,14 +870,14 @@ func _battle_over(result: String) -> void:
# + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。
func _roll_gp_drop() -> int:
var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal"
var drops: Dictionary[Variant, Variant] = db.shop_gp_drop_row(tier)
var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier)
var roll := 0
if drops.is_empty():
push_error("[战斗结算] gp_drops 缺行 tier=%s——掉落掷定按 0 兑付" % tier)
else:
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
var drop := roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0)))
var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0)))
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
@@ -899,14 +899,14 @@ func presentation_snapshot() -> Dictionary:
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行)
var enemy_effects: Array[Dictionary] = []
for eff in _status_effects.query_active("enemy"):
var erow: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]:
enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)})
for i in all.size():
var u: Dictionary[Variant, Variant] = all[i]
var alive: bool = bool(u.get("alive", false))
var pending: String = String(u.get("pending_skill", "")) if alive else ""
var pending_row: Dictionary[Variant, Variant] = db.skill_row(pending)
var pending_row: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(pending)
enemies.append({
"order": i + 1,
"display_name": String(u.display_name),
+1 -1
View File
@@ -22,7 +22,7 @@ static func target_mine_count(params: Dictionary) -> int:
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub)
var row: Dictionary[Variant, Variant] = db.resolve_board_gen(L, D)
var row: Dictionary[Variant, Variant] = db.combat.board_gen.resolve_row(L, D)
if row.is_empty():
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
return null
+39 -319
View File
@@ -1,6 +1,8 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与类型化访问器(垂直切片验证:改表即改玩法,数值零硬编码)。
## 玩法代码禁止绕过本单点自建 load();消费面一律走访问器,禁直读行集。
## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与门面(垂直切片验证:改表即改玩法,数值零硬编码)。
## 玩法代码禁止绕过本单点自建 load();行查询归各表类(表管自己的行——2026-10-27 全清设计定稿),
## 表持有按域收进三容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域),
## 消费面经 db.<容器>.<表> 直连。「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。
## headless --script 模式无 autoload——冒烟脚本显式实例化本类;加载失败 is_ready=false 加 fatal 封印,调用方须查。
class_name ConfigDb
extends Node
@@ -29,39 +31,23 @@ const TABLE_MANIFEST := {
"forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面
}
var action_economy: ActionEconomyTable
var variation: VariationTable
var status_effects: StatusEffectTable
var board_gen: BoardGenTable
var enemy: EnemyTable
var skill: SkillTable
var hero: HeroTable
var reward: RewardTable
var settlement: SettlementTable
var level: LevelTable
var difficulty: DifficultyTable
var level_gen_params: LevelGenParams # 参数档类型化持用(生成器只吃参数档、不认关卡号——卡① 契约 6)
# ShopTable 类型化持用(schema class_name 已已入库;
# 消费面一律走下方 shop_* 访问器,禁直读行集)
var shop: ShopTable = null
# MetaTable 类型化持用(schema 与表同源已入库)
var meta: MetaTable
var meta_tree: MetaTreeTable # 类型化持用(消费面走 tree_node 访问器)
var treasure_items: TreasureItemTable # 类型化持用(消费面走 treasure_* 访问器)
var adventure: AdventureTable
var feats: FeatTable # 类型化持用(消费面走 feat_* 访问器)
var forge: ForgeTable # 锻造工坊配置(默认两轴+六路线行族——消费面走 forge_* 访问器)
var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement)
var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop)
var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge)
var is_ready := false
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
var _resolver: ConfigDbResolver # R1 难度行解析协作类(门面公有签名不变)
var _resolver: ConfigDbResolver # R1 难度行解析协作类(门面公有签名不变;load_tables 装配段注入三表)
var _validator := ConfigDbCrossValidator.new() # 跨表校验协作类(无状态——声明期即建)
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
func _init() -> void:
combat = ConfigDomainCombat.new()
run = ConfigDomainRun.new()
crossrun = ConfigDomainCrossrun.new()
_resolver = ConfigDbResolver.new(push_diag,
func() -> Variant: return board_gen.rows, func() -> Variant: return enemy.rows, func() -> Variant: return difficulty) # 协作类持门面引用(诊断/致命通道回呼)
func() -> Variant: return combat.board_gen.rows, func() -> Variant: return combat.enemy.rows, func() -> Variant: return run.difficulty) # 协作类持门面引用(诊断/致命通道回呼)
func push_diag(severity: String, category: String, message: String) -> void:
@@ -132,312 +118,46 @@ func load_tables(root: String, collect_only: bool) -> bool:
if has_errors and not collect_only:
# 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建
return false
action_economy = loaded.get("action_economy")
variation = loaded.get("variation")
status_effects = loaded.get("status_effects")
board_gen = loaded.get("board_gen")
enemy = loaded.get("enemy")
skill = loaded.get("skill")
hero = loaded.get("hero")
reward = loaded.get("reward")
settlement = loaded.get("settlement")
level = loaded.get("level")
difficulty = loaded.get("difficulty")
level_gen_params = loaded.get("level_gen_params")
shop = loaded.get("shop")
meta = loaded.get("meta")
treasure_items = loaded.get("treasure_items")
adventure = loaded.get("adventure")
feats = loaded.get("feats")
forge = loaded.get("forge")
meta_tree = loaded.get("meta_tree")
# 容器装配(19 表成员按域收进三容器——2026-10-27 全清设计定稿)
combat.board_gen = loaded.get("board_gen")
combat.variation = loaded.get("variation")
combat.status_effects = loaded.get("status_effects")
combat.enemy = loaded.get("enemy")
combat.skill = loaded.get("skill")
combat.hero = loaded.get("hero")
combat.settlement = loaded.get("settlement")
run.level = loaded.get("level")
run.difficulty = loaded.get("difficulty")
run.level_gen_params = loaded.get("level_gen_params")
run.reward = loaded.get("reward")
run.adventure = loaded.get("adventure")
run.action_economy = loaded.get("action_economy")
run.shop = loaded.get("shop")
crossrun.meta = loaded.get("meta")
crossrun.meta_tree = loaded.get("meta_tree")
crossrun.treasure_items = loaded.get("treasure_items")
crossrun.feats = loaded.get("feats")
crossrun.forge = loaded.get("forge")
# resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移)
combat.board_gen._resolver = _resolver
combat.enemy._resolver = _resolver
run.difficulty._resolver = _resolver
is_ready = not has_errors
return not has_errors
# ---- relic 访问器(消费面一律走本组,禁直读行集)----
func feat_row(feat_id: String) -> Dictionary:
return feats.row_by_id(feat_id) if feats != null else {}
# 规则改写型特性带令牌构建源(池接线——CurioDeck.build 消费;行 rarity→投放档映射)
func feat_rows() -> Array:
return feats.rows if feats != null else []
# 天赋树节点行(MetaProgression 树段/营地树 UI 取数口)
func tree_node(node_id: String) -> Dictionary:
return meta_tree.row_by_id(node_id) if meta_tree != null else {}
func tree_rows() -> Array:
return meta_tree.rows if meta_tree != null else []
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体:
# ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名不变。
func _cross_validate(loaded: Dictionary) -> void:
_validator.cross_validate(loaded, push_diag)
# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类)
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类;门面私有——
# 消费面经门面者改 _ 前缀直调,cross_validator 直验入口公开面不变)
func _validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
return _validator.validate_skill_pool_links(skill_table, enemy_table, status_table)
# ---- R1 难度行解析(ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)----
# 解析本体:ConfigDbResolver(core/config_db_resolver.gd)承载——
# 含 _resolve_progress/_ceil_key 与 R1 告警去重注册表;门面同名方法委托,越域 Fatal 语义一致。
func resolve_board_gen(L: int, D: int) -> Dictionary:
return _resolver.resolve_board_gen(L, D)
func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
return _resolver.resolve_enemy_rowset(L, D, row_kind) # 行集数组形态(非单行 Dictionary;D 维带寻址)
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
return _resolver.resolve_difficulty_modifiers(L, D)
# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;字段名对齐
# board_type 词表,无 biome_tags)----
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
var pool: Array[Dictionary] = []
for row in variation.rows:
if String(row.get("scope", "region")) == "global" \
and node_type != "boss" and node_type != "elite":
continue
if int(row.get("unlock_level", 999)) > level_index:
continue
var tags: Array[Variant] = row.get("board_types", [])
if tags.is_empty():
pool.append(row)
continue
if tags.has(board_type):
pool.append(row)
return pool
# 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
# boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
# 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
if node_type != "boss" and node_type != "elite":
return {}
for row in variant_pool(level_index, board_type, node_type):
if String(row.get("scope", "region")) == "global":
return row
return {}
func status_effect_row(effect_id: String) -> Dictionary:
return status_effects.row_by_id(effect_id)
# ---- 关卡行集访问器(表访问收口——level_runner/meta_progression/adventure_runner
# 不直扫 db.level.rows;消费面一律走本访问器,行集解析单点归 LevelTable)----
# 关卡行单点读取(键 = level_index,缺行返回空字典——消费方按既有空行防御路径处理)
func level_row(level_index: int) -> Dictionary:
return level.row_by_level_index(level_index) if level != null else {}
# 参数档单点读取(节点图生成器核心批 卡①——键 = level_index,缺行返回空字典;调用方据此产图,生成器本体不读关卡号)
func level_gen_params_row(level_index: int) -> Dictionary:
return level_gen_params.row_by_level_index(level_index) if level_gen_params != null else {}
# 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径)
func level_count() -> int:
return level.rows.size() if level != null else 0
# ---- ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)----
# ── 秘宝访问器(消费面一律走访问器,禁直读行集)──
# 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径
func treasure_item_row(item_id: String) -> Dictionary:
return treasure_items.row_by_id(item_id) if treasure_items != null else {}
# 过渡渠道活行(form 空 = 切片期过渡形态)——掉落投放/停点池/工坊解锁取数口径;同调行(form 非空)
# 永不入过渡渠道
func treasure_transitional_rows() -> Array:
return treasure_items.transitional_rows() if treasure_items != null else []
# 同调行集(form 非空——装配覆盖层/效果接线的取数口径)
func treasure_attunement_rows() -> Array:
return treasure_items.attunement_rows() if treasure_items != null else []
# 活行词表(掉落投放 effect_id 白名单——过渡渠道口径)
# ── 同调槽位数=树同调位节点点亮数,判定源=Attunement.slots(无 attunement_growth 谓词表访问器);
# boss_kill_count/clear_count 计数域保留——非槽位硬消费在册 ──
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
# level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value,
# base_price, price_increment} · price_level_bonus {level_index, bonus}。
# 行族经 ShopTable 同名数组属性读取(.get 容缺——表已入库前查询恒空,掉落按 0 兑付的兜底在调用方)。
func shop_gp_drop_row(tier: String) -> Dictionary:
for row in _shop_rows("gp_drops"):
if String(row.get("tier", "")) == tier:
return row
return {}
func shop_level_gp_bonus(level_index: int) -> int:
for row in _shop_rows("level_gp_bonus"):
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
func shop_price_level_bonus(level_index: int) -> int:
for row in _shop_rows("price_level_bonus"):
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
func shop_products() -> Array:
return _shop_rows("products")
func shop_product_row(product_id: String) -> Dictionary:
for row in _shop_rows("products"):
if String(row.get("product_id", "")) == product_id:
return row
return {}
# ── 共享池货架访问器(M-4——消费面一律走访问器,禁直读行集)──
# 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
func shop_pool_rows() -> Array:
return _shop_rows("pool_shelf")
func shop_pool_row(kind_id: String) -> Dictionary:
for row in _shop_rows("pool_shelf"):
if String(row.get("kind_id", "")) == kind_id:
return row
return {}
# 乘法价格因子(provisional 投值——0 = 表未投值,消费方按 fail-fast 域校验拦截)
func shop_price_a() -> float:
return float(shop.price_a) if shop != null else 0.0
func shop_price_b() -> float:
return float(shop.price_b) if shop != null else 0.0
# 行族数组读取(ShopTable 属性名 = 行族名;表未已入库/属性缺失 = 空集非错误态)
func _shop_rows(family: String) -> Array:
if shop == null:
return []
var arr = shop.get(family)
return arr if arr is Array else []
# ---- MetaTable 消费面(行集解析单点——MetaProgression/呈现层禁自扫表)----
# 行族四轴(数据契约,行集归 meta.tres,数值全 provisional):
# forge_products {attr, display_name, flat_value, base_price, price_ratio_pct} ·
# boss_keystone {level_index, first_kill, repeat_kill} ·
# workshop_items {item_id, display_name, clear_threshold} ·
# conversion {attr, per_level_shift_pct}。
func meta_boss_keystone_row(level_index: int) -> Dictionary:
for row in _meta_rows("boss_keystone"):
if int(row.get("level_index", -1)) == level_index:
return row
return {}
# 秘宝解锁混合制挂 treasure_items 行内(无 meta_workshop_items 访问器);
# 秘宝解锁投影面=MetaProgression.treasure_shop_status
func meta_conversion_row(attr: String) -> Dictionary:
for row in _meta_rows("conversion"):
if String(row.get("attr", "")) == attr:
return row
return {}
# Y 刷新升级行单点读取(键 = 等级,缺行返回空字典=未购/已满级合法态)
func meta_attune_refresh_row(level: int) -> Dictionary:
for row in _meta_rows("attune_refresh_upgrade"):
if int(row.get("level", -1)) == level:
return row
return {}
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
func _meta_rows(family: String) -> Array:
if meta == null:
return []
var arr = meta.get(family)
return arr if arr is Array else []
# ---- 锻造工坊(ForgeTable 消费面:两根类型轴默认值+六路线行族;禁自扫行集)----
# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:默认值归本表常驻持有(hero.tres 两轴已删——
# 契约 2);路线行族=攻防数值成长唯一数据面(契约 1)。数值全 provisional 归 M4。
# 两根类型轴默认值(表未已入库=裸体态兜底 normal/light——与 ForgeTable 缺省值同口径)
func forge_default_attack_type() -> String:
return String(forge.default_attack_type) if forge != null else "normal"
func forge_default_armor_type() -> String:
return String(forge.default_armor_type) if forge != null else "light"
# 六路线行族(副本——调用方禁改)
func forge_routes() -> Array:
return forge.routes.duplicate() if forge != null else []
# 路线行单点(键 route_id;缺行=空字典)
func forge_route(route_id: String) -> Dictionary:
for row in forge_routes():
if String(row.get("route_id", "")) == route_id:
return row
return {}
# 按类型轴反查武器路线(attack_type → 路线行;甲路线 id=甲型同值的显式化入口,避免消费侧依赖该巧合)
func forge_weapon_route_by_type(attack_type: String) -> Dictionary:
for row in forge_routes():
if String(row.get("kind", "")) == "weapon" and String(row.get("type_axis", "")) == attack_type:
return row
return {}
# 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
func forge_armor_route_by_type(armor_type: String) -> Dictionary:
for row in forge_routes():
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
return row
return {}
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
func skill_row(skill_id: String) -> Dictionary:
return skill.row_by_id(skill_id)
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
func set_reload_gate(gated: bool) -> void:
+12
View File
@@ -0,0 +1,12 @@
## 战斗域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):战斗编排域七表类型化持有。
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainCombat
extends Resource
var board_gen: BoardGenTable
var variation: VariationTable
var status_effects: StatusEffectTable
var enemy: EnemyTable
var skill: SkillTable
var hero: HeroTable
var settlement: SettlementTable
+11
View File
@@ -0,0 +1,11 @@
## 跨局域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):跨局元进度域五表类型化持有
## (跨局循环=单局之外的长线进度域)。零方法零行为——纯持有结构;
## 「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainCrossrun
extends Resource
var meta: MetaTable
var meta_tree: MetaTreeTable
var treasure_items: TreasureItemTable
var feats: FeatTable
var forge: ForgeTable
+12
View File
@@ -0,0 +1,12 @@
## 冒险域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):冒险 runner 域七表类型化持有。
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainRun
extends Resource
var level: LevelTable
var difficulty: DifficultyTable
var level_gen_params: LevelGenParams
var reward: RewardTable
var adventure: AdventureTable
var action_economy: ActionEconomyTable
var shop: ShopTable
+17 -17
View File
@@ -29,13 +29,13 @@ func reset() -> void:
# relic 令牌档 = 行 rarity 直映射,词表外 transitional 值回退映射 rarest→rare/其余→excellent)
func build() -> Array:
var out: Array[Variant] = []
for kind in db.adventure.CURIO_KINDS:
for tier in db.adventure.TIERS:
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
for kind in db.run.adventure.CURIO_KINDS:
for tier in db.run.adventure.TIERS:
for i in int(db.run.adventure.curio_quota(String(kind), String(tier))):
out.append({"kind": String(kind), "tier": String(tier)})
for row in db.feat_rows():
for row in db.crossrun.feats.rows:
var rraw := String(row.get("rarity", ""))
var rtier := rraw if db.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
var rtier := rraw if db.run.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
out.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
return out
@@ -73,7 +73,7 @@ func pool_stock(kind: String) -> int:
# 读数面 = 商店买走 vs 奖励领走 vs 枯竭)
func stock_snapshot() -> Dictionary:
var out := {}
for row in db.shop_pool_rows():
for row in db.run.shop.pool_rows():
var kind := String(row.get("kind_id", ""))
out[kind] = pool_stock(kind)
return out
@@ -97,23 +97,23 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo
func cap_ok(kind: String, feat_id: String = "") -> bool:
var cap := 99
if kind == "relic":
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
return taken.get("relic:" + feat_id, 0) < cap
if not db.adventure.CURIO_KINDS.has(kind):
if not db.run.adventure.CURIO_KINDS.has(kind):
return true
cap = int(db.adventure.curio_cap(kind))
cap = int(db.run.adventure.curio_cap(kind))
return int(taken.get(kind, 0)) < cap
# 专长凭证展示判定(四口径:一局限购一次/稀有只在概率不在价格/计步保底/触发清零。
# 调用即一次展示判定与记账——展示次数计 pity、配额与限购双闸)
func feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
if ticket_sold >= int(db.adventure.feat_ticket_limit):
if ticket_sold >= int(db.run.adventure.feat_ticket_limit):
return false # 限购满——不再触发(保底冻结)
if ticket_appeared >= int(db.adventure.feat_ticket_quota):
if ticket_appeared >= int(db.run.adventure.feat_ticket_quota):
return false # 配额满——本局不再出现
var offer: bool = ticket_pity >= int(db.adventure.feat_ticket_pity_step) or rg.randi_range(1, 100) <= int(db.adventure.feat_ticket_chance)
var offer: bool = ticket_pity >= int(db.run.adventure.feat_ticket_pity_step) or rg.randi_range(1, 100) <= int(db.run.adventure.feat_ticket_chance)
if offer:
ticket_pity = 0
ticket_appeared += 1
@@ -160,7 +160,7 @@ func _candidate_pool(channel: String) -> Array[Variant]:
func _draw_weighted(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
var out: Array[Variant] = []
var seen: Array[Variant] = []
var want := int(db.adventure.curio_candidate_count)
var want := int(db.run.adventure.curio_candidate_count)
while out.size() < want and not pool.is_empty():
var total := _pool_weight(pool)
if total <= 0:
@@ -180,13 +180,13 @@ func _draw_weighted(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
func _pool_weight(pool: Array[Variant]) -> int:
var total := 0
for tok in pool:
total += db.adventure.boss_feat_weight(String((tok as Dictionary).get("tier", "")))
total += db.run.adventure.boss_feat_weight(String((tok as Dictionary).get("tier", "")))
return total
func _weighted_index(pool: Array[Variant], roll: int) -> int:
for i in pool.size():
roll -= db.adventure.boss_feat_weight(String((pool[i] as Dictionary).get("tier", "")))
roll -= db.run.adventure.boss_feat_weight(String((pool[i] as Dictionary).get("tier", "")))
if roll <= 0:
return i
return 0
@@ -198,12 +198,12 @@ func _draw_exposure(pool: Array[Variant], rg: RandomNumberGenerator) -> Array:
_shuffle(pool, rg)
var sub: Array[Variant] = []
for tok in pool:
if db.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
if db.run.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.run.adventure.tier_rank(db.run.adventure.TIER_EXCELLENT):
sub.append(tok)
_shuffle(sub, rg)
var out: Array[Variant] = []
var seen: Array[Variant] = []
var want := int(db.adventure.curio_candidate_count)
var want := int(db.run.adventure.curio_candidate_count)
var n_full := maxi(1, want - 1) if want >= 2 else want
while out.size() < want:
var src: Array[Variant] = sub if out.size() >= n_full and not sub.is_empty() else pool
+9 -9
View File
@@ -56,7 +56,7 @@ func rebind_level_runner(runner: LevelRunner, level_index: int) -> void:
func open_level_clear(payload_base: Dictionary) -> void:
var hp_before := _stats.hero_hp
var max_eff := _stats.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限)
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
var heal := mini(int(floor(float(max_eff) * float(int(db.run.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
_stats.hero_hp = hp_before + heal
_bank.reset_x() # 碰三选一重置 X 余量(不结转)
_bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零)
@@ -109,7 +109,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
return false
if kind == "feat_ticket":
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
var value: int = int(db.adventure.curio_value(kind, tier))
var value: int = int(db.run.adventure.curio_value(kind, tier))
var hp_before := _stats.hero_hp
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[停点件] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
@@ -122,7 +122,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
func _feat_payload_keys(feat_id: String) -> Dictionary:
if feat_id == "":
return {}
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
@@ -133,7 +133,7 @@ func _feat_payload_keys(feat_id: String) -> Dictionary:
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从牌库摘除该令牌(选中踢池;
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
if kind == "relic" and db.feat_row(feat_id).is_empty():
if kind == "relic" and db.crossrun.feats.row_by_id(feat_id).is_empty():
push_error("[停点件] relic 兑付失败——表无此件: %s" % feat_id)
return false # 先校验后摘牌(令牌零丢失)
if not _deck.consume_deck_token(kind, tier, feat_id):
@@ -172,7 +172,7 @@ func _apply_resource_pick(kind: String, value: int, feat_id: String) -> bool:
# 路由加算段(match 收敛进效果类型:属性值提升四类走 adventure_table.CURIO_APPLY_ROUTES 声明
# 路由(bonus 落点键),加新加算类令牌零代码)
func _apply_route_pick(kind: String, value: int) -> bool:
var sink := db.adventure.curio_apply_route(kind)
var sink := db.run.adventure.curio_apply_route(kind)
if sink == "":
return false
_journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍)
@@ -229,7 +229,7 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") ->
if _deck.find_token(candidates, kind, tier, feat_id).is_empty():
push_error("[停点件] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
var value: int = int(db.adventure.curio_value(kind, tier))
var value: int = int(db.run.adventure.curio_value(kind, tier))
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[停点件] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
@@ -348,10 +348,10 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
func consume_pool_purchase(kind: String, tier: String) -> bool:
if kind == "feat_ticket":
return _redeem_feat_ticket()
if not db.adventure.CURIO_KINDS.has(kind):
if not db.run.adventure.CURIO_KINDS.has(kind):
push_error("[停点件] 商店购买消费拒绝——件不在池词表: %s" % kind)
return false
return _apply_curio_pick(kind, tier, int(db.adventure.curio_value(kind, tier)))
return _apply_curio_pick(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
# ---- 冒险级复位(宿主 _reset_curio_state 调用——停点半:池归 deck.reset、停点三态与刷新账清零)----
@@ -376,7 +376,7 @@ func restore_pending(bp_pending: Dictionary) -> void:
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
migrated.append(mig)
else:
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
migrated.append({"kind": String(c), "tier": db.run.adventure.TIER_COMMON})
_pending = {"candidates": migrated}
var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
curio_choice_presented.emit(payload)
+25 -25
View File
@@ -294,7 +294,7 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary:
var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array)
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
var threshold: int = defender_ac(target) + int(db.settlement.damage_roll.attack_bonus)
var threshold: int = defender_ac(target) + int(db.combat.settlement.damage_roll.attack_bonus)
# armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面)
var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
var trade_hit := 0
@@ -383,7 +383,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
var ac_hits: Array[Variant] = ctx["ac_hits"]
var roll_read: Dictionary[Variant, Variant] = ctx["roll_read"]
var atk_type: String = ctx["atk_type"]
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"]))
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.combat.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"]))
var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
payload["roll"] = int(roll_read.roll)
@@ -392,7 +392,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
if bool(ctx["crit"]):
payload["crit"] = true
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
var landed: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled)
var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled)
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
@@ -406,7 +406,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
hero_rider += contrib
if hero_rider > 0:
if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]):
hero_rider = int(round(float(hero_rider) * float(db.settlement.damage_roll.crit_multiplier)))
hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
units.apply_damage(target, landed)
@@ -432,7 +432,7 @@ func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void:
# source_class 用中性名(fx_overflow 泛用通道,不挂件名)
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow", "贯穿弹射",
{"channel": "board",
"base_value": maxi(int(db.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
"base_value": maxi(int(db.combat.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": int(t.round_index),
"origin_cell": t.origin_cell, "attack_type": "pierce"})
if policy == POLICY_SEGMENT:
@@ -492,13 +492,13 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
func _enemy_roll(t: Dictionary) -> Dictionary:
var hero: Dictionary[Variant, Variant] = units.hero_unit()
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
var threshold: int = defender_ac(hero) + int(db.settlement.damage_roll.attack_bonus)
var threshold: int = defender_ac(hero) + int(db.combat.settlement.damage_roll.attack_bonus)
var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold)
var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light")))
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.damage_roll.crit_multiplier), cdelta)
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.combat.settlement.damage_roll.crit_multiplier), cdelta)
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
var total: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
var total: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
@@ -520,7 +520,7 @@ func _enemy_shield_apply(hits: Array, total: int) -> Dictionary:
var set_v = h.get("set", null)
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
if shield > 0:
total = maxi(int(db.settlement.damage_roll.damage_floor), total - shield)
total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield)
return {"total": total, "shield": shield}
@@ -541,7 +541,7 @@ func _resolve_board_typed(t: Dictionary) -> void:
# 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面)
if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "":
cdelta += int(h.delta)
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_roll.damage_floor))
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.combat.settlement.damage_roll.damage_floor))
units.apply_damage(target, dealt)
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
"%s → %s" % [String(t.display), String(target.display_name)], dealt,
@@ -556,7 +556,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
_ineffective_row(t, "无存活敌人")
_outcomes[int(t.ticket_id)] = "zero"
return
var blast_dmg: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
var blast_dmg: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
for u in alive:
units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
@@ -567,7 +567,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
func _resolve_board_flat_hero(t: Dictionary) -> void:
var dealt: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
var dealt: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
units.apply_damage(units.hero_unit(), dealt)
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
for k in t.context.keys():
@@ -580,16 +580,16 @@ func _resolve_board_flat_hero(t: Dictionary) -> void:
# 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit}
func _armor_roll() -> Dictionary:
var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.damage_roll.armor_roll_stream))
var roll: int = stream.randi_range(1, int(db.settlement.damage_roll.roll_faces))
return {"roll": roll, "crit": roll == int(db.settlement.damage_roll.crit_roll)}
var stream: RandomNumberGenerator = rng.open_stream(String(db.combat.settlement.damage_roll.armor_roll_stream))
var roll: int = stream.randi_range(1, int(db.combat.settlement.damage_roll.roll_faces))
return {"roll": roll, "crit": roll == int(db.combat.settlement.damage_roll.crit_roll)}
# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点)
func _roll_discount(roll: int, threshold: int) -> int:
if roll == int(db.settlement.damage_roll.crit_roll):
if roll == int(db.combat.settlement.damage_roll.crit_roll):
return 100
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.damage_roll.roll_tier_bounds, db.settlement.damage_roll.roll_tier_discounts)
return DamageMath.roll_discount_percent(roll, threshold, db.combat.settlement.damage_roll.roll_tier_bounds, db.combat.settlement.damage_roll.roll_tier_discounts)
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
@@ -599,15 +599,15 @@ func defender_ac(unit: Dictionary) -> int:
if bool(unit.get("is_hero", false)):
var total: int = _hero_ac_override
if total < 0:
total = int(db.hero.armor_class)
total = int(db.combat.hero.armor_class)
for eff in effects.query_active("hero"):
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
if not row.is_empty() and String(row.unit) == "ac":
total += int(eff.effective_value)
return total
var total_e: int = int(unit.get("armor_class", 0))
for eff_e in effects.query_active("enemy"):
var erow_ac: Dictionary[Variant, Variant] = db.status_effect_row(eff_e.effect_id)
var erow_ac: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff_e.effect_id)
if not erow_ac.is_empty() and String(erow_ac.unit) == "ac":
total_e += int(eff_e.effective_value)
return total_e
@@ -618,14 +618,14 @@ func defender_ac(unit: Dictionary) -> int:
func _counter_delta(attack_type: String, armor: String) -> int:
if attack_type == "":
return 0
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
var tbl: Dictionary[Variant, Variant] = db.combat.settlement.damage_roll.armor_effectiveness
var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {})
return int(row.get(armor, 0))
# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
func _discount_shift_up(discount: int, shift: int) -> int:
var tbl: Array[Variant] = db.settlement.damage_roll.roll_tier_discounts
var tbl: Array[Variant] = db.combat.settlement.damage_roll.roll_tier_discounts
var idx := tbl.find(discount)
if idx < 0:
return discount
@@ -636,12 +636,12 @@ func _discount_shift_up(discount: int, shift: int) -> int:
func clamped_pool(pool: String) -> int:
var total := 0
for eff in effects.query_active("hero") + effects.query_active("enemy"):
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
if row.is_empty():
continue
if String(db.settlement.core.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
if String(db.combat.settlement.core.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口)
return clampi(total, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_hi))
return clampi(total, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_hi))
# 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制")
+3 -3
View File
@@ -102,7 +102,7 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段)
func _load_feat_triggers(feat_ids: Array) -> void:
if db != null and db.feats != null:
for row in db.feats.rows:
for row in db.crossrun.feats.rows:
var fid := String(row.get("feat_id", ""))
if not feat_ids.has(fid):
continue
@@ -113,7 +113,7 @@ func _load_feat_triggers(feat_ids: Array) -> void:
## 同调秘宝订阅装载域(treasure 同调行序→按装备集过滤→行内数组序——装载声明序第二段)
func _load_attune_triggers(item_ids: Array) -> void:
if db != null:
for row in db.treasure_attunement_rows():
for row in db.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
if not item_ids.has(iid):
continue
@@ -374,7 +374,7 @@ func _counter_delta_of(matchup: String) -> int:
var parts := matchup.split("_vs_")
if parts.size() != 2:
return 0
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
var tbl: Dictionary[Variant, Variant] = db.combat.settlement.damage_roll.armor_effectiveness
var row: Dictionary[Variant, Variant] = tbl.get(String(parts[0]), {})
return int(row.get(String(parts[1]), 0))
+5 -5
View File
@@ -52,12 +52,12 @@ func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]:
# 调试浮窗百分比后乘;pct=0 时乘法恒等——MC 锚零漂移)----
func effective_max_hp() -> int:
var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + journal.bonus_total("max_hp")
var base: int = int(db.combat.hero.max_hp) + int(_base_override.get("max_hp", 0)) + journal.bonus_total("max_hp")
return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0)))
func effective_atk() -> int:
var base: int = int(db.hero.atk) + int(_base_override.get("atk", 0)) + journal.bonus_total("atk")
var base: int = int(db.combat.hero.atk) + int(_base_override.get("atk", 0)) + journal.bonus_total("atk")
return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0)))
@@ -70,10 +70,10 @@ func effective_ac() -> int:
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线,
# 与 hero 表两轴删除前 armor_class=10 行为连续)
func _armor_base_ac() -> int:
var at: String = armor_type if armor_type != "" else db.forge_default_armor_type()
var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at)
var at: String = armor_type if armor_type != "" else db.crossrun.forge.default_armor_type
var row: Dictionary[Variant, Variant] = db.crossrun.forge.armor_route_by_type(at)
if row.is_empty():
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
return int(db.combat.hero.armor_class) # 表缺失防御兜底(现役基准)
return int(row.get("base_ac", 0))
+4 -4
View File
@@ -318,7 +318,7 @@ func restore_fragment(resume_state: Dictionary) -> Dictionary:
func _find_level_row(level_index: int) -> Dictionary:
return db.level_row(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
return db.run.level.row_by_level_index(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
# ---- 图源装配(关卡节点图生成 v1.1——图 = f(参数档, 种子) 纯函数;无模板池、无兜底图)----
@@ -327,7 +327,7 @@ func _find_level_row(level_index: int) -> Dictionary:
# LevelTable.validate_graph(与产出侧自证同源规格、分工不重叠);违规=起关拒绝,不降级不回退。
func _assign_generated_graph(row: Dictionary, seed_val: int) -> bool:
var level_index: int = int(row.get("level_index", 0))
var params: Dictionary[Variant, Variant] = db.level_gen_params_row(level_index)
var params: Dictionary[Variant, Variant] = db.run.level_gen_params.row_by_level_index(level_index)
if params.is_empty():
push_error("[关卡节点地图] level %d 无参数档行(level_gen_params 缺行——配置错误)" % level_index)
return false
@@ -433,7 +433,7 @@ func visited_ids() -> Array:
# ---- 类型可见性读面(信息迷雾 v2.1:近处见类型,远处起雾)----
# 可见集 = Boss/终点(type=="boss")∪ 当前候选(下一跳恒可见——生成承诺)∪
# {id : |层(id) − 锚层| ≤ N};N = db.level.info_visible_layers(禁硬编码);
# {id : |层(id) − 锚层| ≤ N};N = db.run.level.info_visible_layers(禁硬编码);
# 锚层 = 当前节点层,入口停点(无当前节点)= 0;层 = 入口 BFS 深度(与呈现层分层同源口径)。
# 纯函数零状态:已过节点不豁免(单圈语义,零累积记忆);断点快照零新增字段。
func type_visible_ids() -> Array[int]:
@@ -443,7 +443,7 @@ func type_visible_ids() -> Array[int]:
var cur := current_node()
if not cur.is_empty():
anchor = int(layers.get(_node_id(cur), 0))
var n: int = int(db.level.info_visible_layers)
var n: int = int(db.run.level.info_visible_layers)
for node in nodes():
var nid := _node_id(node)
if String(node.get("type", "")) == "boss" \
+35 -35
View File
@@ -50,7 +50,7 @@ func _treasure_unlock_progress(key: String) -> int:
# 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购;
# 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。
func buy_treasure_unlock(item_id: String) -> bool:
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(item_id)
var price := int(arow.get("unlock_keystone_cost", 0))
if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0:
return false # 非购买件(条件件/无价行)拒绝
@@ -66,7 +66,7 @@ func buy_treasure_unlock(item_id: String) -> bool:
# 全部谓词达标才解锁(多谓词=AND——天眼双条件);幂等:已解锁跳过,重复刷新零重复入账。
func refresh_treasure_unlocks() -> Array:
var new_unlocks: Array[String] = []
for row in db.treasure_attunement_rows():
for row in db.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
if meta_state.item_unlocked(iid):
continue
@@ -91,7 +91,7 @@ func refresh_treasure_unlocks() -> Array:
# {item_id, display_name, desc, tier, unlocked, mode(keystone|condition|owned), price, reqs:[{key,threshold,progress}]}
func treasure_shop_status() -> Array:
var out: Array[Dictionary] = []
for row in db.treasure_attunement_rows():
for row in db.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
var unlocked := meta_state.item_unlocked(iid)
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
@@ -124,7 +124,7 @@ func treasure_shop_status() -> Array:
# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处)
func _forge_route_unlocked(route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty():
return false
if bool(row.get("initial_owned", false)):
@@ -133,7 +133,7 @@ func _forge_route_unlocked(route_id: String) -> bool:
return true
var prereq := String(row.get("prereq_route", ""))
if prereq != "":
var prow: Dictionary[Variant, Variant] = db.forge_route(prereq)
var prow: Dictionary[Variant, Variant] = db.crossrun.forge.route(prereq)
if not prow.is_empty() and meta_state.forge_state().levels.get(prereq, 0) >= int(prow.get("cap", 0)):
return true
return false
@@ -142,7 +142,7 @@ func _forge_route_unlocked(route_id: String) -> bool:
# 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。
# 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。
func forge_buy_level(route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty() or not _forge_route_unlocked(route_id):
return false
var lvl := meta_state.forge_state().levels.get(route_id, 0)
@@ -158,7 +158,7 @@ func forge_buy_level(route_id: String) -> bool:
# 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响:
# 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。
func forge_buy_unlock(route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty() or bool(row.get("initial_owned", false)):
return false
if _forge_route_unlocked(route_id):
@@ -173,7 +173,7 @@ func forge_buy_unlock(route_id: String) -> bool:
# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照,
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)。并组入口:kind ∈ {weapon, armor}。
func forge_select(kind: String, route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(route_id):
return false
if kind == "weapon":
@@ -187,12 +187,12 @@ func forge_select(kind: String, route_id: String) -> bool:
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
func hero_type_axes() -> Dictionary:
var fs := meta_state.forge_state()
var wrow: Dictionary[Variant, Variant] = db.forge_route(String(fs["weapon"]))
var attack := db.forge_default_attack_type()
var wrow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["weapon"]))
var attack := db.crossrun.forge.default_attack_type
if not wrow.is_empty() and String(wrow.get("kind", "")) == "weapon" and _forge_route_unlocked(String(fs["weapon"])):
attack = String(wrow.get("type_axis", ""))
var arow: Dictionary[Variant, Variant] = db.forge_route(String(fs["armor"]))
var armor := db.forge_default_armor_type()
var arow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["armor"]))
var armor := db.crossrun.forge.default_armor_type
if not arow.is_empty() and String(arow.get("kind", "")) == "armor" and _forge_route_unlocked(String(fs["armor"])):
armor = String(arow.get("type_axis", ""))
return {"attack": attack, "armor": armor}
@@ -204,14 +204,14 @@ func hero_type_axes() -> Dictionary:
# prereq_level, prereq_cap, prereq_maxed}——纯结构面,禁嵌余额判断(视图按 balance 禁用,同秘宝匣先例)。
func forge_shop_status() -> Array:
var out: Array[Dictionary] = []
for row in db.forge_routes():
for row in db.crossrun.forge.route_rows():
var rid := String(row.get("route_id", ""))
var lvl := meta_state.forge_state().levels.get(rid, 0)
var cap_v := int(row.get("cap", 0))
var unlocked := _forge_route_unlocked(rid)
var prereq := String(row.get("prereq_route", ""))
var plvl := meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0
var pcap := int(db.forge_route(prereq).get("cap", 0)) if prereq != "" else 0
var pcap := int(db.crossrun.forge.route(prereq).get("cap", 0)) if prereq != "" else 0
var selected := (String(row.get("kind", "")) == "weapon" and meta_state.forge_state().weapon == rid) \
or (String(row.get("kind", "")) == "armor" and meta_state.forge_state().armor == rid)
out.append({
@@ -227,7 +227,7 @@ func forge_shop_status() -> Array:
"next_price": ForgeTable.level_price(row, lvl) if (unlocked and lvl < cap_v) else 0,
"unlock_price": int(row.get("unlock_price", 0)),
"prereq_route": prereq,
"prereq_name": String(db.forge_route(prereq).get("display_name", "")) if prereq != "" else "",
"prereq_name": String(db.crossrun.forge.route(prereq).get("display_name", "")) if prereq != "" else "",
"prereq_level": plvl,
"prereq_cap": pcap,
"prereq_maxed": prereq != "" and plvl >= pcap,
@@ -278,7 +278,7 @@ func hero_override() -> Dictionary:
var tree_hp := 0
var tree_ac := 0
var nodes: Dictionary[Variant, Variant] = meta_state.tree_state()["nodes"]
for row in db.tree_rows():
for row in db.crossrun.meta_tree.rows:
var lvl := int(nodes.get(String(row.get("node_id", "")), 0))
if lvl <= 0:
continue
@@ -296,7 +296,7 @@ func hero_override() -> Dictionary:
# 锻造轴加值原语(选中路线等级×每级增量;选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态)
func _forge_axis_bonus(fs: Dictionary, sel_key: String, kind: String) -> int:
var rid := String(fs[sel_key])
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(rid)
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(rid):
return 0
return int(fs["levels"].get(rid, 0)) * int(row.get("per_level_gain", 0))
@@ -314,7 +314,7 @@ func _tree_spent_total() -> int:
# 节点可购校验(行门槛 + 显式前置 + 币 + cap;free 节点=钥匙不经购买,彩蛋=觉醒不经购买)
func tree_can_buy(node_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
if row.is_empty() or String(row.get("kind", "")) == "easter":
return false
@@ -322,10 +322,10 @@ func tree_can_buy(node_id: String) -> bool:
var lvl := int(nodes.get(node_id, 0))
var blocked := lvl >= int(row.get("cap", 1))
# 刷新升级节点:cap 主语=Y 账本(单一事实源——树上入口与装配段同 cap 同价,防双账漂移)
if node_id == "ks_refresh" and db.meta_attune_refresh_row(meta_state.attune_state().refresh_level + 1).is_empty():
if node_id == "ks_refresh" and db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1).is_empty():
blocked = true
# 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1))
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat" and _tree_spent_total() < db.meta_tree.level_gate(lvl + 1):
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat" and _tree_spent_total() < db.crossrun.meta_tree.level_gate(lvl + 1):
blocked = true
for req in (row.get("requires", []) as Array):
if int(nodes.get(String(req), 0)) < 1:
@@ -346,7 +346,7 @@ func tree_can_buy(node_id: String) -> bool:
func tree_buy(node_id: String) -> bool:
if node_id == "ks_refresh":
return buy_attune_refresh()
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
if row.is_empty() or not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
@@ -360,7 +360,7 @@ func tree_buy(node_id: String) -> bool:
# Fork 切换(营地树 UI——一节点双态:购买解锁组默认 A 态,换态走本口零币;同态重复=零操作;
# 未解锁组切换=拒绝——解锁唯一入口=购买面 tree_buy_fork)
func tree_toggle_fork(group: String, side: String) -> bool:
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
if not db.crossrun.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
return false
var cur := meta_state.tree_state().fork_sides.get(group, "")
if cur == "" or cur == side:
@@ -371,7 +371,7 @@ func tree_toggle_fork(group: String, side: String) -> bool:
# Fork 节点购买入口(一节点双态——购=解锁开关并置默认 A 态;重复购买由 cap 经 can_buy 拦截)
func tree_buy_fork(node_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
if row.is_empty() or String(row.get("kind", "")) != "fork":
return tree_buy(node_id)
if not tree_can_buy(node_id):
@@ -388,7 +388,7 @@ func tree_buy_fork(node_id: String) -> bool:
func tree_rule_keys() -> Array:
var out: Array[Variant] = []
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
for row in db.tree_rows():
for row in db.crossrun.meta_tree.rows:
if String(row.get("kind", "")) != "fork":
continue
var active := String(sides.get(String(row.get("fork_group", "")), ""))
@@ -409,7 +409,7 @@ func tree_camp_status() -> Dictionary:
## 树行板投影段(原 tree_camp_rows)
func _tree_camp_rows() -> Array:
var out: Array[Variant] = []
for row in db.tree_rows():
for row in db.crossrun.meta_tree.rows:
if String(row.get("kind", "")) == "fork":
continue # Fork 行由 fork_groups 段承载
out.append({
@@ -430,12 +430,12 @@ func _tree_camp_rows() -> Array:
# 行板门槛文案(R1 机制位呈现——requires_level 未达标项拼「需 {前置名} Lv{n}」;达标/未配置=空串。
# 只呈现未达标项:达标后锁标消失,不做全量前置罗列)
func _tree_gate_text(node_id: String) -> String:
var row: Dictionary[Variant, Variant] = db.tree_node(node_id)
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(node_id)
var rl: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
var parts: Array[String] = []
for req_id in rl.keys():
if meta_state.tree_state().nodes.get(String(req_id), 0) < int(rl[req_id]):
var req_row: Dictionary[Variant, Variant] = db.tree_node(String(req_id))
var req_row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(String(req_id))
parts.append("需 %s Lv%d" % [String(req_row.get("display_name", req_id)), int(rl[req_id])])
return "、".join(parts)
@@ -444,7 +444,7 @@ func _tree_gate_text(node_id: String) -> String:
func _tree_fork_groups() -> Array:
var out: Array[Variant] = []
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
for row in db.tree_rows():
for row in db.crossrun.meta_tree.rows:
if String(row.get("kind", "")) != "fork":
continue
var fg := String(row.get("fork_group", ""))
@@ -470,7 +470,7 @@ func _check_easter_egg() -> void:
if meta_state.tree_state().nodes.get("easter_ley", 0) > 0:
return
for nid in ["sp_hp", "sp_hit"]:
var row: Dictionary[Variant, Variant] = db.tree_node(nid)
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(nid)
if row.is_empty() or meta_state.tree_state().nodes.get(nid, 0) < int(row.get("cap", 1)):
return
for i in range(1, 4):
@@ -489,14 +489,14 @@ func _check_easter_egg() -> void:
func toggle_attunement(item_id: String) -> bool:
var equipped: Array[Variant] = meta_state.attune_state().equipped
if not equipped.has(item_id):
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(item_id)
if arow.is_empty() or String(arow.get("form", "")) == "":
return false # 非同调行(过渡/退役行)禁装配
if not meta_state.item_unlocked(item_id):
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
if equipped.size() >= Attunement.slots(db, save):
return false # 满位禁选
var rank: int = int(db.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99))
var rank: int = int(db.crossrun.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99))
if rank > equipped.size() + 1:
return false # 件档高于将占格档(第 k 格档=k——普通/优秀/稀有递增)
equipped.append(item_id)
@@ -510,7 +510,7 @@ func toggle_attunement(item_id: String) -> bool:
func attune_camp_status() -> Dictionary:
var equipped: Array[Variant] = meta_state.attune_state().equipped
var items: Array[Dictionary] = []
for row in db.treasure_attunement_rows():
for row in db.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
items.append({
"item_id": iid,
@@ -522,7 +522,7 @@ func attune_camp_status() -> Dictionary:
"unlocked": meta_state.item_unlocked(iid),
})
var lvl := meta_state.attune_state().refresh_level
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(lvl + 1)
var next_row: Dictionary[Variant, Variant] = db.crossrun.meta.attune_refresh_row(lvl + 1)
return {
"slots": Attunement.slots(db, save),
"equipped": equipped,
@@ -538,12 +538,12 @@ func attune_camp_status() -> Dictionary:
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
func attune_refresh_charges() -> int:
return int(db.meta_attune_refresh_row(meta_state.attune_state().refresh_level).get("refresh_charges", 0))
return int(db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level).get("refresh_charges", 0))
# Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款)
func buy_attune_refresh() -> bool:
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(meta_state.attune_state().refresh_level + 1)
var next_row: Dictionary[Variant, Variant] = db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1)
if next_row.is_empty():
return false
if not meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
+2 -2
View File
@@ -81,8 +81,8 @@ func reset_paid() -> void:
# 本次 GP 付费刷新价(与商店货架刷新同源:ceil(基价 × 曲线^本次停点已付费刷))
func gp_cost() -> int:
return int(ceil(float(db.adventure.shop_refresh_cost_base)
* pow(db.adventure.shop_refresh_cost_curve, float(_paid_refreshes))))
return int(ceil(float(db.run.adventure.shop_refresh_cost_base)
* pow(db.run.adventure.shop_refresh_cost_curve, float(_paid_refreshes))))
# 冒险级复位(宿主 reset_adventure_state 调用——刷新三源与付费计数全零)
+4 -4
View File
@@ -167,7 +167,7 @@ func retire_mapping() -> bool:
# ---- 极性路由字典面(战斗结算 授予段消费):(类型, 极性)→ 格面池字典行(拷贝出口)----
func polarity_row(cell_type: String, polarity: String) -> Dictionary:
for row in db.reward.rows:
for row in db.run.reward.rows:
if String(row.get("pool_tag", "")) == "cell" \
and String(row.get("cell_type", "")) == cell_type \
and String(row.get("polarity", "")) == polarity:
@@ -309,7 +309,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array[Dictionary] = []
for row in db.reward.rows:
for row in db.run.reward.rows:
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
var iid := String(row.get("effect_id", ""))
if iid == "" or not _treasure_unlocked.has(iid):
@@ -321,7 +321,7 @@ func _rollable_rows() -> Array:
func _pool_rows(pool_tag: String) -> Array:
var out: Array[Dictionary] = []
for row in db.reward.rows:
for row in db.run.reward.rows:
if row.pool_tag == pool_tag and int(row.weight) > 0:
out.append(row)
return out
@@ -335,7 +335,7 @@ func _pool_weight(rows: Array) -> int:
func _row_by_id(rid: String) -> Dictionary:
for row in db.reward.rows:
for row in db.run.reward.rows:
if row.reward_id == rid:
return row
return {}
+4 -4
View File
@@ -120,7 +120,7 @@ func _charge_gate(v: Dictionary, target: Vector2i) -> Dictionary:
for h in tu_hits:
if String(h.get("kind", "")) == "counter_spend":
if not EffectEngine.counter_spend(fx_host.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 1))):
var vrow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.item_id))
var vrow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.item_id))
var cn_info: int = int(vrow.get("charge_n", 0)) # 读受理行自身
var seen := int(attune_safe_seen_cb.call())
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), seen % cn_info if cn_info > 0 else 0, cn_info]}
@@ -199,8 +199,8 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool:
# premium_pct 贯穿 _apply_percent_row 应用与呈现同乘)——标空/挖格/清图不乘,雷安同池冻结不动。
# 取整=四舍五入(与伤害管线 round_direction=floor 分域——registry clause3_inequality 登记;
# 1 点量纲行 ac_up/action 取整后不变=整数粒度地板)。
if source_class == "seal_hit" and int(db.settlement.core.seal_hit_premium) != 100:
var prem_pct: int = int(db.settlement.core.seal_hit_premium)
if source_class == "seal_hit" and int(db.combat.settlement.core.seal_hit_premium) != 100:
var prem_pct: int = int(db.combat.settlement.core.seal_hit_premium)
row = row.duplicate()
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
row["premium_pct"] = prem_pct
@@ -229,7 +229,7 @@ func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool:
var eff := String(row.effect_id)
if eff == "gp_gain" or eff == "gp_loss":
return _apply_gp_row(row, att)
var srow: Dictionary[Variant, Variant] = db.status_effect_row(eff)
var srow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff)
# 封印溢价贯穿(仅封印命中正向行携带 premium_pct;其余授予路径缺省 100 零扰动):
# 应用幅度与呈现行值同乘,容器经 magnitude_override 拿同一快照(抗表重载口径保持)。
var prem_pct: int = int(row.get("premium_pct", 100))
+2 -2
View File
@@ -78,7 +78,7 @@ func _enemy_actions(ri: int) -> void:
if int(oc._enemy_system.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
var skill_id := oc._enemy_system.pending_action(u)
var srow: Dictionary[Variant, Variant] = oc.db.skill_row(skill_id)
var srow: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(skill_id)
if srow.is_empty():
oc._fatal("敌实例待发动作悬空: %s" % skill_id)
continue
@@ -97,7 +97,7 @@ func _enemy_actions(ri: int) -> void:
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0
if srow.has("rider_mult"):
rider = maxi(int(oc.db.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
rider = maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action",
"%s %s" % [u.display_name, String(srow.display_name)],
{"channel": "combat", "base_value": baseline, "target_rule": DamageController.TARGET_HERO,
+2 -2
View File
@@ -147,7 +147,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
if fx_host._fx_bonus_delta.has(axis):
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(oc.db.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var blast_v := maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell})
@@ -163,7 +163,7 @@ func _terrain_batch(ri: int) -> void:
for evt in oc._facts_reveals:
var vids: Array[Variant] = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary[Variant, Variant] = oc.db.variation.row_by_id(String(vid))
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
+16 -16
View File
@@ -34,7 +34,7 @@ func bind(refs: Dictionary, hooks: Dictionary) -> void:
# 商店覆盖层数据源(只读投影——现价/已购档数在此现算,呈现层禁复算定价公式)
func shop_products() -> Array:
var out: Array[Dictionary] = []
for prow in db.shop_products():
for prow in db.run.shop.product_rows():
out.append({
"product_id": String(prow.get("product_id", "")),
"display_name": String(prow.get("display_name", "")),
@@ -49,7 +49,7 @@ func shop_products() -> Array:
# 现价 = base_price + price_level_bonus(level_index) + price_increment × 该商品已购档数
# (散折价:economy_spend 激活时商品标价 ×80%——见 _spend_discounted)
func current_price(prow: Dictionary) -> int:
return _spend_discounted(int(prow.get("base_price", 0)) + db.shop_price_level_bonus(_level_index()) \
return _spend_discounted(int(prow.get("base_price", 0)) + db.run.shop.price_level_bonus_at(_level_index()) \
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", ""))))
@@ -58,7 +58,7 @@ func current_price(prow: Dictionary) -> int:
# + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折)
func _spend_discounted(price: int) -> int:
if _rules().has("economy_spend"):
return maxi(1, int(floor(float(price) * float(db.shop.spend_discount))))
return maxi(1, int(floor(float(price) * float(db.run.shop.spend_discount))))
return price
@@ -69,7 +69,7 @@ func purchased_tier(product_id: String) -> int:
# 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用):
# 出账 → 加算区 += flat_value → 购血同步当前生命(封顶新上限)→ 已购档数 +1
func buy_product(product_id: String) -> bool:
var prow: Dictionary[Variant, Variant] = db.shop_product_row(product_id)
var prow: Dictionary[Variant, Variant] = db.run.shop.product_row(product_id)
if prow.is_empty():
push_error("[关卡节点地图] 商店房未知商品: %s" % product_id)
return false
@@ -133,7 +133,7 @@ func leave() -> void:
func _draw_shelf() -> void:
shelf = []
var sellable: Array[Variant] = []
for row in db.shop_pool_rows():
for row in db.run.shop.pool_rows():
var shelf_kind := String(row.get("kind_id", ""))
if _pool_cap.is_valid() and not bool(_pool_cap.call(shelf_kind, "")):
continue # cap 满剔除(货架候选视图剔除;牌库本体不动)
@@ -184,14 +184,14 @@ func pool_stock(kind: String) -> int:
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外)
# 基价 provisional 15(表 knob relic_base_price——shop.tres 词表扩列归数值批)。
func pool_price(kind: String) -> int:
var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind)
var row: Dictionary[Variant, Variant] = db.run.shop.pool_row(kind)
if row.is_empty():
if kind == "relic":
return _spend_discounted(int(ceil(float(db.shop.relic_base_price) * pow(db.shop_price_b(), float(maxi(0, _level_index() - 1))) * pow(db.shop_price_a(), float(visit_buys)))))
return _spend_discounted(int(ceil(float(db.run.shop.relic_base_price) * pow(db.run.shop.price_b, float(maxi(0, _level_index() - 1))) * pow(db.run.shop.price_a, float(visit_buys)))))
return 999999
var base := float(maxi(1, int(row.get("base_price", 1))))
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, _level_index() - 1))) \
* pow(db.shop_price_a(), float(visit_buys))
var price_f: float = base * pow(db.run.shop.price_b, float(maxi(0, _level_index() - 1))) \
* pow(db.run.shop.price_a, float(visit_buys))
return _spend_discounted(int(ceil(price_f)))
@@ -202,11 +202,11 @@ func shop_shelf() -> Array:
for tok in shelf:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind)
var row: Dictionary[Variant, Variant] = db.run.shop.pool_row(kind)
var display := String(row.get("display_name", kind))
var value := int(db.adventure.curio_value(kind, tier))
var value := int(db.run.adventure.curio_value(kind, tier))
if row.is_empty() and kind == "relic":
var rrow: Dictionary[Variant, Variant] = db.feat_row(String(tok.get("feat_id", "")))
var rrow: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(String(tok.get("feat_id", "")))
display = String(rrow.get("display_name", "特性"))
value = 0
out.append({
@@ -227,22 +227,22 @@ func shop_shelf() -> Array:
func shop_refresh_free_left() -> int:
# 刷新执照 shop_refresh_bonus 装配合计叠加(进店入账语义=读数即本店可用,
# 与基础 knob 同生命周期;未装配=基值)
return maxi(0, int(db.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
return maxi(0, int(db.run.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
# 刷新执照装配合计(装配集 kind 分发扫描;零装配=0)
func _attune_shop_refresh_bonus() -> int:
var total := 0
for iid_v in _attunement():
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"):
total += int(arow.get("magnitude", 0))
return total
func shop_refresh_cost() -> int:
return int(ceil(float(db.adventure.shop_refresh_cost_base)
* pow(db.adventure.shop_refresh_cost_curve, float(paid_refreshes))))
return int(ceil(float(db.run.adventure.shop_refresh_cost_base)
* pow(db.run.adventure.shop_refresh_cost_curve, float(paid_refreshes))))
func shop_refresh() -> bool:
+2 -2
View File
@@ -29,7 +29,7 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int
if not _open:
push_warning("[增益状态] 域闭授予(EC1)")
return false
var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id)
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
if row.is_empty():
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
return false
@@ -98,7 +98,7 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
func query_active(side: String) -> Array:
var out: Array[Dictionary] = []
for inst in _instances:
var row: Dictionary[Variant, Variant] = db.status_effect_row(inst.effect_id)
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(inst.effect_id)
if row.is_empty():
continue
if String(row.target_side) != side:
+4 -3
View File
@@ -68,9 +68,10 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool:
func _item_row(item_id: String) -> Dictionary:
if _db == null:
return {}
if _db.has_method("treasure_item_row"):
return _db.treasure_item_row(item_id) # 表访问收口(正式面走 ConfigDB 访问器)
return _db.treasure_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役)
var db_crossrun = _db.get("crossrun")
if db_crossrun != null:
return db_crossrun.treasure_items.row_by_id(item_id) # 表访问收口(正式面走 ConfigDB 容器直连)
return _db.treasure_items.row_by_id(item_id) # 桩表面(单测 StubDB 无容器——直读保留,随测试面演进退役)
# 不标具体类型原因:单元测试注入 StubDB 桩表(treasure_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注
var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表)
+8 -8
View File
@@ -40,21 +40,21 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
return false
# 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序)
var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth, row_kind)
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(level_index, node_depth, row_kind)
if rowset.is_empty():
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind])
assert(false, "遭遇行悬空(R1 前置守卫)")
return false
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp)
var eff_atk: int = atk_override if atk_override >= 1 else int(db.combat.hero.atk)
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.combat.hero.max_hp)
if hero_hp > eff_max_hp or hero_hp <= 0:
push_warning("[敌人系统] hero_hp 入参越界(EC16): %d" % hero_hp)
return false
# max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁
# (恢复点/治疗包的满血守卫与治疗上限随之失效)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": String(db.hero.display_name), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": String(db.combat.hero.display_name), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.crossrun.forge.default_armor_type, "attack_type": hero_attack_type if hero_attack_type != "" else db.crossrun.forge.default_attack_type} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面)
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰:施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15)
var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth)
var mod: Dictionary[Variant, Variant] = db.run.difficulty.resolve_modifiers(level_index, node_depth)
var hp_mult: float = float(mod.get("hp_mult", 1.0))
var atk_mult: float = float(mod.get("atk_mult", 1.0))
# 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值;
@@ -115,7 +115,7 @@ func _pool_snapshot(row: Dictionary) -> Array[Dictionary]:
"weight": int(entry.get("weight", 0)),
"cooldown_left": 0,
}
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(String(entry.get("skill_id", "")))
var srow_snap: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(String(entry.get("skill_id", "")))
if srow_snap.has("trigger"):
snap_entry["trigger"] = (srow_snap.get("trigger", {}) as Dictionary).duplicate(true)
if bool((snap_entry["trigger"] as Dictionary).get("latch", false)):
@@ -218,7 +218,7 @@ func special_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
for e in u.pool:
if _entry_blocked(e, u, facts):
continue
var srow: Dictionary[Variant, Variant] = db.skill_row(String(e.skill_id))
var srow: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(String(e.skill_id))
if not srow.is_empty() and String(srow.type) != "basic":
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
return out
@@ -276,7 +276,7 @@ func commit_action(u: Dictionary) -> void:
if sid == "":
_diag("commit_action 无待发动作(兑现序违例)")
return
var srow: Dictionary[Variant, Variant] = db.skill_row(sid)
var srow: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
var cd: int = int(srow.get("cooldown_beats", 0)) if not srow.is_empty() else 0
for e in u.pool:
if String(e.skill_id) == sid:
+8
View File
@@ -108,3 +108,11 @@ func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
if r0_cap > (w * h) / 2:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 进度行解析委托(消费面 db.combat.board_gen.resolve_row——R1 ceil/clamp 语义归 ConfigDbResolver)
func resolve_row(L: int, D: int) -> Dictionary:
return _resolver.combat.board_gen.resolve_row(L, D)
+8
View File
@@ -28,3 +28,11 @@ func validate() -> Array:
if v < MULT_MIN or v > MULT_MAX:
errs.append({"category": "mult_oor_warning", "message": "%s (%s) 越域 [0.1,10] 使用时 clamp: %f" % [key, k, v]})
return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 难度修饰解析委托(消费面 db.run.difficulty.resolve_modifiers——clamp 语义归 ConfigDbResolver)
func resolve_modifiers(L: int, D: int) -> Dictionary:
return _resolver.run.difficulty.resolve_modifiers(L, D)
+8
View File
@@ -207,3 +207,11 @@ static func band_of(band_keys: Array, D: int) -> int:
# 进度键解析单点收口(全仓统走
# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧),
# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver)
func resolve_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
return _resolver.combat.enemy.resolve_rowset(L, D, row_kind)
+24
View File
@@ -158,3 +158,27 @@ func _validate_initial_coverage(initial_ids: Dictionary) -> Array[Dictionary]:
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
return errs
# ---- 行族访问器(自 ConfigDB 归位;消费面 db.crossrun.forge.<m> 直连)——
# routes 属性在册故行族读取命名 route_rows(避开成员名冲突)----
## 六路线行族(副本——调用方禁改)
func route_rows() -> Array:
return routes.duplicate()
## 路线行单点(键 route_id;缺行=空字典)
func route(route_id: String) -> Dictionary:
for row in routes:
if String(row.get("route_id", "")) == route_id:
return row
return {}
## 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
func armor_route_by_type(armor_type: String) -> Dictionary:
for row in routes:
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
return row
return {}
+1 -1
View File
@@ -1,5 +1,5 @@
## 关卡节点图参数档 schema(节点图生成器核心批 卡① T1):五旋钮+战斗保底+岔口去路数的行式数据表。
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.level_gen_params_row() 读取;生成器只吃参数档、不认关卡号(契约 6)。
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.run.level_gen_params.row_by_level_index() 读取;生成器只吃参数档、不认关卡号(契约 6)。
## 数值全 provisional(定值批落终值);域校验沿关卡节点图生成.md v1.1 调参表已锁条款——层深 L1≥6 递增增幅变大、
## 配比四类合计 1、出口 L1 恒 1/L2-L3 1-3、岔口去路数恒 2/3、战斗保底≥0。
class_name LevelGenParams
+5
View File
@@ -170,3 +170,8 @@ static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> v
if (outs[nid] as Array).has(cur) and not seen.has(int(nid)):
seen[int(nid)] = true
queue.append(int(nid))
## 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径)
func level_count() -> int:
return rows.size()
+18
View File
@@ -90,3 +90,21 @@ func _validate_attune_refresh() -> Array[Dictionary]:
errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]})
break
return errs
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.crossrun.meta.<m> 直连)----
## Boss 钥石两档行单点(键 level_index;缺行=空字典)
func boss_keystone_row(level_index: int) -> Dictionary:
for row in boss_keystone:
if int(row.get("level_index", -1)) == level_index:
return row
return {}
## Y 刷新升级行单点(键 = 等级,缺行返回空字典=未购/已满级合法态)
func attune_refresh_row(level: int) -> Dictionary:
for row in attune_refresh_upgrade:
if int(row.get("level", -1)) == level:
return row
return {}
+52
View File
@@ -133,3 +133,55 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void:
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
if spend_discount <= 0.0 or spend_discount > 1.0:
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop.<m> 直连)----
## GP 掉落行单点(键 tier;缺行=空字典——掉落按 0 兑付的兜底在调用方)
func gp_drop_row(tier: String) -> Dictionary:
for row in gp_drops:
if String(row.get("tier", "")) == tier:
return row
return {}
## 关卡 GP 加成单点(键 level_index;缺行=0)
func level_gp_bonus_at(level_index: int) -> int:
for row in level_gp_bonus:
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
## 价格关卡加成单点(键 level_index;缺行=0)
func price_level_bonus_at(level_index: int) -> int:
for row in price_level_bonus:
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
## 商品行族({product_id, display_name, attr, flat_value, base_price, price_increment})
func product_rows() -> Array:
return products
## 商品行单点(键 product_id;缺行=空字典)
func product_row(product_id: String) -> Dictionary:
for row in products:
if String(row.get("product_id", "")) == product_id:
return row
return {}
## 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
func pool_rows() -> Array:
return pool_shelf
## 货架行单点(键 kind_id;缺行=空字典)
func pool_row(kind_id: String) -> Dictionary:
for row in pool_shelf:
if String(row.get("kind_id", "")) == kind_id:
return row
return {}
+35
View File
@@ -110,3 +110,38 @@ func row_by_id(variant_id: String) -> Dictionary:
if row.get("variant_id", "") == variant_id:
return row
return {}
# ---- 候选池访问器(自 ConfigDB 归位;消费面 db.combat.variation.<m> 直连)----
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
## 候选池门控(∅ board_types = 通配;字段名对齐 board_type 词表,无 biome_tags)
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
var pool: Array[Dictionary] = []
for row in rows:
if String(row.get("scope", "region")) == "global" \
and node_type != "boss" and node_type != "elite":
continue
if int(row.get("unlock_level", 999)) > level_index:
continue
var tags: Array[Variant] = row.get("board_types", [])
if tags.is_empty():
pool.append(row)
continue
if tags.has(board_type):
pool.append(row)
return pool
## 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
## boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
## 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
if node_type != "boss" and node_type != "elite":
return {}
for row in variant_pool(level_index, board_type, node_type):
if String(row.get("scope", "region")) == "global":
return row
return {}
+1 -1
View File
@@ -73,7 +73,7 @@ static func boss_name(runner: Variant, db: ConfigDb) -> String:
if runner != null:
var node: Dictionary[Variant, Variant] = runner.current_node()
if String(node.get("type", "")) == "boss":
var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
if not rowset.is_empty():
return String(rowset[0].get("display_name", ""))
return "遗迹守卫"
+4 -4
View File
@@ -19,8 +19,8 @@ func _init(db_ref: ConfigDb, debug_overlay_ref: DebugOverlay, settlement_getter:
# 禁表写路径(双写叠加隐患源头);
# 合成式单点在 UiKit.scaled——与调试浮窗生效值预览共用,本类不持有同式私有实现)
func debug_hero_override() -> Dictionary:
return {"atk": UiKit.scaled(int(_db.hero.atk), _debug_overlay.atk_pct()),
"max_hp": UiKit.scaled(int(_db.hero.max_hp), _debug_overlay.hp_pct())}
return {"atk": UiKit.scaled(int(_db.combat.hero.atk), _debug_overlay.atk_pct()),
"max_hp": UiKit.scaled(int(_db.combat.hero.max_hp), _debug_overlay.hp_pct())}
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音;
@@ -45,7 +45,7 @@ func variant_mask_lookup(c: Vector2i) -> bool:
if b == null:
return false
for vid in b.variant_ids(c):
var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid))
var vrow: Dictionary[Variant, Variant] = _db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) == "hide_number":
return true
return false
@@ -61,7 +61,7 @@ func variant_mark_lookup(c: Vector2i) -> String:
if b == null:
return ""
for vid in b.variant_ids(c):
var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid))
var vrow: Dictionary[Variant, Variant] = _db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) == "flat_damage":
return "flat_damage"
return ""
+4 -4
View File
@@ -107,7 +107,7 @@ func on_camp_tree_buy(node_id: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
var ok: bool = meta.tree_buy_fork(node_id) if String(db.crossrun.meta_tree.row_by_id(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
if not ok:
toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
return
@@ -157,7 +157,7 @@ func on_camp_forge_select(route_id: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
var ok: bool = meta.forge_select("weapon", route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \
var ok: bool = meta.forge_select("weapon", route_id) if String(db.crossrun.forge.route(route_id).get("kind", "")) == "weapon" \
else meta.forge_select("armor", route_id)
if not ok:
toast.show_toast("切换被拒——路线未解锁", "warn")
@@ -177,7 +177,7 @@ func on_shop_buy(product_id: String) -> void:
toast.show_toast("GP不足——买不起这件商品", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
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
@@ -199,7 +199,7 @@ func on_shop_refresh() -> void:
toast.show_toast("GP不足——刷新失败", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
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()
@@ -165,9 +165,9 @@ func _curio_card_texts(candidates: Array) -> Array:
text = "专长获取
选中或买下后立刻开一次专长三选一"
elif kind == "relic":
text = Narrator.feat_card_text(db.feat_row(rid))
text = Narrator.feat_card_text(db.crossrun.feats.row_by_id(rid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
text = Narrator.bonus_card_text(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
@@ -251,6 +251,6 @@ func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = _meta_get.call()
if meta != null:
for row in db.treasure_attunement_rows():
for row in db.crossrun.treasure_items.attunement_rows():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup
+7 -7
View File
@@ -79,7 +79,7 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态——RAP 读数在本条) ----
static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, refresh_remaining: int) -> void:
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
ap_bar.set_ap(int(s.ap), int(db.run.action_economy.ap_per_round))
ap_bar.set_rap(int(s.rap))
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
ap_bar.set_refresh_state(refresh_remaining,
@@ -92,11 +92,11 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, ref
# 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制)
static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]:
var rlines: Array[String] = []
if db == null or db.feats == null or (db.feats.rows as Array).is_empty() or held.is_empty():
if db == null or db.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty():
return rlines
var counts := {}
for rid in held:
var rrow: Dictionary[Variant, Variant] = db.feat_row(String(rid))
var rrow: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(String(rid))
var fname := String(rrow.get("display_name", String(rid)))
counts[fname] = int(counts.get(fname, 0)) + 1
for fname in counts.keys():
@@ -112,7 +112,7 @@ static func board_variant_lines(db: ConfigDb, ids: Array) -> Array[String]:
if db == null or db.variation == null:
return lines
for vid in ids:
var row: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid))
var row: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid))
if not row.is_empty():
lines.append(String(row.get("display_name", vid)))
lines.append("回复与护甲资源减少")
@@ -257,9 +257,9 @@ static func status_curios(treasure_ui: Variant) -> Array:
static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaProgressionCore) -> Dictionary:
var clears: Array[Dictionary] = []
var boss_first := 0
for i in db.level_count():
for i in db.run.level.level_count():
var li: int = i + 1
clears.append({"display_name": String(db.level_row(li).get("display_name", "")),
clears.append({"display_name": String(db.run.level.row_by_level_index(li).get("display_name", "")),
"count": save_manager.meta_state.career_state().clear_counts.get(str(li), 0)})
if save_manager.meta_state.career_state().boss_first_kill.has(str(li)):
boss_first += 1
@@ -268,7 +268,7 @@ static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaP
"cleared": save_manager.meta_state.career_state().adventure_cleared,
"clears": clears,
"boss_first_kills": boss_first,
"boss_first_total": db.level_count(),
"boss_first_total": db.run.level.level_count(),
"boss_kills": save_manager.meta_state.career_state().boss_kill_count,
"keystone": meta.meta_state.keystone_balance(),
}
+4 -4
View File
@@ -447,7 +447,7 @@ func _on_battle_finished(payload: Dictionary) -> void:
match String(decision.outcome):
"next_layer":
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
int(decision.layer), int(decision.carry_hp), int(db.hero.max_hp)]
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
_inter_overlay.visible = true
_refresh_presentations()
return
@@ -772,11 +772,11 @@ func _on_refresh_confirmed() -> void:
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold")
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
var cost: int = int(db.action_economy.rap_refresh_cost)
var cost: int = int(db.run.action_economy.rap_refresh_cost)
var result := "rejected_rap" if rap < cost else "rejected_cap"
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
if result == "rejected_rap":
@@ -811,7 +811,7 @@ func _refresh_used_snapshot() -> int:
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining)
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
_sync_settlement_visuals()
+1 -1
View File
@@ -80,7 +80,7 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
root._open_modal(root.shop_overlay)
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.shop_price_a())) # 爬价规则行
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_products(), int(root.run_scope.runner.gp))
+3 -3
View File
@@ -81,7 +81,7 @@ static func _build_battle_wiring(root: Variant) -> void:
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
root.logger = PlaytestLogger.new()
root.logger.ap_per_round = int(root.db.action_economy.ap_per_round)
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
static func _build_overlays(root: Variant) -> void:
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
@@ -193,7 +193,7 @@ static func _build_debug_and_resolvers(root: Variant) -> void:
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.hero.atk), int(root.db.hero.max_hp))
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
root.add_child(root.debug_overlay)
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
@@ -235,7 +235,7 @@ static func _build_modal_flow(root: Variant) -> void:
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
root._apply_layer_contract()
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.hero.max_hp))
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
static func fatal_screen(root: Variant, msg: String) -> void:
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
@@ -44,7 +44,7 @@ func unlocked_set() -> Dictionary:
var out := {}
if meta == null:
return out
for row in db.treasure_transitional_rows():
for row in db.crossrun.treasure_items.transitional_rows():
var iid := String(row.get("item_id", ""))
if meta.meta_state.item_unlocked(iid):
out[iid] = true
@@ -79,7 +79,7 @@ func on_slot_clicked(slot_handle: int) -> void:
break
if rec.is_empty() or bool(rec.get("used", false)):
return
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
if String(arow.get("target", "none")) == "none":
attempt_use(slot_handle) # AP 类:显式无目标即用(不再伪造哨兵坐标)
return
@@ -106,7 +106,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void:
refresh_slots()
refresh_overlays()
board_view.refresh() # 移除格即时落图(无移除态的件 diff 零变化幂等)
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.get("item_id", "")))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.get("item_id", "")))
toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info")
@@ -144,7 +144,7 @@ func inventory_projection() -> Array:
# 库存投影行(字形=名首字;usable=整图零信息预判回调接排雷玩法只读面)+
# 积点进度浮窗行(自动兑付件 C 区悬停 tooltip 补 N/M 进度;件 id 锁定=装配行唯一)
func _projection_row(rec: Dictionary, cs: Dictionary) -> Dictionary:
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
var dn := String(arow.get("display_name", ""))
var note := ""
if bool(cs.get("assembled", false)) and String(cs.get("item_id", "")) == String(rec.get("item_id", "")):
+1 -1
View File
@@ -38,7 +38,7 @@ func run(n: int) -> void:
# 共享日志会话(模拟运行与真人试玩同日志合规——操作/轮次/战斗/关卡完成全量 JSONL;
# 一文件多局,run 行携带种子切分局次)
var logger = PlaytestLogger.new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
var log_path: String = logger.start_session(SEED_BASE)
print("[tune_sim] 模拟日志: %s" % log_path)
panel.set_sim_running(true)
+2 -2
View File
@@ -32,7 +32,7 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
var board_zone := BoardZoneView.new()
bottom.add_child(board_zone)
board_zone.ap_bar.set_refresh_cost(int(db.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
board_zone.ap_bar.set_refresh_cost(int(db.run.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
# 结算演出覆盖层(A–D 之上——z 序 = addChild 序,模态覆盖层组在 build_overlays 中更晚挂入)
var settlement_fx := SettlementFxView.new()
@@ -65,7 +65,7 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
# 本组不含明细回看 overlay——冒险中回看归「生涯」页;
# 本组不含营地覆盖层——在 camp_screen 屏壳构建)
static func build_overlays(host: Control, db: ConfigDb) -> Dictionary:
var legend_overlay := LegendOverlay.new(int(db.action_economy.rap_refresh_cost)) # RAP 行表值注入
var legend_overlay := LegendOverlay.new(int(db.run.action_economy.rap_refresh_cost)) # RAP 行表值注入
host.add_child(legend_overlay)
var node_overlay := NodeOverlay.new()
+2 -2
View File
@@ -133,7 +133,7 @@ func _flush_pending_agg() -> void:
func _source_label(src_key: String) -> String:
if src_key == "":
return ""
return String(_db.settlement.core.source_label_map.get(src_key, src_key))
return String(_db.combat.settlement.core.source_label_map.get(src_key, src_key))
func _gold_frame(text: String) -> String:
@@ -171,7 +171,7 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
if row.get("roll", null) != null:
roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)]
if bool(row.get("crit", false)):
roll_txt += " · 暴击×%d" % int(_db.settlement.damage_roll.crit_multiplier)
roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier)
elif int(row.tier) < 100:
roll_txt += " · ×%d%%" % int(row.tier)
roll_txt += "〕"
+3 -3
View File
@@ -82,7 +82,7 @@ func _ready() -> void:
_problems.append("脊柱生命行缺失(点击路径)")
else:
var bal_before: int = iso_c.meta_state.keystone_balance()
var price: int = int(main.db.tree_node("sp_hp").get("price", 0))
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
@@ -834,7 +834,7 @@ func _run_modal_probes_once() -> void:
var oracle: Array = []
for rec in rnr.treasure_inventory.query_inventory(Callable()):
var rr: Dictionary = rec
oracle.append({"name": String(main.db.treasure_item_row(String(rr.get("item_id", ""))).get("display_name", "")),
oracle.append({"name": String(main.db.crossrun.treasure_items.row_by_id(String(rr.get("item_id", ""))).get("display_name", "")),
"used": bool(rr.get("used", false))})
var sd_curios: Array = (sd as Dictionary).get("curios", [])
var curios_ok: bool = (sd_curios as Array).size() == (oracle as Array).size()
@@ -896,7 +896,7 @@ func _run_modal_probes_once() -> void:
var used_now: int = int(main.run_scope.settlement.presentation_snapshot().get("refresh_used", 0))
if used_now != 1:
_problems.append("刷新轮内计数错误(%d)" % used_now)
var remain_expect: int = maxi(0, int(main.db.action_economy.refresh_cap_per_round) - used_now)
var remain_expect: int = maxi(0, int(main.db.run.action_economy.refresh_cap_per_round) - used_now)
if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect])
+1 -1
View File
@@ -99,7 +99,7 @@ func _ready() -> void:
problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text)
if main.stage.enemy_box.get_child_count() < 1:
problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count())
if main.ap_bar._dots.get_child_count() != int(main.db.action_economy.ap_per_round):
if main.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count())
if main.ap_bar.refresh_remaining_text() == "":
problems.append("D 区弃图刷新按钮文案缺失")
+2 -2
View File
@@ -201,8 +201,8 @@ func _ready() -> void:
], 20)
_settle()
var ticket_row: Dictionary = main.shop_overlay._shelf_rows[0]
if String(main.db.shop_pool_row("feat_ticket").get("display_name", "")) != "专长获取":
problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.shop_pool_row("feat_ticket").get("display_name", "")))
if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取":
problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")))
if ticket_row.card.name_l.text != "专长获取":
problems.append("[shop_term] 专长凭证件名未用物品名「专长获取」(现=%s)" % ticket_row.card.name_l.text)
if ticket_row.btn.get_parent() != ticket_row.card.body or ticket_row.btn.get_index() >= ticket_row.card.foot_l.get_index():
+4 -4
View File
@@ -36,8 +36,8 @@ func _ready() -> void:
await get_tree().process_frame
var problems: Array = []
# 基线读表(表值驱动——不写字面量)
var base_atk := int(main.db.hero.atk)
var base_hp := int(main.db.hero.max_hp)
var base_atk := int(main.db.combat.hero.atk)
var base_hp := int(main.db.combat.hero.max_hp)
var boosted_atk := base_atk * 2 # 攻击滑条 100 → ×2
var boosted_hp := int(round(base_hp * 1.5)) # 生命滑条 50 → +50%
# ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡——
@@ -86,8 +86,8 @@ func _ready() -> void:
if main.debug_overlay.visible or not main._start_overlay.visible:
problems.append("关闭后层级状态错误")
# ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级)
if int(main.db.hero.atk) != base_atk or int(main.db.hero.max_hp) != base_hp:
problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.hero.atk), str(main.db.hero.max_hp), base_atk, base_hp])
if int(main.db.combat.hero.atk) != base_atk or int(main.db.combat.hero.max_hp) != base_hp:
problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.combat.hero.atk), str(main.db.combat.hero.max_hp), base_atk, base_hp])
# ⑧ 单次加成生效回归断言:应用加成 + 启动单场战斗 → 呈现快照 hero 必须是"基线 × 单次加成"
# (攻击 ×2 / 生命 +50%)——旧缺陷(残留值被当基线再乘)在 encounter 参数链上同样被抓获
# 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
+2 -2
View File
@@ -78,7 +78,7 @@ func _initialize() -> void:
var slots: int = 3
var passives: Array = []
var actives: Array = []
for r in db.treasure_attunement_rows():
for r in db.crossrun.treasure_items.attunement_rows():
if String(r.get("form", "")) == "passive":
passives.append(String(r.get("item_id", "")))
elif String(r.get("form", "")) == "active_charged":
@@ -102,7 +102,7 @@ func _initialize() -> void:
for policy in policies:
# 每策略一个共享日志会话(全量 JSONL 体量随局数增长,分文件便于定位)
var logger = load("res://presentation/playtest_logger.gd").new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
var log_path: String = logger.start_session(seed0)
var tally := {}
for i in range(n):
+1 -1
View File
@@ -24,7 +24,7 @@ func _initialize() -> void:
var gen = load("res://core/board_generator.gd").new()
print("=== 板形统计:N=%d/行 · 种子基 %d ===" % [n, seed0])
print("%-4s %-4s %-8s %-8s %-10s %-10s %-10s %-10s %-10s %-10s" % ["L", "D", "宽x高", "类型", "|M|均", "|M|min", "|M|max", "|S|均", "雷密度实测", "达标率"])
for row in db.board_gen.rows:
for row in db.combat.board_gen.rows:
var L: int = int(row.get("level_index", 0))
var D: int = int(row.get("node_depth", -1))
var w: int = int(row.get("board_width", 0))
+8 -4
View File
@@ -9,9 +9,13 @@ extends SceneTree
class FakeDB:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
class _FakeBoardGen:
var row: Dictionary
func resolve_row(_L: int, _D: int) -> Dictionary:
return row
class _FakeCombat:
var board_gen := _FakeBoardGen.new()
var combat := _FakeCombat.new()
func status_effect_row(_id: String) -> Dictionary:
return {}
@@ -28,7 +32,7 @@ func _initialize() -> void:
return
var gen = load("res://core/board_generator.gd").new()
print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,r0_sd,r0_p90,r0_max,inf_mean,cap_rej_mean")
for row in db.board_gen.rows:
for row in db.combat.board_gen.rows:
var rd := {
"level_index": int(row.level_index), "node_depth": int(row.node_depth),
"board_width": int(row.board_width), "board_height": int(row.board_height),
+16 -16
View File
@@ -53,17 +53,17 @@ func _initialize() -> void:
func _probe_boss(db, seed_val: int) -> void:
var math = load("res://core/damage_math.gd")
var clamp_lo: int = int(db.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.core.pool_clamp_hi)
var clamp_lo: int = int(db.combat.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.combat.settlement.core.pool_clamp_hi)
var hero_def_negative := 0
for eff_id in db.settlement.core.effect_pool_map.keys():
if String(db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
for eff_id in db.combat.settlement.core.effect_pool_map.keys():
if String(db.combat.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
continue
var row: Dictionary = db.status_effect_row(eff_id)
var row: Dictionary = db.combat.status_effects.row_by_id(eff_id)
if not row.is_empty() and String(row.get("unit", "")) == "percent":
hero_def_negative = mini(hero_def_negative, int(row.get("magnitude", 0)))
print("表值面:hero_defense 池最负 percent 行 %d · pool_clamp [%d, %d] · damage_floor %d · AC 基准 %d(+ac_up×%d 层)" % [
hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_roll.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))])
hero_def_negative, clamp_lo, clamp_hi, int(db.combat.settlement.damage_roll.damage_floor), int(db.combat.hero.armor_class), int(db.combat.status_effects.row_by_id("ac_up").get("max_stack", 0))])
print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---")
var run_a := _run_boss(db, seed_val, false)
@@ -92,7 +92,7 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
settlement.step_row.connect(func(row): cap["rows"].append(row))
settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.combat.hero.max_hp), "node_type": "boss"}):
cap["result"] = "begin_failed"
return cap
var rounds := 0
@@ -175,11 +175,11 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径)
var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0}
for eid in last_pool:
var pool_name: String = String(db.settlement.core.effect_pool_map.get(eid, ""))
var pool_name: String = String(db.combat.settlement.core.effect_pool_map.get(eid, ""))
if pools.has(pool_name):
pools[pool_name] += int(last_pool[eid])
var clamp_lo: int = int(db.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.core.pool_clamp_hi)
var clamp_lo: int = int(db.combat.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.combat.settlement.core.pool_clamp_hi)
var pd: int = clampi(int(pools["hero_defense"]), clamp_lo, clamp_hi)
var pe: int = clampi(int(pools["enemy_output"]), clamp_lo, clamp_hi)
var lines: Array = []
@@ -194,11 +194,11 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
# 掷点口径重建:基线按行留档档位打折(暴击 ×倍率),附加必中
var tier: int = int(row.get("tier", 100))
var crit: bool = bool(row.get("crit", false))
var base_factor: float = float(tier) * (float(db.settlement.damage_roll.crit_multiplier) / 100.0 if crit else 1.0)
var base_factor: float = float(tier) * (float(db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if crit else 1.0)
var rolled_base := int(floor(float(baseline) * base_factor / 100.0))
var raw: int = rolled_base + attach
var tick: int = int(row.get("value", 0))
var expected: int = maxi(int(db.settlement.damage_roll.damage_floor), math.pool_damage(raw, pd, pe))
var expected: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), math.pool_damage(raw, pd, pe))
var roll_txt := "掷%s" % str(int(row.get("roll", -1)))
var zeroed := "✓" if tick == expected else "✗ 算术不符!"
lines.append("拍=%s %s/档%d%s 基线%d→掷后%d 附加%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [
@@ -266,7 +266,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary:
if t != "battle" and t != "boss":
continue
var depth := int(node.get("node_depth", -1))
var row: Dictionary = db.resolve_enemy_rowset(1, depth, t)[0] # 行集首行(row_kind 随节点类型)
var row: Dictionary = db.combat.enemy.resolve_rowset(1, depth, t)[0] # 行集首行(row_kind 随节点类型)
var lo := int(row.get("unit_count", 1))
var hi := int(row.get("unit_count_max", lo))
var rolled := lo
@@ -280,7 +280,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary:
func _level_row(db, level_index: int) -> Dictionary:
for row in db.level.rows:
for row in db.run.level.rows:
if int(row.level_index) == level_index:
return row
return {}
@@ -415,11 +415,11 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary:
var kind: String = String(dep.get("kind", "battle"))
total += 1
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
if not settlement.begin_battle({"level_index": 1, "node_depth": depth, "node_type": kind, "hero_hp": int(db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": depth, "node_type": kind, "hero_hp": int(db.combat.hero.max_hp)}):
bad += 1
continue
var n: int = settlement.presentation_snapshot().enemies.size()
var row: Dictionary = db.resolve_enemy_rowset(1, depth, kind)[0] # 行集首行(同上,row_kind 随节点类型)
var row: Dictionary = db.combat.enemy.resolve_rowset(1, depth, kind)[0] # 行集首行(同上,row_kind 随节点类型)
var lo := int(row.get("unit_count", 1))
var hi := int(row.get("unit_count_max", lo))
if n < 1 or n < lo or n > hi:
+1 -1
View File
@@ -19,7 +19,7 @@ func _initialize() -> void:
level = int(OS.get_cmdline_user_args()[0])
if OS.get_cmdline_user_args().size() > 1:
rng.set_master_seed(int(OS.get_cmdline_user_args()[1]))
settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.combat.hero.max_hp})
print("=== 棋盘摘要 L%d ===" % level)
var bd = settlement.minesweeper.current_board()
print("尺寸 %dx%d · 雷 %d · 安全格 %d · R₀ %d" % [bd.width, bd.height, bd.mines.size(), bd.safe_cells().size(), bd.metrics.r0_count])
+8 -4
View File
@@ -56,13 +56,17 @@ func _params_from(row: Dictionary) -> Dictionary:
}
class FakeDB:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
class _FakeBoardGen:
var row: Dictionary
func resolve_row(_L: int, _D: int) -> Dictionary:
return row
class _FakeCombat:
var board_gen := _FakeBoardGen.new()
var combat := _FakeCombat.new()
func status_effect_row(_id: String) -> Dictionary:
return {}
func _db(_real, row: Dictionary):
var f := FakeDB.new()
f.row = row
f.combat.board_gen.row = row
return f
+2 -2
View File
@@ -39,7 +39,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
if own_session:
logger = load("res://presentation/playtest_logger.gd").new()
logger.start_session(seed_val)
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
logger.log_run({"seed": seed_val, "level_index": level_index, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route})
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
@@ -117,7 +117,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
if own_session:
logger = load("res://presentation/playtest_logger.gd").new()
logger.start_session(seed_val)
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct,
"policy": policy, "route": route, "hero_override": hero_override})
var rng = load("res://core/rng_service.gd").new()
+2 -2
View File
@@ -44,7 +44,7 @@ func _run_encounter(db, L: int) -> void:
print("=== L%d · %d 种子(败 %d / 超时 %d)===" % [L, N, losses, timeouts])
print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct_f(rounds_v, 0.1), _pct_f(rounds_v, 0.9), _hist(rounds_v)])
print(" 每轮承伤 均值 %.2f 中位 %.1f p90 %.1f" % [_mean(dmg_per_round), _median_f(dmg_per_round), _pct_f(dmg_per_round, 0.9)])
print(" 整场承伤 均值 %.1f 中位 %.0f(主角 max_hp=%d)" % [_mean(dmg_total), _median_f(dmg_total), int(db.hero.max_hp)])
print(" 整场承伤 均值 %.1f 中位 %.0f(主角 max_hp=%d)" % [_mean(dmg_total), _median_f(dmg_total), int(db.combat.hero.max_hp)])
print(" 开安全格 均值 %.1f 中位 %d" % [_mean(safe_opened), _median(safe_opened)])
print(" 清图(换板) 均值 %.2f 中位 %d" % [_mean(clears), _median(clears)])
print(" 攻防buff授予 均值 %.2f" % [_mean(buff_grants)])
@@ -65,7 +65,7 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary:
settlement.minesweeper.cell_revealed.connect(func(evt):
if evt.kind == "safe" and evt.channel != "initial":
out.safe_opened += 1)
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}):
return out
var guard_rounds := 40
while out.result == "" and guard_rounds > 0:
+1 -1
View File
@@ -19,7 +19,7 @@ func _initialize() -> void:
quit(1)
return
var logger = load("res://presentation/playtest_logger.gd").new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
var log_path: String = logger.start_session(seed0)
print("节奏批日志: %s" % log_path)
var autoplay = load("res://sim/level_autoplay.gd")
+2 -2
View File
@@ -14,7 +14,7 @@ func _initialize() -> void:
db.load_tables("res://data/tables", false)
# ① 表侧(行集数组取首行,战斗节点查 battle 键域)
var row = db.resolve_enemy_rowset(1, 1, "battle")[0]
var row = db.combat.enemy.resolve_rowset(1, 1, "battle")[0]
print("[1] row(1,1) display=", row.get("display_name"), " unit_count=", row.get("unit_count"),
" pool=", row.get("pool"))
@@ -50,7 +50,7 @@ func _run_battle(db, seed_val: int, L: int, depth: int, casts: Dictionary) -> Di
if row.type == "damage" and row.source_class == "enemy_action":
var sid: String = str(row.get("skill_id", "?"))
casts[sid] = casts.get(sid, 0) + 1)
if not settlement.begin_battle({"level_index": L, "node_depth": depth, "hero_hp": int(db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": L, "node_depth": depth, "hero_hp": int(db.combat.hero.max_hp)}):
return out
var guard_rounds := 40
while out.result == "" and guard_rounds > 0:
+2 -2
View File
@@ -39,7 +39,7 @@ func _initialize() -> void:
quit(1)
return
var logger = load("res://presentation/playtest_logger.gd").new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
var log_path: String = logger.start_session(opt.seed0)
print("模拟日志: %s" % log_path)
var autoplay = load("res://sim/level_autoplay.gd")
@@ -193,7 +193,7 @@ func _spend(meta, policy_name: String) -> Array:
guard -= 1
var bought := false
for nid in order:
var price: int = int(meta.db.tree_node(nid).get("price", 0))
var price: int = int(meta.db.crossrun.meta_tree.row_by_id(nid).get("price", 0))
if price > 0 and meta.meta_state.keystone_balance() >= price and meta.tree_buy(nid):
out.append({"node_id": nid, "level": meta.save.meta_state.tree_state().nodes.get(nid, 0), "price": price})
bought = true
@@ -114,8 +114,8 @@ func _save_summary_integration() -> void:
var f9 = sm_script.new(path9)
f9.meta_state.apply_adventure_summary(st9.get("summary", {}))
var ks_run := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
for li in range(1, int(_db.run.level.rows.size()) + 1):
ks_run += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
Kit.check("摘要集成:冒险驱动满配冒险成功(seed 42)", String(st9.get("result", "")) == "completed")
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.meta_state.keystone_balance(), f9.meta_state.keystone_balance() == ks_run)
Kit.check("集成关卡完成次数表全 1", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1)
@@ -147,7 +147,7 @@ func _smoke_playtest_logger() -> void:
var snaps: Array = []
settlement.settlement_snapshot.connect(func(snap): snaps.append(snap))
Kit.check("会话文件开启(%s)" % path, path.contains("playtest_20260912"))
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}))
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
var minesweeper = settlement.minesweeper
for i in range(2):
minesweeper.settle_batch([{"action": "stop"}])
@@ -159,7 +159,7 @@ func _smoke_playtest_logger() -> void:
if not s1.has(k):
missing.append(k)
Kit.check("快照字段齐备(缺 %s)" % str(missing), missing.is_empty())
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.combat.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
# spawn_count 回归(playtest-2026-09-12 报告 Bug#1):停手两轮零击杀(主角攻击 6×2 < 24 HP 确定)→
# unit_count 必须 == spawn_count(全灭才允许 <);快照级区分「首轮全灭」vs「空场」的数据面在位
Kit.check("快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))],
@@ -35,7 +35,7 @@ func _init() -> void:
func _smoke_generated_map_determinism() -> void:
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
var gen = NodeMapGenerator.new(_fresh_rng(910001))
var params2: Dictionary = _db.level_gen_params_row(2)
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
var g_a: Dictionary = gen.generate(params2, 910002)
var g_b: Dictionary = gen.generate(params2, 910002)
var g_c: Dictionary = gen.generate(params2, 910003)
@@ -60,7 +60,7 @@ func _smoke_generated_map_determinism() -> void:
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
Callable(self, "_smoke_rest_stub"), true))
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
var params3: Dictionary = _db.level_gen_params_row(3)
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
var entry_count: int = int(params3.get("entry_count", -1))
Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count,
entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count)
@@ -81,7 +81,7 @@ func _smoke_generated_map_determinism() -> void:
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕",
adr_x.runner().nodes() == adr_y.runner().nodes())
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []))
adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []))
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
var bp: Dictionary = sv_w.breakpoint_snapshot()
@@ -96,11 +96,11 @@ func _smoke_generated_map_determinism() -> void:
adr_old.resume_adventure(bp_old, {}) == false)
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
var tpl_free := true
for li in range(1, _db.level_count() + 1):
if (_db.level_row(li) as Dictionary).has("templates"):
for li in range(1, _db.run.level.level_count() + 1):
if (_db.run.level.row_by_level_index(li) as Dictionary).has("templates"):
tpl_free = false
Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕",
tpl_free and _db.level_count() == 3)
tpl_free and _db.run.level.level_count() == 3)
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
sv.load_from_disk()
@@ -53,7 +53,7 @@ func _harness(db):
var ctrl = DamageControllerScript.new(db, rng, units, effects)
ctrl.emit_row_cb = Callable(self, "_on_row")
effects.notify_battle_start()
var spawn_ok: bool = units.spawn(1, 0, int(db.hero.max_hp))
var spawn_ok: bool = units.spawn(1, 0, int(db.combat.hero.max_hp))
return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok}
@@ -79,15 +79,15 @@ func _run_recipe_sequence(db) -> Array:
for e in units.all_enemies():
enemy_hp_before.append(int(e.hp))
var front = units.default_target()
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.settlement.damage_roll.attack_bonus)
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.combat.settlement.damage_roll.attack_bonus)
var atk_type := ""
var eff_keys: Array = h.db.settlement.damage_roll.armor_effectiveness.keys()
var eff_keys: Array = h.db.combat.settlement.damage_roll.armor_effectiveness.keys()
if eff_keys.size() > 0:
atk_type = String(eff_keys[0])
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T1主角攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1}))
# 敌方技来源取后位敌(前位是攻击/直伤/群伤的靶位——来源与靶位分离,免死置干扰配方 redeemed 断言)
var alive_list: Array = units.query_alive("enemy")
@@ -124,9 +124,9 @@ func _run_recipe_sequence(db) -> Array:
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
var ex1: Dictionary = r1.get("extra", {})
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces))
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.combat.settlement.damage_roll.roll_faces))
var tier_ok := false
for dv in h.db.settlement.damage_roll.roll_tier_discounts:
for dv in h.db.combat.settlement.damage_roll.roll_tier_discounts:
if int(dv) == int(ex1.get("tier", -1)):
tier_ok = true
if int(ex1.get("tier", -1)) == 100:
@@ -159,7 +159,7 @@ func _run_recipe_sequence(db) -> Array:
continue
if String(r.source_class).is_empty() or String(r.display).is_empty():
protocol_ok = false
if int(r.value) < int(h.db.settlement.damage_roll.damage_floor):
if int(r.value) < int(h.db.combat.settlement.damage_roll.damage_floor):
protocol_ok = false
if not (r.extra is Dictionary) or not r.extra.has("target"):
protocol_ok = false
@@ -187,11 +187,11 @@ func _test_policy_immediate_mode(db) -> void:
if not h.spawn_ok:
return
h.ctrl.policy = DamageControllerScript.POLICY_IMMEDIATE
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "A模式反噬",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.combat.hero.max_hp) - floor_v)
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
@@ -267,7 +267,7 @@ func _test_target_id_payload(db: ConfigDb) -> void:
var expect_id := String(target.get("enemy_id", ""))
Kit.check("前置:目标敌携非空 enemy_id", expect_id != "")
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T6目标id",
{"channel": "战斗段", "base_value": int(db.hero.atk),
{"channel": "战斗段", "base_value": int(db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
var fx_payload: Dictionary = ticket.context.get("fx_attack_payload", {})
Kit.check("attack_commit fx 载荷携 target_id〔wd12〕",
@@ -284,7 +284,7 @@ func _test_requires_gate_and_zero(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
# 先清场(flush_submit 同步开单——清图批专用通道的契约面同时被覆盖)
var front_hp: int = int(h.units.query_alive("enemy")[0].max_hp)
var big: int = front_hp * 10
@@ -294,7 +294,7 @@ func _test_requires_gate_and_zero(db) -> void:
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "空场群伤",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "前置门攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
gated_ticket["requires"] = t_zero
var t_gated: int = h.ctrl.submit(gated_ticket)
@@ -310,7 +310,7 @@ func _test_hero_death_halt(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var max_hp: int = int(h.db.hero.max_hp)
var max_hp: int = int(h.db.combat.hero.max_hp)
var t_kill: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "致死反噬",
{"channel": "棋盘兑付", "base_value": max_hp, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "余单",
@@ -327,14 +327,14 @@ func _test_ac_merge(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var base_ac: int = int(h.db.hero.armor_class)
var base_ac: int = int(h.db.combat.hero.armor_class)
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
var override_v: int = base_ac + int(h.db.settlement.damage_roll.damage_floor)
var override_v: int = base_ac + int(h.db.combat.settlement.damage_roll.damage_floor)
h.ctrl.set_hero_ac_override(override_v)
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
var ac_eff := {}
for row in h.db.status_effects.rows:
for row in h.db.combat.status_effects.rows:
if String(row.get("unit", "")) == "ac" and String(row.get("target_side", "")) == "hero":
ac_eff = row
break
@@ -354,15 +354,15 @@ func _test_pool_clamp(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var lo: int = int(h.db.settlement.core.pool_clamp_lo)
var hi: int = int(h.db.settlement.core.pool_clamp_hi)
var lo: int = int(h.db.combat.settlement.core.pool_clamp_lo)
var hi: int = int(h.db.combat.settlement.core.pool_clamp_hi)
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
# 找 hero_offense 池的 percent 型 hero 侧效果行
var pool_eff := {}
for row in h.db.status_effects.rows:
for row in h.db.combat.status_effects.rows:
var eid := String(row.get("effect_id", ""))
if String(row.get("unit", "")) == "percent" and String(row.get("target_side", "")) == "hero" \
and String(h.db.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
and String(h.db.combat.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
pool_eff = row
break
if pool_eff.is_empty():
@@ -377,7 +377,7 @@ func _test_pool_clamp(db) -> void:
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
# ac 型效果不入 percent 池
var ac_eff2 := {}
for row2 in h.db.status_effects.rows:
for row2 in h.db.combat.status_effects.rows:
if String(row2.get("unit", "")) == "ac" and String(row2.get("target_side", "")) == "hero":
ac_eff2 = row2
break
@@ -387,7 +387,7 @@ func _test_pool_clamp(db) -> void:
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
var stack_eff := {}
for row3 in h.db.status_effects.rows:
for row3 in h.db.combat.status_effects.rows:
if String(row3.get("stack_policy", "")) == "stack":
stack_eff = row3
break
@@ -24,7 +24,7 @@ func _init() -> void:
func _floor_val() -> int:
return int(db.settlement.damage_roll.damage_floor)
return int(db.combat.settlement.damage_roll.damage_floor)
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
@@ -32,11 +32,11 @@ func _floor_val() -> int:
func _test_pool_damage() -> void:
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.combat.settlement.core.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_hi), 0) >= d)
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0)
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_lo)) >= 0)
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
@@ -54,8 +54,8 @@ func _test_counter_component() -> void:
func _test_roll_discount_percent() -> void:
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
var bounds: Array = db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.settlement.damage_roll.roll_tier_discounts
var bounds: Array = db.combat.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.combat.settlement.damage_roll.roll_tier_discounts
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
@@ -78,7 +78,7 @@ func _test_roll_discount_percent() -> void:
func _test_rolled_component() -> void:
var base := float(_floor_val() + 7) # 表值推导探针基线
var crit_mult: int = int(db.settlement.damage_roll.crit_multiplier)
var crit_mult: int = int(db.combat.settlement.damage_roll.crit_multiplier)
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
@@ -47,13 +47,13 @@ func _smoke_enemy_attack_type() -> void:
# ① 表列:20 行全填 normal(N3 全填不做差异化——Q9)
var at_ok := true
var at_detail := ""
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var at := String(row.get("attack_type", ""))
if at != "normal":
at_ok = false
at_detail += " %s:%s" % [String(row.get("enemy_id", "?")), at]
Kit.check("敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [
_db.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
_db.combat.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
# ②③④ 矩阵命中证词扫描:(3,0) 灰矮人 atk 区间 [2,4] 基线足够((1,2) 熊地精 atk 1 会塌地板);
# 同 seed 三甲型各开一场、各取首个敌方伤害行,对拍 ±1。
var matrix_found := false
@@ -70,7 +70,7 @@ func _smoke_enemy_attack_type() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "armor_type": armor}):
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "armor_type": armor}):
totals[armor] = -1
continue
if armor == "light":
@@ -109,7 +109,7 @@ func _smoke_armor_roll() -> void:
var dist := {100: 0, 75: 0, 50: 0, 25: 0}
var tier_ok := true
for roll in range(1, 21):
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
if not dist.has(d):
tier_ok = false
continue
@@ -117,7 +117,7 @@ func _smoke_armor_roll() -> void:
Kit.check("查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])], tier_ok and dist == cfg.expect)
var ev_sum := 0
for roll in range(1, 21):
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
Kit.check("AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum, ev_sum == 1575)
# ② nat20 暴击(行为面):探针按行为面扫描
# 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位
@@ -125,21 +125,21 @@ func _smoke_armor_roll() -> void:
var crit_rows := {}
for s in range(1, 800):
crit_rows = _first_hero_attack_rows(s)
if int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll):
if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll):
crit_seed = s
break
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.damage_roll.crit_roll)], crit_seed > 0)
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0)
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)",
int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk))
int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk))
# ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置
# (削弱为续时语义,无叠层通道)
var weaken_probe := _weaken_refresh_probe()
Kit.check("削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)",
int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2)
Kit.check("削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)",
int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude))
int(weaken_probe.get("pool", -999)) == int(_db.combat.status_effects.row_by_id("weaken").magnitude))
# ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定
# (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值)
var tiers := _ac11_tier_samples()
@@ -172,7 +172,7 @@ func _smoke_armor_roll() -> void:
Kit.check("棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)", board_ok)
# ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂
# 全部收在 damage_floor 闸内)
var df: int = int(_db.settlement.damage_roll.damage_floor)
var df: int = int(_db.combat.settlement.damage_roll.damage_floor)
Kit.check("棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)", DamageMath.counter_component(2, -1, df) == 1)
Kit.check("克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)", DamageMath.counter_component(1, -3, df) == 1)
Kit.check("空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)", DamageMath.counter_component(0, 5, df) == 0)
@@ -185,7 +185,7 @@ func _first_hero_attack_rows(seed_val: int) -> Dictionary:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}):
return {}
settlement.minesweeper.settle_batch([{"action": "stop"}])
for row in rows:
@@ -201,7 +201,7 @@ func _weaken_refresh_probe() -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260916)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
return {}
var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}
settlement._status_effects.apply_effect("weaken", src)
@@ -224,7 +224,7 @@ func _ac11_tier_samples() -> Array:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
continue
var guard := 12
while settlement.battle_result == "" and guard > 0:
@@ -259,7 +259,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -325,7 +325,7 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": hero_atk_val}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": hero_atk_val}):
return {}
for eff in opts.get("effects", []):
settlement._status_effects.apply_effect(String(eff), {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
@@ -343,15 +343,15 @@ func _smoke_damage_types() -> void:
# ① 护甲类型快照传播(spawn 行值 → 单位字典)
var enemy_system = load("res://core/unit_container.gd").new(_db)
Kit.check("goblin 护甲 = light(轻甲——教学位)",
enemy_system.spawn(1, 0, int(_db.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
enemy_system.spawn(1, 0, int(_db.combat.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
Kit.check("bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
enemy_system_b.spawn(1, 2, int(_db.combat.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
Kit.check("ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
enemy_system_c.spawn(1, 4, int(_db.combat.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
var am: Dictionary = _db.settlement.damage_roll.armor_effectiveness
var am: Dictionary = _db.combat.settlement.damage_roll.armor_effectiveness
Kit.check("普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)",
int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1)
Kit.check("穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)",
@@ -387,7 +387,7 @@ func _run_seal_backlash_round(seed_val: int) -> Dictionary:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""}
var minesweeper = settlement.minesweeper
var safe_t := _find_seal_target(minesweeper, false)
@@ -443,7 +443,7 @@ func _smoke_attack_chain_fresh_pool() -> void:
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("rider", 0)), "roll": int(row.roll),
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
Kit.check("fresh 池回归战斗启动(Boss 行)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
# 停手轮:零挖掘零封印 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计)
for i in range(6):
if settlement.battle_result != "":
@@ -452,9 +452,9 @@ func _smoke_attack_chain_fresh_pool() -> void:
Kit.check("非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size(), ticks.size() >= 2)
var all_full := true
for t in ticks:
var base_factor: float = float(t.tier) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
var base_factor: float = float(t.tier) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
var rolled_base := int(floor(float(t.baseline) * base_factor / 100.0))
var expected: int = maxi(int(_db.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
var expected: int = maxi(int(_db.combat.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
if int(t.tick) != expected or int(t.tick) <= 0:
all_full = false
Kit.check("fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)", all_full)
@@ -69,7 +69,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
hero_attacks += 1
# 单分量伤害构成行:hero_attack 行必须携带实际攻击类型键 + 掷点读数(P2 归因)
# 类型键主语 = 锻造工坊配置默认值(hero.tres 无两轴字段;本夹具零注入)
var atk_t: String = String(_db.forge_default_attack_type())
var atk_t: String = String(_db.crossrun.forge.default_attack_type)
if not (row.has(atk_t) and row.has("roll") and row.has("ac") and row.has("tier")):
hero_breakdown_ok = false
Kit.check("%s 步骤流含判定行" % label, has_verdict)
@@ -93,7 +93,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -157,12 +157,12 @@ func _smoke_active_refresh() -> void:
if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]):
ct["waiting_probe_hit"] = true
ct["waiting_refresh_rejected"] = not settlement.player_request_refresh())
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var minesweeper = settlement.minesweeper
# 轮 1–3:零挖掘停手攒 RAP = 3×AP(每轮全转存;表驱动——AP 从表读不写死)
for i in range(3):
minesweeper.settle_batch([{"action": "stop"}])
var rap_banked: int = 3 * int(_db.action_economy.ap_per_round)
var rap_banked: int = 3 * int(_db.run.action_economy.ap_per_round)
Kit.check("三轮停手攒 RAP=%d(round=%d rap=%d)" % [rap_banked, settlement.round_index, settlement.action_economy.rap], settlement.round_index == 4 and settlement.action_economy.rap == rap_banked)
Kit.check("第 4 轮 Active 相位", settlement.action_economy.phase == "Active")
# 挖 1 格 → 刷 → 挖 1 格 → 刷 → 挖 1 格 → 第 3 刷拒(上限)
@@ -176,7 +176,7 @@ func _smoke_active_refresh() -> void:
var ok1: bool = settlement.player_request_refresh()
Kit.check("轮内第 1 次刷新受理", ok1)
Kit.check("相位保持 Active、AP 保留不重置(ap=%d)" % settlement.action_economy.ap, settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before)
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.action_economy.rap_refresh_cost))
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.run.action_economy.rap_refresh_cost))
Kit.check("当场换新图(board_changed +1)", int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before)
var plan2 = AutoPlayer.choose_dig(minesweeper)
if plan2.hero_cell != minesweeper.hero_pos():
@@ -221,7 +221,7 @@ func _active_refresh_replay() -> Array:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
return rows
var minesweeper = settlement.minesweeper
for i in range(3):
@@ -251,7 +251,7 @@ func _smoke_encounter_roll() -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(30000 + i)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
counts.append(settlement.presentation_snapshot().enemies.size())
# 区间生效:20 个种子应出现 1 和 2(D0 [1,2])
Kit.check("D0 区间 [1,2] 两值均出现(分布 %s)" % str(counts), counts.has(1) and counts.has(2))
@@ -269,13 +269,13 @@ func _smoke_encounter_roll() -> void:
var rng3 = load("res://core/rng_service.gd").new()
rng3.set_master_seed(31000)
var settlement_d1 = load("res://core/battle_orchestrator.gd").new(_db, rng3)
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}))
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.combat.hero.max_hp)}))
var n1: int = settlement_d1.presentation_snapshot().enemies.size()
Kit.check("D1 区间 [2,3] 域内(%d 只)" % n1, n1 >= 2 and n1 <= 3)
# 熊地精首遇带固定(教学位不加骰)——首遇带数据驱动定位(契约 8,禁写死坐标)
var bugbear_lead_band := _first_encounter_band(1, "bugbear")
var bugbear_lead_row: Dictionary = {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.level_index) == 1 and String(row.row_kind) == "battle" \
and int(row.depth_band) == bugbear_lead_band and String(row.enemy_id) == "bugbear":
bugbear_lead_row = row
@@ -283,7 +283,7 @@ func _smoke_encounter_roll() -> void:
var rng4 = load("res://core/rng_service.gd").new()
rng4.set_master_seed(31000)
var settlement_d2 = load("res://core/battle_orchestrator.gd").new(_db, rng4)
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.hero.max_hp)})
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" % bugbear_lead_band,
settlement_d2.presentation_snapshot().enemies.size() == 1 \
and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max"))
@@ -300,11 +300,11 @@ func _smoke_encounter_roll() -> void:
for depth_v in depths:
scan_total += 1
var settlement_scan = load("res://core/battle_orchestrator.gd").new(_db, rngs)
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.hero.max_hp)}):
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.combat.hero.max_hp)}):
scan_bad += 1
continue
var n: int = settlement_scan.presentation_snapshot().enemies.size()
var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
var erow2: Dictionary = _db.combat.enemy.resolve_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
var lo2: int = int(erow2.get("unit_count", 1))
var hi2: int = int(erow2.get("unit_count_max", lo2))
if n < 1 or n < lo2 or n > hi2:
@@ -329,7 +329,7 @@ func _row_kind_of(level_index: int, depth: int) -> String:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -345,12 +345,12 @@ func _level_active_nodes(level_index: int) -> Array:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var gen = NodeMapGenerator.new(rng)
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
func _level_battle_nodes(level_index: int) -> Array:
var out: Array = []
for row in _db.level.rows:
for row in _db.run.level.rows:
if int(row.level_index) == level_index:
for node in _level_active_nodes(level_index):
var t := String(node.get("type", ""))
@@ -364,7 +364,7 @@ func _encounter_replay(seed_val: int) -> Array:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
out.append(settlement.presentation_snapshot().enemies.size())
return out
@@ -378,7 +378,7 @@ func _encounter_replay(seed_val: int) -> Array:
func _smoke_settlement_segments() -> void:
print("--- 五段全序 + 行型封闭 ---")
Kit.check("segment_order 五段冻结在位(表值 = 拍板序恒等)",
Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
Array(_db.combat.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"])
var run = _run_battle(1, 0)
var legacy := {"item": 0, "consumed_ineffective": 0, "use_rejected": 0, "window_open": 0, "window_skip": 0}
@@ -493,53 +493,53 @@ func _smoke_skill_pool() -> void:
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
func _smoke_skill_pool_data() -> void:
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.skill.rows.size() == 16)
var basic: Dictionary = _db.skill.row_by_id("attack")
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.combat.skill.rows.size() == 16)
var basic: Dictionary = _db.combat.skill.row_by_id("attack")
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)))
var lunge: Dictionary = _db.skill.row_by_id("lunge")
var lunge: Dictionary = _db.combat.skill.row_by_id("lunge")
Kit.check("突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)", not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2)
var charge_row: Dictionary = _db.skill.row_by_id("charge")
var charge_row: Dictionary = _db.combat.skill.row_by_id("charge")
Kit.check("蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)", not charge_row.is_empty() and String(charge_row.buff_ref) == "empower")
var bite: Dictionary = _db.skill.row_by_id("bite")
var bite: Dictionary = _db.combat.skill.row_by_id("bite")
Kit.check("撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)", not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1)
var venom: Dictionary = _db.skill.row_by_id("venom_burst")
var venom: Dictionary = _db.combat.skill.row_by_id("venom_burst")
Kit.check("毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)",
not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4
and is_equal_approx(float(venom.rider_mult), 0.75))
var venom_sole_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
venom_sole_ok = false
Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok)
# 妨害技首发行 + off_guard 落点 + 实装面收窄
var snare: Dictionary = _db.skill.row_by_id("web_snare")
var snare: Dictionary = _db.combat.skill.row_by_id("web_snare")
Kit.check("蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)",
not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard"
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4)
var snare_sole_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
snare_sole_ok = false
Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok)
var off_guard_row: Dictionary = _db.status_effect_row("off_guard")
var off_guard_row: Dictionary = _db.combat.status_effects.row_by_id("off_guard")
Kit.check("失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)", not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac")
# 普通敌错峰两新行——毒吐/盾击(V3)
var spit: Dictionary = _db.skill.row_by_id("venom_spit")
var spit: Dictionary = _db.combat.skill.row_by_id("venom_spit")
Kit.check("毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)",
not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \
and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult"))
var bash: Dictionary = _db.skill.row_by_id("shield_bash")
var bash: Dictionary = _db.combat.skill.row_by_id("shield_bash")
Kit.check("盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)",
not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \
and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult"))
var spider_lead_band := _first_encounter_band(2, "giant_spider") # 首遇带数据驱动定位(契约 8)
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
var bash_sole_ok := true # 盾击只挂灰矮人族三行
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
var sid := String(entry.skill_id)
if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \
@@ -551,7 +551,7 @@ func _smoke_skill_pool_data() -> void:
Kit.check("盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)", bash_sole_ok)
# 首遇带行池恒 {attack:6, bite:2, charge:1}(去写死深度判据——带键口径)
var spider_lead_pool := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.level_index) == 2 and String(row.row_kind) == "battle" \
and int(row.depth_band) == spider_lead_band and String(row.enemy_id) == "giant_spider":
for entry in row.pool:
@@ -592,7 +592,7 @@ func _smoke_skill_pool_data() -> void:
"3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
}
var pools_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var rk := "%d_%s_%d_%s" % [int(row.level_index), String(row.row_kind), int(row.depth_band), String(row.enemy_id)]
if not expect_pools.has(rk):
pools_ok = false
@@ -604,10 +604,10 @@ func _smoke_skill_pool_data() -> void:
pools_ok = false
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
var basic_count_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n := 0
for entry in row.pool:
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow.is_empty() and String(srow.type) == "basic":
n += 1
if n != 1:
@@ -616,11 +616,11 @@ func _smoke_skill_pool_data() -> void:
# 池构成纪律复查(V4):零违反——增益 ≤1 / 妨害 ≤1(敌人系统 v0.13 结构冻结;载入期
# validate_skill_pool_links 已硬闸,此处冒烟侧显式复核新投放后仍绿)
var comp_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n_buff := 0
var n_debuff := 0
for entry in row.pool:
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if srow.is_empty():
continue
if String(srow.type) == "buff":
@@ -632,10 +632,10 @@ func _smoke_skill_pool_data() -> void:
Kit.check("逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)", comp_ok)
# 池构成纪律复查(触发条件批 V7 冒烟侧):每池至少一行无条件特殊技(普攻不计;trigger 缺省行计数)
var uncond_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n_uncond := 0
for entry in row.pool:
var srow_u: Dictionary = _db.skill_row(String(entry.skill_id))
var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
n_uncond += 1
if n_uncond < 1:
@@ -645,7 +645,7 @@ func _smoke_skill_pool_data() -> void:
var st_unlock: Dictionary = {"rows": []}
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面)
sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65))
Kit.check("前置:Boss 压到阈值 65(=round(130×0.5))", int(boss_u.hp) == 65)
@@ -661,25 +661,25 @@ func _smoke_skill_pool_data() -> void:
var rollable_ok := true
for depth in [0, 1, 2, 3, 4]:
var es = load("res://core/unit_container.gd").new(_db)
es.spawn(1, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
es.spawn(1, int(depth), int(_db.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
for u in es.all_enemies():
if es.rollable_skills(u).is_empty():
rollable_ok = false
for depth in [0, 1, 4, 5, 6, 7, 8]:
var es2 = load("res://core/unit_container.gd").new(_db)
es2.spawn(2, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
es2.spawn(2, int(depth), int(_db.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
for u in es2.all_enemies():
if es2.rollable_skills(u).is_empty():
rollable_ok = false
Kit.check("全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)", rollable_ok)
# 状态表新行 + 敌方防御池映射
var empower: Dictionary = _db.status_effect_row("empower")
var fortify: Dictionary = _db.status_effect_row("fortify")
var empower: Dictionary = _db.combat.status_effects.row_by_id("empower")
var fortify: Dictionary = _db.combat.status_effects.row_by_id("fortify")
Kit.check("蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)", not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100)
Kit.check("硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)", not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50)
Kit.check("effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)",
String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
String(_db.combat.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
# ② 同种子回放逐位复现(D3 大地精,多实例同种)
@@ -764,11 +764,11 @@ func _smoke_skill_pool_ev_anchor() -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260921)
var n_beats := 5000
var pity_m: int = int(_db.skill.pity_threshold)
var pity_m: int = int(_db.combat.skill.pity_threshold)
for si in species.size():
var eid: String = species[si]
var template: Array = []
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.enemy_id) == eid:
template = row.pool
break
@@ -785,13 +785,13 @@ func _smoke_skill_pool_ev_anchor() -> void:
if streak >= pity_m:
var specials: Array = []
for e in pool:
var sr: Dictionary = _db.skill_row(String(e.skill_id))
var sr: Dictionary = _db.combat.skill.row_by_id(String(e.skill_id))
if not sr.is_empty() and String(sr.type) != "basic":
specials.append({"skill_id": e.skill_id, "weight": e.weight})
if not specials.is_empty():
entries = specials
var sid := SkillLottery.draw(entries, rng.open_stream("behavior_roll"))
var srow: Dictionary = _db.skill_row(sid)
var srow: Dictionary = _db.combat.skill.row_by_id(sid)
var mult := 0.0
if String(srow.type) == "basic":
mult = 1.0
@@ -818,12 +818,12 @@ func _smoke_skill_pool_death_void() -> void:
for s in range(1, 400):
var rg = load("res://core/rng_service.gd").new()
rg.set_master_seed(s)
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
var erow: Dictionary = _db.combat.enemy.resolve_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
var entries: Array = []
for entry in erow.pool:
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
var sid := SkillLottery.draw(entries, rg.open_stream("behavior_roll"))
if String(_db.skill_row(sid).type) != "basic":
if String(_db.combat.skill.row_by_id(sid).type) != "basic":
void_seed = s
break
Kit.check("死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" % void_seed, void_seed > 0)
@@ -832,7 +832,7 @@ func _smoke_skill_pool_death_void() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}))
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}))
var u: Dictionary = settlement._enemy_system.all_enemies()[0] # 句柄预捕获(胜利 despawn 后容器已清,字典引用仍可读)
var pending: String = settlement._enemy_system.pending_action(u)
Kit.check("开轮已摇定本拍动作(待发 = %s)" % pending, pending != "")
@@ -856,14 +856,14 @@ func _smoke_skill_pool_validation_reject() -> void:
var skill_table = load("res://data/schemas/skill_table.gd").new()
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}])
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
skill_table.rows.assign([{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}])
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([
{"skill_id": "b1", "display_name": "b1", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 3},
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}])
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}])
Kit.check("加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)", not skill_table.validate().is_empty())
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}])
@@ -880,7 +880,7 @@ func _smoke_skill_pool_validation_reject() -> void:
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4},
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}])
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}])
Kit.check("加载校验拒绝:妨害技缺 buff_ref(效果引用)", not skill_table.validate().is_empty())
skill_table.rows.assign([{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}])
@@ -892,7 +892,7 @@ func _smoke_skill_pool_validation_reject() -> void:
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}])
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
func _has_err_category(errs: Array, category: String) -> bool:
@@ -915,9 +915,9 @@ func _smoke_debuff_rider() -> void:
var venom_check_ok := true
var venom_rows_seen := 0
var snare_seed := -1
var base_ac: int = int(_db.hero.armor_class)
var off_m: int = int(_db.status_effect_row("off_guard").magnitude)
var floor_v: int = int(_db.settlement.damage_roll.damage_floor)
var base_ac: int = int(_db.combat.hero.armor_class)
var off_m: int = int(_db.combat.status_effects.row_by_id("off_guard").magnitude)
var floor_v: int = int(_db.combat.settlement.damage_roll.damage_floor)
for s in range(1, 401):
if venom_seed > 0 and snare_seed > 0:
break
@@ -926,7 +926,7 @@ func _smoke_debuff_rider() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
continue
var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放)
for i in range(6):
@@ -949,7 +949,7 @@ func _smoke_debuff_rider() -> void:
if rider_v != maxi(floor_v, int(floor(float(boss_atk) * 0.75))):
venom_check_ok = false
# 终值精确复算(停手轮池恒空):主分量链结果 + 必中附加后置
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var base_factor: float = float(int(row.tier)) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var rolled_main: int = int(floor(float(int(row.baseline)) * base_factor / 100.0))
var expected: int = maxi(floor_v, rolled_main + int(row.get("attach", 0))) + rider_v
if int(row.value) != expected:
@@ -52,10 +52,10 @@ func _smoke_seal_ap_exhausted() -> void:
var rounds: Array = []
settlement.round_started.connect(func(ri): rounds.append(ri))
Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var minesweeper = settlement.minesweeper
# 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死)
var ap_total: int = int(_db.action_economy.ap_per_round)
var ap_total: int = int(_db.run.action_economy.ap_per_round)
for i in range(ap_total - 1):
if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0:
break
@@ -151,7 +151,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -169,7 +169,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# 清图驱动(真实流):标一面真雷封印(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。
# from_refresh=true 时先三轮停手攒 RAP、第 4 轮轮内刷新换图,再清刷新图。
func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
@@ -177,7 +177,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
var res := {"cleared": false, "refreshed": false, "rounds": 0}
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss",
"hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}):
_db.difficulty.rows.clear()
_db.run.difficulty.rows.clear()
return res
if from_refresh:
for i in range(3):
@@ -186,7 +186,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
var drive := _drive_clear(settlement)
res["cleared"] = drive.cleared
res["rounds"] = drive.rounds
_db.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
_db.run.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
return res
@@ -266,7 +266,7 @@ func _polarity_probe_accepts(probe: Dictionary) -> bool:
# 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。
# 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。
func _run_polarity_probe(seed_val: int) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
@@ -305,7 +305,7 @@ func _run_polarity_probe(seed_val: int) -> Dictionary:
while settlement.battle_result == "" and guard > 0:
guard -= 1
_auto_play_round(settlement, out)
_db.difficulty.rows.clear() # 测试台账还原
_db.run.difficulty.rows.clear() # 测试台账还原
return probe
@@ -494,7 +494,7 @@ func _smoke_polarity_routing() -> void:
# δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast
# 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变
# =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。
var prem_pct: int = int(_db.settlement.core.seal_hit_premium)
var prem_pct: int = int(_db.combat.settlement.core.seal_hit_premium)
Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125)
var prem_rows := 0
var prem_ok := true
@@ -511,7 +511,7 @@ func _smoke_polarity_routing() -> void:
elif eff == "ap_delta":
prem_ok = prem_ok and v == 1 # +1AP:1 点量纲(round(1.25)=1——整数粒度地板)
else:
var mag: int = int(_db.status_effect_row(eff).get("magnitude", -999))
var mag: int = int(_db.combat.status_effects.row_by_id(eff).get("magnitude", -999))
prem_ok = prem_ok and v == int(round(mag * prem_pct / 100.0)) # 增伤50→63/削弱65→81/护甲1→1
elif rtype == "heal":
# 急救 2→3(clamp 封顶只降不升:满血走 reward_ineffective 不入本行——区间断言)
@@ -568,7 +568,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
var rows_a: Array = []
settle_a.step_row.connect(func(row): rows_a.append(row))
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_a = settle_a.minesweeper
var t_a := _find_seal_target(ms_a, false)
Kit.check("四情况 A:找到安全格封印目标", not t_a.is_empty())
@@ -606,7 +606,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
var rows_b: Array = []
settle_b.step_row.connect(func(row): rows_b.append(row))
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_b = settle_b.minesweeper
var t_b := _find_seal_target(ms_b, true)
Kit.check("四情况 B:找到危险格封印目标", not t_b.is_empty())
@@ -636,7 +636,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
var rows_c: Array = []
settle_c.step_row.connect(func(row): rows_c.append(row))
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_c = settle_c.minesweeper
var t_safe := _find_seal_target(ms_c, false)
var t_mine := _find_seal_target(ms_c, true)
@@ -671,12 +671,12 @@ func _smoke_seal_irreversible_quadrants() -> void:
func _smoke_negative_dictionary() -> void:
print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---")
# 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位)
var sapped: Dictionary = _db.status_effect_row("sapped")
var sapped: Dictionary = _db.combat.status_effects.row_by_id("sapped")
Kit.check("sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)",
not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \
and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \
and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.combat.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
# heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然:
# 本断言多板扫描,取第一块五型齐备的板做行为断言。
@@ -692,7 +692,7 @@ func _smoke_negative_dictionary() -> void:
settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
rows = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_max_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_max_hp": int(_db.combat.hero.max_hp)}):
continue
board = settlement.minesweeper.current_board()
var r0 := {}
@@ -732,7 +732,7 @@ func _smoke_negative_dictionary() -> void:
settlement.reward_grant().grant_routed(snap_h, "negative", {"source_class": "mine_hit", "cell": by_type["heal"], "ri": 1})
var hp_after: int = int(settlement.presentation_snapshot().hero.hp)
var backlash_power: int = 0
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("reward_id", "")) == "heal_backlash":
backlash_power = int(row.power)
Kit.check("heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" % [backlash_power, hp_before, hp_after], hp_after == hp_before - backlash_power)
@@ -789,7 +789,7 @@ func _smoke_resource_surface() -> void:
var rng3 = load("res://core/rng_service.gd").new()
rng3.set_master_seed(20260920)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
var board = settlement.minesweeper.current_board()
var r0_first: Variant = null
for c in board.compute_r0():
@@ -26,7 +26,7 @@ func _init() -> void:
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
func _boss(db, L: int, D: int, kind := "boss"):
var ec = UnitContainerScript.new(db)
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", kind)
var ok: bool = ec.spawn(L, D, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", kind)
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
return ec
@@ -83,7 +83,7 @@ func _test_latch_irreversible(db) -> void:
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
func _test_plain_rows_zero_drift(db) -> void:
var ec = load("res://core/unit_container.gd").new(db)
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
var u: Dictionary = ec.all_enemies()[0]
var any_unlock_key := false
for e in u.pool:
@@ -96,7 +96,7 @@ func _test_plain_rows_zero_drift(db) -> void:
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
# 同种子确定性:容器零随机——两实例同输入查询逐位同(同款零扰动载体)
var ec2 = load("res://core/unit_container.gd").new(db)
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
var u2: Dictionary = ec2.all_enemies()[0]
ec.apply_damage(u, 3)
ec2.apply_damage(u2, 3)
@@ -75,7 +75,7 @@ func _init() -> void:
{"skill_id": "desperate_guard", "weight": 9999, "cooldown_left": 0,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}, # B=条件假技 w9999
]
g3["basic_streak"] = int(db.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
g3["basic_streak"] = int(db.combat.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
var specials_v3: Array = ec_v3.special_skills(g3)
Kit.check_quiet(_has(specials_v3, "heavy") and not _has(specials_v3, "desperate_guard"),
"V3 保底集合含 A 排除 B(special_skills 真实入口——条件假技不逼出)〔触发器批 V3〕")
@@ -139,24 +139,24 @@ func _init() -> void:
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
comp_violations += 1
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
var enemy_backup: Array = db.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
var enemy_backup: Array = db.combat.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.combat.enemy.rows[0] as Dictionary).duplicate(true)
var cond_pool: Array[Variant] = []
for entry in (cond_only_row.get("pool", []) as Array):
var sid := String((entry as Dictionary).get("skill_id", ""))
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
var srow_c: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
cond_only_row["pool"] = cond_pool
db.enemy.rows[0] = cond_only_row
db.combat.enemy.rows[0] = cond_only_row
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var hit2 := false
for e in errs2:
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
hit2 = true
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
db.enemy.rows = enemy_backup
db.combat.enemy.rows = enemy_backup
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
var ec_v8 = load("res://core/unit_container.gd").new(db)
@@ -56,7 +56,7 @@ func _smoke_variant_placement(L: int) -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}):
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp}):
Kit.check("L%d 布置段战斗启动失败" % L, false)
return
var b = settlement.minesweeper.current_board()
@@ -64,7 +64,7 @@ func _smoke_variant_placement(L: int) -> void:
if b == null:
return
var pool_ids: Array = []
for r in _db.variant_pool(L, String(b.board_type)):
for r in _db.combat.variation.variant_pool(L, String(b.board_type)):
pool_ids.append(String(r.variant_id))
var vcells: Array = b.variants.keys()
var vid0 := ""
@@ -95,13 +95,13 @@ func _smoke_variant_placement(L: int) -> void:
for c in b.safe_cells():
if not r0.has(c):
cand_count += 1
var expect_cap: int = int(round(float(cand_count) * float(_db.variation.percent_of_cells) / 100.0)) \
+ (L - 1) * int(_db.variation.per_level_bonus)
var expect_cap: int = int(round(float(cand_count) * float(_db.combat.variation.percent_of_cells) / 100.0)) \
+ (L - 1) * int(_db.combat.variation.per_level_bonus)
Kit.check("L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" % [L, vcells.size(), expect_cap], vcells.size() <= expect_cap)
# 数量公式量随候选池缩放:L1 无关卡加成,极小池(<10 格)可 round 至 0——
# 零布置为合法态(同池空口径);非零断言为条件式,公式量>0 时才要求非零(L2/L3 关卡加成保底恒>0)
Kit.check("L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" % L, expect_cap == 0 or vcells.size() > 0)
var pack_max: int = int(_db.variation.pack_max)
var pack_max: int = int(_db.combat.variation.pack_max)
var vset := {}
for c in vcells:
vset[c] = true
@@ -127,7 +127,7 @@ func _smoke_variant_placement(L: int) -> void:
var rng2 = load("res://core/rng_service.gd").new()
rng2.set_master_seed(42)
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp})
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp})
var b2 = s2.minesweeper.current_board()
Kit.check("L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L, b2 != null and _variant_dump(b2) == _variant_dump(b))
@@ -175,24 +175,24 @@ func _smoke_global_variant() -> void:
Kit.check("互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)", domain_hit)
# ② 门控第三维(节点身份——global 行仅 boss/elite 入池;缺省/普通节点静默跳过零报错)
var pool_boss_ids: Array = []
for r in _db.variant_pool(2, "underdark", "boss"):
for r in _db.combat.variation.variant_pool(2, "underdark", "boss"):
pool_boss_ids.append(String(r.variant_id))
Kit.check("门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)", pool_boss_ids == ["magma", "ash"])
var pool_battle_ids: Array = []
for r in _db.variant_pool(2, "underdark", "battle"):
for r in _db.combat.variation.variant_pool(2, "underdark", "battle"):
pool_battle_ids.append(String(r.variant_id))
Kit.check("门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)", pool_battle_ids == ["magma", "ash"])
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2)
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.variant_pool(1, "underdark", "boss").is_empty())
Kit.check("全局决议:battle 节点恒无全局型", _db.global_variant_row(2, "underdark", "battle").is_empty())
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.global_variant_row(2, "underdark", "boss").is_empty())
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty())
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.global_variant_row(2, "underdark", "elite").is_empty())
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.combat.variation.variant_pool(2, "underdark").is_empty() == false and _db.combat.variation.variant_pool(2, "underdark").size() == 2)
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.combat.variation.variant_pool(1, "underdark", "boss").is_empty())
Kit.check("全局决议:battle 节点恒无全局型", _db.combat.variation.global_variant_row(2, "underdark", "battle").is_empty())
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.combat.variation.global_variant_row(2, "underdark", "boss").is_empty())
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.combat.variation.global_variant_row(1, "underdark", "boss").is_empty() and _db.combat.variation.global_variant_row(1, "dungeon", "boss").is_empty())
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.combat.variation.global_variant_row(2, "underdark", "elite").is_empty())
# ③ 兜底回落:全局池空 → Boss 图改走 region 候选池(恰一行 region 变异 + 格子级布置 + 正常取号)
var crng = _CountVariantRng.new()
crng.set_master_seed(42)
var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng)
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})
Kit.check("L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)", boss_ok)
var boss1_ids: Array = []
var boss1_variants: Dictionary = {}
@@ -216,7 +216,7 @@ func _smoke_global_variant() -> void:
var crng2 = _CountVariantRng.new()
crng2.set_master_seed(42)
var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2)
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
var bb2 = boss2.minesweeper.current_board()
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
@@ -267,12 +267,12 @@ func _smoke_global_variant() -> void:
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)
var normal1 = load("res://core/battle_orchestrator.gd").new(_db, nrng)
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
var n1: Dictionary = _board_resource_dump(normal1)
var nrng2 = load("res://core/rng_service.gd").new()
nrng2.set_master_seed(42)
var normal2 = load("res://core/battle_orchestrator.gd").new(_db, nrng2)
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)", n1 == _board_resource_dump(normal2) and not n1.is_empty())
@@ -60,7 +60,7 @@ func _test_damage_controller_vocabularies() -> void:
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
func _test_rng_tag_closure(db) -> void:
var stream_label: String = String(db.settlement.damage_roll.armor_roll_stream)
var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream)
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
RngServiceScript.PURPOSE_TAGS.has(stream_label))
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
@@ -102,8 +102,8 @@ func _test_status_boolean_acceptance(db) -> void:
var effects = StatusContainerScript.new(db)
# EC1 域闭:notify_battle_start 前授予 = false(受理面封闭)
var any_row: Dictionary = {}
if db.status_effects.rows.size() > 0:
any_row = db.status_effects.rows[0]
if db.combat.status_effects.rows.size() > 0:
any_row = db.combat.status_effects.rows[0]
Kit.check("前置:状态表非空", not any_row.is_empty())
if any_row.is_empty():
return
+33 -33
View File
@@ -115,10 +115,10 @@ func _smoke_adventure() -> void:
var autoplay = load("res://sim/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;节点源 = 参数档在固定种子下的生成图,
# 每个战斗/Boss 节点经 ConfigDB 真实解析链查敌行集与棋盘行——顶 clamp 消解禁〔fv24〕)
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.level.rows.size()) == 3)
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.run.level.rows.size()) == 3)
var cov_miss := 0
var cov_scan := 0
for li in range(1, int(_db.level.rows.size()) + 1):
for li in range(1, int(_db.run.level.rows.size()) + 1):
for n in _level_active_nodes(li):
var nt := String(n.get("type", ""))
if nt != "battle" and nt != "boss":
@@ -126,10 +126,10 @@ func _smoke_adventure() -> void:
var d: int = int(n.get("depth", -1))
cov_scan += 1
var rk := "boss" if nt == "boss" else "battle"
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(li, d, rk)
var rs: Array[Dictionary] = _db.combat.enemy.resolve_rowset(li, d, rk)
if rs.is_empty() or int(rs[0].get("depth_band", -1)) < d:
cov_miss += 1
if not (_db.resolve_board_gen(li, d) as Dictionary).is_empty():
if not (_db.combat.board_gen.resolve_row(li, d) as Dictionary).is_empty():
continue
cov_miss += 1
Kit.check("生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" % [cov_scan, cov_miss],
@@ -140,8 +140,8 @@ func _smoke_adventure() -> void:
Kit.check("满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)", String(st["result"]) == "completed" and bool(sum.get("cleared", false)))
Kit.check("完成几关 = 3(levels_cleared)", int(sum.get("levels_cleared", -1)) == 3)
var ks_exp := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_exp += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
for li in range(1, int(_db.run.level.rows.size()) + 1):
ks_exp += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
Kit.check("钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)), int(sum.get("keystone_total", -1)) == ks_exp)
var drops: Array = sum.get("keystone_drops", []) as Array
Kit.check("逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)", drops.size() == 3)
@@ -160,7 +160,7 @@ func _smoke_adventure() -> void:
# 奖励节点在生成图里是可选路位(旧单链模板的「L1 必经 1 个」形状前提随模板消亡)——
# 接线证明拆两半:图侧每关含 ≥1 奖励节点,行进侧种子扫描内确有经停(扫描零经停=三选一接线不可达)
var reward_levels := 0
for li in range(1, int(_db.level.rows.size()) + 1):
for li in range(1, int(_db.run.level.rows.size()) + 1):
var rc := 0
for n in _level_active_nodes(li):
if String(n.get("type", "")) == "reward":
@@ -208,8 +208,8 @@ func _smoke_adventure() -> void:
var cand_ok := picks.size() == 2
var ks_gain_ok := picks.size() == 2
var resolve_ok := level_resolves.size() == 2
var max_flat := int(_db.adventure.curio_value_max_hp_common)
var heal_pct := int(_db.adventure.clear_heal_pct)
var max_flat := int(_db.run.adventure.curio_value_max_hp_common)
var heal_pct := int(_db.run.adventure.clear_heal_pct)
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导——TP0 起按兑付令牌实值,大小档分别计)
var ks_prefix := 0
for i in picks.size():
@@ -231,18 +231,18 @@ func _smoke_adventure() -> void:
# 账本生效上限口径:置满/回血封顶按生效上限(flat 后乘调试浮窗 pct)——本段 pct=400
# 加算区 = 树/锻造件(hp_acc)+购血观测值(level_autoplay 停点快照 bonus_max_hp)——精确断言不再窗口拟合
var extra_obs := int(p.get("bonus_max_hp", -1))
var m_obs := int(round(float(int(_db.hero.max_hp) + hp_acc + extra_obs) * 5.0))
var m_obs := int(round(float(int(_db.combat.hero.max_hp) + hp_acc + extra_obs) * 5.0))
var heal_exp := mini(int(floor(float(m_obs) * float(heal_pct) / 100.0)), m_obs - hp_before)
if extra_obs < 0 or heal != heal_exp or hp_after != hp_before + heal or hp_after > m_obs:
heal_ok = false
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_prefix += int(_db.crossrun.meta.boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.crossrun.meta.boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_gain_ok = false
if resolve_ok:
var r: Dictionary = level_resolves[i]
var rk := String(r.get("picked", ""))
var rt := String(r.get("tier", ""))
if _smoke_find_token(p.get("candidates", []) as Array, rk, rt).is_empty() or int(r.get("value", -1)) != int(_db.adventure.curio_value(rk, rt)):
if _smoke_find_token(p.get("candidates", []) as Array, rk, rt).is_empty() or int(r.get("value", -1)) != int(_db.run.adventure.curio_value(rk, rt)):
resolve_ok = false
if rk == "max_hp":
hp_acc += int(r.get("value", max_flat))
@@ -252,10 +252,10 @@ func _smoke_adventure() -> void:
Kit.check("点选恰兑付选中一件(值=令牌档位表值;未选项零生效)", resolve_ok)
var seq: Array = st["encounter_max_hp_seq"]
var k := 5.0 # hp+400 ⇒ 调试浮窗百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
var exp_base := maxi(1, int(round(float(int(_db.combat.hero.max_hp)) * k)))
# 模拟档 v3.4 商店贪心购血合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
# 大生命 +10(优秀档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽承袭)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
var hp_ceiling := maxi(1, int(round(float(int(_db.combat.hero.max_hp) + 3 * int(_db.run.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
for v in (seq as Array):
if int(v) < exp_base or int(v) > hp_ceiling:
@@ -294,7 +294,7 @@ func _smoke_adventure() -> void:
var sh: Dictionary = half["st"]["summary"]
Kit.check("L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层",
not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \
and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \
and int(sh["keystone_total"]) == int(_db.crossrun.meta.boss_keystone_row(1).get("first_kill", 0)) \
and int(sh["stopped_at"]) == 2)
var dh: Dictionary = sh.get("defeat", {})
Kit.check("L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)",
@@ -344,11 +344,11 @@ func _smoke_adventure() -> void:
var r8 = a8.runner()
var gp0: int = int(r8.gp)
var hp_price := -1
for p in _db.shop.products:
for p in _db.run.shop.products:
if String(p.get("product_id", "")) == "hp_up":
hp_price = int(p.get("base_price", -1))
var l2_price_bonus := 0
for pb in _db.shop.price_level_bonus:
for pb in _db.run.shop.price_level_bonus:
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)
@@ -437,7 +437,7 @@ func _smoke_adventure() -> void:
a13.stop().present_node_curio(ncands13)
a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}])
var gp13: int = a13.wallet().gp
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common")))
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.run.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common")))
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty())
var s10: Dictionary = a10.summary
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
@@ -451,8 +451,8 @@ func _smoke_adventure() -> void:
func _smoke_feats() -> void:
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.feats.validate().is_empty())
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.crossrun.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty())
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
var feat_tokens := 0
@@ -464,32 +464,32 @@ func _smoke_feats() -> void:
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132)
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty())
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty())
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
var kinds_ok := true
for t in a_r0._curio.deck:
var kk := String((t as Dictionary).get("kind", ""))
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
if kk != "relic" and not _db.run.adventure.CURIO_KINDS.has(kk):
kinds_ok = false
Kit.check("层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕", kinds_ok)
var rewrite_rows := 0
for row_v in (_db.feats.rows as Array):
for row_v in (_db.crossrun.feats.rows as Array):
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
rewrite_rows += 1
Kit.check("棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕", rewrite_rows == 0)
var bad_words := 0
for row_v in (_db.feats.rows as Array):
for row_v in (_db.crossrun.feats.rows as Array):
var r_d: Dictionary = row_v as Dictionary
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
if String(txt_v).contains("雷") or String(txt_v).contains("旗"):
bad_words += 1
Kit.check("对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕", bad_words == 0)
Kit.check("数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕",
float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3)
float((_db.crossrun.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.crossrun.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.crossrun.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3)
# ④ 临时表注入(新 schema 形态——零 archetype/cap 直用):5 行 relic 入牌库/兑付/踢池/断点往返
var rt := FeatTable.new()
rt.rows = [
@@ -547,12 +547,12 @@ func _smoke_feats() -> void:
Kit.check("A5 专长面兑付受理(专长凭证开出候选可正常入持有集)", (a_t.feats_held() as Array).size() == feats_n + 1)
# A6 保底:pity 直填 5 → 下次展示必出(第 6 次必出口径);限购满 → 恒不触发
a_t.stop()._deck.ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t))
Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0)
a_t.stop()._deck.ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
a_t.stop()._deck.ticket_sold = int(a_t.db.run.adventure.feat_ticket_limit)
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t))
a_t.stop()._deck.ticket_sold = 0
a_t.stop()._deck.ticket_pity = 0
@@ -899,4 +899,4 @@ func _fresh_rng(seed_val: int):
# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next)
func _level_active_nodes(level_index: int) -> Array:
var gen = NodeMapGenerator.new(_fresh_rng(42))
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
+21 -17
View File
@@ -105,7 +105,7 @@ func _smoke_reward_identity_unit() -> void:
var cs2: Dictionary = reward_economy.grant_clear()
Kit.check("清图批尾身份 = clear_blast(clear_default 唯一活行)", String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast")
var cb_range := [-1, -1]
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.reward_id) == "clear_blast":
cb_range = [int(row.get("power_min", row.power)), int(row.get("power_max", row.power))]
Kit.check("清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" % [cb_range[0], cb_range[1], int(cs1.power)],
@@ -120,9 +120,13 @@ func _smoke_reward_identity_unit() -> void:
# 板行覆盖桩(cap 压线行为断言;接口面 = resolve_board_gen 单点)
class _BoardRowStub:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
class _StubBoardGen:
var row: Dictionary
func resolve_row(_L: int, _D: int) -> Dictionary:
return row
class _StubCombat:
var board_gen := _StubBoardGen.new()
var combat := _StubCombat.new()
func status_effect_row(_id: String) -> Dictionary:
return {}
@@ -131,27 +135,27 @@ func _smoke_board_generator() -> void:
print("--- 棋盘生成 ---")
var board_generator = load("res://core/board_generator.gd").new()
for L in [1, 2]:
var row = _db.resolve_board_gen(L, 0)
var row = _db.combat.board_gen.resolve_row(L, 0)
Kit.check("BoardGen 行解析 (%d,0)" % L, not row.is_empty())
if L == 2:
# 迭代 35 生成图深度扩面覆盖批随动(声明):旧口径「(2,3) 无精确行→ceil 上取到 4」的判据对象
# 是模板时代键域缺口;覆盖批补齐 0–13 后 (2,3) 有精确行,生产表上不再存在可走 ceil 的空位。
# 判据不丢——精确命中改判在册,ceil 能力用「临时摘除该行」的夹具面保留,随后原样还原。
var exact_row: Dictionary = _db.resolve_board_gen(2, 3)
var exact_row: Dictionary = _db.combat.board_gen.resolve_row(2, 3)
Kit.check("BoardGen 精确命中 (2,3)(覆盖批键域补齐)", int(exact_row.get("node_depth", -1)) == 3)
var gap_idx := -1
for i in _db.board_gen.rows.size():
if int(_db.board_gen.rows[i].get("level_index", -1)) == 2 and int(_db.board_gen.rows[i].get("node_depth", -1)) == 3:
for i in _db.combat.board_gen.rows.size():
if int(_db.combat.board_gen.rows[i].get("level_index", -1)) == 2 and int(_db.combat.board_gen.rows[i].get("node_depth", -1)) == 3:
gap_idx = i
break
var saved: Dictionary = _db.board_gen.rows[gap_idx]
var rows_before: int = _db.board_gen.rows.size()
_db.board_gen.rows.remove_at(gap_idx)
var ceil_row: Dictionary = _db.resolve_board_gen(2, 3)
var saved: Dictionary = _db.combat.board_gen.rows[gap_idx]
var rows_before: int = _db.combat.board_gen.rows.size()
_db.combat.board_gen.rows.remove_at(gap_idx)
var ceil_row: Dictionary = _db.combat.board_gen.resolve_row(2, 3)
Kit.check("R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)", int(ceil_row.get("node_depth", -1)) == 4)
_db.board_gen.rows.insert(gap_idx, saved)
_db.combat.board_gen.rows.insert(gap_idx, saved)
Kit.check("夹具表原样还原(行数与解析回到覆盖批现值)",
_db.board_gen.rows.size() == rows_before and int(_db.resolve_board_gen(2, 3).get("node_depth", -1)) == 3)
_db.combat.board_gen.rows.size() == rows_before and int(_db.combat.board_gen.resolve_row(2, 3).get("node_depth", -1)) == 3)
var b1 = board_generator.generate(L, 0, _rng.open_stream("board_gen"), _db)
Kit.check("L%d 生成产物非空" % L, b1 != null)
if b1 == null:
@@ -202,7 +206,7 @@ func _smoke_board_generator() -> void:
row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS
row9["max_gen_attempts"] = 4
var stub9 := _BoardRowStub.new()
stub9.row = row9
stub9.combat.board_gen.row = row9
var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9)
Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"],
b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product))
@@ -229,7 +233,7 @@ func _smoke_power_interval() -> void:
# 行清单与区间值全部随表走——现行区间行 heal_minor/clear_blast 只是恰然,不再是锚。
var interval_rows := {} # rid → [lo, hi](读表)
var anchors_ok := true
for row in _db.reward.rows:
for row in _db.run.reward.rows:
var rid := String(row.reward_id)
if row.has("power_min") or row.has("power_max"):
var lo: int = int(row.get("power_min", -1))
@@ -299,7 +303,7 @@ func _smoke_power_interval() -> void:
func _row_power(rid: String) -> int:
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.reward_id) == rid:
return int(row.power)
return -1
+3 -3
View File
@@ -41,7 +41,7 @@ func _test_generate_shape_and_mines() -> void:
if b == null:
Kit.check("生成返回棋盘", false)
return
var row: Dictionary = _db.resolve_board_gen(1, 0)
var row: Dictionary = _db.combat.board_gen.resolve_row(1, 0)
var want_w: int = int(row.get("board_width", 0))
var want_h: int = int(row.get("board_height", 0))
Kit.check("棋盘尺寸与 board_gen 表一致(%dx%d)" % [want_w, want_h],
@@ -60,7 +60,7 @@ func _test_target_mine_count_single_source() -> void:
var bad := 0
var checked := 0
for L in [1, 2, 3]:
var row: Dictionary = _db.resolve_board_gen(L, 0)
var row: Dictionary = _db.combat.board_gen.resolve_row(L, 0)
if row.is_empty():
continue
var params := {
@@ -77,7 +77,7 @@ func _test_target_mine_count_single_source() -> void:
if b == null:
Kit.check("生成返回棋盘(雷数单点段)", false)
return
var row1: Dictionary = _db.resolve_board_gen(1, 0)
var row1: Dictionary = _db.combat.board_gen.resolve_row(1, 0)
Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕",
int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({
"w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}))
+12 -12
View File
@@ -54,16 +54,16 @@ func _difficulty_table():
func _test_band_hit() -> void:
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
var hit: Array = _resolver.combat.enemy.resolve_rowset(1, 4, "battle")
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle")
var exact: Array = _resolver.combat.enemy.resolve_rowset(1, 5, "battle")
Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕",
exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
func _test_rowset_order_preserved() -> void:
var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2)
var ids: Array[String] = []
for r in rows:
@@ -74,7 +74,7 @@ func _test_rowset_order_preserved() -> void:
func _test_top_band_clamp() -> void:
_diags.clear()
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 99, "battle")
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
var clamp_hit := false
@@ -83,13 +83,13 @@ func _test_top_band_clamp() -> void:
clamp_hit = true
Kit.check("越顶带发 warning resolve_depth_clamp〔fv3〕", clamp_hit)
var warn_before: int = _diags.size()
_resolver.resolve_enemy_rowset(1, 99, "battle")
_resolver.combat.enemy.resolve_rowset(1, 99, "battle")
Kit.check("同键重复请求警告去重(四元组注册表)〔fv3〕", _diags.size() == warn_before)
func _test_row_kind_routing() -> void:
var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss")
var battle_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
var boss_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "boss")
Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕",
battle_rows.size() == 2 and boss_rows.size() == 1
and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief")
@@ -97,13 +97,13 @@ func _test_row_kind_routing() -> void:
func _test_request_oob() -> void:
_diags.clear()
_resolver.resolve_enemy_rowset(0, 0, "battle")
_resolver.combat.enemy.resolve_rowset(0, 0, "battle")
Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob())
_diags.clear()
_resolver.resolve_enemy_rowset(1, -1, "battle")
_resolver.combat.enemy.resolve_rowset(1, -1, "battle")
Kit.check("节点深度 <0 → fatal request_oob〔fv5〕", _has_fatal_oob())
_diags.clear()
_resolver.resolve_enemy_rowset(1, 0, "elite")
_resolver.combat.enemy.resolve_rowset(1, 0, "elite")
Kit.check("row_kind 非法 → fatal request_oob〔fv5〕", _has_fatal_oob())
@@ -115,11 +115,11 @@ func _has_fatal_oob() -> bool:
func _test_difficulty_clamp() -> void:
var m: Dictionary = _resolver.resolve_difficulty_modifiers(1, 0)
var m: Dictionary = _resolver.run.difficulty.resolve_modifiers(1, 0)
Kit.check("难度乘数越域被 clamp:hp 99 → 上限 10", absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001)
Kit.check("难度乘数越域被 clamp:atk 0.01 → 下限 0.1", absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001)
Kit.check("未登记坐标返回空修饰(零值不伪装成 ×1)",
_resolver.resolve_difficulty_modifiers(3, 3).is_empty())
_resolver.run.difficulty.resolve_modifiers(3, 3).is_empty())
class _FakeDifficulty:
@@ -13,8 +13,8 @@ func _init() -> void:
print("[curio_refresh_paid_test] FAIL — 数值表加载失败")
quit(1)
return
var base: int = int(db.adventure.shop_refresh_cost_base)
var curve: float = float(db.adventure.shop_refresh_cost_curve)
var base: int = int(db.run.adventure.shop_refresh_cost_base)
var curve: float = float(db.run.adventure.shop_refresh_cost_curve)
Kit.check_quiet(base > 0 and curve > 1.0, "商店刷新旋钮在档(base=%d curve=%.2f——三选一付费档同源读取)" % [base, curve])
# ---- ① 停点在途谓词:无停点时免费/付费刷新皆拒绝(付费路径扣费前置校验防空转烧钱包)----
+12 -12
View File
@@ -58,15 +58,15 @@ func _init() -> void:
Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
# V23 两件出池(R1——同调电池删除/完美保险迁专长)
Kit.check_quiet(not db.adventure.CURIO_KINDS.has("attune_battery")
and not db.adventure.CURIO_KINDS.has("perfect_insurance"),
Kit.check_quiet(not db.run.adventure.CURIO_KINDS.has("attune_battery")
and not db.run.adventure.CURIO_KINDS.has("perfect_insurance"),
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1)
and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1),
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
Kit.check_quiet(int(db.adventure.curio_cap("attune_battery")) == 99,
Kit.check_quiet(int(db.run.adventure.curio_cap("attune_battery")) == 99,
"出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕")
# 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配)
a._attunement = ["incense_stick"]
@@ -78,16 +78,16 @@ func _init() -> void:
# V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102)
var rare_sum := 0
var rare_kinds: Array[String] = []
for k in db.adventure.CURIO_KINDS:
var q := int(db.adventure.curio_quota(k, "rare"))
for k in db.run.adventure.CURIO_KINDS:
var q := int(db.run.adventure.curio_quota(k, "rare"))
rare_sum += q
if q > 0:
rare_kinds.append(k)
Kit.check_quiet(int(db.adventure.TIER_QUOTAS["rare"]) == 7 and rare_sum == 7
and int(db.adventure.deck_total()) == 102 and rare_kinds.has("ap") and rare_kinds.has("rap") and rare_kinds.has("time_sand")
Kit.check_quiet(int(db.run.adventure.TIER_QUOTAS["rare"]) == 7 and rare_sum == 7
and int(db.run.adventure.deck_total()) == 102 and rare_kinds.has("ap") and rare_kinds.has("rap") and rare_kinds.has("time_sand")
and rare_kinds.size() == 3,
"配额锚:稀有档合计 7、投放品种={ap,rap,time_sand}、令牌池总量 102(validate 硬断言同批)〔卡② R3〕")
Kit.check_quiet(db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
Kit.check_quiet(db.run.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)〔拆件批 V2——cs3 凭证拿到即开现役断言承继〕
a._curio.deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
@@ -103,13 +103,13 @@ func _init() -> void:
"留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
var hp_before: int = a.stats().hero_hp
a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.run.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
"留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕")
# V10-2 行动点上限归属性轴:ap kind 走加算区第四轴
a._curio.deck = [{"kind": "ap", "tier": "rare"}]
a.stop()._pending = {"candidates": a._curio.deck.duplicate()}
var ap_bonus_before: int = a.journal().bonus_total("ap")
Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.run.adventure.curio_value("ap", "rare")),
"行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕")
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
@@ -142,11 +142,11 @@ func _init() -> void:
Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_bank().refresh_stock() == 2,
"V16:续玩回填逐项一致(adventure 桶随断点往返·计数恢复)〔无状态化 V3〕")
# V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
var fi: Dictionary = db.crossrun.feats.row_by_id("perfect_insurance")
Kit.check_quiet(not fi.is_empty() and String(fi.get("rarity", "")) == "rare"
and String(fi.get("desc", "")).find("雷") == -1 and String(fi.get("desc", "")).find("封印") >= 0,
"专长行合法:第 30 行 perfect_insurance 在表、rarity rare、desc 零「雷」字面〔卡② R2〕")
Kit.check_quiet(db.feats.validate().is_empty(), "feats.validate() 零违例(新行订阅形态与词表合法)〔卡② R2〕")
Kit.check_quiet(db.crossrun.feats.validate().is_empty(), "feats.validate() 零违例(新行订阅形态与词表合法)〔卡② R2〕")
var RigScript := load("res://core/battle_orchestrator.gd")
var RngScript := load("res://core/rng_service.gd")
var rig_rng = RngScript.new()
+5 -5
View File
@@ -23,18 +23,18 @@ func _init() -> void:
# ---- hr1:生效公式逐值(表值+永久强化 flat+加算区后乘调试 pct——与改前同式)----
var journal_a = GainJournalScript.new()
var hero = HeroStatsScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}, journal_a)
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5,
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5,
"hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)")
Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3,
Kit.check_quiet(hero.effective_atk() == int(db.combat.hero.atk) + 3,
"hr1 生效攻击 = 表值+永久强化 flat")
Kit.check_quiet(hero.effective_ac() == int(db.forge_armor_route_by_type(db.forge_default_armor_type()).get("base_ac", 0)) + 2,
Kit.check_quiet(hero.effective_ac() == int(db.crossrun.forge.armor_route_by_type(db.crossrun.forge.default_armor_type).get("base_ac", 0)) + 2,
"hr1 生效护甲 = 甲型基础 AC+甲路线等级加值(纯 flat 无 pct)")
hero.set_debug_override(50, 50)
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)),
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.combat.hero.max_hp) + 5) * 1.5)),
"hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)")
hero.set_debug_override(0, 0)
journal_a.gain_bonus("max_hp", 4, "battle_export", "max_hp")
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4,
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5 + 4,
"hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加——公式消费日记汇总)")
# hr1 回血封顶公式逐值(算术宿主=HeroStats.rest_heal 单点——改前 LevelRunner.rest_heal 同式)
@@ -102,7 +102,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
# ---- ⑥ N 进数据表(含越域校验)〔fv6〕----
func _test_field_in_table(db) -> void:
var n_in_table: int = int(db.level.info_visible_layers)
var n_in_table: int = int(db.run.level.info_visible_layers)
Kit.check("N 落表:level.tres 读数 2(实际 %d)〔fv6〕" % n_in_table, n_in_table == 2)
var t := LevelTableScript.new()
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": _valid_graph_nodes()}]
@@ -184,11 +184,11 @@ func _test_single_chain_entry(db) -> void:
func _test_n_from_table(db) -> void:
var nodes := _graph_nodes()
var r = _runner(db, nodes, 1, [nodes[2]]) # 锚层 1
db.level.info_visible_layers = 1
db.run.level.info_visible_layers = 1
Kit.check("N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕", not r.type_visible_ids().has(4))
db.level.info_visible_layers = 3
db.run.level.info_visible_layers = 3
Kit.check("N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕", r.type_visible_ids().has(5))
db.level.info_visible_layers = 10
db.run.level.info_visible_layers = 10
var all_in := true
for nid in 7: # 非豁免节点 0..6(7 = Boss 恒显,不作判据)
if not r.type_visible_ids().has(nid):
+59 -59
View File
@@ -33,7 +33,7 @@ var _graph_gen = null # 生成图取数单实例(图 = f(参数档, 种子)
# 参数档 × 种子产图(与 LevelRunner._assign_generated_graph 同一产图路径与同一参数档表)
func _generated_graph(level_index: int, seed_val: int) -> Dictionary:
var params: Dictionary = _db.level_gen_params_row(level_index)
var params: Dictionary = _db.run.level_gen_params.row_by_level_index(level_index)
if params.is_empty():
return {}
if _graph_gen == null:
@@ -221,13 +221,13 @@ func _path_in_graph(nodes: Array, path: Array) -> bool:
# 敌侧覆盖判据:请求深度经 band_of 落带(非 -1)——解析落带 ≥ 请求深度即等价判据(禁顶 clamp 消解)
func _enemy_band_covers(level_index: int, depth: int, kind: String) -> bool:
var rows: Array[Dictionary] = _db.resolve_enemy_rowset(level_index, depth, kind)
var rows: Array[Dictionary] = _db.combat.enemy.resolve_rowset(level_index, depth, kind)
return not rows.is_empty() and int(rows[0].get("depth_band", -1)) >= depth
# 板侧覆盖判据:该深度在 board_gen 精确命中(node_depth 等值)
func _board_row_exact(level_index: int, depth: int) -> bool:
var board: Dictionary = _db.resolve_board_gen(level_index, depth)
var board: Dictionary = _db.combat.board_gen.resolve_row(level_index, depth)
return not board.is_empty() and int(board.get("node_depth", -1)) == depth
@@ -259,14 +259,14 @@ func _init() -> void:
func _smoke_table_migration() -> void:
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop.relic_base_price == 15.0)
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop.spend_discount == 0.8)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero.display_name == "探险者")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0)
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.run.shop.spend_discount == 0.8)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者")
# 消费端读表:spawn 真实构造链的英雄单位显示名与表值同
var units = load("res://core/unit_container.gd").new(_db)
var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp))
var spawned: bool = units.spawn(1, 0, int(_db.combat.hero.max_hp))
Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕",
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name))
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.combat.hero.display_name))
# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图,
@@ -279,7 +279,7 @@ func _smoke_generated_coverage() -> void:
var scanned := 0
var readings: Array = []
for L in [1, 2, 3]:
if _db.level_gen_params_row(L).is_empty():
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
Kit.check("L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" % L, false)
continue
var dmin := 999
@@ -306,7 +306,7 @@ func _smoke_generated_coverage() -> void:
board_ok = false
var bands: Array = []
for tid in _terminal_ids(nodes):
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
var rs: Array[Dictionary] = _db.combat.enemy.resolve_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
bands.append(int(rs[0].get("depth_band", -1)) if not rs.is_empty() else -1)
for i in range(bands.size()):
for j in range(i + 1, bands.size()):
@@ -325,9 +325,9 @@ func _smoke_generated_coverage() -> void:
func _smoke_level() -> void:
print("--- 关卡节点图·生成图推进(参数档×种子产图·满配冒险成功/恢复公式/败北线/回放确定性)---")
var autoplay = load("res://sim/level_autoplay.gd")
var hero_base := {"atk": int(_db.hero.atk), "hp": int(_db.hero.max_hp), "ac": int(_db.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
var hero_base := {"atk": int(_db.combat.hero.atk), "hp": int(_db.combat.hero.max_hp), "ac": int(_db.combat.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
var nodes1: Array = _level_active_nodes(1)
var params1: Dictionary = _db.level_gen_params_row(1)
var params1: Dictionary = _db.run.level_gen_params.row_by_level_index(1)
Kit.check("L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes1.size()], not nodes1.is_empty())
# ① 满配冒险成功路径(atk+200%/hp+100%——测试面板同源的属性覆盖)
var win = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 1, 200, 100)
@@ -354,10 +354,10 @@ func _smoke_level() -> void:
boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1)
Kit.check("L1 终点恰一个(大岔口自 L2 起的图侧投影)", _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1)
Kit.check("L1 生成图含岔口(出度 ≥2 节点 %d 处)" % _fork_ids(nodes1).size(), _fork_ids(nodes1).size() >= 1)
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3)
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.combat.hero.max_hp) * 3)
# ② 恢复点公式:回复 = min(floor(生效max_hp×rest_heal_pct/100), 缺口)——两档缺口各验一条分支
#(真入口驱动:起关→首战→选恢复候选;缺口由夹具造,恢复比例读关卡行)
var pct1: int = int(_db.level_row(1).get("rest_heal_pct", 0))
var pct1: int = int(_db.run.level.row_by_level_index(1).get("rest_heal_pct", 0))
var rest_wide := _drive_to_rest(1, ACTIVE_GRAPH_SEED, 100000)
if rest_wide.is_empty():
Kit.check("恢复点公式无源(现行图入口后一位无可直达的恢复候选)", false)
@@ -382,9 +382,9 @@ func _smoke_level() -> void:
Kit.check("关卡回放断言:同种子两遍结算步骤流逐行相同", JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows))
Kit.check("关卡步骤流非空(%d 行)" % rep1.rows.size(), rep1.rows.size() > 0)
# ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随)
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)],
int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \
and int(_db.hero.armor_class) == int(hero_base.ac))
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.combat.hero.atk), int(_db.combat.hero.max_hp), int(_db.combat.hero.armor_class)],
int(_db.combat.hero.atk) == int(hero_base.atk) and int(_db.combat.hero.max_hp) == int(hero_base.hp) \
and int(_db.combat.hero.armor_class) == int(hero_base.ac))
# 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。
@@ -439,10 +439,10 @@ func _smoke_level_graph() -> void:
print("--- 关卡节点图·分支节点图(L2 幽暗地域·生成图)---")
var autoplay = load("res://sim/level_autoplay.gd")
# ① 关卡表行级校验零错误(关卡表只承载序号/显示名/恢复比例——图合法性由 validate_graph 在起关时承担)
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.level.validate().is_empty())
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.run.level.validate().is_empty())
# ② L2 生成图形状读数(性质断言——规模/型别/岔口全部由参数档与逐节点扫描互校)
var nodes2: Array = _level_active_nodes(2)
var params2: Dictionary = _db.level_gen_params_row(2)
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
Kit.check("L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes2.size()], not nodes2.is_empty())
var tcount := _type_tally(nodes2)
var branch := _fork_ids(nodes2).size()
@@ -600,7 +600,7 @@ func _smoke_level_graph() -> void:
# ⑦a 定点:L2 首遇带行集与各终点 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标;
# 多终点图逐终点解析,断言行集构成而非单行:行序 = 文件序,首行即叙述取数面)
var l2_spider_band: int = _first_encounter_band(2, "giant_spider")
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle")
var l2_spider_set: Array = _db.combat.enemy.resolve_rowset(2, l2_spider_band, "battle")
var l2_spider_ids := {}
for r in l2_spider_set:
l2_spider_ids[String(r.get("enemy_id", ""))] = true
@@ -610,14 +610,14 @@ func _smoke_level_graph() -> void:
var l2_terms := _terminal_depths(nodes2)
var l2_boss_ok := not l2_terms.is_empty()
for d in l2_terms:
var l2_boss_set: Array[Dictionary] = _db.resolve_enemy_rowset(2, int(d), "boss")
var l2_boss_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(2, int(d), "boss")
if l2_boss_set.size() != 1 or String(l2_boss_set[0].get("enemy_id", "")) != "drider" \
or String(l2_boss_set[0].get("armor_type", "")) != "ward":
l2_boss_ok = false
Kit.check("L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % str(l2_terms), l2_boss_ok)
# ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面:
# 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4)
var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle")
var l2_mix_set: Array = _db.combat.enemy.resolve_rowset(2, 5, "battle")
var mix_shape_ok: bool = l2_mix_set.size() == 2 \
and String(l2_mix_set[0].get("enemy_id", "")) == "bugbear" \
and String(l2_mix_set[1].get("enemy_id", "")) == "giant_spider"
@@ -632,7 +632,7 @@ func _smoke_level_graph() -> void:
# ---- L3 灰矮人矿城断言段 ----
# ⑧ L3 生成图形状读数(与 ② 同一性质判据面——规模/型别/岔口/多终点全部对参数档与扫描读数互校)
var nodes3: Array = _level_active_nodes(3)
var params3: Dictionary = _db.level_gen_params_row(3)
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
Kit.check("L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes3.size()], not nodes3.is_empty())
var tcount3 := _type_tally(nodes3)
var branch3 := _fork_ids(nodes3).size()
@@ -654,7 +654,7 @@ func _smoke_level_graph() -> void:
Kit.check("L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" % int(cov3["board_bad"]), int(cov3["board_bad"]) == 0)
# ⑩ 定点:L3 首遇带行集与各终点 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a)
var l3_lead_band: int = _first_encounter_band(3, "duergar")
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle")
var l3_lead_set: Array = _db.combat.enemy.resolve_rowset(3, l3_lead_band, "battle")
var l3_lead_ids := {}
for r in l3_lead_set:
l3_lead_ids[String(r.get("enemy_id", ""))] = true
@@ -665,7 +665,7 @@ func _smoke_level_graph() -> void:
var l3_terms := _terminal_depths(nodes3)
var l3_boss_ok := not l3_terms.is_empty()
for d in l3_terms:
var l3_king_set: Array[Dictionary] = _db.resolve_enemy_rowset(3, int(d), "boss")
var l3_king_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(3, int(d), "boss")
if l3_king_set.size() != 1 or String(l3_king_set[0].get("enemy_id", "")) != "duergar_king" \
or String(l3_king_set[0].get("armor_type", "")) != "heavy":
l3_boss_ok = false
@@ -700,23 +700,23 @@ func _smoke_level_graph() -> void:
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
_db.enemy.rows.append(mix_row_a)
_db.enemy.rows.append(mix_row_b)
_db.combat.enemy.rows.append(mix_row_a)
_db.combat.enemy.rows.append(mix_row_b)
var mix_sys = load("res://core/unit_container.gd").new(_db)
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn(1, 5, int(_db.hero.max_hp)))
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn(1, 5, int(_db.combat.hero.max_hp)))
var mix_units: Array = mix_sys.all_enemies()
Kit.check("混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)", mix_units.size() == 3)
Kit.check("混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)", String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear")
Kit.check("混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)", String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3)
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
var over_sys = load("res://core/unit_container.gd").new(_db)
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn(1, 6, int(_db.hero.max_hp)))
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.enemy.rows.size()) == 28)
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn(1, 6, int(_db.combat.hero.max_hp)))
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 28)
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
@@ -734,7 +734,7 @@ func _smoke_level_graph() -> void:
if not vis_ids.has(int(bid)):
vis_boss_ok = false
Kit.check("可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" % str(_ids_of_type(nodes2, "boss")), vis_boss_ok)
var vis_n: int = int(_db.level.info_visible_layers)
var vis_n: int = int(_db.run.level.info_visible_layers)
Kit.check("可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" % vis_n, vis_n == 2)
# 层距判据按图自算入口 BFS 层(与 runner 同源口径:入口层 0、出边逐层 +1)——
# 锚层 0 时层距 ≤ N 者恒可见、层距 > N 的非终点者恒不可见(两条扫描面都须非空)
@@ -823,7 +823,7 @@ func _smoke_gp_wallet() -> void:
var fin_b := {}
boss.step_row.connect(func(row): rows_b.append(row))
boss.battle_finished.connect(func(p): fin_b["p"] = p)
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.hero.max_hp)}))
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.combat.hero.max_hp)}))
var guard_b := 60
while boss.battle_result == "" and guard_b > 0:
guard_b -= 1
@@ -862,7 +862,7 @@ func _smoke_gp_escrow() -> void:
settlement.step_row.connect(func(row): rows.append(row))
var fin := {}
settlement.battle_finished.connect(func(p): fin["p"] = p)
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}):
continue
var target := _find_gp_dig(settlement.minesweeper, settlement)
if target.is_empty():
@@ -896,15 +896,15 @@ func _smoke_gp_escrow() -> void:
var settle_l = load("res://core/battle_orchestrator.gd").new(_db, rng_l)
var fin_l := {}
settle_l.battle_finished.connect(func(p): fin_l["p"] = p)
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
for i in range(3):
settle_l.reward_grant().grant_routed({"reward_id": "gp_lose", "cell_type": "gp", "polarity": "negative", "display_name": "破财", "power": 2, "effect_id": "gp_loss", "stacks": 1},
"negative", {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
Kit.check("破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)", int(settle_l.reward_grant().escrow) == -6)
settle_l.minesweeper.settle_batch([{"action": "stop"}])
var gp_l: int = int(fin_l.get("p", {}).get("gp_dropped", 999))
var boss_drop: Dictionary = _db.shop_gp_drop_row("boss")
var boss_bonus1: int = _db.shop_level_gp_bonus(1)
var boss_drop: Dictionary = _db.run.shop.gp_drop_row("boss")
var boss_bonus1: int = _db.run.shop.level_gp_bonus_at(1)
Kit.check("破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" % [
gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1],
gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1)
@@ -1048,7 +1048,7 @@ func _smoke_shop_room() -> void:
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.buy_product("armor_up"))
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.hero.armor_class) + 1)
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 受理断言)——总数 = 前段 + 本循环
@@ -1068,12 +1068,12 @@ func _smoke_shop_room() -> void:
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))
# 下一战参数 = 基础表值 + 加算区(百分比已归零——加算先乘后乘序的前半在案)
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.hero.atk) + 1)
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.combat.hero.atk) + 1)
Kit.check("选终点 Boss(句柄 %d)开战" % int(shop_outs[0]), runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null)
var snap: Dictionary = runner.current_battle().presentation_snapshot()
# max_hp 期望按实际购买笔数推导(可购笔数
# 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不锚死恰好 1 笔)
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys)
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.combat.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.combat.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.combat.hero.max_hp) + 3 * hp_buys)
# 冒险收束(Boss 胜或败北——基线裸装口径任一结局)→ 钱包清零
var fin_end := {}
runner.level_finished.connect(func(p): fin_end["p"] = p)
@@ -1106,7 +1106,7 @@ func _smoke_shop_graph() -> void:
var path_total := 0
var graph_total := 0
for L in [1, 2, 3]:
if _db.level_gen_params_row(L).is_empty():
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
Kit.check("L%d 参数档缺行(商店编排断言无源可扫)" % L, false)
continue
for seed_val in range(SWEEP_SEED_FROM, SWEEP_SEED_TO):
@@ -1238,7 +1238,7 @@ func _smoke_shop_pool() -> void:
print("--- 商店共享池(抽3买1全重抽 · 乘法定价 ceil · 池空硬闸 · 切边零入店)---")
var autoplay = load("res://sim/level_autoplay.gd")
var shelf_kinds: Array = []
for row in _db.shop_pool_rows():
for row in _db.run.shop.pool_rows():
shelf_kinds.append(String(row.get("kind_id", "")))
# ① 池模式场景可达(_adv_drive_to_shop 复用——L1 全程+L2 推进到商店房;pool 模式进店即抽架)
var scen := {}
@@ -1268,12 +1268,12 @@ func _smoke_shop_pool() -> void:
var r8 = scen["r"]
var gp0: int = int(r8.gp)
var cost0: int = r8.shop_refresh_cost()
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.adventure.shop_refresh_cost_base))
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base))
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_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.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,7 +1290,7 @@ func _smoke_shop_pool() -> void:
var l2 := int(r8.level_row.level_index)
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
var relic_price_now: int = r8.pool_price("relic")
var relic_price_pre: int = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
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_shelf():
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
@@ -1300,10 +1300,10 @@ func _smoke_shop_pool() -> void:
Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind))
var expect_p: int
if is_relic:
expect_p = int(ceil(float(_db.shop.relic_base_price) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
expect_p = int(ceil(float(_db.run.shop.relic_base_price) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
else:
var prow: Dictionary = _db.shop_pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
var prow: Dictionary = _db.run.shop.pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
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 = {}
@@ -1321,7 +1321,7 @@ func _smoke_shop_pool() -> void:
var val0: int = a8.journal().bonus_total(String(val_map[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.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)))
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._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1)
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
@@ -1332,8 +1332,8 @@ func _smoke_shop_pool() -> void:
second = c
break
if not second.is_empty():
var prow2: Dictionary = _db.shop_pool_row(String(second.get("kind_id", "")))
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * _db.shop_price_a()))
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.buy_pool_item(String(second.get("kind_id", ""))))
else:
@@ -1384,8 +1384,8 @@ func _smoke_shop_pool() -> void:
var ps_ok := true
for k in ps.keys():
var quota := 0
for tier in _db.adventure.TIERS:
quota += int(_db.adventure.curio_quota(String(k), tier))
for tier in _db.run.adventure.TIERS:
quota += int(_db.run.adventure.curio_quota(String(k), tier))
if int(ps[k]) < 0 or int(ps[k]) > quota:
ps_ok = false
Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok)
@@ -1458,7 +1458,7 @@ func _adv_st() -> Dictionary:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -89,7 +89,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -134,7 +134,7 @@ func _meta_pricing_and_buy() -> void:
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币)
sv.meta_state._add_keystone(1000)
var bal0: int = sv.meta_state.keystone_balance()
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
var price0: int = int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
Kit.check("树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0],
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv.meta_state.keystone_balance() == bal0 - price0)
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
@@ -156,8 +156,8 @@ func _meta_settle_and_workshop() -> void:
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
var row1: Dictionary = _db.meta_boss_keystone_row(1)
var row2: Dictionary = _db.meta_boss_keystone_row(2)
var row1: Dictionary = _db.crossrun.meta.boss_keystone_row(1)
var row2: Dictionary = _db.crossrun.meta.boss_keystone_row(2)
# ⑥ 逐杀掉落收束入账(掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐关卡完成关次置位)
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
@@ -351,7 +351,7 @@ func _meta_injection() -> void:
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
sv.meta_state.set_tree_level("sp_hp", 2)
Kit.check("生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)",
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)))
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("effects", {}).get("max_hp", 0)))
Kit.check("攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)",
int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0)
Kit.check("命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)",
@@ -362,10 +362,10 @@ func _meta_injection() -> void:
var r = adr.runner()
var ov: Dictionary = mp.hero_override()
Kit.check("首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)",
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
int(r.effective_atk()) == int(_db.combat.hero.atk) + int(ov.get("atk", 0)))
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
int(r.effective_max_hp()) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.stats().hero_hp) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0)))
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
@@ -376,11 +376,11 @@ func _meta_injection() -> void:
var rw = adr_w.runner()
var rz = adr_z.runner()
Kit.check("白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)",
int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
int(rw.effective_atk()) == int(_db.combat.hero.atk) and int(rw.effective_max_hp()) == int(_db.combat.hero.max_hp)
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
and int(rz.effective_ac()) == int(rw.effective_ac()))
Kit.check("ac 轴:白板恒表值、未换强化时注入零位移",
int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class))
int(rw.effective_ac()) == int(_db.combat.hero.armor_class) and int(r.effective_ac()) == int(_db.combat.hero.armor_class))
_meta_smoke_cleanup()
@@ -393,10 +393,10 @@ func _smoke_forge_axes() -> void:
not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero))
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕",
_db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light")
_db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light")
# 单位快照回退面:spawn 三参形态(零注入)= 锻造配置默认值
var uc = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.combat.hero.max_hp)))
Kit.check("主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕",
String(uc.hero_unit().get("attack_type", "")) == "normal"
and String(uc.hero_unit().get("armor_type", "")) == "light")
@@ -527,12 +527,12 @@ func _smoke_forge_axes() -> void:
# spawn 显式注入面(三参旧形态零注入已在上方覆盖——此处钉显式值)
var uc2 = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕",
uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy")
uc2.spawn(1, 0, int(_db.combat.hero.max_hp), [], -1, -1, [], "magic", "heavy")
and String(uc2.hero_unit().get("attack_type", "")) == "magic"
and String(uc2.hero_unit().get("armor_type", "")) == "heavy")
# V10 复核:树表 12 行 effects 键集无 atk/armor_class(契约 1——树零攻防来源)
var tree_atk_ac := false
for trow in _db.tree_rows():
for trow in _db.crossrun.meta_tree.rows:
var eff: Dictionary = trow.get("effects", {})
if eff.has("atk") or eff.has("armor_class"):
tree_atk_ac = true
@@ -546,11 +546,11 @@ func _smoke_tree() -> void:
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔V1〕结构:12 行 + schema 零违例
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12)
_db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12)
Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)",
_db.meta_tree.validate().is_empty())
_db.crossrun.meta_tree.validate().is_empty())
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}
var ley: Dictionary = _db.meta_tree.row_by_id("easter_ley")
var ley: Dictionary = _db.crossrun.meta_tree.row_by_id("easter_ley")
var ley_ok: bool = not ley.is_empty()
for k in (ley.get("effects", {}) as Dictionary).keys():
if not (String(k) == "max_hp" or String(k) == "hit"):
@@ -559,23 +559,23 @@ func _smoke_tree() -> void:
ley_ok)
# V1/V4 反例:出域行逐类构造被拒(复制行副本塞词表外值)
var t2 = load("res://data/schemas/meta_tree_table.gd").new()
var bad_vein: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_vein: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_vein["node_id"] = "bad_vein"
bad_vein["vein"] = "underworld"
t2.rows = [bad_vein]
Kit.check("schema 反例:vein 出域被拒", not t2.validate().is_empty())
var bad_fork: Dictionary = (_db.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
var bad_fork: Dictionary = (_db.crossrun.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
bad_fork["node_id"] = "bad_fork"
bad_fork["fork_group"] = "fg_mastery"
t2.rows = [bad_fork]
Kit.check("schema 反例:fork_group 出域(fg_mastery 已退役组)被拒", not t2.validate().is_empty())
var bad_arch: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_arch: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_arch["node_id"] = "bad_arch"
bad_arch["effects"] = {"archetype": 1}
t2.rows = [bad_arch]
Kit.check("schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕",
not t2.validate().is_empty())
var bad_dangling: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_dangling: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_dangling["requires"] = ["ghost_node"]
t2.rows = [bad_dangling]
Kit.check("schema 反例:requires 悬空被拒", not t2.validate().is_empty())
@@ -589,7 +589,7 @@ func _smoke_tree() -> void:
not mp.tree_can_buy("sp_hp"))
Kit.check("命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)",
mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1"))
Kit.check("锻造工坊钥匙零币(free)", int(_db.tree_node("forge_workshop").get("price", -1)) == 0)
Kit.check("锻造工坊钥匙零币(free)", int(_db.crossrun.meta_tree.row_by_id("forge_workshop").get("price", -1)) == 0)
Kit.check("行门槛恰达开放(总等级 4 ≥ 4×1)", mp.tree_can_buy("sp_hp"))
Kit.check("第 2 级购买受理(门槛恰达当刻开放)",
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 2)
@@ -35,7 +35,7 @@ func _init() -> void:
func _params(L: int) -> Dictionary:
return _db.level_gen_params_row(L)
return _db.run.level_gen_params.row_by_level_index(L)
func _graph(L: int, seed_val: int) -> Dictionary:
+3 -3
View File
@@ -28,7 +28,7 @@ func _init() -> void:
func _reading(gen, db, L: int) -> Dictionary:
var g: Dictionary = gen.generate(db.level_gen_params_row(L), 42)
var g: Dictionary = gen.generate(db.run.level_gen_params.row_by_level_index(L), 42)
var maxd := 0
for n in g["nodes"]:
maxd = maxi(maxd, int(n["depth"]))
@@ -62,8 +62,8 @@ func _test_growth(gen, db) -> void:
func _test_fail_fast_static(gen, db) -> void:
Kit.check("生产三档参数档可行性判定零错误〔fv4〕",
NodeMapGenerator.check_params(db.level_gen_params_row(1)).is_empty()
and NodeMapGenerator.check_params(db.level_gen_params_row(3)).is_empty())
NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(1)).is_empty()
and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty())
var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4,
"fork_ways_min": 2, "fork_ways_max": 3}
+5 -5
View File
@@ -78,7 +78,7 @@ func check_v7(a, db) -> void:
continue
var hi := 0
for t in cands:
if db.adventure.tier_rank(String(t.get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
if db.run.adventure.tier_rank(String(t.get("tier", ""))) >= db.run.adventure.tier_rank(db.run.adventure.TIER_EXCELLENT):
hi += 1
if hi >= 1:
hi_seen = true
@@ -161,10 +161,10 @@ func check_v8(c, db) -> void:
Kit.check_quiet(false, "V8 前置:冒险启动")
return
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
var cap_field_before := int(db.adventure.curio_cap_gp)
db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
var cap_field_before := int(db.run.adventure.curio_cap_gp)
db.run.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
var gp_tokens: int = c.deck().pool_stock("gp")
Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20,
Kit.check_quiet(gp_tokens > 0 and int(db.run.adventure.curio_value("gp", "common")) == 20,
"改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens)
# cap 只在拿了才计:刷出不计
c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
@@ -187,4 +187,4 @@ func check_v8(c, db) -> void:
gp_cand = true
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
"提前退役:cap 满后候选视图零 gp(牌库本体张数不动=%d——概率位独立存活)〔专长批 V8〕" % gp_in_deck)
db.adventure.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
db.run.adventure.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
+1 -1
View File
@@ -40,7 +40,7 @@ func _init() -> void:
settlement.step_row.connect(func(row): rows.append(row))
var result := {"r": ""}
settlement.battle_finished.connect(func(payload): result.r = payload.get("result", ""))
var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.hero.max_hp)}
var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}
Kit.check_quiet(settlement.begin_battle(enc), "V4 前置:begin_battle 受理")
# 停手触发轮末结算(stop 经批次提交——_maybe_settle 在 WAITING 相位放行 _settle 段序)
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
+20 -20
View File
@@ -40,7 +40,7 @@ func _init() -> void:
# 反例=未装配零命中
var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222)
var ob1b = orch_s.new(db, rng1b)
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
"空装配基线复用:未装配件参数读数 0")
var hits_none = ob1b.fx_host().engine.dispatch(ob1b.fx_host().counters_ctx, "round_start", {"round_index": 1})
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
@@ -48,7 +48,7 @@ func _init() -> void:
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231)
var ob2 = orch_s.new(db, rng2)
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["twin_loader"]}),
"双管装填:装配起战")
Kit.check_quiet(_ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 1})) == 1
and _ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 2})) == 1,
@@ -59,7 +59,7 @@ func _init() -> void:
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241)
var ob3 = orch_s.new(db, rng3)
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}),
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["wide_scout_band"]}),
"阔视野带:装配起战")
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
@@ -69,7 +69,7 @@ func _init() -> void:
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒)
var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251)
var ob4 = orch_s.new(db, rng4)
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}),
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]}),
"试炼镐:装配起战")
# 自动兑付:无玩家入口——揭示缓冲点即时计数,满 charge_n 自动
# grant_ap(cell_revealed 先于本格扣费→AP 归零恰逢满点时先兑再扣、轮不结束)。直调揭示处理器
@@ -88,9 +88,9 @@ func _init() -> void:
"试炼镐读数口:兑付后进度归零(8 取余)、阈值=charge_n、C 区浮窗数据源")
var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252)
var ob4b = orch_s.new(db, rng4b)
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
ob4b._on_cell_revealed({"kind": "safe", "cell": Vector2i(0, 0)})
var ap_base := int(db.action_economy.ap_per_round)
var ap_base := int(db.run.action_economy.ap_per_round)
Kit.check_quiet(int(ob4b.action_economy.ap) == ap_base and not bool(ob4b.attune_convert_state().get("assembled", true)),
"试炼镐反例:未装配零兑付零计数(AP=整轮表值、读数口 assembled=false)")
@@ -98,7 +98,7 @@ func _init() -> void:
# 展示行=C 区悬停浮窗进度唯一承载:handle=-1 非库存句柄+note 进度;空骑库存桩直构投影链)
var rng4c = load("res://core/rng_service.gd").new(); rng4c.set_master_seed(253)
var ob4c = orch_s.new(db, rng4c)
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]})
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]})
var ctrl4 = load("res://presentation/treasure_interact_controller.gd").new({
"db": db, "meta": null, "info_panel": null, "toast": null, "board_view": null,
"settlement_get": func(): return ob4c, "runner_get": func(): return StubAutoInvRunner.new(),
@@ -126,19 +126,19 @@ func _init() -> void:
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
var ae6 = load("res://core/action_economy.gd").new(db)
ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.notify_round_start()
ae6.request_stop()
ae6.finish_round_stop(false, 100)
Kit.check_quiet(ae6.rap == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
var ae6b = load("res://core/action_economy.gd").new(db)
ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round))
ae6b.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round))
ae6b.notify_round_start()
ae6b.request_stop()
ae6b.finish_round_stop(false, 125)
Kit.check_quiet(ae6b.rap == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
var ae6c = load("res://core/action_economy.gd").new(db)
ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round))
ae6c.queue_ap_delta(3 - int(db.run.action_economy.ap_per_round))
ae6c.notify_round_start()
ae6c.request_stop()
ae6c.finish_round_stop(false, 125)
@@ -147,7 +147,7 @@ func _init() -> void:
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
var ob7 = orch_s.new(db, rng7)
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}),
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["nest_forecast"]}),
"巢穴预告:装配起战(board_installed 发射)")
var total7: int = ob7.minesweeper.mine_total()
Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7,
@@ -158,7 +158,7 @@ func _init() -> void:
# ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量)
var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281)
var ob8 = orch_s.new(db, rng8)
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}),
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["incense_stick"]}),
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
@@ -177,7 +177,7 @@ func _init() -> void:
# ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实)
var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291)
var ob9 = orch_s.new(db, rng9)
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}),
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["ranged_pick"]}),
"远程镐:装配起战(dig_range_bonus 注入)")
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
@@ -226,17 +226,17 @@ 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_refresh_free_left()) == int(db.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.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_refresh_free_left()) == int(db.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 真视+安全格直开/危险格不开/封印格跳过)
var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301)
var ob11 = orch_s.new(db, rng11)
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}),
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["sky_eye"]}),
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
@@ -265,7 +265,7 @@ func _init() -> void:
# ⑫ 信标网络(封印不限相邻/封印价照付)
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
var ob12 = orch_s.new(db, rng12)
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}),
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["beacon_network"]}),
"信标网络:装配起战(seal_range_free 注入)")
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
if far12 != Vector2i(-9, -9):
@@ -277,7 +277,7 @@ func _init() -> void:
"信标网络正常:封印可达豁免在册(封印仍按标准动作价计费=try_spend 照走)")
var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312)
var ob12b = orch_s.new(db, rng12b)
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
Kit.check_quiet(ob12b.minesweeper.seal_range_free == false,
"信标网络反例:未装配封印可达豁免=缺省 false(零装配零扰动)")
@@ -285,7 +285,7 @@ func _init() -> void:
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
var ob13 = orch_s.new(db, rng13)
var lg13 = load("res://core/gain_journal.gd").new()
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
"计数链:装配起战(GainJournal 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
@@ -24,7 +24,7 @@ func _init() -> void:
func _trigger_oob() -> void:
# 越域请求(关卡序号 0 < 合法域 ≥1)——_fatal 内部 push fatal 诊断并 quit(1)(帧末退出)
var got: Dictionary = _db.resolve_board_gen(0, 0)
var got: Dictionary = _db.combat.board_gen.resolve_row(0, 0)
if _db.diagnostics_count("fatal") >= 1:
print("[fatal_oob_test] fatal 通道已触发(诊断在案)")
quit(1) # 与 _fatal 的 quit(1) 同码双保险
+27 -22
View File
@@ -30,7 +30,7 @@ func _init() -> void:
# ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)----
func _test_progress_resolution(db) -> void:
var bg_rows: Array = db.board_gen.rows
var bg_rows: Array = db.combat.board_gen.rows
# 行键全集自表推导(禁写死关卡/深度数值)
var level_keys: Array = []
var depth_keys_by_level: Dictionary = {}
@@ -51,7 +51,7 @@ func _test_progress_resolution(db) -> void:
# 精确命中:请求 = 表内现存 (L,D) → 原行返回(字段逐位相等)
var probe_row: Dictionary = bg_rows[0]
var got: Dictionary = db.resolve_board_gen(int(probe_row.level_index), int(probe_row.node_depth))
var got: Dictionary = db.combat.board_gen.resolve_row(int(probe_row.level_index), int(probe_row.node_depth))
Kit.check("进度解析精确命中返回原行(board_gen 首行)",
int(got.get("level_index", -99)) == int(probe_row.level_index)
and int(got.get("node_depth", -99)) == int(probe_row.node_depth)
@@ -59,12 +59,12 @@ func _test_progress_resolution(db) -> void:
# enemy 行集同通道:同 (L,D) 行集数组非空(关卡节点地图 前置——battle 节点双表精确行集契约;
# 行集面契约:返回形态是行集数组,精确性断言随行集面)
var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle")
var en_got: Array = db.combat.enemy.resolve_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle")
Kit.check("enemy 行集同坐标解析非空(双表精确行契约——battle 行类)", not en_got.is_empty())
# 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)
db.reset_diagnostics()
var deep: Dictionary = db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
var deep: Dictionary = db.combat.board_gen.resolve_row(min_l, int(min_depths[-1]) + 1)
Kit.check("深度越最大行 clamp 取该关最大深度行",
int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]))
Kit.check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
@@ -74,19 +74,19 @@ func _test_progress_resolution(db) -> void:
found_clamp = true
Kit.check("警告类别 = resolve_depth_clamp", found_clamp)
var warn_before: int = db.diagnostics_count("warning")
db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
db.combat.board_gen.resolve_row(min_l, int(min_depths[-1]) + 1)
Kit.check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
# 关卡 clamp:请求关卡超最大行 → 取最大关卡行
db.reset_diagnostics()
var high: Dictionary = db.resolve_board_gen(max_l + 1, 0)
var high: Dictionary = db.combat.board_gen.resolve_row(max_l + 1, 0)
Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配 + 节点身份第三维)----
func _test_variant_pool_gate(db) -> void:
var rows: Array = db.variation.rows
var rows: Array = db.combat.variation.rows
var gated_row := {}
var wildcard_row := {}
var global_row := {}
@@ -103,19 +103,19 @@ func _test_variant_pool_gate(db) -> void:
if not gated_row.is_empty():
var u: int = int(gated_row.unlock_level)
var tag: String = String(gated_row.board_types[0])
var before: Array = db.variant_pool(u - 1, tag)
var after: Array = db.variant_pool(u, tag)
var before: Array = db.combat.variation.variant_pool(u - 1, tag)
var after: Array = db.combat.variation.variant_pool(u, tag)
Kit.check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
Kit.check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
if not wildcard_row.is_empty():
var u2: int = int(wildcard_row.unlock_level)
var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag")
var any_pool: Array = db.combat.variation.variant_pool(u2, "nonexistent_board_type_tag")
Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
if not global_row.is_empty():
var u3: int = int(global_row.unlock_level)
Kit.check("global 行 boss 节点入池(board_types ∅ 通配)", _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row))
Kit.check("global 行缺省双参调用跳过(既有调用口径零扰动)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag"), global_row))
Kit.check("global 行 battle 节点静默跳过(门控兜底零报错)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row))
Kit.check("global 行 boss 节点入池(board_types ∅ 通配)", _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row))
Kit.check("global 行缺省双参调用跳过(既有调用口径零扰动)", not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag"), global_row))
Kit.check("global 行 battle 节点静默跳过(门控兜底零报错)", not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row))
func _rows_contain(pool: Array, row: Dictionary) -> bool:
@@ -128,28 +128,28 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool:
# ---- ShopTable 消费面(行集解析单点——runner/编排禁自扫表的契约消费侧验证)----
func _test_shop_accessors(db) -> void:
var products: Array = db.shop_products()
var products: Array = db.run.shop.product_rows()
if products.is_empty():
Kit.check("ShopTable products 行族非空(表已已入库前提)", false)
return
Kit.check("ShopTable products 行族非空", true)
var first: Dictionary = products[0]
var got: Dictionary = db.shop_product_row(String(first.product_id))
var got: Dictionary = db.run.shop.product_row(String(first.product_id))
Kit.check("shop_product_row 按 product_id 精确命中首行", got == first)
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.run.shop.product_row("no_such_product_id__").is_empty())
var tier_rows: Array = db._shop_rows("gp_drops")
if not tier_rows.is_empty():
var tier0: String = String(tier_rows[0].tier)
Kit.check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
Kit.check("shop_gp_drop_row 按 tier 命中", not db.run.shop.gp_drop_row(tier0).is_empty())
var bonus_rows: Array = db._shop_rows("level_gp_bonus")
if not bonus_rows.is_empty():
var blv: int = int(bonus_rows[0].level_index)
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.run.shop.level_gp_bonus_at(blv) == int(bonus_rows[0].bonus))
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.run.shop.level_gp_bonus_at(-1) == 0)
var price_rows: Array = db._shop_rows("price_level_bonus")
if not price_rows.is_empty():
var plv: int = int(price_rows[0].level_index)
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.run.shop.price_level_bonus_at(plv) == int(price_rows[0].bonus))
# ---- MetaTable 消费面 ----
@@ -159,11 +159,16 @@ func _test_meta_accessors(db) -> void:
var key_rows: Array = db._meta_rows("boss_keystone")
if not key_rows.is_empty():
var klv: int = int(key_rows[0].level_index)
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.crossrun.meta.boss_keystone_row(klv) == key_rows[0])
var conv_rows: Array = db._meta_rows("conversion")
if not conv_rows.is_empty():
var cattr: String = String(conv_rows[0].attr)
Kit.check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
var conv_hit := {}
for r in db.crossrun.meta.conversion:
if String(r.get("attr", "")) == cattr:
conv_hit = r
break
Kit.check("meta.conversion 行集按 attr 命中(访问器退役改直扫同判据)", conv_hit == conv_rows[0])
# ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)----
+24 -19
View File
@@ -26,7 +26,7 @@ func _init() -> void:
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
func _test_production_rows(db) -> void:
var rows: Array = db.forge_routes()
var rows: Array = db.crossrun.forge.route_rows()
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
var ids := []
for row in rows:
@@ -52,39 +52,44 @@ func _test_production_rows(db) -> void:
initial.sort()
Kit.check("初始持有恰 sword+light〔锻造工坊批 V1〕", initial == ["light", "sword"])
# 默认两轴(契约 2 所有权移交——裸体态 normal/light)
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.forge_default_attack_type() == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.forge_default_armor_type() == "light")
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_attack_type == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_armor_type == "light")
# 类型轴分族与并联双通道前置(4b-1 裁)
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("spear").get("prereq_route", "")) == "sword"
and String(db.forge_route("staff").get("prereq_route", "")) == "sword")
String(db.crossrun.forge.route("spear").get("prereq_route", "")) == "sword"
and String(db.crossrun.forge.route("staff").get("prereq_route", "")) == "sword")
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("heavy").get("prereq_route", "")) == "light"
and String(db.forge_route("ward").get("prereq_route", "")) == "light")
String(db.crossrun.forge.route("heavy").get("prereq_route", "")) == "light"
and String(db.crossrun.forge.route("ward").get("prereq_route", "")) == "light")
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
int(db.forge_route("spear").get("unlock_price", 0)) == 200
and int(db.forge_route("staff").get("unlock_price", 0)) == 200
and int(db.forge_route("heavy").get("unlock_price", 0)) == 200
and int(db.forge_route("ward").get("unlock_price", 0)) == 200)
int(db.crossrun.forge.route("spear").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("staff").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("heavy").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("ward").get("unlock_price", 0)) == 200)
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
int(db.forge_route("light").get("base_ac", 0)) == 10
and int(db.forge_route("heavy").get("base_ac", 0)) == 12
and int(db.forge_route("ward").get("base_ac", 0)) == 9)
int(db.crossrun.forge.route("light").get("base_ac", 0)) == 10
and int(db.crossrun.forge.route("heavy").get("base_ac", 0)) == 12
and int(db.crossrun.forge.route("ward").get("base_ac", 0)) == 9)
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward")
Kit.check("forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕",
String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear")
String(db.crossrun.forge.armor_route_by_type("ward").get("route_id", "")) == "ward")
var wpn_hit := {}
for r in db.crossrun.forge.route_rows():
if String(r.get("kind", "")) == "weapon" and String(r.get("type_axis", "")) == "pierce":
wpn_hit = r
break
Kit.check("forge route_rows 按轴反查武器路线(pierce)=长枪锻造〔锻造工坊批 V1——访问器退役改直扫同判据〕",
String(wpn_hit.get("route_id", "")) == "spear")
# 缺行/未知轴 = 空字典合法态
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty())
db.crossrun.forge.route("blade").is_empty() and db.crossrun.forge.armor_route_by_type("plate").is_empty())
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
func _test_price_ladder(db) -> void:
var row: Dictionary = db.forge_route("sword")
var row: Dictionary = db.crossrun.forge.route("sword")
var prices := []
for owned in range(5):
prices.append(ForgeTableScript.level_price(row, owned))
@@ -41,9 +41,9 @@ func _table(rows: Array) -> Variant:
# ---- 生产三行正向(V2)----
func _test_production(db) -> void:
var r1: Dictionary = db.level_gen_params_row(1)
var r2: Dictionary = db.level_gen_params_row(2)
var r3: Dictionary = db.level_gen_params_row(3)
var r1: Dictionary = db.run.level_gen_params.row_by_level_index(1)
var r2: Dictionary = db.run.level_gen_params.row_by_level_index(2)
var r3: Dictionary = db.run.level_gen_params.row_by_level_index(3)
Kit.check("访问器读回 L1 行五旋钮齐备〔fv2〕",
int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3)
and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1)
@@ -52,7 +52,7 @@ func _test_production(db) -> void:
Kit.check("三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕",
int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2
and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8)
Kit.check("生产表 validate 零错误〔fv2〕", db.level_gen_params.validate().is_empty())
Kit.check("生产表 validate 零错误〔fv2〕", db.run.level_gen_params.validate().is_empty())
# ---- 访问器缺行防御 ----
@@ -75,7 +75,7 @@ static func _errs_has_cross_field(errs: Array) -> bool:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -92,28 +92,28 @@ func _smoke_adventure_table() -> void:
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null)
if _db.adventure == null:
return
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.adventure.validate().is_empty())
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.adventure.clear_heal_pct),
int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100)
for tier in _db.adventure.TIERS:
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])],
int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]))
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.adventure.deck_total()) == 102)
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty())
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct),
int(_db.run.adventure.clear_heal_pct) >= 0 and int(_db.run.adventure.clear_heal_pct) <= 100)
for tier in _db.run.adventure.TIERS:
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.run.adventure.TIER_QUOTAS[tier])],
int(_db.run.adventure.tier_total(tier)) == int(_db.run.adventure.TIER_QUOTAS[tier]))
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.run.adventure.deck_total()) == 102)
Kit.check("数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2",
int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2)
int(_db.run.adventure.curio_value("gp", "common")) == 20 and int(_db.run.adventure.curio_value("gp", "excellent")) == 50
and int(_db.run.adventure.curio_value("attack", "common")) == 1 and int(_db.run.adventure.curio_value("attack", "excellent")) == 2
and int(_db.run.adventure.curio_value("max_hp", "common")) == 5 and int(_db.run.adventure.curio_value("max_hp", "excellent")) == 10
and int(_db.run.adventure.curio_value("armor", "common")) == 1 and int(_db.run.adventure.curio_value("armor", "excellent")) == 2)
Kit.check("恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕",
int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1)
int(_db.run.adventure.curio_value("heal", "excellent")) == 6 and int(_db.run.adventure.curio_value("ap", "rare")) == 1)
Kit.check("cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕",
int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99)
int(_db.run.adventure.curio_cap("gp")) == 99 and int(_db.run.adventure.curio_cap("attack")) == 99)
Kit.check("同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, null) == 0)
Kit.check("同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, _TreeSaveMock.new()) == 1)
# 同调目录 12 件(V1/V2 断言落点;装配→战斗贯通与空装配基线的现行主语 = morning_order)
var trows: Array = _db.treasure_attunement_rows()
var trows: Array = _db.crossrun.treasure_items.attunement_rows()
var tid_set := {}
for r in trows:
tid_set[String(r.get("item_id", ""))] = true
@@ -160,11 +160,11 @@ func _smoke_adventure_table() -> void:
alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2)
var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191))
Kit.check("装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕",
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]})
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "attunement": ["morning_order"]})
and orch_l.attune_has("morning_order"))
var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192))
Kit.check("空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕",
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
and not orch_e.attune_has("morning_order"))
# V9 静态词表断言(棋盘事实不可变契约 3:12 行 triggers do.kind 与 effect_kinds
# 全集零棋盘改写类词〔rewrite_board/remove_mines/cell_removal=改写族——现表零引用〕;
@@ -223,14 +223,14 @@ func _smoke_enemy_bands() -> void:
"3|boss|9|duergar_king", "3|boss|11|duergar_king", "3|boss|12|duergar_king", "3|boss|13|duergar_king",
]
var got_keys: Array[String] = []
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕",
_db.enemy.rows.size() == 28 and got_keys == expect_keys)
_db.combat.enemy.rows.size() == 28 and got_keys == expect_keys)
# ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行)
var hob := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.get("enemy_id", "")) == "hobgoblin":
hob = row
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕",
@@ -242,7 +242,7 @@ func _smoke_enemy_bands() -> void:
# 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行——
# 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕)
var boss_bands := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.get("row_kind", "")) == "boss":
var lk: int = int(row.get("level_index", -1))
if not boss_bands.has(lk):
@@ -269,47 +269,47 @@ func _smoke_enemy_bands() -> void:
func _smoke_ac_swap_guard() -> void:
print("--- 护甲等级掷点批·换装护栏 ---")
# ① 三处无双轨
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.status_effect_row("defense_up").is_empty())
var ac_row: Dictionary = _db.status_effect_row("ac_up")
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.combat.status_effects.row_by_id("defense_up").is_empty())
var ac_row: Dictionary = _db.combat.status_effects.row_by_id("ac_up")
Kit.check("ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)",
not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \
and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \
and String(ac_row.get("stack_policy", "")) == "refresh")
var ac_reward := {}
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive":
ac_reward = row
Kit.check("奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)",
String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护")
Kit.check("effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)",
not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up"))
not _db.combat.settlement.core.effect_pool_map.has("defense_up") and not _db.combat.settlement.core.effect_pool_map.has("ac_up"))
# ② 免伤墙归零护栏(泛化——BUG-0003 免疫)
var wall_ok := true
var wall_detail := ""
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
continue
var row: Dictionary = _db.status_effect_row(eff_id)
var row: Dictionary = _db.combat.status_effects.row_by_id(eff_id)
if row.is_empty() or String(row.get("unit", "")) != "percent":
continue
var floor_v: int = int(row.get("magnitude", 0)) * int(row.get("max_stack", 1))
if floor_v <= int(_db.settlement.core.pool_clamp_lo):
if floor_v <= int(_db.combat.settlement.core.pool_clamp_lo):
wall_ok = false
wall_detail += " %s(%d)" % [eff_id, floor_v]
Kit.check("hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)], wall_ok)
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.combat.settlement.core.pool_clamp_lo)], wall_ok)
var hd_maps := 0
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
hd_maps += 1
Kit.check("hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)", hd_maps == 0)
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class),
int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30)
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.combat.hero.armor_class),
int(_db.combat.hero.armor_class) >= 0 and int(_db.combat.hero.armor_class) <= 30)
var ac_band := true
var ac_detail := ""
var heavy_names := ["熊地精重击手", "灰矮人重装手"] # 逐行手设值 8/9,非精英编码位
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var ac: int = int(row.get("armor_class", -1))
var dname := String(row.get("display_name", "?"))
var boss_row: bool = String(row.get("enemy_id", "")).ends_with("_chief") or \
@@ -325,7 +325,7 @@ func _smoke_ac_swap_guard() -> void:
ac_detail if ac_detail != "" else "无"], ac_band)
# ③b Boss 台阶断言(关卡即台阶——L1 8 < L2 10 < L3 12)+ 首遇位教学位低 AC(≤3)
var boss_ac := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var eid := String(row.get("enemy_id", ""))
if eid == "ogre_chief" or eid == "drider" or eid == "duergar_king":
boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0))
@@ -337,7 +337,7 @@ func _smoke_ac_swap_guard() -> void:
# 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带)
var l12_band := _first_encounter_band(1, "bugbear")
var l12_row: Dictionary = {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
and int(row.get("depth_band", -1)) == l12_band and String(row.get("enemy_id", "")) == "bugbear":
l12_row = row
@@ -350,7 +350,7 @@ func _smoke_ac_swap_guard() -> void:
and int(l12_row.get("armor_class", -1)) == 2 \
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2})
var l1_totals := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != 1:
continue
var cnt_lo: int = int(row.get("unit_count", 1))
@@ -370,12 +370,12 @@ func _smoke_ac_swap_guard() -> void:
Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [
curve_detail if curve_detail != "" else "无"], curve_ok)
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
var bounds: Array = _db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = _db.settlement.damage_roll.roll_tier_discounts
var bounds: Array = _db.combat.settlement.damage_roll.roll_tier_bounds
var discounts: Array = _db.combat.settlement.damage_roll.roll_tier_discounts
Kit.check("掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)",
int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \
and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \
and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll")
int(_db.combat.settlement.damage_roll.roll_faces) == 20 and int(_db.combat.settlement.damage_roll.crit_roll) == 20 \
and int(_db.combat.settlement.damage_roll.crit_multiplier) == 200 and int(_db.combat.settlement.damage_roll.damage_floor) == 1 \
and int(_db.combat.settlement.damage_roll.attack_bonus) == 0 and String(_db.combat.settlement.damage_roll.armor_roll_stream) == "armor_roll")
Kit.check("档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)",
bounds == [3, 7] and discounts == [75, 50, 25])
@@ -385,24 +385,24 @@ func _smoke_ac_swap_guard() -> void:
func _smoke_variation_framework() -> void:
print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---")
Kit.check("变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)",
_db.variation.rows.size() == 6)
_db.combat.variation.rows.size() == 6)
Kit.check("变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)",
_db.variation.validate().is_empty())
_db.combat.variation.validate().is_empty())
var pool1_ids: Array = []
for r in _db.variant_pool(1, "dungeon"):
for r in _db.combat.variation.variant_pool(1, "dungeon"):
pool1_ids.append(String(r.variant_id))
Kit.check("L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)", pool1_ids == ["thorn", "vine"])
var pool2_ids: Array = []
for r in _db.variant_pool(2, "underdark"):
for r in _db.combat.variation.variant_pool(2, "underdark"):
pool2_ids.append(String(r.variant_id))
Kit.check("L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)", pool2_ids == ["magma", "ash"])
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.variant_pool(1, "underdark").is_empty())
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.combat.variation.variant_pool(1, "underdark").is_empty())
var pool2d_ids: Array = []
for r in _db.variant_pool(2, "dungeon"):
for r in _db.combat.variation.variant_pool(2, "dungeon"):
pool2d_ids.append(String(r.variant_id))
Kit.check("board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)",
pool2d_ids == ["thorn", "vine"])
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.variation.row_by_id("nonexistent").is_empty())
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.combat.variation.row_by_id("nonexistent").is_empty())
# 跨字段互锁负样本(加载期致命——枚举语义一致性闸)
var vt = load("res://data/schemas/variation_table.gd").new()
vt.rows.assign([{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
@@ -419,30 +419,30 @@ func _smoke_variation_framework() -> void:
func _smoke_difficulty_table() -> void:
print("--- 难度曲线与Boss·DifficultyTable 结构位 ---")
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.difficulty.rows.is_empty())
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty())
# ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样
var enemy_system = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理", enemy_system.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("spawn 受理", enemy_system.spawn(1, 0, int(_db.combat.hero.max_hp)))
var unit = enemy_system.all_enemies()[0]
var d0: Dictionary = _db.resolve_enemy_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
var d0: Dictionary = _db.combat.enemy.resolve_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
Kit.check("空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)],
int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk))
# ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn(1, 0, int(_db.combat.hero.max_hp)))
var g2 = enemy_system_b.all_enemies()[0]
Kit.check("hp 修饰 ×1.5(%d→%d,max_hp 同步)" % [int(d0.hp), int(g2.hp)],
int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp))
Kit.check("atk 修饰 ×2.0(%d→%d)" % [int(d0.atk), int(g2.atk)], int(g2.atk) == int(d0.atk) * 2)
# ③ 越域 clamp:乘数 50 → clamp 10
_db.difficulty.rows.clear()
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
_db.run.difficulty.rows.clear()
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn(1, 0, int(_db.combat.hero.max_hp)))
Kit.check("越域乘数 clamp 至 10(%d→%d)" % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)],
int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10)
_db.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
_db.run.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
# ---- ⑤ 格面资源九型 + gp_drop 流(黄金向量)----
@@ -455,7 +455,7 @@ func _smoke_nine_types_gp_stream() -> void:
var pos_weight := 0
var gp_pos := -1
var gp_neg := -1
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("pool_tag", "")) == "cell":
cell_rows += 1
if String(row.get("polarity", "")) == "positive":
@@ -468,7 +468,7 @@ func _smoke_nine_types_gp_stream() -> void:
cell_rows == 18)
# 危险格负向直伤当量(负向≈正向 2 倍定价——真人反馈「三雷 3/4 血无后果感」的投值口径)
var mine_power := {"heal_backlash": 6, "pierce_snare": 8, "magic_recoil": 8} # 危险格负向直伤当量(抬档值;依据见 numfix 报告)
for row in _db.reward.rows:
for row in _db.run.reward.rows:
var rid := String(row.get("reward_id", ""))
if mine_power.has(rid):
Kit.check("危险格负向直伤 %s power=%d(卡⑧投值)" % [rid, int(mine_power[rid])],
@@ -477,7 +477,7 @@ func _smoke_nine_types_gp_stream() -> void:
# 合计即配置自身的和(改表自动跟随,不再硬编码合计值)。
var pos_all_in_pool := true
var neg_all_zero := true
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("pool_tag", "")) != "cell":
continue
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", 0)) < 1:
@@ -506,11 +506,11 @@ func _smoke_meta() -> void:
and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2)
var attrs_ok := true
for a in ["max_hp", "atk", "armor_class"]:
attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty()
attrs_ok = attrs_ok and not _conversion_row_of(_db.crossrun.meta.conversion, a).is_empty()
Kit.check("conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)", attrs_ok)
var boss_ok := true
for li in [1, 2, 3]:
var brow: Dictionary = _db.meta_boss_keystone_row(li)
var brow: Dictionary = _db.crossrun.meta.boss_keystone_row(li)
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
Kit.check("boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)", boss_ok)
@@ -519,3 +519,10 @@ class _TreeSaveMock:
# 核代码增补:最小 save 形态——attunement.slots 只读 tree_level("att_socketN")
func tree_level(n: String) -> int:
return 1 if n == "att_socket1" else 0
func _conversion_row_of(rows: Array, attr: String) -> Dictionary:
for r in rows:
if String(r.get("attr", "")) == attr:
return r
return {}
@@ -64,7 +64,7 @@ func _test_boss_name_uses_boss_rowset() -> void:
if not loaded:
return
var boss_row := {}
for row in db.enemy.rows:
for row in db.combat.enemy.rows:
if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss":
boss_row = row
break
@@ -72,8 +72,8 @@ func _test_boss_name_uses_boss_rowset() -> void:
if boss_row.is_empty():
return
var boss_depth: int = int(boss_row.depth_band)
var boss_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "boss")
var battle_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "battle")
var boss_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "boss")
var battle_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "battle")
var boss_expect := String(boss_rows[0].get("display_name", ""))
var battle_name := String(battle_rows[0].get("display_name", ""))
var runner := _BossRunner.new()
@@ -96,7 +96,7 @@ func _sweep_source_names() -> void:
row_view._tip_label.text.contains("来源:危险格"))
row_view._hide_tip()
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db)
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))
@@ -27,7 +27,7 @@ func _init() -> void:
return
# board_type 词表自 board_gen 行集推导(禁测试侧写死词表)
var types: Array = []
for row in db.board_gen.rows:
for row in db.combat.board_gen.rows:
var bt := String(row.get("board_type", ""))
if bt != "" and not types.has(bt):
types.append(bt)
@@ -839,11 +839,11 @@ func _career_overlay_projection() -> void:
sv.write_to_disk()
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
var data := {"total": sv.meta_state.career_state().adventure_total, "cleared": sv.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.meta_state.keystone_balance()}
for i in _db.level_count():
for i in _db.run.level.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
data["clears"].append({"display_name": String(_db.run.level.row_by_level_index(li).get("display_name", "")),
"count": sv.meta_state.career_state().clear_counts.get(str(li), 0)})
if sv.meta_state.career_state().boss_first_kill.has(str(li)):
data["boss_first_kills"] += 1
@@ -884,10 +884,10 @@ func _career_overlay_projection() -> void:
v5.load_from_disk() and v5.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0)
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.meta_state.career_state().adventure_total, "cleared": v5.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.meta_state.keystone_balance()}
for i in _db.level_count():
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
for i in _db.run.level.level_count():
data_v5["clears"].append({"display_name": String(_db.run.level.row_by_level_index(i + 1).get("display_name", "")),
"count": v5.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
co_v5.open_career(data_v5)