diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 17a48750..979b7e5f 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -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 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 46bd9efe..44ccda53 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -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}) # 逐杀明细+累计单写原语 diff --git a/game/core/attune_loadout.gd b/game/core/attune_loadout.gd index bd6fcf4a..55fabc8e 100644 --- a/game/core/attune_loadout.gd +++ b/game/core/attune_loadout.gd @@ -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} diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index 5ffa6955..a4a3bc42 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -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)) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index fbaed186..2efd161f 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -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), diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index fea8b7cc..accfd631 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -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 diff --git a/game/core/config_db.gd b/game/core/config_db.gd index e6370402..bd535c90 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -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: diff --git a/game/core/config_domain_combat.gd b/game/core/config_domain_combat.gd new file mode 100644 index 00000000..3ea1e5f1 --- /dev/null +++ b/game/core/config_domain_combat.gd @@ -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 diff --git a/game/core/config_domain_crossrun.gd b/game/core/config_domain_crossrun.gd new file mode 100644 index 00000000..e001d1ba --- /dev/null +++ b/game/core/config_domain_crossrun.gd @@ -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 diff --git a/game/core/config_domain_run.gd b/game/core/config_domain_run.gd new file mode 100644 index 00000000..0bdaa330 --- /dev/null +++ b/game/core/config_domain_run.gd @@ -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 diff --git a/game/core/curio_deck.gd b/game/core/curio_deck.gd index c69c4092..4df30645 100644 --- a/game/core/curio_deck.gd +++ b/game/core/curio_deck.gd @@ -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 diff --git a/game/core/curio_stop.gd b/game/core/curio_stop.gd index 76dec168..fdaaa76a 100644 --- a/game/core/curio_stop.gd +++ b/game/core/curio_stop.gd @@ -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) diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 6afbef7b..8d72cb9b 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -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)) # 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制") diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 1766db07..9388a003 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -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)) diff --git a/game/core/hero_stats.gd b/game/core/hero_stats.gd index aad9753d..dc0390ac 100644 --- a/game/core/hero_stats.gd +++ b/game/core/hero_stats.gd @@ -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)) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index cb860f5f..9469a2f0 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -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" \ diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index b8797b51..0d9687c1 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -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))): diff --git a/game/core/refresh_bank.gd b/game/core/refresh_bank.gd index 4502be5b..1259006d 100644 --- a/game/core/refresh_bank.gd +++ b/game/core/refresh_bank.gd @@ -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 调用——刷新三源与付费计数全零) diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index e02515f2..eed41036 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -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 {} diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index 5b0dfe17..3267c2ac 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -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)) diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index ccb739cd..db8bae28 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -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, diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 162a1e3e..2de0123c 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -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 外组合 = 数据行新词表未接线(显式忽略面) # 地形变异伤害单据——无类型必中,下限保底在控制器管线内 diff --git a/game/core/shop_session.gd b/game/core/shop_session.gd index 028a98c6..531610d6 100644 --- a/game/core/shop_session.gd +++ b/game/core/shop_session.gd @@ -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: diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 896ee8b0..52def7f8 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -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: diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index 786a60ec..e3015e22 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -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 注入;测试可注入桩表) diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 066f5ca0..6031509a 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -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: diff --git a/game/data/schemas/board_gen_table.gd b/game/data/schemas/board_gen_table.gd index 2401f512..78420031 100644 --- a/game/data/schemas/board_gen_table.gd +++ b/game/data/schemas/board_gen_table.gd @@ -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) diff --git a/game/data/schemas/difficulty_table.gd b/game/data/schemas/difficulty_table.gd index 964eabdd..db21e2bd 100644 --- a/game/data/schemas/difficulty_table.gd +++ b/game/data/schemas/difficulty_table.gd @@ -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) diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 388a3cf3..851f2a6c 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -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) diff --git a/game/data/schemas/forge_table.gd b/game/data/schemas/forge_table.gd index 4c34606c..57447c6a 100644 --- a/game/data/schemas/forge_table.gd +++ b/game/data/schemas/forge_table.gd @@ -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. 直连)—— +# 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 {} diff --git a/game/data/schemas/level_gen_params.gd b/game/data/schemas/level_gen_params.gd index c3e963f6..53284bcd 100644 --- a/game/data/schemas/level_gen_params.gd +++ b/game/data/schemas/level_gen_params.gd @@ -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 diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index 1d7fabea..b52a3dec 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -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() diff --git a/game/data/schemas/meta_table.gd b/game/data/schemas/meta_table.gd index 779a0e8f..3150b389 100644 --- a/game/data/schemas/meta_table.gd +++ b/game/data/schemas/meta_table.gd @@ -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. 直连)---- + +## 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 {} diff --git a/game/data/schemas/shop_table.gd b/game/data/schemas/shop_table.gd index 8028e36e..768493e2 100644 --- a/game/data/schemas/shop_table.gd +++ b/game/data/schemas/shop_table.gd @@ -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. 直连)---- + +## 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 {} diff --git a/game/data/schemas/variation_table.gd b/game/data/schemas/variation_table.gd index 1b00199e..1b4af099 100644 --- a/game/data/schemas/variation_table.gd +++ b/game/data/schemas/variation_table.gd @@ -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. 直连)---- +# 门控第三维 = 节点身份(全局作用域变异):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 {} diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index 066aa8d9..d8874ef6 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -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 "遗迹守卫" diff --git a/game/presentation/board_resolvers.gd b/game/presentation/board_resolvers.gd index 554d8c5e..0856ee2f 100644 --- a/game/presentation/board_resolvers.gd +++ b/game/presentation/board_resolvers.gd @@ -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 "" diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index a85bf399..fea7cb36 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -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() diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index afa4bece..f5cbf94f 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -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 diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index e57b7f97..09e8219c 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -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(), } diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 729df063..fb8b14f5 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -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() diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index f0c2a4ea..f05b8c18 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -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)) diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index 862c51f7..badcc2bd 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -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) diff --git a/game/presentation/treasure_interact_controller.gd b/game/presentation/treasure_interact_controller.gd index 759be778..de978289 100644 --- a/game/presentation/treasure_interact_controller.gd +++ b/game/presentation/treasure_interact_controller.gd @@ -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", "")): diff --git a/game/presentation/tune_sim_controller.gd b/game/presentation/tune_sim_controller.gd index 4f681dab..bf089c07 100644 --- a/game/presentation/tune_sim_controller.gd +++ b/game/presentation/tune_sim_controller.gd @@ -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) diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index d1b2706e..4c815653 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -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() diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index 3fe1ca55..fb036e42 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -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 += "〕" diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index be6b5550..bc44cc7d 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -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]) diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 26896b6d..b82bc2c1 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -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 区弃图刷新按钮文案缺失") diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index a268521c..8ec33796 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -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(): diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index 6d15d787..4cd3b9a7 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -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;探针禁碰真实档,同族探针隔离纪律; diff --git a/game/sim/adventure_monte_carlo.gd b/game/sim/adventure_monte_carlo.gd index 718a2e9c..64277218 100644 --- a/game/sim/adventure_monte_carlo.gd +++ b/game/sim/adventure_monte_carlo.gd @@ -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): diff --git a/game/sim/board_stat_probe.gd b/game/sim/board_stat_probe.gd index 70ef61d8..1c036e44 100644 --- a/game/sim/board_stat_probe.gd +++ b/game/sim/board_stat_probe.gd @@ -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)) diff --git a/game/sim/calibrate_boards.gd b/game/sim/calibrate_boards.gd index afdbe38a..71daea40 100644 --- a/game/sim/calibrate_boards.gd +++ b/game/sim/calibrate_boards.gd @@ -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), diff --git a/game/sim/damage_probe.gd b/game/sim/damage_probe.gd index ed146547..da833a38 100644 --- a/game/sim/damage_probe.gd +++ b/game/sim/damage_probe.gd @@ -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: diff --git a/game/sim/dump_battle.gd b/game/sim/dump_battle.gd index 2d44544e..e13a342a 100644 --- a/game/sim/dump_battle.gd +++ b/game/sim/dump_battle.gd @@ -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]) diff --git a/game/sim/experiment_density.gd b/game/sim/experiment_density.gd index 03c46dc6..26879000 100644 --- a/game/sim/experiment_density.gd +++ b/game/sim/experiment_density.gd @@ -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 diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index bae08513..61d2597c 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -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() diff --git a/game/sim/monte_carlo.gd b/game/sim/monte_carlo.gd index f9600231..93d1705c 100644 --- a/game/sim/monte_carlo.gd +++ b/game/sim/monte_carlo.gd @@ -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: diff --git a/game/sim/rhythm_batch.gd b/game/sim/rhythm_batch.gd index ab4a7f4a..057f2b78 100644 --- a/game/sim/rhythm_batch.gd +++ b/game/sim/rhythm_batch.gd @@ -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") diff --git a/game/sim/skill_rhythm_probe.gd b/game/sim/skill_rhythm_probe.gd index 456fe6c8..f2355fe5 100644 --- a/game/sim/skill_rhythm_probe.gd +++ b/game/sim/skill_rhythm_probe.gd @@ -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: diff --git a/game/sim/verifier_batch.gd b/game/sim/verifier_batch.gd index ed732439..f3038855 100644 --- a/game/sim/verifier_batch.gd +++ b/game/sim/verifier_batch.gd @@ -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 diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index 4946923f..431864b7 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -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))], diff --git a/game/tests/integration/node_map_determinism_test.gd b/game/tests/integration/node_map_determinism_test.gd index e6ea7d47..773fce03 100644 --- a/game/tests/integration/node_map_determinism_test.gd +++ b/game/tests/integration/node_map_determinism_test.gd @@ -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() diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index d2a58b9d..31e53d14 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -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 diff --git a/game/tests/unit/battle_settlement/damage_math_test.gd b/game/tests/unit/battle_settlement/damage_math_test.gd index 24bac4be..db07970d 100644 --- a/game/tests/unit/battle_settlement/damage_math_test.gd +++ b/game/tests/unit/battle_settlement/damage_math_test.gd @@ -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)) diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index 2edf9278..65240185 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 17c78e42..31a9b214 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -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: diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index 5ba420aa..e8242bae 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -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(): diff --git a/game/tests/unit/battle_settlement/skill_unlock_test.gd b/game/tests/unit/battle_settlement/skill_unlock_test.gd index 245cd099..6793d808 100644 --- a/game/tests/unit/battle_settlement/skill_unlock_test.gd +++ b/game/tests/unit/battle_settlement/skill_unlock_test.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index 0929ed4f..e75be3fb 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/variant_placement_test.gd b/game/tests/unit/battle_settlement/variant_placement_test.gd index a623b242..0456555c 100644 --- a/game/tests/unit/battle_settlement/variant_placement_test.gd +++ b/game/tests/unit/battle_settlement/variant_placement_test.gd @@ -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()) diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index aacd48cc..11b0a352 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -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 diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index e14ea2d7..8d8b6d01 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -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", []) diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index 270f6335..4e481a60 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -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 diff --git a/game/tests/unit/core/board_generator_test.gd b/game/tests/unit/core/board_generator_test.gd index 3e7cc062..e4340644 100644 --- a/game/tests/unit/core/board_generator_test.gd +++ b/game/tests/unit/core/board_generator_test.gd @@ -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)})) diff --git a/game/tests/unit/core/config_db_resolver_test.gd b/game/tests/unit/core/config_db_resolver_test.gd index cc60df4a..ebec43b2 100644 --- a/game/tests/unit/core/config_db_resolver_test.gd +++ b/game/tests/unit/core/config_db_resolver_test.gd @@ -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: diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index e317fc72..4d9558f4 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -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]) # ---- ① 停点在途谓词:无停点时免费/付费刷新皆拒绝(付费路径扣费前置校验防空转烧钱包)---- diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 8c691ae7..a5dec54e 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -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() diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index de96b9d1..37fe2e18 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -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 同式) diff --git a/game/tests/unit/core/level_map_type_vis_test.gd b/game/tests/unit/core/level_map_type_vis_test.gd index 94664ad8..06120fd9 100644 --- a/game/tests/unit/core/level_map_type_vis_test.gd +++ b/game/tests/unit/core/level_map_type_vis_test.gd @@ -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): diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 704bd083..def51633 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -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: diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index fa2bc037..b82e85c9 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -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) diff --git a/game/tests/unit/core/node_map_generator_test.gd b/game/tests/unit/core/node_map_generator_test.gd index 343984d8..9c1bc48d 100644 --- a/game/tests/unit/core/node_map_generator_test.gd +++ b/game/tests/unit/core/node_map_generator_test.gd @@ -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: diff --git a/game/tests/unit/core/node_map_growth_test.gd b/game/tests/unit/core/node_map_growth_test.gd index ef8f7608..86d3227f 100644 --- a/game/tests/unit/core/node_map_growth_test.gd +++ b/game/tests/unit/core/node_map_growth_test.gd @@ -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} diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index c7768486..839ca48f 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -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 # 还原表(同进程后续套件防御) diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index 7e15cf4c..513e10c3 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -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"}]) diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 36076598..13db7148 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -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) diff --git a/game/tests/unit/data_config/config_db_fatal_oob_test.gd b/game/tests/unit/data_config/config_db_fatal_oob_test.gd index 31d1d348..5265be3b 100644 --- a/game/tests/unit/data_config/config_db_fatal_oob_test.gd +++ b/game/tests/unit/data_config/config_db_fatal_oob_test.gd @@ -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) 同码双保险 diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index 3d229531..46346262 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -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 单点契约的登记面)---- diff --git a/game/tests/unit/data_config/forge_table_test.gd b/game/tests/unit/data_config/forge_table_test.gd index a9054b27..c1a75a4e 100644 --- a/game/tests/unit/data_config/forge_table_test.gd +++ b/game/tests/unit/data_config/forge_table_test.gd @@ -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)) diff --git a/game/tests/unit/data_config/level_gen_params_test.gd b/game/tests/unit/data_config/level_gen_params_test.gd index d99beb67..a866d6ac 100644 --- a/game/tests/unit/data_config/level_gen_params_test.gd +++ b/game/tests/unit/data_config/level_gen_params_test.gd @@ -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()) # ---- 访问器缺行防御 ---- diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index a9f21456..ad020e0e 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -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 {} diff --git a/game/tests/unit/presentation/battle_narrator_test.gd b/game/tests/unit/presentation/battle_narrator_test.gd index e58a8085..13362dd7 100644 --- a/game/tests/unit/presentation/battle_narrator_test.gd +++ b/game/tests/unit/presentation/battle_narrator_test.gd @@ -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() diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index ce9511d9..d0e055cc 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -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})) diff --git a/game/tests/unit/presentation/terrain_tile_sets_test.gd b/game/tests/unit/presentation/terrain_tile_sets_test.gd index 94599bc5..a7f295e4 100644 --- a/game/tests/unit/presentation/terrain_tile_sets_test.gd +++ b/game/tests/unit/presentation/terrain_tile_sets_test.gd @@ -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) diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index f8a9403a..1b5651eb 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -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)