卡①T7(核心数据面结构收敛批): config_db归位+域容器——三容器建件(ConfigDomainCombat 7表/Run 7表/Crossrun 5表,类型化纯持有)/config_db瘦身44→8公开(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)+类变量9(容器3+is_ready+诊断/resolver/validator/reload_gate/fatal_flag)/溶解16+迁移14+委托3访问器归位表类(shop_table 7方法/meta_table 2/forge_table 3[route_rows避routes属性]/variation_table 2/level_table level_count)+resolver装配段注入三表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers委托,resolver文件零搬移)/删2(meta_conversion_row/forge_weapon_route_by_type生产零消费)/私1(validate_skill_pool_links→_);全域改址86文件(组①db.<表>→db.<容器>.<表>组②33访问器归位新名+删2消费面直扫同判据+桩件三处适配FakeDB/_BoardRowStub嵌套holder+treasure_items has_method改crossrun探测);首跑清扫漏表名bug已整树回滚重跑修正;gdstyle 168→92键NEW=0;[测试动作移交用户:套件停跑]
This commit is contained in:
1 parent
ced64d9cb3
commit
a9a163f924
97 files changed
+1015
-1072
No files matched your search
@@ -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
|
||||
|
||||
@@ -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}) # 逐杀明细+累计单写原语
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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))
|
||||
@@ -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),
|
||||
|
||||
@@ -22,7 +22,7 @@ static func target_mine_count(params: Dictionary) -> int:
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
|
||||
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表
|
||||
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub)
|
||||
var row: Dictionary[Variant, Variant] = db.resolve_board_gen(L, D)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.board_gen.resolve_row(L, D)
|
||||
if row.is_empty():
|
||||
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
|
||||
return null
|
||||
|
||||
+39
-319
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
## 冒险域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):冒险 runner 域七表类型化持有。
|
||||
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
|
||||
class_name ConfigDomainRun
|
||||
extends Resource
|
||||
|
||||
var level: LevelTable
|
||||
var difficulty: DifficultyTable
|
||||
var level_gen_params: LevelGenParams
|
||||
var reward: RewardTable
|
||||
var adventure: AdventureTable
|
||||
var action_economy: ActionEconomyTable
|
||||
var shop: ShopTable
|
||||
+17
-17
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
# 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制")
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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))):
|
||||
|
||||
@@ -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 调用——刷新三源与付费计数全零)
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -147,7 +147,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
|
||||
if fx_host._fx_bonus_delta.has(axis):
|
||||
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
|
||||
"blast_scaled":
|
||||
var blast_v := maxi(int(oc.db.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
|
||||
var blast_v := maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
|
||||
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
|
||||
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
|
||||
"ri": ri, "origin_cell": cell})
|
||||
@@ -163,7 +163,7 @@ func _terrain_batch(ri: int) -> void:
|
||||
for evt in oc._facts_reveals:
|
||||
var vids: Array[Variant] = evt.get("variant_ids", [])
|
||||
for vid in vids:
|
||||
var vrow: Dictionary[Variant, Variant] = oc.db.variation.row_by_id(String(vid))
|
||||
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) != "flat_damage":
|
||||
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
|
||||
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
|
||||
|
||||
+16
-16
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 注入;测试可注入桩表)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -158,3 +158,27 @@ func _validate_initial_coverage(initial_ids: Dictionary) -> Array[Dictionary]:
|
||||
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
|
||||
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
|
||||
return errs
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位;消费面 db.crossrun.forge.<m> 直连)——
|
||||
# routes 属性在册故行族读取命名 route_rows(避开成员名冲突)----
|
||||
|
||||
## 六路线行族(副本——调用方禁改)
|
||||
func route_rows() -> Array:
|
||||
return routes.duplicate()
|
||||
|
||||
|
||||
## 路线行单点(键 route_id;缺行=空字典)
|
||||
func route(route_id: String) -> Dictionary:
|
||||
for row in routes:
|
||||
if String(row.get("route_id", "")) == route_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
|
||||
func armor_route_by_type(armor_type: String) -> Dictionary:
|
||||
for row in routes:
|
||||
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
|
||||
return row
|
||||
return {}
|
||||
@@ -1,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
|
||||
|
||||
@@ -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()
|
||||
@@ -90,3 +90,21 @@ func _validate_attune_refresh() -> Array[Dictionary]:
|
||||
errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]})
|
||||
break
|
||||
return errs
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.crossrun.meta.<m> 直连)----
|
||||
|
||||
## Boss 钥石两档行单点(键 level_index;缺行=空字典)
|
||||
func boss_keystone_row(level_index: int) -> Dictionary:
|
||||
for row in boss_keystone:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## Y 刷新升级行单点(键 = 等级,缺行返回空字典=未购/已满级合法态)
|
||||
func attune_refresh_row(level: int) -> Dictionary:
|
||||
for row in attune_refresh_upgrade:
|
||||
if int(row.get("level", -1)) == level:
|
||||
return row
|
||||
return {}
|
||||
@@ -133,3 +133,55 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void:
|
||||
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
|
||||
if spend_discount <= 0.0 or spend_discount > 1.0:
|
||||
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop.<m> 直连)----
|
||||
|
||||
## GP 掉落行单点(键 tier;缺行=空字典——掉落按 0 兑付的兜底在调用方)
|
||||
func gp_drop_row(tier: String) -> Dictionary:
|
||||
for row in gp_drops:
|
||||
if String(row.get("tier", "")) == tier:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 关卡 GP 加成单点(键 level_index;缺行=0)
|
||||
func level_gp_bonus_at(level_index: int) -> int:
|
||||
for row in level_gp_bonus:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
## 价格关卡加成单点(键 level_index;缺行=0)
|
||||
func price_level_bonus_at(level_index: int) -> int:
|
||||
for row in price_level_bonus:
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return int(row.get("bonus", 0))
|
||||
return 0
|
||||
|
||||
|
||||
## 商品行族({product_id, display_name, attr, flat_value, base_price, price_increment})
|
||||
func product_rows() -> Array:
|
||||
return products
|
||||
|
||||
|
||||
## 商品行单点(键 product_id;缺行=空字典)
|
||||
func product_row(product_id: String) -> Dictionary:
|
||||
for row in products:
|
||||
if String(row.get("product_id", "")) == product_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
|
||||
func pool_rows() -> Array:
|
||||
return pool_shelf
|
||||
|
||||
|
||||
## 货架行单点(键 kind_id;缺行=空字典)
|
||||
func pool_row(kind_id: String) -> Dictionary:
|
||||
for row in pool_shelf:
|
||||
if String(row.get("kind_id", "")) == kind_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -110,3 +110,38 @@ func row_by_id(variant_id: String) -> Dictionary:
|
||||
if row.get("variant_id", "") == variant_id:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 候选池访问器(自 ConfigDB 归位;消费面 db.combat.variation.<m> 直连)----
|
||||
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
|
||||
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
|
||||
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
|
||||
|
||||
## 候选池门控(∅ board_types = 通配;字段名对齐 board_type 词表,无 biome_tags)
|
||||
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
|
||||
var pool: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if String(row.get("scope", "region")) == "global" \
|
||||
and node_type != "boss" and node_type != "elite":
|
||||
continue
|
||||
if int(row.get("unlock_level", 999)) > level_index:
|
||||
continue
|
||||
var tags: Array[Variant] = row.get("board_types", [])
|
||||
if tags.is_empty():
|
||||
pool.append(row)
|
||||
continue
|
||||
if tags.has(board_type):
|
||||
pool.append(row)
|
||||
return pool
|
||||
|
||||
|
||||
## 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
|
||||
## boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
|
||||
## 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
|
||||
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
|
||||
if node_type != "boss" and node_type != "elite":
|
||||
return {}
|
||||
for row in variant_pool(level_index, board_type, node_type):
|
||||
if String(row.get("scope", "region")) == "global":
|
||||
return row
|
||||
return {}
|
||||
@@ -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 "遗迹守卫"
|
||||
|
||||
@@ -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 ""
|
||||
@@ -107,7 +107,7 @@ func on_camp_tree_buy(node_id: String) -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
|
||||
var ok: bool = meta.tree_buy_fork(node_id) if String(db.crossrun.meta_tree.row_by_id(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
|
||||
if not ok:
|
||||
toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
|
||||
return
|
||||
@@ -157,7 +157,7 @@ func on_camp_forge_select(route_id: String) -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
var ok: bool = meta.forge_select("weapon", route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \
|
||||
var ok: bool = meta.forge_select("weapon", route_id) if String(db.crossrun.forge.route(route_id).get("kind", "")) == "weapon" \
|
||||
else meta.forge_select("armor", route_id)
|
||||
if not ok:
|
||||
toast.show_toast("切换被拒——路线未解锁", "warn")
|
||||
@@ -177,7 +177,7 @@ func on_shop_buy(product_id: String) -> void:
|
||||
toast.show_toast("GP不足——买不起这件商品", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
return
|
||||
@@ -199,7 +199,7 @@ func on_shop_refresh() -> void:
|
||||
toast.show_toast("GP不足——刷新失败", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
|
||||
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
|
||||
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -165,9 +165,9 @@ func _curio_card_texts(candidates: Array) -> Array:
|
||||
text = "专长获取
|
||||
选中或买下后立刻开一次专长三选一"
|
||||
elif kind == "relic":
|
||||
text = Narrator.feat_card_text(db.feat_row(rid))
|
||||
text = Narrator.feat_card_text(db.crossrun.feats.row_by_id(rid))
|
||||
else:
|
||||
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)))
|
||||
text = Narrator.bonus_card_text(kind, tier, int(db.run.adventure.curio_value(kind, tier)))
|
||||
out.append({"id": cid, "text": text})
|
||||
return out
|
||||
|
||||
@@ -251,6 +251,6 @@ func item_name_lookup() -> Dictionary:
|
||||
var lookup := {}
|
||||
var meta = _meta_get.call()
|
||||
if meta != null:
|
||||
for row in db.treasure_attunement_rows():
|
||||
for row in db.crossrun.treasure_items.attunement_rows():
|
||||
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||||
return lookup
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -81,7 +81,7 @@ static func _build_battle_wiring(root: Variant) -> void:
|
||||
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
|
||||
|
||||
root.logger = PlaytestLogger.new()
|
||||
root.logger.ap_per_round = int(root.db.action_economy.ap_per_round)
|
||||
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
|
||||
|
||||
static func _build_overlays(root: Variant) -> void:
|
||||
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
|
||||
@@ -193,7 +193,7 @@ static func _build_debug_and_resolvers(root: Variant) -> void:
|
||||
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
|
||||
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
|
||||
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
|
||||
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.hero.atk), int(root.db.hero.max_hp))
|
||||
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
|
||||
root.add_child(root.debug_overlay)
|
||||
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
|
||||
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
|
||||
@@ -235,7 +235,7 @@ static func _build_modal_flow(root: Variant) -> void:
|
||||
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
|
||||
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
|
||||
root._apply_layer_contract()
|
||||
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.hero.max_hp))
|
||||
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
|
||||
|
||||
static func fatal_screen(root: Variant, msg: String) -> void:
|
||||
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
|
||||
|
||||
@@ -44,7 +44,7 @@ func unlocked_set() -> Dictionary:
|
||||
var out := {}
|
||||
if meta == null:
|
||||
return out
|
||||
for row in db.treasure_transitional_rows():
|
||||
for row in db.crossrun.treasure_items.transitional_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if meta.meta_state.item_unlocked(iid):
|
||||
out[iid] = true
|
||||
@@ -79,7 +79,7 @@ func on_slot_clicked(slot_handle: int) -> void:
|
||||
break
|
||||
if rec.is_empty() or bool(rec.get("used", false)):
|
||||
return
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
|
||||
if String(arow.get("target", "none")) == "none":
|
||||
attempt_use(slot_handle) # AP 类:显式无目标即用(不再伪造哨兵坐标)
|
||||
return
|
||||
@@ -106,7 +106,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void:
|
||||
refresh_slots()
|
||||
refresh_overlays()
|
||||
board_view.refresh() # 移除格即时落图(无移除态的件 diff 零变化幂等)
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.get("item_id", "")))
|
||||
toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info")
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ func inventory_projection() -> Array:
|
||||
# 库存投影行(字形=名首字;usable=整图零信息预判回调接排雷玩法只读面)+
|
||||
# 积点进度浮窗行(自动兑付件 C 区悬停 tooltip 补 N/M 进度;件 id 锁定=装配行唯一)
|
||||
func _projection_row(rec: Dictionary, cs: Dictionary) -> Dictionary:
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", "")))
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(rec.get("item_id", "")))
|
||||
var dn := String(arow.get("display_name", ""))
|
||||
var note := ""
|
||||
if bool(cs.get("assembled", false)) and String(cs.get("item_id", "")) == String(rec.get("item_id", "")):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 += "〕"
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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 区弃图刷新按钮文案缺失")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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;探针禁碰真实档,同族探针隔离纪律;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -9,9 +9,13 @@ extends SceneTree
|
||||
|
||||
|
||||
class FakeDB:
|
||||
var row: Dictionary
|
||||
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
|
||||
return row
|
||||
class _FakeBoardGen:
|
||||
var row: Dictionary
|
||||
func resolve_row(_L: int, _D: int) -> Dictionary:
|
||||
return row
|
||||
class _FakeCombat:
|
||||
var board_gen := _FakeBoardGen.new()
|
||||
var combat := _FakeCombat.new()
|
||||
func status_effect_row(_id: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
@@ -28,7 +32,7 @@ func _initialize() -> void:
|
||||
return
|
||||
var gen = load("res://core/board_generator.gd").new()
|
||||
print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,r0_sd,r0_p90,r0_max,inf_mean,cap_rej_mean")
|
||||
for row in db.board_gen.rows:
|
||||
for row in db.combat.board_gen.rows:
|
||||
var rd := {
|
||||
"level_index": int(row.level_index), "node_depth": int(row.node_depth),
|
||||
"board_width": int(row.board_width), "board_height": int(row.board_height),
|
||||
|
||||
+16
-16
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -39,7 +39,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(opt.seed0)
|
||||
print("模拟日志: %s" % log_path)
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
@@ -193,7 +193,7 @@ func _spend(meta, policy_name: String) -> Array:
|
||||
guard -= 1
|
||||
var bought := false
|
||||
for nid in order:
|
||||
var price: int = int(meta.db.tree_node(nid).get("price", 0))
|
||||
var price: int = int(meta.db.crossrun.meta_tree.row_by_id(nid).get("price", 0))
|
||||
if price > 0 and meta.meta_state.keystone_balance() >= price and meta.tree_buy(nid):
|
||||
out.append({"node_id": nid, "level": meta.save.meta_state.tree_state().nodes.get(nid, 0), "price": price})
|
||||
bought = true
|
||||
|
||||
@@ -114,8 +114,8 @@ func _save_summary_integration() -> void:
|
||||
var f9 = sm_script.new(path9)
|
||||
f9.meta_state.apply_adventure_summary(st9.get("summary", {}))
|
||||
var ks_run := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
for li in range(1, int(_db.run.level.rows.size()) + 1):
|
||||
ks_run += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
|
||||
Kit.check("摘要集成:冒险驱动满配冒险成功(seed 42)", String(st9.get("result", "")) == "completed")
|
||||
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.meta_state.keystone_balance(), f9.meta_state.keystone_balance() == ks_run)
|
||||
Kit.check("集成关卡完成次数表全 1", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1)
|
||||
@@ -147,7 +147,7 @@ func _smoke_playtest_logger() -> void:
|
||||
var snaps: Array = []
|
||||
settlement.settlement_snapshot.connect(func(snap): snaps.append(snap))
|
||||
Kit.check("会话文件开启(%s)" % path, path.contains("playtest_20260912"))
|
||||
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}))
|
||||
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
|
||||
var minesweeper = settlement.minesweeper
|
||||
for i in range(2):
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
@@ -159,7 +159,7 @@ func _smoke_playtest_logger() -> void:
|
||||
if not s1.has(k):
|
||||
missing.append(k)
|
||||
Kit.check("快照字段齐备(缺 %s)" % str(missing), missing.is_empty())
|
||||
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
|
||||
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.combat.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
|
||||
# spawn_count 回归(playtest-2026-09-12 报告 Bug#1):停手两轮零击杀(主角攻击 6×2 < 24 HP 确定)→
|
||||
# unit_count 必须 == spawn_count(全灭才允许 <);快照级区分「首轮全灭」vs「空场」的数据面在位
|
||||
Kit.check("快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))],
|
||||
|
||||
@@ -35,7 +35,7 @@ func _init() -> void:
|
||||
func _smoke_generated_map_determinism() -> void:
|
||||
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
|
||||
var gen = NodeMapGenerator.new(_fresh_rng(910001))
|
||||
var params2: Dictionary = _db.level_gen_params_row(2)
|
||||
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
|
||||
var g_a: Dictionary = gen.generate(params2, 910002)
|
||||
var g_b: Dictionary = gen.generate(params2, 910002)
|
||||
var g_c: Dictionary = gen.generate(params2, 910003)
|
||||
@@ -60,7 +60,7 @@ func _smoke_generated_map_determinism() -> void:
|
||||
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
|
||||
Callable(self, "_smoke_rest_stub"), true))
|
||||
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
|
||||
var params3: Dictionary = _db.level_gen_params_row(3)
|
||||
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
|
||||
var entry_count: int = int(params3.get("entry_count", -1))
|
||||
Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count,
|
||||
entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count)
|
||||
@@ -81,7 +81,7 @@ func _smoke_generated_map_determinism() -> void:
|
||||
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕",
|
||||
adr_x.runner().nodes() == adr_y.runner().nodes())
|
||||
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
|
||||
adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []))
|
||||
adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []))
|
||||
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
|
||||
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
|
||||
var bp: Dictionary = sv_w.breakpoint_snapshot()
|
||||
@@ -96,11 +96,11 @@ func _smoke_generated_map_determinism() -> void:
|
||||
adr_old.resume_adventure(bp_old, {}) == false)
|
||||
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
|
||||
var tpl_free := true
|
||||
for li in range(1, _db.level_count() + 1):
|
||||
if (_db.level_row(li) as Dictionary).has("templates"):
|
||||
for li in range(1, _db.run.level.level_count() + 1):
|
||||
if (_db.run.level.row_by_level_index(li) as Dictionary).has("templates"):
|
||||
tpl_free = false
|
||||
Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕",
|
||||
tpl_free and _db.level_count() == 3)
|
||||
tpl_free and _db.run.level.level_count() == 3)
|
||||
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
|
||||
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
|
||||
sv.load_from_disk()
|
||||
|
||||
@@ -53,7 +53,7 @@ func _harness(db):
|
||||
var ctrl = DamageControllerScript.new(db, rng, units, effects)
|
||||
ctrl.emit_row_cb = Callable(self, "_on_row")
|
||||
effects.notify_battle_start()
|
||||
var spawn_ok: bool = units.spawn(1, 0, int(db.hero.max_hp))
|
||||
var spawn_ok: bool = units.spawn(1, 0, int(db.combat.hero.max_hp))
|
||||
return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok}
|
||||
|
||||
|
||||
@@ -79,15 +79,15 @@ func _run_recipe_sequence(db) -> Array:
|
||||
for e in units.all_enemies():
|
||||
enemy_hp_before.append(int(e.hp))
|
||||
var front = units.default_target()
|
||||
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.settlement.damage_roll.attack_bonus)
|
||||
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.combat.settlement.damage_roll.attack_bonus)
|
||||
var atk_type := ""
|
||||
var eff_keys: Array = h.db.settlement.damage_roll.armor_effectiveness.keys()
|
||||
var eff_keys: Array = h.db.combat.settlement.damage_roll.armor_effectiveness.keys()
|
||||
if eff_keys.size() > 0:
|
||||
atk_type = String(eff_keys[0])
|
||||
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
|
||||
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
|
||||
|
||||
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T1主角攻击",
|
||||
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
|
||||
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
|
||||
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1}))
|
||||
# 敌方技来源取后位敌(前位是攻击/直伤/群伤的靶位——来源与靶位分离,免死置干扰配方 redeemed 断言)
|
||||
var alive_list: Array = units.query_alive("enemy")
|
||||
@@ -124,9 +124,9 @@ func _run_recipe_sequence(db) -> Array:
|
||||
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
|
||||
var ex1: Dictionary = r1.get("extra", {})
|
||||
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
|
||||
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces))
|
||||
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.combat.settlement.damage_roll.roll_faces))
|
||||
var tier_ok := false
|
||||
for dv in h.db.settlement.damage_roll.roll_tier_discounts:
|
||||
for dv in h.db.combat.settlement.damage_roll.roll_tier_discounts:
|
||||
if int(dv) == int(ex1.get("tier", -1)):
|
||||
tier_ok = true
|
||||
if int(ex1.get("tier", -1)) == 100:
|
||||
@@ -159,7 +159,7 @@ func _run_recipe_sequence(db) -> Array:
|
||||
continue
|
||||
if String(r.source_class).is_empty() or String(r.display).is_empty():
|
||||
protocol_ok = false
|
||||
if int(r.value) < int(h.db.settlement.damage_roll.damage_floor):
|
||||
if int(r.value) < int(h.db.combat.settlement.damage_roll.damage_floor):
|
||||
protocol_ok = false
|
||||
if not (r.extra is Dictionary) or not r.extra.has("target"):
|
||||
protocol_ok = false
|
||||
@@ -187,11 +187,11 @@ func _test_policy_immediate_mode(db) -> void:
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
h.ctrl.policy = DamageControllerScript.POLICY_IMMEDIATE
|
||||
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
|
||||
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
|
||||
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "A模式反噬",
|
||||
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
|
||||
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
|
||||
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
|
||||
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.combat.hero.max_hp) - floor_v)
|
||||
|
||||
|
||||
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
|
||||
@@ -267,7 +267,7 @@ func _test_target_id_payload(db: ConfigDb) -> void:
|
||||
var expect_id := String(target.get("enemy_id", ""))
|
||||
Kit.check("前置:目标敌携非空 enemy_id", expect_id != "")
|
||||
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T6目标id",
|
||||
{"channel": "战斗段", "base_value": int(db.hero.atk),
|
||||
{"channel": "战斗段", "base_value": int(db.combat.hero.atk),
|
||||
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
|
||||
var fx_payload: Dictionary = ticket.context.get("fx_attack_payload", {})
|
||||
Kit.check("attack_commit fx 载荷携 target_id〔wd12〕",
|
||||
@@ -284,7 +284,7 @@ func _test_requires_gate_and_zero(db) -> void:
|
||||
var h = _harness(db)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
|
||||
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
|
||||
# 先清场(flush_submit 同步开单——清图批专用通道的契约面同时被覆盖)
|
||||
var front_hp: int = int(h.units.query_alive("enemy")[0].max_hp)
|
||||
var big: int = front_hp * 10
|
||||
@@ -294,7 +294,7 @@ func _test_requires_gate_and_zero(db) -> void:
|
||||
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "空场群伤",
|
||||
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
|
||||
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "前置门攻击",
|
||||
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
|
||||
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
|
||||
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
|
||||
gated_ticket["requires"] = t_zero
|
||||
var t_gated: int = h.ctrl.submit(gated_ticket)
|
||||
@@ -310,7 +310,7 @@ func _test_hero_death_halt(db) -> void:
|
||||
var h = _harness(db)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var max_hp: int = int(h.db.hero.max_hp)
|
||||
var max_hp: int = int(h.db.combat.hero.max_hp)
|
||||
var t_kill: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "致死反噬",
|
||||
{"channel": "棋盘兑付", "base_value": max_hp, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
|
||||
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "余单",
|
||||
@@ -327,14 +327,14 @@ func _test_ac_merge(db) -> void:
|
||||
var h = _harness(db)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var base_ac: int = int(h.db.hero.armor_class)
|
||||
var base_ac: int = int(h.db.combat.hero.armor_class)
|
||||
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
|
||||
var override_v: int = base_ac + int(h.db.settlement.damage_roll.damage_floor)
|
||||
var override_v: int = base_ac + int(h.db.combat.settlement.damage_roll.damage_floor)
|
||||
h.ctrl.set_hero_ac_override(override_v)
|
||||
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
|
||||
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
|
||||
var ac_eff := {}
|
||||
for row in h.db.status_effects.rows:
|
||||
for row in h.db.combat.status_effects.rows:
|
||||
if String(row.get("unit", "")) == "ac" and String(row.get("target_side", "")) == "hero":
|
||||
ac_eff = row
|
||||
break
|
||||
@@ -354,15 +354,15 @@ func _test_pool_clamp(db) -> void:
|
||||
var h = _harness(db)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var lo: int = int(h.db.settlement.core.pool_clamp_lo)
|
||||
var hi: int = int(h.db.settlement.core.pool_clamp_hi)
|
||||
var lo: int = int(h.db.combat.settlement.core.pool_clamp_lo)
|
||||
var hi: int = int(h.db.combat.settlement.core.pool_clamp_hi)
|
||||
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
|
||||
# 找 hero_offense 池的 percent 型 hero 侧效果行
|
||||
var pool_eff := {}
|
||||
for row in h.db.status_effects.rows:
|
||||
for row in h.db.combat.status_effects.rows:
|
||||
var eid := String(row.get("effect_id", ""))
|
||||
if String(row.get("unit", "")) == "percent" and String(row.get("target_side", "")) == "hero" \
|
||||
and String(h.db.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
|
||||
and String(h.db.combat.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
|
||||
pool_eff = row
|
||||
break
|
||||
if pool_eff.is_empty():
|
||||
@@ -377,7 +377,7 @@ func _test_pool_clamp(db) -> void:
|
||||
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
# ac 型效果不入 percent 池
|
||||
var ac_eff2 := {}
|
||||
for row2 in h.db.status_effects.rows:
|
||||
for row2 in h.db.combat.status_effects.rows:
|
||||
if String(row2.get("unit", "")) == "ac" and String(row2.get("target_side", "")) == "hero":
|
||||
ac_eff2 = row2
|
||||
break
|
||||
@@ -387,7 +387,7 @@ func _test_pool_clamp(db) -> void:
|
||||
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
|
||||
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
|
||||
var stack_eff := {}
|
||||
for row3 in h.db.status_effects.rows:
|
||||
for row3 in h.db.combat.status_effects.rows:
|
||||
if String(row3.get("stack_policy", "")) == "stack":
|
||||
stack_eff = row3
|
||||
break
|
||||
|
||||
@@ -24,7 +24,7 @@ func _init() -> void:
|
||||
|
||||
|
||||
func _floor_val() -> int:
|
||||
return int(db.settlement.damage_roll.damage_floor)
|
||||
return int(db.combat.settlement.damage_roll.damage_floor)
|
||||
|
||||
|
||||
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
|
||||
@@ -32,11 +32,11 @@ func _floor_val() -> int:
|
||||
func _test_pool_damage() -> void:
|
||||
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
|
||||
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
|
||||
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d)
|
||||
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d)
|
||||
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.combat.settlement.core.pool_clamp_lo)) <= d)
|
||||
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_hi), 0) >= d)
|
||||
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
|
||||
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
|
||||
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0)
|
||||
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_lo)) >= 0)
|
||||
|
||||
|
||||
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
|
||||
@@ -54,8 +54,8 @@ func _test_counter_component() -> void:
|
||||
|
||||
func _test_roll_discount_percent() -> void:
|
||||
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
|
||||
var bounds: Array = db.settlement.damage_roll.roll_tier_bounds
|
||||
var discounts: Array = db.settlement.damage_roll.roll_tier_discounts
|
||||
var bounds: Array = db.combat.settlement.damage_roll.roll_tier_bounds
|
||||
var discounts: Array = db.combat.settlement.damage_roll.roll_tier_discounts
|
||||
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
|
||||
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
|
||||
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
|
||||
@@ -78,7 +78,7 @@ func _test_roll_discount_percent() -> void:
|
||||
|
||||
func _test_rolled_component() -> void:
|
||||
var base := float(_floor_val() + 7) # 表值推导探针基线
|
||||
var crit_mult: int = int(db.settlement.damage_roll.crit_multiplier)
|
||||
var crit_mult: int = int(db.combat.settlement.damage_roll.crit_multiplier)
|
||||
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
|
||||
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
|
||||
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
|
||||
|
||||
@@ -47,13 +47,13 @@ func _smoke_enemy_attack_type() -> void:
|
||||
# ① 表列:20 行全填 normal(N3 全填不做差异化——Q9)
|
||||
var at_ok := true
|
||||
var at_detail := ""
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var at := String(row.get("attack_type", ""))
|
||||
if at != "normal":
|
||||
at_ok = false
|
||||
at_detail += " %s:%s" % [String(row.get("enemy_id", "?")), at]
|
||||
Kit.check("敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [
|
||||
_db.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
|
||||
_db.combat.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
|
||||
# ②③④ 矩阵命中证词扫描:(3,0) 灰矮人 atk 区间 [2,4] 基线足够((1,2) 熊地精 atk 1 会塌地板);
|
||||
# 同 seed 三甲型各开一场、各取首个敌方伤害行,对拍 ±1。
|
||||
var matrix_found := false
|
||||
@@ -70,7 +70,7 @@ func _smoke_enemy_attack_type() -> void:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "armor_type": armor}):
|
||||
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "armor_type": armor}):
|
||||
totals[armor] = -1
|
||||
continue
|
||||
if armor == "light":
|
||||
@@ -109,7 +109,7 @@ func _smoke_armor_roll() -> void:
|
||||
var dist := {100: 0, 75: 0, 50: 0, 25: 0}
|
||||
var tier_ok := true
|
||||
for roll in range(1, 21):
|
||||
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
|
||||
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
|
||||
if not dist.has(d):
|
||||
tier_ok = false
|
||||
continue
|
||||
@@ -117,7 +117,7 @@ func _smoke_armor_roll() -> void:
|
||||
Kit.check("查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])], tier_ok and dist == cfg.expect)
|
||||
var ev_sum := 0
|
||||
for roll in range(1, 21):
|
||||
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
|
||||
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
|
||||
Kit.check("AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum, ev_sum == 1575)
|
||||
# ② nat20 暴击(行为面):探针按行为面扫描
|
||||
# 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位
|
||||
@@ -125,21 +125,21 @@ func _smoke_armor_roll() -> void:
|
||||
var crit_rows := {}
|
||||
for s in range(1, 800):
|
||||
crit_rows = _first_hero_attack_rows(s)
|
||||
if int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll):
|
||||
if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll):
|
||||
crit_seed = s
|
||||
break
|
||||
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.damage_roll.crit_roll)], crit_seed > 0)
|
||||
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0)
|
||||
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)",
|
||||
int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
|
||||
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)],
|
||||
int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk))
|
||||
int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
|
||||
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)],
|
||||
int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk))
|
||||
# ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置
|
||||
# (削弱为续时语义,无叠层通道)
|
||||
var weaken_probe := _weaken_refresh_probe()
|
||||
Kit.check("削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)",
|
||||
int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2)
|
||||
Kit.check("削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)",
|
||||
int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude))
|
||||
int(weaken_probe.get("pool", -999)) == int(_db.combat.status_effects.row_by_id("weaken").magnitude))
|
||||
# ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定
|
||||
# (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值)
|
||||
var tiers := _ac11_tier_samples()
|
||||
@@ -172,7 +172,7 @@ func _smoke_armor_roll() -> void:
|
||||
Kit.check("棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)", board_ok)
|
||||
# ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂
|
||||
# 全部收在 damage_floor 闸内)
|
||||
var df: int = int(_db.settlement.damage_roll.damage_floor)
|
||||
var df: int = int(_db.combat.settlement.damage_roll.damage_floor)
|
||||
Kit.check("棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)", DamageMath.counter_component(2, -1, df) == 1)
|
||||
Kit.check("克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)", DamageMath.counter_component(1, -3, df) == 1)
|
||||
Kit.check("空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)", DamageMath.counter_component(0, 5, df) == 0)
|
||||
@@ -185,7 +185,7 @@ func _first_hero_attack_rows(seed_val: int) -> Dictionary:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
return {}
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
for row in rows:
|
||||
@@ -201,7 +201,7 @@ func _weaken_refresh_probe() -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260916)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
return {}
|
||||
var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}
|
||||
settlement._status_effects.apply_effect("weaken", src)
|
||||
@@ -224,7 +224,7 @@ func _ac11_tier_samples() -> Array:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
var guard := 12
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
@@ -259,7 +259,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
|
||||
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
|
||||
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
|
||||
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
|
||||
if not settlement.begin_battle(enc):
|
||||
out.stream.append({"type": "verdict", "value": "begin_failed"})
|
||||
@@ -325,7 +325,7 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": hero_atk_val}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": hero_atk_val}):
|
||||
return {}
|
||||
for eff in opts.get("effects", []):
|
||||
settlement._status_effects.apply_effect(String(eff), {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
@@ -343,15 +343,15 @@ func _smoke_damage_types() -> void:
|
||||
# ① 护甲类型快照传播(spawn 行值 → 单位字典)
|
||||
var enemy_system = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("goblin 护甲 = light(轻甲——教学位)",
|
||||
enemy_system.spawn(1, 0, int(_db.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
|
||||
enemy_system.spawn(1, 0, int(_db.combat.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
|
||||
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
|
||||
enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
|
||||
enemy_system_b.spawn(1, 2, int(_db.combat.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
|
||||
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
|
||||
enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
|
||||
enemy_system_c.spawn(1, 4, int(_db.combat.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
|
||||
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
|
||||
var am: Dictionary = _db.settlement.damage_roll.armor_effectiveness
|
||||
var am: Dictionary = _db.combat.settlement.damage_roll.armor_effectiveness
|
||||
Kit.check("普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)",
|
||||
int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1)
|
||||
Kit.check("穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)",
|
||||
@@ -387,7 +387,7 @@ func _run_seal_backlash_round(seed_val: int) -> Dictionary:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
|
||||
return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""}
|
||||
var minesweeper = settlement.minesweeper
|
||||
var safe_t := _find_seal_target(minesweeper, false)
|
||||
@@ -443,7 +443,7 @@ func _smoke_attack_chain_fresh_pool() -> void:
|
||||
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("rider", 0)), "roll": int(row.roll),
|
||||
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
|
||||
Kit.check("fresh 池回归战斗启动(Boss 行)",
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
# 停手轮:零挖掘零封印 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计)
|
||||
for i in range(6):
|
||||
if settlement.battle_result != "":
|
||||
@@ -452,9 +452,9 @@ func _smoke_attack_chain_fresh_pool() -> void:
|
||||
Kit.check("非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size(), ticks.size() >= 2)
|
||||
var all_full := true
|
||||
for t in ticks:
|
||||
var base_factor: float = float(t.tier) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
|
||||
var base_factor: float = float(t.tier) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
|
||||
var rolled_base := int(floor(float(t.baseline) * base_factor / 100.0))
|
||||
var expected: int = maxi(int(_db.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
|
||||
var expected: int = maxi(int(_db.combat.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
|
||||
if int(t.tick) != expected or int(t.tick) <= 0:
|
||||
all_full = false
|
||||
Kit.check("fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)", all_full)
|
||||
@@ -69,7 +69,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
|
||||
hero_attacks += 1
|
||||
# 单分量伤害构成行:hero_attack 行必须携带实际攻击类型键 + 掷点读数(P2 归因)
|
||||
# 类型键主语 = 锻造工坊配置默认值(hero.tres 无两轴字段;本夹具零注入)
|
||||
var atk_t: String = String(_db.forge_default_attack_type())
|
||||
var atk_t: String = String(_db.crossrun.forge.default_attack_type)
|
||||
if not (row.has(atk_t) and row.has("roll") and row.has("ac") and row.has("tier")):
|
||||
hero_breakdown_ok = false
|
||||
Kit.check("%s 步骤流含判定行" % label, has_verdict)
|
||||
@@ -93,7 +93,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
|
||||
var minesweeper = settlement.minesweeper
|
||||
var action_economy = settlement.action_economy
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
|
||||
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
|
||||
if not settlement.begin_battle(enc):
|
||||
out.stream.append({"type": "verdict", "value": "begin_failed"})
|
||||
@@ -157,12 +157,12 @@ func _smoke_active_refresh() -> void:
|
||||
if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]):
|
||||
ct["waiting_probe_hit"] = true
|
||||
ct["waiting_refresh_rejected"] = not settlement.player_request_refresh())
|
||||
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 轮 1–3:零挖掘停手攒 RAP = 3×AP(每轮全转存;表驱动——AP 从表读不写死)
|
||||
for i in range(3):
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
var rap_banked: int = 3 * int(_db.action_economy.ap_per_round)
|
||||
var rap_banked: int = 3 * int(_db.run.action_economy.ap_per_round)
|
||||
Kit.check("三轮停手攒 RAP=%d(round=%d rap=%d)" % [rap_banked, settlement.round_index, settlement.action_economy.rap], settlement.round_index == 4 and settlement.action_economy.rap == rap_banked)
|
||||
Kit.check("第 4 轮 Active 相位", settlement.action_economy.phase == "Active")
|
||||
# 挖 1 格 → 刷 → 挖 1 格 → 刷 → 挖 1 格 → 第 3 刷拒(上限)
|
||||
@@ -176,7 +176,7 @@ func _smoke_active_refresh() -> void:
|
||||
var ok1: bool = settlement.player_request_refresh()
|
||||
Kit.check("轮内第 1 次刷新受理", ok1)
|
||||
Kit.check("相位保持 Active、AP 保留不重置(ap=%d)" % settlement.action_economy.ap, settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before)
|
||||
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.action_economy.rap_refresh_cost))
|
||||
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.run.action_economy.rap_refresh_cost))
|
||||
Kit.check("当场换新图(board_changed +1)", int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before)
|
||||
var plan2 = AutoPlayer.choose_dig(minesweeper)
|
||||
if plan2.hero_cell != minesweeper.hero_pos():
|
||||
@@ -221,7 +221,7 @@ func _active_refresh_replay() -> Array:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
|
||||
return rows
|
||||
var minesweeper = settlement.minesweeper
|
||||
for i in range(3):
|
||||
@@ -251,7 +251,7 @@ func _smoke_encounter_roll() -> void:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(30000 + i)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
|
||||
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
|
||||
counts.append(settlement.presentation_snapshot().enemies.size())
|
||||
# 区间生效:20 个种子应出现 1 和 2(D0 [1,2])
|
||||
Kit.check("D0 区间 [1,2] 两值均出现(分布 %s)" % str(counts), counts.has(1) and counts.has(2))
|
||||
@@ -269,13 +269,13 @@ func _smoke_encounter_roll() -> void:
|
||||
var rng3 = load("res://core/rng_service.gd").new()
|
||||
rng3.set_master_seed(31000)
|
||||
var settlement_d1 = load("res://core/battle_orchestrator.gd").new(_db, rng3)
|
||||
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}))
|
||||
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.combat.hero.max_hp)}))
|
||||
var n1: int = settlement_d1.presentation_snapshot().enemies.size()
|
||||
Kit.check("D1 区间 [2,3] 域内(%d 只)" % n1, n1 >= 2 and n1 <= 3)
|
||||
# 熊地精首遇带固定(教学位不加骰)——首遇带数据驱动定位(契约 8,禁写死坐标)
|
||||
var bugbear_lead_band := _first_encounter_band(1, "bugbear")
|
||||
var bugbear_lead_row: Dictionary = {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.level_index) == 1 and String(row.row_kind) == "battle" \
|
||||
and int(row.depth_band) == bugbear_lead_band and String(row.enemy_id) == "bugbear":
|
||||
bugbear_lead_row = row
|
||||
@@ -283,7 +283,7 @@ func _smoke_encounter_roll() -> void:
|
||||
var rng4 = load("res://core/rng_service.gd").new()
|
||||
rng4.set_master_seed(31000)
|
||||
var settlement_d2 = load("res://core/battle_orchestrator.gd").new(_db, rng4)
|
||||
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.hero.max_hp)})
|
||||
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
Kit.check("熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" % bugbear_lead_band,
|
||||
settlement_d2.presentation_snapshot().enemies.size() == 1 \
|
||||
and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max"))
|
||||
@@ -300,11 +300,11 @@ func _smoke_encounter_roll() -> void:
|
||||
for depth_v in depths:
|
||||
scan_total += 1
|
||||
var settlement_scan = load("res://core/battle_orchestrator.gd").new(_db, rngs)
|
||||
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
scan_bad += 1
|
||||
continue
|
||||
var n: int = settlement_scan.presentation_snapshot().enemies.size()
|
||||
var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
|
||||
var erow2: Dictionary = _db.combat.enemy.resolve_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
|
||||
var lo2: int = int(erow2.get("unit_count", 1))
|
||||
var hi2: int = int(erow2.get("unit_count_max", lo2))
|
||||
if n < 1 or n < lo2 or n > hi2:
|
||||
@@ -329,7 +329,7 @@ func _row_kind_of(level_index: int, depth: int) -> String:
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
var best := -1
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != level_index:
|
||||
continue
|
||||
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
|
||||
@@ -345,12 +345,12 @@ func _level_active_nodes(level_index: int) -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42)
|
||||
var gen = NodeMapGenerator.new(rng)
|
||||
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
|
||||
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
|
||||
|
||||
|
||||
func _level_battle_nodes(level_index: int) -> Array:
|
||||
var out: Array = []
|
||||
for row in _db.level.rows:
|
||||
for row in _db.run.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
for node in _level_active_nodes(level_index):
|
||||
var t := String(node.get("type", ""))
|
||||
@@ -364,7 +364,7 @@ func _encounter_replay(seed_val: int) -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
out.append(settlement.presentation_snapshot().enemies.size())
|
||||
return out
|
||||
|
||||
@@ -378,7 +378,7 @@ func _encounter_replay(seed_val: int) -> Array:
|
||||
func _smoke_settlement_segments() -> void:
|
||||
print("--- 五段全序 + 行型封闭 ---")
|
||||
Kit.check("segment_order 五段冻结在位(表值 = 拍板序恒等)",
|
||||
Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
|
||||
Array(_db.combat.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
|
||||
and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"])
|
||||
var run = _run_battle(1, 0)
|
||||
var legacy := {"item": 0, "consumed_ineffective": 0, "use_rejected": 0, "window_open": 0, "window_skip": 0}
|
||||
@@ -493,53 +493,53 @@ func _smoke_skill_pool() -> void:
|
||||
|
||||
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
|
||||
func _smoke_skill_pool_data() -> void:
|
||||
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.skill.rows.size() == 16)
|
||||
var basic: Dictionary = _db.skill.row_by_id("attack")
|
||||
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.combat.skill.rows.size() == 16)
|
||||
var basic: Dictionary = _db.combat.skill.row_by_id("attack")
|
||||
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
|
||||
not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
|
||||
and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)))
|
||||
var lunge: Dictionary = _db.skill.row_by_id("lunge")
|
||||
var lunge: Dictionary = _db.combat.skill.row_by_id("lunge")
|
||||
Kit.check("突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)", not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2)
|
||||
var charge_row: Dictionary = _db.skill.row_by_id("charge")
|
||||
var charge_row: Dictionary = _db.combat.skill.row_by_id("charge")
|
||||
Kit.check("蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)", not charge_row.is_empty() and String(charge_row.buff_ref) == "empower")
|
||||
var bite: Dictionary = _db.skill.row_by_id("bite")
|
||||
var bite: Dictionary = _db.combat.skill.row_by_id("bite")
|
||||
Kit.check("撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)", not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1)
|
||||
var venom: Dictionary = _db.skill.row_by_id("venom_burst")
|
||||
var venom: Dictionary = _db.combat.skill.row_by_id("venom_burst")
|
||||
Kit.check("毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)",
|
||||
not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4
|
||||
and is_equal_approx(float(venom.rider_mult), 0.75))
|
||||
var venom_sole_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
|
||||
venom_sole_ok = false
|
||||
Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok)
|
||||
# 妨害技首发行 + off_guard 落点 + 实装面收窄
|
||||
var snare: Dictionary = _db.skill.row_by_id("web_snare")
|
||||
var snare: Dictionary = _db.combat.skill.row_by_id("web_snare")
|
||||
Kit.check("蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)",
|
||||
not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard"
|
||||
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4)
|
||||
var snare_sole_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
|
||||
snare_sole_ok = false
|
||||
Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok)
|
||||
var off_guard_row: Dictionary = _db.status_effect_row("off_guard")
|
||||
var off_guard_row: Dictionary = _db.combat.status_effects.row_by_id("off_guard")
|
||||
Kit.check("失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)", not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac")
|
||||
# 普通敌错峰两新行——毒吐/盾击(V3)
|
||||
var spit: Dictionary = _db.skill.row_by_id("venom_spit")
|
||||
var spit: Dictionary = _db.combat.skill.row_by_id("venom_spit")
|
||||
Kit.check("毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)",
|
||||
not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \
|
||||
and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult"))
|
||||
var bash: Dictionary = _db.skill.row_by_id("shield_bash")
|
||||
var bash: Dictionary = _db.combat.skill.row_by_id("shield_bash")
|
||||
Kit.check("盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)",
|
||||
not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \
|
||||
and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult"))
|
||||
var spider_lead_band := _first_encounter_band(2, "giant_spider") # 首遇带数据驱动定位(契约 8)
|
||||
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
|
||||
var bash_sole_ok := true # 盾击只挂灰矮人族三行
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
for entry in row.pool:
|
||||
var sid := String(entry.skill_id)
|
||||
if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \
|
||||
@@ -551,7 +551,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
Kit.check("盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)", bash_sole_ok)
|
||||
# 首遇带行池恒 {attack:6, bite:2, charge:1}(去写死深度判据——带键口径)
|
||||
var spider_lead_pool := {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.level_index) == 2 and String(row.row_kind) == "battle" \
|
||||
and int(row.depth_band) == spider_lead_band and String(row.enemy_id) == "giant_spider":
|
||||
for entry in row.pool:
|
||||
@@ -592,7 +592,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
"3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
}
|
||||
var pools_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var rk := "%d_%s_%d_%s" % [int(row.level_index), String(row.row_kind), int(row.depth_band), String(row.enemy_id)]
|
||||
if not expect_pools.has(rk):
|
||||
pools_ok = false
|
||||
@@ -604,10 +604,10 @@ func _smoke_skill_pool_data() -> void:
|
||||
pools_ok = false
|
||||
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
|
||||
var basic_count_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n := 0
|
||||
for entry in row.pool:
|
||||
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
|
||||
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) == "basic":
|
||||
n += 1
|
||||
if n != 1:
|
||||
@@ -616,11 +616,11 @@ func _smoke_skill_pool_data() -> void:
|
||||
# 池构成纪律复查(V4):零违反——增益 ≤1 / 妨害 ≤1(敌人系统 v0.13 结构冻结;载入期
|
||||
# validate_skill_pool_links 已硬闸,此处冒烟侧显式复核新投放后仍绿)
|
||||
var comp_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n_buff := 0
|
||||
var n_debuff := 0
|
||||
for entry in row.pool:
|
||||
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
|
||||
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if srow.is_empty():
|
||||
continue
|
||||
if String(srow.type) == "buff":
|
||||
@@ -632,10 +632,10 @@ func _smoke_skill_pool_data() -> void:
|
||||
Kit.check("逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)", comp_ok)
|
||||
# 池构成纪律复查(触发条件批 V7 冒烟侧):每池至少一行无条件特殊技(普攻不计;trigger 缺省行计数)
|
||||
var uncond_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var n_uncond := 0
|
||||
for entry in row.pool:
|
||||
var srow_u: Dictionary = _db.skill_row(String(entry.skill_id))
|
||||
var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
|
||||
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
|
||||
n_uncond += 1
|
||||
if n_uncond < 1:
|
||||
@@ -645,7 +645,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
var st_unlock: Dictionary = {"rows": []}
|
||||
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
|
||||
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
|
||||
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面)
|
||||
sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65))
|
||||
Kit.check("前置:Boss 压到阈值 65(=round(130×0.5))", int(boss_u.hp) == 65)
|
||||
@@ -661,25 +661,25 @@ func _smoke_skill_pool_data() -> void:
|
||||
var rollable_ok := true
|
||||
for depth in [0, 1, 2, 3, 4]:
|
||||
var es = load("res://core/unit_container.gd").new(_db)
|
||||
es.spawn(1, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
|
||||
es.spawn(1, int(depth), int(_db.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
|
||||
for u in es.all_enemies():
|
||||
if es.rollable_skills(u).is_empty():
|
||||
rollable_ok = false
|
||||
for depth in [0, 1, 4, 5, 6, 7, 8]:
|
||||
var es2 = load("res://core/unit_container.gd").new(_db)
|
||||
es2.spawn(2, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
|
||||
es2.spawn(2, int(depth), int(_db.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
|
||||
for u in es2.all_enemies():
|
||||
if es2.rollable_skills(u).is_empty():
|
||||
rollable_ok = false
|
||||
Kit.check("全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)", rollable_ok)
|
||||
# 状态表新行 + 敌方防御池映射
|
||||
var empower: Dictionary = _db.status_effect_row("empower")
|
||||
var fortify: Dictionary = _db.status_effect_row("fortify")
|
||||
var empower: Dictionary = _db.combat.status_effects.row_by_id("empower")
|
||||
var fortify: Dictionary = _db.combat.status_effects.row_by_id("fortify")
|
||||
Kit.check("蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)", not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100)
|
||||
Kit.check("硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)", not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50)
|
||||
Kit.check("effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)",
|
||||
String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
|
||||
and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
|
||||
String(_db.combat.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
|
||||
and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
|
||||
|
||||
|
||||
# ② 同种子回放逐位复现(D3 大地精,多实例同种)
|
||||
@@ -764,11 +764,11 @@ func _smoke_skill_pool_ev_anchor() -> void:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260921)
|
||||
var n_beats := 5000
|
||||
var pity_m: int = int(_db.skill.pity_threshold)
|
||||
var pity_m: int = int(_db.combat.skill.pity_threshold)
|
||||
for si in species.size():
|
||||
var eid: String = species[si]
|
||||
var template: Array = []
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if String(row.enemy_id) == eid:
|
||||
template = row.pool
|
||||
break
|
||||
@@ -785,13 +785,13 @@ func _smoke_skill_pool_ev_anchor() -> void:
|
||||
if streak >= pity_m:
|
||||
var specials: Array = []
|
||||
for e in pool:
|
||||
var sr: Dictionary = _db.skill_row(String(e.skill_id))
|
||||
var sr: Dictionary = _db.combat.skill.row_by_id(String(e.skill_id))
|
||||
if not sr.is_empty() and String(sr.type) != "basic":
|
||||
specials.append({"skill_id": e.skill_id, "weight": e.weight})
|
||||
if not specials.is_empty():
|
||||
entries = specials
|
||||
var sid := SkillLottery.draw(entries, rng.open_stream("behavior_roll"))
|
||||
var srow: Dictionary = _db.skill_row(sid)
|
||||
var srow: Dictionary = _db.combat.skill.row_by_id(sid)
|
||||
var mult := 0.0
|
||||
if String(srow.type) == "basic":
|
||||
mult = 1.0
|
||||
@@ -818,12 +818,12 @@ func _smoke_skill_pool_death_void() -> void:
|
||||
for s in range(1, 400):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
|
||||
var erow: Dictionary = _db.combat.enemy.resolve_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
|
||||
var entries: Array = []
|
||||
for entry in erow.pool:
|
||||
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
|
||||
var sid := SkillLottery.draw(entries, rg.open_stream("behavior_roll"))
|
||||
if String(_db.skill_row(sid).type) != "basic":
|
||||
if String(_db.combat.skill.row_by_id(sid).type) != "basic":
|
||||
void_seed = s
|
||||
break
|
||||
Kit.check("死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" % void_seed, void_seed > 0)
|
||||
@@ -832,7 +832,7 @@ func _smoke_skill_pool_death_void() -> void:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}))
|
||||
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}))
|
||||
var u: Dictionary = settlement._enemy_system.all_enemies()[0] # 句柄预捕获(胜利 despawn 后容器已清,字典引用仍可读)
|
||||
var pending: String = settlement._enemy_system.pending_action(u)
|
||||
Kit.check("开轮已摇定本拍动作(待发 = %s)" % pending, pending != "")
|
||||
@@ -856,14 +856,14 @@ func _smoke_skill_pool_validation_reject() -> void:
|
||||
var skill_table = load("res://data/schemas/skill_table.gd").new()
|
||||
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
|
||||
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}])
|
||||
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
|
||||
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
|
||||
skill_table.rows.assign([{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}])
|
||||
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
skill_table.rows.assign([
|
||||
{"skill_id": "b1", "display_name": "b1", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 3},
|
||||
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}])
|
||||
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}])
|
||||
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}])
|
||||
Kit.check("加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)", not skill_table.validate().is_empty())
|
||||
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}])
|
||||
@@ -880,7 +880,7 @@ func _smoke_skill_pool_validation_reject() -> void:
|
||||
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4},
|
||||
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}])
|
||||
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}])
|
||||
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
|
||||
skill_table.rows.assign([{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}])
|
||||
Kit.check("加载校验拒绝:妨害技缺 buff_ref(效果引用)", not skill_table.validate().is_empty())
|
||||
skill_table.rows.assign([{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}])
|
||||
@@ -892,7 +892,7 @@ func _smoke_skill_pool_validation_reject() -> void:
|
||||
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
|
||||
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}])
|
||||
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}])
|
||||
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
|
||||
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
|
||||
|
||||
|
||||
func _has_err_category(errs: Array, category: String) -> bool:
|
||||
@@ -915,9 +915,9 @@ func _smoke_debuff_rider() -> void:
|
||||
var venom_check_ok := true
|
||||
var venom_rows_seen := 0
|
||||
var snare_seed := -1
|
||||
var base_ac: int = int(_db.hero.armor_class)
|
||||
var off_m: int = int(_db.status_effect_row("off_guard").magnitude)
|
||||
var floor_v: int = int(_db.settlement.damage_roll.damage_floor)
|
||||
var base_ac: int = int(_db.combat.hero.armor_class)
|
||||
var off_m: int = int(_db.combat.status_effects.row_by_id("off_guard").magnitude)
|
||||
var floor_v: int = int(_db.combat.settlement.damage_roll.damage_floor)
|
||||
for s in range(1, 401):
|
||||
if venom_seed > 0 and snare_seed > 0:
|
||||
break
|
||||
@@ -926,7 +926,7 @@ func _smoke_debuff_rider() -> void:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
|
||||
continue
|
||||
var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放)
|
||||
for i in range(6):
|
||||
@@ -949,7 +949,7 @@ func _smoke_debuff_rider() -> void:
|
||||
if rider_v != maxi(floor_v, int(floor(float(boss_atk) * 0.75))):
|
||||
venom_check_ok = false
|
||||
# 终值精确复算(停手轮池恒空):主分量链结果 + 必中附加后置
|
||||
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
|
||||
var base_factor: float = float(int(row.tier)) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
|
||||
var rolled_main: int = int(floor(float(int(row.baseline)) * base_factor / 100.0))
|
||||
var expected: int = maxi(floor_v, rolled_main + int(row.get("attach", 0))) + rider_v
|
||||
if int(row.value) != expected:
|
||||
|
||||
@@ -52,10 +52,10 @@ func _smoke_seal_ap_exhausted() -> void:
|
||||
var rounds: Array = []
|
||||
settlement.round_started.connect(func(ri): rounds.append(ri))
|
||||
Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)",
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死)
|
||||
var ap_total: int = int(_db.action_economy.ap_per_round)
|
||||
var ap_total: int = int(_db.run.action_economy.ap_per_round)
|
||||
for i in range(ap_total - 1):
|
||||
if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0:
|
||||
break
|
||||
@@ -151,7 +151,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
|
||||
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
|
||||
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
|
||||
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
|
||||
if not settlement.begin_battle(enc):
|
||||
out.stream.append({"type": "verdict", "value": "begin_failed"})
|
||||
@@ -169,7 +169,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
# 清图驱动(真实流):标一面真雷封印(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。
|
||||
# from_refresh=true 时先三轮停手攒 RAP、第 4 轮轮内刷新换图,再清刷新图。
|
||||
func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictionary:
|
||||
_db.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
|
||||
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
@@ -177,7 +177,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
|
||||
var res := {"cleared": false, "refreshed": false, "rounds": 0}
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss",
|
||||
"hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}):
|
||||
_db.difficulty.rows.clear()
|
||||
_db.run.difficulty.rows.clear()
|
||||
return res
|
||||
if from_refresh:
|
||||
for i in range(3):
|
||||
@@ -186,7 +186,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
|
||||
var drive := _drive_clear(settlement)
|
||||
res["cleared"] = drive.cleared
|
||||
res["rounds"] = drive.rounds
|
||||
_db.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
|
||||
_db.run.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
|
||||
return res
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ func _polarity_probe_accepts(probe: Dictionary) -> bool:
|
||||
# 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。
|
||||
# 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。
|
||||
func _run_polarity_probe(seed_val: int) -> Dictionary:
|
||||
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
|
||||
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
@@ -305,7 +305,7 @@ func _run_polarity_probe(seed_val: int) -> Dictionary:
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
guard -= 1
|
||||
_auto_play_round(settlement, out)
|
||||
_db.difficulty.rows.clear() # 测试台账还原
|
||||
_db.run.difficulty.rows.clear() # 测试台账还原
|
||||
return probe
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ func _smoke_polarity_routing() -> void:
|
||||
# δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast
|
||||
# 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变
|
||||
# =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。
|
||||
var prem_pct: int = int(_db.settlement.core.seal_hit_premium)
|
||||
var prem_pct: int = int(_db.combat.settlement.core.seal_hit_premium)
|
||||
Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125)
|
||||
var prem_rows := 0
|
||||
var prem_ok := true
|
||||
@@ -511,7 +511,7 @@ func _smoke_polarity_routing() -> void:
|
||||
elif eff == "ap_delta":
|
||||
prem_ok = prem_ok and v == 1 # +1AP:1 点量纲(round(1.25)=1——整数粒度地板)
|
||||
else:
|
||||
var mag: int = int(_db.status_effect_row(eff).get("magnitude", -999))
|
||||
var mag: int = int(_db.combat.status_effects.row_by_id(eff).get("magnitude", -999))
|
||||
prem_ok = prem_ok and v == int(round(mag * prem_pct / 100.0)) # 增伤50→63/削弱65→81/护甲1→1
|
||||
elif rtype == "heal":
|
||||
# 急救 2→3(clamp 封顶只降不升:满血走 reward_ineffective 不入本行——区间断言)
|
||||
@@ -568,7 +568,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
|
||||
var rows_a: Array = []
|
||||
settle_a.step_row.connect(func(row): rows_a.append(row))
|
||||
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var ms_a = settle_a.minesweeper
|
||||
var t_a := _find_seal_target(ms_a, false)
|
||||
Kit.check("四情况 A:找到安全格封印目标", not t_a.is_empty())
|
||||
@@ -606,7 +606,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
|
||||
var rows_b: Array = []
|
||||
settle_b.step_row.connect(func(row): rows_b.append(row))
|
||||
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var ms_b = settle_b.minesweeper
|
||||
var t_b := _find_seal_target(ms_b, true)
|
||||
Kit.check("四情况 B:找到危险格封印目标", not t_b.is_empty())
|
||||
@@ -636,7 +636,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
|
||||
var rows_c: Array = []
|
||||
settle_c.step_row.connect(func(row): rows_c.append(row))
|
||||
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
|
||||
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
|
||||
var ms_c = settle_c.minesweeper
|
||||
var t_safe := _find_seal_target(ms_c, false)
|
||||
var t_mine := _find_seal_target(ms_c, true)
|
||||
@@ -671,12 +671,12 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
func _smoke_negative_dictionary() -> void:
|
||||
print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---")
|
||||
# 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位)
|
||||
var sapped: Dictionary = _db.status_effect_row("sapped")
|
||||
var sapped: Dictionary = _db.combat.status_effects.row_by_id("sapped")
|
||||
Kit.check("sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)",
|
||||
not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \
|
||||
and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \
|
||||
and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack")
|
||||
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
|
||||
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.combat.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
|
||||
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
|
||||
# heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然:
|
||||
# 本断言多板扫描,取第一块五型齐备的板做行为断言。
|
||||
@@ -692,7 +692,7 @@ func _smoke_negative_dictionary() -> void:
|
||||
settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
rows = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_max_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_max_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
board = settlement.minesweeper.current_board()
|
||||
var r0 := {}
|
||||
@@ -732,7 +732,7 @@ func _smoke_negative_dictionary() -> void:
|
||||
settlement.reward_grant().grant_routed(snap_h, "negative", {"source_class": "mine_hit", "cell": by_type["heal"], "ri": 1})
|
||||
var hp_after: int = int(settlement.presentation_snapshot().hero.hp)
|
||||
var backlash_power: int = 0
|
||||
for row in _db.reward.rows:
|
||||
for row in _db.run.reward.rows:
|
||||
if String(row.get("reward_id", "")) == "heal_backlash":
|
||||
backlash_power = int(row.power)
|
||||
Kit.check("heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" % [backlash_power, hp_before, hp_after], hp_after == hp_before - backlash_power)
|
||||
@@ -789,7 +789,7 @@ func _smoke_resource_surface() -> void:
|
||||
var rng3 = load("res://core/rng_service.gd").new()
|
||||
rng3.set_master_seed(20260920)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3)
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
|
||||
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
var board = settlement.minesweeper.current_board()
|
||||
var r0_first: Variant = null
|
||||
for c in board.compute_r0():
|
||||
|
||||
@@ -26,7 +26,7 @@ func _init() -> void:
|
||||
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
|
||||
func _boss(db, L: int, D: int, kind := "boss"):
|
||||
var ec = UnitContainerScript.new(db)
|
||||
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", kind)
|
||||
var ok: bool = ec.spawn(L, D, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", kind)
|
||||
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
|
||||
return ec
|
||||
|
||||
@@ -83,7 +83,7 @@ func _test_latch_irreversible(db) -> void:
|
||||
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
|
||||
func _test_plain_rows_zero_drift(db) -> void:
|
||||
var ec = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
var u: Dictionary = ec.all_enemies()[0]
|
||||
var any_unlock_key := false
|
||||
for e in u.pool:
|
||||
@@ -96,7 +96,7 @@ func _test_plain_rows_zero_drift(db) -> void:
|
||||
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
|
||||
# 同种子确定性:容器零随机——两实例同输入查询逐位同(同款零扰动载体)
|
||||
var ec2 = load("res://core/unit_container.gd").new(db)
|
||||
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
|
||||
var u2: Dictionary = ec2.all_enemies()[0]
|
||||
ec.apply_damage(u, 3)
|
||||
ec2.apply_damage(u2, 3)
|
||||
|
||||
@@ -75,7 +75,7 @@ func _init() -> void:
|
||||
{"skill_id": "desperate_guard", "weight": 9999, "cooldown_left": 0,
|
||||
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}, # B=条件假技 w9999
|
||||
]
|
||||
g3["basic_streak"] = int(db.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
|
||||
g3["basic_streak"] = int(db.combat.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
|
||||
var specials_v3: Array = ec_v3.special_skills(g3)
|
||||
Kit.check_quiet(_has(specials_v3, "heavy") and not _has(specials_v3, "desperate_guard"),
|
||||
"V3 保底集合含 A 排除 B(special_skills 真实入口——条件假技不逼出)〔触发器批 V3〕")
|
||||
@@ -139,24 +139,24 @@ func _init() -> void:
|
||||
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
|
||||
comp_violations += 1
|
||||
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
|
||||
var enemy_backup: Array = db.enemy.rows.duplicate(true)
|
||||
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
|
||||
var enemy_backup: Array = db.combat.enemy.rows.duplicate(true)
|
||||
var cond_only_row: Dictionary = (db.combat.enemy.rows[0] as Dictionary).duplicate(true)
|
||||
var cond_pool: Array[Variant] = []
|
||||
for entry in (cond_only_row.get("pool", []) as Array):
|
||||
var sid := String((entry as Dictionary).get("skill_id", ""))
|
||||
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
|
||||
var srow_c: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
|
||||
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
|
||||
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
|
||||
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
|
||||
cond_only_row["pool"] = cond_pool
|
||||
db.enemy.rows[0] = cond_only_row
|
||||
db.combat.enemy.rows[0] = cond_only_row
|
||||
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
|
||||
var hit2 := false
|
||||
for e in errs2:
|
||||
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
|
||||
hit2 = true
|
||||
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
|
||||
db.enemy.rows = enemy_backup
|
||||
db.combat.enemy.rows = enemy_backup
|
||||
|
||||
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
|
||||
var ec_v8 = load("res://core/unit_container.gd").new(db)
|
||||
|
||||
@@ -56,7 +56,7 @@ func _smoke_variant_placement(L: int) -> void:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}):
|
||||
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp}):
|
||||
Kit.check("L%d 布置段战斗启动失败" % L, false)
|
||||
return
|
||||
var b = settlement.minesweeper.current_board()
|
||||
@@ -64,7 +64,7 @@ func _smoke_variant_placement(L: int) -> void:
|
||||
if b == null:
|
||||
return
|
||||
var pool_ids: Array = []
|
||||
for r in _db.variant_pool(L, String(b.board_type)):
|
||||
for r in _db.combat.variation.variant_pool(L, String(b.board_type)):
|
||||
pool_ids.append(String(r.variant_id))
|
||||
var vcells: Array = b.variants.keys()
|
||||
var vid0 := ""
|
||||
@@ -95,13 +95,13 @@ func _smoke_variant_placement(L: int) -> void:
|
||||
for c in b.safe_cells():
|
||||
if not r0.has(c):
|
||||
cand_count += 1
|
||||
var expect_cap: int = int(round(float(cand_count) * float(_db.variation.percent_of_cells) / 100.0)) \
|
||||
+ (L - 1) * int(_db.variation.per_level_bonus)
|
||||
var expect_cap: int = int(round(float(cand_count) * float(_db.combat.variation.percent_of_cells) / 100.0)) \
|
||||
+ (L - 1) * int(_db.combat.variation.per_level_bonus)
|
||||
Kit.check("L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" % [L, vcells.size(), expect_cap], vcells.size() <= expect_cap)
|
||||
# 数量公式量随候选池缩放:L1 无关卡加成,极小池(<10 格)可 round 至 0——
|
||||
# 零布置为合法态(同池空口径);非零断言为条件式,公式量>0 时才要求非零(L2/L3 关卡加成保底恒>0)
|
||||
Kit.check("L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" % L, expect_cap == 0 or vcells.size() > 0)
|
||||
var pack_max: int = int(_db.variation.pack_max)
|
||||
var pack_max: int = int(_db.combat.variation.pack_max)
|
||||
var vset := {}
|
||||
for c in vcells:
|
||||
vset[c] = true
|
||||
@@ -127,7 +127,7 @@ func _smoke_variant_placement(L: int) -> void:
|
||||
var rng2 = load("res://core/rng_service.gd").new()
|
||||
rng2.set_master_seed(42)
|
||||
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
|
||||
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp})
|
||||
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp})
|
||||
var b2 = s2.minesweeper.current_board()
|
||||
Kit.check("L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L, b2 != null and _variant_dump(b2) == _variant_dump(b))
|
||||
|
||||
@@ -175,24 +175,24 @@ func _smoke_global_variant() -> void:
|
||||
Kit.check("互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)", domain_hit)
|
||||
# ② 门控第三维(节点身份——global 行仅 boss/elite 入池;缺省/普通节点静默跳过零报错)
|
||||
var pool_boss_ids: Array = []
|
||||
for r in _db.variant_pool(2, "underdark", "boss"):
|
||||
for r in _db.combat.variation.variant_pool(2, "underdark", "boss"):
|
||||
pool_boss_ids.append(String(r.variant_id))
|
||||
Kit.check("门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)", pool_boss_ids == ["magma", "ash"])
|
||||
var pool_battle_ids: Array = []
|
||||
for r in _db.variant_pool(2, "underdark", "battle"):
|
||||
for r in _db.combat.variation.variant_pool(2, "underdark", "battle"):
|
||||
pool_battle_ids.append(String(r.variant_id))
|
||||
Kit.check("门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)", pool_battle_ids == ["magma", "ash"])
|
||||
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2)
|
||||
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.variant_pool(1, "underdark", "boss").is_empty())
|
||||
Kit.check("全局决议:battle 节点恒无全局型", _db.global_variant_row(2, "underdark", "battle").is_empty())
|
||||
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.global_variant_row(2, "underdark", "boss").is_empty())
|
||||
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty())
|
||||
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.global_variant_row(2, "underdark", "elite").is_empty())
|
||||
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.combat.variation.variant_pool(2, "underdark").is_empty() == false and _db.combat.variation.variant_pool(2, "underdark").size() == 2)
|
||||
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.combat.variation.variant_pool(1, "underdark", "boss").is_empty())
|
||||
Kit.check("全局决议:battle 节点恒无全局型", _db.combat.variation.global_variant_row(2, "underdark", "battle").is_empty())
|
||||
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.combat.variation.global_variant_row(2, "underdark", "boss").is_empty())
|
||||
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.combat.variation.global_variant_row(1, "underdark", "boss").is_empty() and _db.combat.variation.global_variant_row(1, "dungeon", "boss").is_empty())
|
||||
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.combat.variation.global_variant_row(2, "underdark", "elite").is_empty())
|
||||
# ③ 兜底回落:全局池空 → Boss 图改走 region 候选池(恰一行 region 变异 + 格子级布置 + 正常取号)
|
||||
var crng = _CountVariantRng.new()
|
||||
crng.set_master_seed(42)
|
||||
var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng)
|
||||
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
|
||||
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})
|
||||
Kit.check("L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)", boss_ok)
|
||||
var boss1_ids: Array = []
|
||||
var boss1_variants: Dictionary = {}
|
||||
@@ -216,7 +216,7 @@ func _smoke_global_variant() -> void:
|
||||
var crng2 = _CountVariantRng.new()
|
||||
crng2.set_master_seed(42)
|
||||
var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2)
|
||||
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
|
||||
var bb2 = boss2.minesweeper.current_board()
|
||||
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
|
||||
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
|
||||
@@ -267,12 +267,12 @@ func _smoke_global_variant() -> void:
|
||||
var nrng = load("res://core/rng_service.gd").new()
|
||||
nrng.set_master_seed(42)
|
||||
var normal1 = load("res://core/battle_orchestrator.gd").new(_db, nrng)
|
||||
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
|
||||
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
var n1: Dictionary = _board_resource_dump(normal1)
|
||||
var nrng2 = load("res://core/rng_service.gd").new()
|
||||
nrng2.set_master_seed(42)
|
||||
var normal2 = load("res://core/battle_orchestrator.gd").new(_db, nrng2)
|
||||
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
|
||||
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
Kit.check("确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)", n1 == _board_resource_dump(normal2) and not n1.is_empty())
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
|
||||
|
||||
func _test_rng_tag_closure(db) -> void:
|
||||
var stream_label: String = String(db.settlement.damage_roll.armor_roll_stream)
|
||||
var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream)
|
||||
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
RngServiceScript.PURPOSE_TAGS.has(stream_label))
|
||||
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
@@ -102,8 +102,8 @@ func _test_status_boolean_acceptance(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
# EC1 域闭:notify_battle_start 前授予 = false(受理面封闭)
|
||||
var any_row: Dictionary = {}
|
||||
if db.status_effects.rows.size() > 0:
|
||||
any_row = db.status_effects.rows[0]
|
||||
if db.combat.status_effects.rows.size() > 0:
|
||||
any_row = db.combat.status_effects.rows[0]
|
||||
Kit.check("前置:状态表非空", not any_row.is_empty())
|
||||
if any_row.is_empty():
|
||||
return
|
||||
|
||||
@@ -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", [])
|
||||
@@ -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
|
||||
@@ -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)}))
|
||||
|
||||
@@ -54,16 +54,16 @@ func _difficulty_table():
|
||||
|
||||
|
||||
func _test_band_hit() -> void:
|
||||
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
|
||||
var hit: Array = _resolver.combat.enemy.resolve_rowset(1, 4, "battle")
|
||||
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
|
||||
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle")
|
||||
var exact: Array = _resolver.combat.enemy.resolve_rowset(1, 5, "battle")
|
||||
Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕",
|
||||
exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
|
||||
|
||||
func _test_rowset_order_preserved() -> void:
|
||||
var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
|
||||
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
|
||||
Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2)
|
||||
var ids: Array[String] = []
|
||||
for r in rows:
|
||||
@@ -74,7 +74,7 @@ func _test_rowset_order_preserved() -> void:
|
||||
|
||||
func _test_top_band_clamp() -> void:
|
||||
_diags.clear()
|
||||
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
|
||||
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 99, "battle")
|
||||
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
|
||||
rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
var clamp_hit := false
|
||||
@@ -83,13 +83,13 @@ func _test_top_band_clamp() -> void:
|
||||
clamp_hit = true
|
||||
Kit.check("越顶带发 warning resolve_depth_clamp〔fv3〕", clamp_hit)
|
||||
var warn_before: int = _diags.size()
|
||||
_resolver.resolve_enemy_rowset(1, 99, "battle")
|
||||
_resolver.combat.enemy.resolve_rowset(1, 99, "battle")
|
||||
Kit.check("同键重复请求警告去重(四元组注册表)〔fv3〕", _diags.size() == warn_before)
|
||||
|
||||
|
||||
func _test_row_kind_routing() -> void:
|
||||
var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
|
||||
var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss")
|
||||
var battle_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
|
||||
var boss_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "boss")
|
||||
Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕",
|
||||
battle_rows.size() == 2 and boss_rows.size() == 1
|
||||
and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief")
|
||||
@@ -97,13 +97,13 @@ func _test_row_kind_routing() -> void:
|
||||
|
||||
func _test_request_oob() -> void:
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(0, 0, "battle")
|
||||
_resolver.combat.enemy.resolve_rowset(0, 0, "battle")
|
||||
Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(1, -1, "battle")
|
||||
_resolver.combat.enemy.resolve_rowset(1, -1, "battle")
|
||||
Kit.check("节点深度 <0 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(1, 0, "elite")
|
||||
_resolver.combat.enemy.resolve_rowset(1, 0, "elite")
|
||||
Kit.check("row_kind 非法 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
|
||||
|
||||
@@ -115,11 +115,11 @@ func _has_fatal_oob() -> bool:
|
||||
|
||||
|
||||
func _test_difficulty_clamp() -> void:
|
||||
var m: Dictionary = _resolver.resolve_difficulty_modifiers(1, 0)
|
||||
var m: Dictionary = _resolver.run.difficulty.resolve_modifiers(1, 0)
|
||||
Kit.check("难度乘数越域被 clamp:hp 99 → 上限 10", absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001)
|
||||
Kit.check("难度乘数越域被 clamp:atk 0.01 → 下限 0.1", absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001)
|
||||
Kit.check("未登记坐标返回空修饰(零值不伪装成 ×1)",
|
||||
_resolver.resolve_difficulty_modifiers(3, 3).is_empty())
|
||||
_resolver.run.difficulty.resolve_modifiers(3, 3).is_empty())
|
||||
|
||||
|
||||
class _FakeDifficulty:
|
||||
|
||||
@@ -13,8 +13,8 @@ func _init() -> void:
|
||||
print("[curio_refresh_paid_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var base: int = int(db.adventure.shop_refresh_cost_base)
|
||||
var curve: float = float(db.adventure.shop_refresh_cost_curve)
|
||||
var base: int = int(db.run.adventure.shop_refresh_cost_base)
|
||||
var curve: float = float(db.run.adventure.shop_refresh_cost_curve)
|
||||
Kit.check_quiet(base > 0 and curve > 1.0, "商店刷新旋钮在档(base=%d curve=%.2f——三选一付费档同源读取)" % [base, curve])
|
||||
|
||||
# ---- ① 停点在途谓词:无停点时免费/付费刷新皆拒绝(付费路径扣费前置校验防空转烧钱包)----
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -23,18 +23,18 @@ func _init() -> void:
|
||||
# ---- hr1:生效公式逐值(表值+永久强化 flat+加算区后乘调试 pct——与改前同式)----
|
||||
var journal_a = GainJournalScript.new()
|
||||
var hero = HeroStatsScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}, journal_a)
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5,
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5,
|
||||
"hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)")
|
||||
Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3,
|
||||
Kit.check_quiet(hero.effective_atk() == int(db.combat.hero.atk) + 3,
|
||||
"hr1 生效攻击 = 表值+永久强化 flat")
|
||||
Kit.check_quiet(hero.effective_ac() == int(db.forge_armor_route_by_type(db.forge_default_armor_type()).get("base_ac", 0)) + 2,
|
||||
Kit.check_quiet(hero.effective_ac() == int(db.crossrun.forge.armor_route_by_type(db.crossrun.forge.default_armor_type).get("base_ac", 0)) + 2,
|
||||
"hr1 生效护甲 = 甲型基础 AC+甲路线等级加值(纯 flat 无 pct)")
|
||||
hero.set_debug_override(50, 50)
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)),
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.combat.hero.max_hp) + 5) * 1.5)),
|
||||
"hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)")
|
||||
hero.set_debug_override(0, 0)
|
||||
journal_a.gain_bonus("max_hp", 4, "battle_export", "max_hp")
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4,
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5 + 4,
|
||||
"hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加——公式消费日记汇总)")
|
||||
|
||||
# hr1 回血封顶公式逐值(算术宿主=HeroStats.rest_heal 单点——改前 LevelRunner.rest_heal 同式)
|
||||
|
||||
@@ -102,7 +102,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
|
||||
# ---- ⑥ N 进数据表(含越域校验)〔fv6〕----
|
||||
|
||||
func _test_field_in_table(db) -> void:
|
||||
var n_in_table: int = int(db.level.info_visible_layers)
|
||||
var n_in_table: int = int(db.run.level.info_visible_layers)
|
||||
Kit.check("N 落表:level.tres 读数 2(实际 %d)〔fv6〕" % n_in_table, n_in_table == 2)
|
||||
var t := LevelTableScript.new()
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": _valid_graph_nodes()}]
|
||||
@@ -184,11 +184,11 @@ func _test_single_chain_entry(db) -> void:
|
||||
func _test_n_from_table(db) -> void:
|
||||
var nodes := _graph_nodes()
|
||||
var r = _runner(db, nodes, 1, [nodes[2]]) # 锚层 1
|
||||
db.level.info_visible_layers = 1
|
||||
db.run.level.info_visible_layers = 1
|
||||
Kit.check("N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕", not r.type_visible_ids().has(4))
|
||||
db.level.info_visible_layers = 3
|
||||
db.run.level.info_visible_layers = 3
|
||||
Kit.check("N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕", r.type_visible_ids().has(5))
|
||||
db.level.info_visible_layers = 10
|
||||
db.run.level.info_visible_layers = 10
|
||||
var all_in := true
|
||||
for nid in 7: # 非豁免节点 0..6(7 = Boss 恒显,不作判据)
|
||||
if not r.type_visible_ids().has(nid):
|
||||
|
||||
@@ -33,7 +33,7 @@ var _graph_gen = null # 生成图取数单实例(图 = f(参数档, 种子)
|
||||
|
||||
# 参数档 × 种子产图(与 LevelRunner._assign_generated_graph 同一产图路径与同一参数档表)
|
||||
func _generated_graph(level_index: int, seed_val: int) -> Dictionary:
|
||||
var params: Dictionary = _db.level_gen_params_row(level_index)
|
||||
var params: Dictionary = _db.run.level_gen_params.row_by_level_index(level_index)
|
||||
if params.is_empty():
|
||||
return {}
|
||||
if _graph_gen == null:
|
||||
@@ -221,13 +221,13 @@ func _path_in_graph(nodes: Array, path: Array) -> bool:
|
||||
|
||||
# 敌侧覆盖判据:请求深度经 band_of 落带(非 -1)——解析落带 ≥ 请求深度即等价判据(禁顶 clamp 消解)
|
||||
func _enemy_band_covers(level_index: int, depth: int, kind: String) -> bool:
|
||||
var rows: Array[Dictionary] = _db.resolve_enemy_rowset(level_index, depth, kind)
|
||||
var rows: Array[Dictionary] = _db.combat.enemy.resolve_rowset(level_index, depth, kind)
|
||||
return not rows.is_empty() and int(rows[0].get("depth_band", -1)) >= depth
|
||||
|
||||
|
||||
# 板侧覆盖判据:该深度在 board_gen 精确命中(node_depth 等值)
|
||||
func _board_row_exact(level_index: int, depth: int) -> bool:
|
||||
var board: Dictionary = _db.resolve_board_gen(level_index, depth)
|
||||
var board: Dictionary = _db.combat.board_gen.resolve_row(level_index, depth)
|
||||
return not board.is_empty() and int(board.get("node_depth", -1)) == depth
|
||||
|
||||
|
||||
@@ -259,14 +259,14 @@ func _init() -> void:
|
||||
|
||||
func _smoke_table_migration() -> void:
|
||||
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
|
||||
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop.relic_base_price == 15.0)
|
||||
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop.spend_discount == 0.8)
|
||||
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero.display_name == "探险者")
|
||||
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0)
|
||||
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.run.shop.spend_discount == 0.8)
|
||||
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者")
|
||||
# 消费端读表:spawn 真实构造链的英雄单位显示名与表值同
|
||||
var units = load("res://core/unit_container.gd").new(_db)
|
||||
var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp))
|
||||
var spawned: bool = units.spawn(1, 0, int(_db.combat.hero.max_hp))
|
||||
Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕",
|
||||
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name))
|
||||
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.combat.hero.display_name))
|
||||
|
||||
|
||||
# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图,
|
||||
@@ -279,7 +279,7 @@ func _smoke_generated_coverage() -> void:
|
||||
var scanned := 0
|
||||
var readings: Array = []
|
||||
for L in [1, 2, 3]:
|
||||
if _db.level_gen_params_row(L).is_empty():
|
||||
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
|
||||
Kit.check("L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" % L, false)
|
||||
continue
|
||||
var dmin := 999
|
||||
@@ -306,7 +306,7 @@ func _smoke_generated_coverage() -> void:
|
||||
board_ok = false
|
||||
var bands: Array = []
|
||||
for tid in _terminal_ids(nodes):
|
||||
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
|
||||
var rs: Array[Dictionary] = _db.combat.enemy.resolve_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
|
||||
bands.append(int(rs[0].get("depth_band", -1)) if not rs.is_empty() else -1)
|
||||
for i in range(bands.size()):
|
||||
for j in range(i + 1, bands.size()):
|
||||
@@ -325,9 +325,9 @@ func _smoke_generated_coverage() -> void:
|
||||
func _smoke_level() -> void:
|
||||
print("--- 关卡节点图·生成图推进(参数档×种子产图·满配冒险成功/恢复公式/败北线/回放确定性)---")
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
var hero_base := {"atk": int(_db.hero.atk), "hp": int(_db.hero.max_hp), "ac": int(_db.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
|
||||
var hero_base := {"atk": int(_db.combat.hero.atk), "hp": int(_db.combat.hero.max_hp), "ac": int(_db.combat.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
|
||||
var nodes1: Array = _level_active_nodes(1)
|
||||
var params1: Dictionary = _db.level_gen_params_row(1)
|
||||
var params1: Dictionary = _db.run.level_gen_params.row_by_level_index(1)
|
||||
Kit.check("L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes1.size()], not nodes1.is_empty())
|
||||
# ① 满配冒险成功路径(atk+200%/hp+100%——测试面板同源的属性覆盖)
|
||||
var win = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 1, 200, 100)
|
||||
@@ -354,10 +354,10 @@ func _smoke_level() -> void:
|
||||
boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1)
|
||||
Kit.check("L1 终点恰一个(大岔口自 L2 起的图侧投影)", _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1)
|
||||
Kit.check("L1 生成图含岔口(出度 ≥2 节点 %d 处)" % _fork_ids(nodes1).size(), _fork_ids(nodes1).size() >= 1)
|
||||
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3)
|
||||
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.combat.hero.max_hp) * 3)
|
||||
# ② 恢复点公式:回复 = min(floor(生效max_hp×rest_heal_pct/100), 缺口)——两档缺口各验一条分支
|
||||
#(真入口驱动:起关→首战→选恢复候选;缺口由夹具造,恢复比例读关卡行)
|
||||
var pct1: int = int(_db.level_row(1).get("rest_heal_pct", 0))
|
||||
var pct1: int = int(_db.run.level.row_by_level_index(1).get("rest_heal_pct", 0))
|
||||
var rest_wide := _drive_to_rest(1, ACTIVE_GRAPH_SEED, 100000)
|
||||
if rest_wide.is_empty():
|
||||
Kit.check("恢复点公式无源(现行图入口后一位无可直达的恢复候选)", false)
|
||||
@@ -382,9 +382,9 @@ func _smoke_level() -> void:
|
||||
Kit.check("关卡回放断言:同种子两遍结算步骤流逐行相同", JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows))
|
||||
Kit.check("关卡步骤流非空(%d 行)" % rep1.rows.size(), rep1.rows.size() > 0)
|
||||
# ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随)
|
||||
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)],
|
||||
int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \
|
||||
and int(_db.hero.armor_class) == int(hero_base.ac))
|
||||
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.combat.hero.atk), int(_db.combat.hero.max_hp), int(_db.combat.hero.armor_class)],
|
||||
int(_db.combat.hero.atk) == int(hero_base.atk) and int(_db.combat.hero.max_hp) == int(hero_base.hp) \
|
||||
and int(_db.combat.hero.armor_class) == int(hero_base.ac))
|
||||
|
||||
|
||||
# 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。
|
||||
@@ -439,10 +439,10 @@ func _smoke_level_graph() -> void:
|
||||
print("--- 关卡节点图·分支节点图(L2 幽暗地域·生成图)---")
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
# ① 关卡表行级校验零错误(关卡表只承载序号/显示名/恢复比例——图合法性由 validate_graph 在起关时承担)
|
||||
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.level.validate().is_empty())
|
||||
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.run.level.validate().is_empty())
|
||||
# ② L2 生成图形状读数(性质断言——规模/型别/岔口全部由参数档与逐节点扫描互校)
|
||||
var nodes2: Array = _level_active_nodes(2)
|
||||
var params2: Dictionary = _db.level_gen_params_row(2)
|
||||
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
|
||||
Kit.check("L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes2.size()], not nodes2.is_empty())
|
||||
var tcount := _type_tally(nodes2)
|
||||
var branch := _fork_ids(nodes2).size()
|
||||
@@ -600,7 +600,7 @@ func _smoke_level_graph() -> void:
|
||||
# ⑦a 定点:L2 首遇带行集与各终点 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标;
|
||||
# 多终点图逐终点解析,断言行集构成而非单行:行序 = 文件序,首行即叙述取数面)
|
||||
var l2_spider_band: int = _first_encounter_band(2, "giant_spider")
|
||||
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle")
|
||||
var l2_spider_set: Array = _db.combat.enemy.resolve_rowset(2, l2_spider_band, "battle")
|
||||
var l2_spider_ids := {}
|
||||
for r in l2_spider_set:
|
||||
l2_spider_ids[String(r.get("enemy_id", ""))] = true
|
||||
@@ -610,14 +610,14 @@ func _smoke_level_graph() -> void:
|
||||
var l2_terms := _terminal_depths(nodes2)
|
||||
var l2_boss_ok := not l2_terms.is_empty()
|
||||
for d in l2_terms:
|
||||
var l2_boss_set: Array[Dictionary] = _db.resolve_enemy_rowset(2, int(d), "boss")
|
||||
var l2_boss_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(2, int(d), "boss")
|
||||
if l2_boss_set.size() != 1 or String(l2_boss_set[0].get("enemy_id", "")) != "drider" \
|
||||
or String(l2_boss_set[0].get("armor_type", "")) != "ward":
|
||||
l2_boss_ok = false
|
||||
Kit.check("L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % str(l2_terms), l2_boss_ok)
|
||||
# ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面:
|
||||
# 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4)
|
||||
var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle")
|
||||
var l2_mix_set: Array = _db.combat.enemy.resolve_rowset(2, 5, "battle")
|
||||
var mix_shape_ok: bool = l2_mix_set.size() == 2 \
|
||||
and String(l2_mix_set[0].get("enemy_id", "")) == "bugbear" \
|
||||
and String(l2_mix_set[1].get("enemy_id", "")) == "giant_spider"
|
||||
@@ -632,7 +632,7 @@ func _smoke_level_graph() -> void:
|
||||
# ---- L3 灰矮人矿城断言段 ----
|
||||
# ⑧ L3 生成图形状读数(与 ② 同一性质判据面——规模/型别/岔口/多终点全部对参数档与扫描读数互校)
|
||||
var nodes3: Array = _level_active_nodes(3)
|
||||
var params3: Dictionary = _db.level_gen_params_row(3)
|
||||
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
|
||||
Kit.check("L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes3.size()], not nodes3.is_empty())
|
||||
var tcount3 := _type_tally(nodes3)
|
||||
var branch3 := _fork_ids(nodes3).size()
|
||||
@@ -654,7 +654,7 @@ func _smoke_level_graph() -> void:
|
||||
Kit.check("L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" % int(cov3["board_bad"]), int(cov3["board_bad"]) == 0)
|
||||
# ⑩ 定点:L3 首遇带行集与各终点 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a)
|
||||
var l3_lead_band: int = _first_encounter_band(3, "duergar")
|
||||
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle")
|
||||
var l3_lead_set: Array = _db.combat.enemy.resolve_rowset(3, l3_lead_band, "battle")
|
||||
var l3_lead_ids := {}
|
||||
for r in l3_lead_set:
|
||||
l3_lead_ids[String(r.get("enemy_id", ""))] = true
|
||||
@@ -665,7 +665,7 @@ func _smoke_level_graph() -> void:
|
||||
var l3_terms := _terminal_depths(nodes3)
|
||||
var l3_boss_ok := not l3_terms.is_empty()
|
||||
for d in l3_terms:
|
||||
var l3_king_set: Array[Dictionary] = _db.resolve_enemy_rowset(3, int(d), "boss")
|
||||
var l3_king_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(3, int(d), "boss")
|
||||
if l3_king_set.size() != 1 or String(l3_king_set[0].get("enemy_id", "")) != "duergar_king" \
|
||||
or String(l3_king_set[0].get("armor_type", "")) != "heavy":
|
||||
l3_boss_ok = false
|
||||
@@ -700,23 +700,23 @@ func _smoke_level_graph() -> void:
|
||||
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
|
||||
var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
_db.enemy.rows.append(mix_row_a)
|
||||
_db.enemy.rows.append(mix_row_b)
|
||||
_db.combat.enemy.rows.append(mix_row_a)
|
||||
_db.combat.enemy.rows.append(mix_row_b)
|
||||
var mix_sys = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn(1, 5, int(_db.hero.max_hp)))
|
||||
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn(1, 5, int(_db.combat.hero.max_hp)))
|
||||
var mix_units: Array = mix_sys.all_enemies()
|
||||
Kit.check("混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)", mix_units.size() == 3)
|
||||
Kit.check("混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)", String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear")
|
||||
Kit.check("混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)", String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3)
|
||||
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
var over_sys = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn(1, 6, int(_db.hero.max_hp)))
|
||||
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
|
||||
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
|
||||
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
|
||||
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
|
||||
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.enemy.rows.size()) == 28)
|
||||
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn(1, 6, int(_db.combat.hero.max_hp)))
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
|
||||
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 28)
|
||||
|
||||
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
|
||||
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
|
||||
@@ -734,7 +734,7 @@ func _smoke_level_graph() -> void:
|
||||
if not vis_ids.has(int(bid)):
|
||||
vis_boss_ok = false
|
||||
Kit.check("可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" % str(_ids_of_type(nodes2, "boss")), vis_boss_ok)
|
||||
var vis_n: int = int(_db.level.info_visible_layers)
|
||||
var vis_n: int = int(_db.run.level.info_visible_layers)
|
||||
Kit.check("可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" % vis_n, vis_n == 2)
|
||||
# 层距判据按图自算入口 BFS 层(与 runner 同源口径:入口层 0、出边逐层 +1)——
|
||||
# 锚层 0 时层距 ≤ N 者恒可见、层距 > N 的非终点者恒不可见(两条扫描面都须非空)
|
||||
@@ -823,7 +823,7 @@ func _smoke_gp_wallet() -> void:
|
||||
var fin_b := {}
|
||||
boss.step_row.connect(func(row): rows_b.append(row))
|
||||
boss.battle_finished.connect(func(p): fin_b["p"] = p)
|
||||
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.hero.max_hp)}))
|
||||
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.combat.hero.max_hp)}))
|
||||
var guard_b := 60
|
||||
while boss.battle_result == "" and guard_b > 0:
|
||||
guard_b -= 1
|
||||
@@ -862,7 +862,7 @@ func _smoke_gp_escrow() -> void:
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
var fin := {}
|
||||
settlement.battle_finished.connect(func(p): fin["p"] = p)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}):
|
||||
continue
|
||||
var target := _find_gp_dig(settlement.minesweeper, settlement)
|
||||
if target.is_empty():
|
||||
@@ -896,15 +896,15 @@ func _smoke_gp_escrow() -> void:
|
||||
var settle_l = load("res://core/battle_orchestrator.gd").new(_db, rng_l)
|
||||
var fin_l := {}
|
||||
settle_l.battle_finished.connect(func(p): fin_l["p"] = p)
|
||||
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
|
||||
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
|
||||
for i in range(3):
|
||||
settle_l.reward_grant().grant_routed({"reward_id": "gp_lose", "cell_type": "gp", "polarity": "negative", "display_name": "破财", "power": 2, "effect_id": "gp_loss", "stacks": 1},
|
||||
"negative", {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
|
||||
Kit.check("破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)", int(settle_l.reward_grant().escrow) == -6)
|
||||
settle_l.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var gp_l: int = int(fin_l.get("p", {}).get("gp_dropped", 999))
|
||||
var boss_drop: Dictionary = _db.shop_gp_drop_row("boss")
|
||||
var boss_bonus1: int = _db.shop_level_gp_bonus(1)
|
||||
var boss_drop: Dictionary = _db.run.shop.gp_drop_row("boss")
|
||||
var boss_bonus1: int = _db.run.shop.level_gp_bonus_at(1)
|
||||
Kit.check("破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" % [
|
||||
gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1],
|
||||
gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1)
|
||||
@@ -1048,7 +1048,7 @@ func _smoke_shop_room() -> void:
|
||||
Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()))
|
||||
# armor_up:AC 10+1
|
||||
Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up"))
|
||||
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.hero.armor_class) + 1)
|
||||
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1)
|
||||
# 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝
|
||||
var guard_drain := 20
|
||||
var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环
|
||||
@@ -1068,12 +1068,12 @@ func _smoke_shop_room() -> void:
|
||||
Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs),
|
||||
runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs))
|
||||
# 下一战参数 = 基础表值 + 加算区(百分比已归零——加算先乘后乘序的前半在案)
|
||||
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.hero.atk) + 1)
|
||||
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.combat.hero.atk) + 1)
|
||||
Kit.check("选终点 Boss(句柄 %d)开战" % int(shop_outs[0]), runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null)
|
||||
var snap: Dictionary = runner.current_battle().presentation_snapshot()
|
||||
# max_hp 期望按实际购买笔数推导(可购笔数
|
||||
# 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不锚死恰好 1 笔)
|
||||
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys)
|
||||
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.combat.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.combat.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.combat.hero.max_hp) + 3 * hp_buys)
|
||||
# 冒险收束(Boss 胜或败北——基线裸装口径任一结局)→ 钱包清零
|
||||
var fin_end := {}
|
||||
runner.level_finished.connect(func(p): fin_end["p"] = p)
|
||||
@@ -1106,7 +1106,7 @@ func _smoke_shop_graph() -> void:
|
||||
var path_total := 0
|
||||
var graph_total := 0
|
||||
for L in [1, 2, 3]:
|
||||
if _db.level_gen_params_row(L).is_empty():
|
||||
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
|
||||
Kit.check("L%d 参数档缺行(商店编排断言无源可扫)" % L, false)
|
||||
continue
|
||||
for seed_val in range(SWEEP_SEED_FROM, SWEEP_SEED_TO):
|
||||
@@ -1238,7 +1238,7 @@ func _smoke_shop_pool() -> void:
|
||||
print("--- 商店共享池(抽3买1全重抽 · 乘法定价 ceil · 池空硬闸 · 切边零入店)---")
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
var shelf_kinds: Array = []
|
||||
for row in _db.shop_pool_rows():
|
||||
for row in _db.run.shop.pool_rows():
|
||||
shelf_kinds.append(String(row.get("kind_id", "")))
|
||||
# ① 池模式场景可达(_adv_drive_to_shop 复用——L1 全程+L2 推进到商店房;pool 模式进店即抽架)
|
||||
var scen := {}
|
||||
@@ -1268,12 +1268,12 @@ func _smoke_shop_pool() -> void:
|
||||
var r8 = scen["r"]
|
||||
var gp0: int = int(r8.gp)
|
||||
var cost0: int = r8.shop_refresh_cost()
|
||||
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.adventure.shop_refresh_cost_base))
|
||||
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base))
|
||||
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0)
|
||||
var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true)
|
||||
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh())
|
||||
Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0)
|
||||
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.adventure.shop_refresh_cost_curve))))
|
||||
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
|
||||
var same_shelf := first_shelf_a.size() == first_shelf_b.size()
|
||||
if same_shelf:
|
||||
for i in first_shelf_a.size():
|
||||
@@ -1290,7 +1290,7 @@ func _smoke_shop_pool() -> void:
|
||||
var l2 := int(r8.level_row.level_index)
|
||||
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
|
||||
var relic_price_now: int = r8.pool_price("relic")
|
||||
var relic_price_pre: int = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
|
||||
var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
|
||||
Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre)
|
||||
for c in r8.shop_shelf():
|
||||
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
|
||||
@@ -1300,10 +1300,10 @@ func _smoke_shop_pool() -> void:
|
||||
Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind))
|
||||
var expect_p: int
|
||||
if is_relic:
|
||||
expect_p = int(ceil(float(_db.shop.relic_base_price) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
|
||||
expect_p = int(ceil(float(_db.run.shop.relic_base_price) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
|
||||
else:
|
||||
var prow: Dictionary = _db.shop_pool_row(ckind)
|
||||
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
|
||||
var prow: Dictionary = _db.run.shop.pool_row(ckind)
|
||||
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
|
||||
Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p)
|
||||
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
|
||||
var buy_c: Dictionary = {}
|
||||
@@ -1321,7 +1321,7 @@ func _smoke_shop_pool() -> void:
|
||||
var val0: int = a8.journal().bonus_total(String(val_map[kind]))
|
||||
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
|
||||
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier)))
|
||||
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1)
|
||||
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
|
||||
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
|
||||
@@ -1332,8 +1332,8 @@ func _smoke_shop_pool() -> void:
|
||||
second = c
|
||||
break
|
||||
if not second.is_empty():
|
||||
var prow2: Dictionary = _db.shop_pool_row(String(second.get("kind_id", "")))
|
||||
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * _db.shop_price_a()))
|
||||
var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", "")))
|
||||
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a))
|
||||
Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2)
|
||||
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", ""))))
|
||||
else:
|
||||
@@ -1384,8 +1384,8 @@ func _smoke_shop_pool() -> void:
|
||||
var ps_ok := true
|
||||
for k in ps.keys():
|
||||
var quota := 0
|
||||
for tier in _db.adventure.TIERS:
|
||||
quota += int(_db.adventure.curio_quota(String(k), tier))
|
||||
for tier in _db.run.adventure.TIERS:
|
||||
quota += int(_db.run.adventure.curio_quota(String(k), tier))
|
||||
if int(ps[k]) < 0 or int(ps[k]) > quota:
|
||||
ps_ok = false
|
||||
Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok)
|
||||
@@ -1458,7 +1458,7 @@ func _adv_st() -> Dictionary:
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
var best := -1
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != level_index:
|
||||
continue
|
||||
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
|
||||
|
||||
@@ -89,7 +89,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
|
||||
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
|
||||
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
|
||||
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
|
||||
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
|
||||
if not settlement.begin_battle(enc):
|
||||
out.stream.append({"type": "verdict", "value": "begin_failed"})
|
||||
@@ -134,7 +134,7 @@ func _meta_pricing_and_buy() -> void:
|
||||
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
var bal0: int = sv.meta_state.keystone_balance()
|
||||
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
|
||||
var price0: int = int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
|
||||
Kit.check("树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0],
|
||||
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv.meta_state.keystone_balance() == bal0 - price0)
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
@@ -156,8 +156,8 @@ func _meta_settle_and_workshop() -> void:
|
||||
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var row1: Dictionary = _db.meta_boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.meta_boss_keystone_row(2)
|
||||
var row1: Dictionary = _db.crossrun.meta.boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.crossrun.meta.boss_keystone_row(2)
|
||||
# ⑥ 逐杀掉落收束入账(掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
|
||||
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐关卡完成关次置位)
|
||||
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
@@ -351,7 +351,7 @@ func _meta_injection() -> void:
|
||||
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
|
||||
sv.meta_state.set_tree_level("sp_hp", 2)
|
||||
Kit.check("生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)",
|
||||
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)))
|
||||
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("effects", {}).get("max_hp", 0)))
|
||||
Kit.check("攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)",
|
||||
int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0)
|
||||
Kit.check("命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)",
|
||||
@@ -362,10 +362,10 @@ func _meta_injection() -> void:
|
||||
var r = adr.runner()
|
||||
var ov: Dictionary = mp.hero_override()
|
||||
Kit.check("首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)",
|
||||
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
|
||||
int(r.effective_atk()) == int(_db.combat.hero.atk) + int(ov.get("atk", 0)))
|
||||
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
|
||||
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
int(r.effective_max_hp()) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.stats().hero_hp) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
|
||||
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
|
||||
@@ -376,11 +376,11 @@ func _meta_injection() -> void:
|
||||
var rw = adr_w.runner()
|
||||
var rz = adr_z.runner()
|
||||
Kit.check("白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)",
|
||||
int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
|
||||
int(rw.effective_atk()) == int(_db.combat.hero.atk) and int(rw.effective_max_hp()) == int(_db.combat.hero.max_hp)
|
||||
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
|
||||
and int(rz.effective_ac()) == int(rw.effective_ac()))
|
||||
Kit.check("ac 轴:白板恒表值、未换强化时注入零位移",
|
||||
int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class))
|
||||
int(rw.effective_ac()) == int(_db.combat.hero.armor_class) and int(r.effective_ac()) == int(_db.combat.hero.armor_class))
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
@@ -393,10 +393,10 @@ func _smoke_forge_axes() -> void:
|
||||
not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero))
|
||||
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
|
||||
Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕",
|
||||
_db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light")
|
||||
_db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light")
|
||||
# 单位快照回退面:spawn 三参形态(零注入)= 锻造配置默认值
|
||||
var uc = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.hero.max_hp)))
|
||||
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.combat.hero.max_hp)))
|
||||
Kit.check("主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕",
|
||||
String(uc.hero_unit().get("attack_type", "")) == "normal"
|
||||
and String(uc.hero_unit().get("armor_type", "")) == "light")
|
||||
@@ -527,12 +527,12 @@ func _smoke_forge_axes() -> void:
|
||||
# spawn 显式注入面(三参旧形态零注入已在上方覆盖——此处钉显式值)
|
||||
var uc2 = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕",
|
||||
uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy")
|
||||
uc2.spawn(1, 0, int(_db.combat.hero.max_hp), [], -1, -1, [], "magic", "heavy")
|
||||
and String(uc2.hero_unit().get("attack_type", "")) == "magic"
|
||||
and String(uc2.hero_unit().get("armor_type", "")) == "heavy")
|
||||
# V10 复核:树表 12 行 effects 键集无 atk/armor_class(契约 1——树零攻防来源)
|
||||
var tree_atk_ac := false
|
||||
for trow in _db.tree_rows():
|
||||
for trow in _db.crossrun.meta_tree.rows:
|
||||
var eff: Dictionary = trow.get("effects", {})
|
||||
if eff.has("atk") or eff.has("armor_class"):
|
||||
tree_atk_ac = true
|
||||
@@ -546,11 +546,11 @@ func _smoke_tree() -> void:
|
||||
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
|
||||
# 〔V1〕结构:12 行 + schema 零违例
|
||||
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
|
||||
_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12)
|
||||
_db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12)
|
||||
Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)",
|
||||
_db.meta_tree.validate().is_empty())
|
||||
_db.crossrun.meta_tree.validate().is_empty())
|
||||
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}
|
||||
var ley: Dictionary = _db.meta_tree.row_by_id("easter_ley")
|
||||
var ley: Dictionary = _db.crossrun.meta_tree.row_by_id("easter_ley")
|
||||
var ley_ok: bool = not ley.is_empty()
|
||||
for k in (ley.get("effects", {}) as Dictionary).keys():
|
||||
if not (String(k) == "max_hp" or String(k) == "hit"):
|
||||
@@ -559,23 +559,23 @@ func _smoke_tree() -> void:
|
||||
ley_ok)
|
||||
# V1/V4 反例:出域行逐类构造被拒(复制行副本塞词表外值)
|
||||
var t2 = load("res://data/schemas/meta_tree_table.gd").new()
|
||||
var bad_vein: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
var bad_vein: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
bad_vein["node_id"] = "bad_vein"
|
||||
bad_vein["vein"] = "underworld"
|
||||
t2.rows = [bad_vein]
|
||||
Kit.check("schema 反例:vein 出域被拒", not t2.validate().is_empty())
|
||||
var bad_fork: Dictionary = (_db.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
|
||||
var bad_fork: Dictionary = (_db.crossrun.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
|
||||
bad_fork["node_id"] = "bad_fork"
|
||||
bad_fork["fork_group"] = "fg_mastery"
|
||||
t2.rows = [bad_fork]
|
||||
Kit.check("schema 反例:fork_group 出域(fg_mastery 已退役组)被拒", not t2.validate().is_empty())
|
||||
var bad_arch: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
var bad_arch: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
bad_arch["node_id"] = "bad_arch"
|
||||
bad_arch["effects"] = {"archetype": 1}
|
||||
t2.rows = [bad_arch]
|
||||
Kit.check("schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕",
|
||||
not t2.validate().is_empty())
|
||||
var bad_dangling: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
var bad_dangling: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
|
||||
bad_dangling["requires"] = ["ghost_node"]
|
||||
t2.rows = [bad_dangling]
|
||||
Kit.check("schema 反例:requires 悬空被拒", not t2.validate().is_empty())
|
||||
@@ -589,7 +589,7 @@ func _smoke_tree() -> void:
|
||||
not mp.tree_can_buy("sp_hp"))
|
||||
Kit.check("命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)",
|
||||
mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1"))
|
||||
Kit.check("锻造工坊钥匙零币(free)", int(_db.tree_node("forge_workshop").get("price", -1)) == 0)
|
||||
Kit.check("锻造工坊钥匙零币(free)", int(_db.crossrun.meta_tree.row_by_id("forge_workshop").get("price", -1)) == 0)
|
||||
Kit.check("行门槛恰达开放(总等级 4 ≥ 4×1)", mp.tree_can_buy("sp_hp"))
|
||||
Kit.check("第 2 级购买受理(门槛恰达当刻开放)",
|
||||
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 2)
|
||||
|
||||
@@ -35,7 +35,7 @@ func _init() -> void:
|
||||
|
||||
|
||||
func _params(L: int) -> Dictionary:
|
||||
return _db.level_gen_params_row(L)
|
||||
return _db.run.level_gen_params.row_by_level_index(L)
|
||||
|
||||
|
||||
func _graph(L: int, seed_val: int) -> Dictionary:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 # 还原表(同进程后续套件防御)
|
||||
@@ -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"}])
|
||||
|
||||
@@ -40,7 +40,7 @@ func _init() -> void:
|
||||
# 反例=未装配零命中
|
||||
var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222)
|
||||
var ob1b = orch_s.new(db, rng1b)
|
||||
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
|
||||
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
|
||||
"空装配基线复用:未装配件参数读数 0")
|
||||
var hits_none = ob1b.fx_host().engine.dispatch(ob1b.fx_host().counters_ctx, "round_start", {"round_index": 1})
|
||||
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
|
||||
@@ -48,7 +48,7 @@ func _init() -> void:
|
||||
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
|
||||
var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231)
|
||||
var ob2 = orch_s.new(db, rng2)
|
||||
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
|
||||
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["twin_loader"]}),
|
||||
"双管装填:装配起战")
|
||||
Kit.check_quiet(_ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 1})) == 1
|
||||
and _ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 2})) == 1,
|
||||
@@ -59,7 +59,7 @@ func _init() -> void:
|
||||
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
|
||||
var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241)
|
||||
var ob3 = orch_s.new(db, rng3)
|
||||
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}),
|
||||
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["wide_scout_band"]}),
|
||||
"阔视野带:装配起战")
|
||||
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
|
||||
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
|
||||
@@ -69,7 +69,7 @@ func _init() -> void:
|
||||
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒)
|
||||
var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251)
|
||||
var ob4 = orch_s.new(db, rng4)
|
||||
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}),
|
||||
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]}),
|
||||
"试炼镐:装配起战")
|
||||
# 自动兑付:无玩家入口——揭示缓冲点即时计数,满 charge_n 自动
|
||||
# grant_ap(cell_revealed 先于本格扣费→AP 归零恰逢满点时先兑再扣、轮不结束)。直调揭示处理器
|
||||
@@ -88,9 +88,9 @@ func _init() -> void:
|
||||
"试炼镐读数口:兑付后进度归零(8 取余)、阈值=charge_n、C 区浮窗数据源")
|
||||
var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252)
|
||||
var ob4b = orch_s.new(db, rng4b)
|
||||
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
|
||||
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
|
||||
ob4b._on_cell_revealed({"kind": "safe", "cell": Vector2i(0, 0)})
|
||||
var ap_base := int(db.action_economy.ap_per_round)
|
||||
var ap_base := int(db.run.action_economy.ap_per_round)
|
||||
Kit.check_quiet(int(ob4b.action_economy.ap) == ap_base and not bool(ob4b.attune_convert_state().get("assembled", true)),
|
||||
"试炼镐反例:未装配零兑付零计数(AP=整轮表值、读数口 assembled=false)")
|
||||
|
||||
@@ -98,7 +98,7 @@ func _init() -> void:
|
||||
# 展示行=C 区悬停浮窗进度唯一承载:handle=-1 非库存句柄+note 进度;空骑库存桩直构投影链)
|
||||
var rng4c = load("res://core/rng_service.gd").new(); rng4c.set_master_seed(253)
|
||||
var ob4c = orch_s.new(db, rng4c)
|
||||
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]})
|
||||
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]})
|
||||
var ctrl4 = load("res://presentation/treasure_interact_controller.gd").new({
|
||||
"db": db, "meta": null, "info_panel": null, "toast": null, "board_view": null,
|
||||
"settlement_get": func(): return ob4c, "runner_get": func(): return StubAutoInvRunner.new(),
|
||||
@@ -126,19 +126,19 @@ func _init() -> void:
|
||||
|
||||
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
|
||||
var ae6 = load("res://core/action_economy.gd").new(db)
|
||||
ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
|
||||
ae6.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
|
||||
ae6.notify_round_start()
|
||||
ae6.request_stop()
|
||||
ae6.finish_round_stop(false, 100)
|
||||
Kit.check_quiet(ae6.rap == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
|
||||
var ae6b = load("res://core/action_economy.gd").new(db)
|
||||
ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round))
|
||||
ae6b.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round))
|
||||
ae6b.notify_round_start()
|
||||
ae6b.request_stop()
|
||||
ae6b.finish_round_stop(false, 125)
|
||||
Kit.check_quiet(ae6b.rap == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
|
||||
var ae6c = load("res://core/action_economy.gd").new(db)
|
||||
ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round))
|
||||
ae6c.queue_ap_delta(3 - int(db.run.action_economy.ap_per_round))
|
||||
ae6c.notify_round_start()
|
||||
ae6c.request_stop()
|
||||
ae6c.finish_round_stop(false, 125)
|
||||
@@ -147,7 +147,7 @@ func _init() -> void:
|
||||
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
|
||||
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
|
||||
var ob7 = orch_s.new(db, rng7)
|
||||
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}),
|
||||
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["nest_forecast"]}),
|
||||
"巢穴预告:装配起战(board_installed 发射)")
|
||||
var total7: int = ob7.minesweeper.mine_total()
|
||||
Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7,
|
||||
@@ -158,7 +158,7 @@ func _init() -> void:
|
||||
# ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量)
|
||||
var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281)
|
||||
var ob8 = orch_s.new(db, rng8)
|
||||
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}),
|
||||
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["incense_stick"]}),
|
||||
"线香:装配起战(battle 级充能 1)")
|
||||
var inv8 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
@@ -177,7 +177,7 @@ func _init() -> void:
|
||||
# ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实)
|
||||
var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291)
|
||||
var ob9 = orch_s.new(db, rng9)
|
||||
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}),
|
||||
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["ranged_pick"]}),
|
||||
"远程镐:装配起战(dig_range_bonus 注入)")
|
||||
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
|
||||
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
|
||||
@@ -226,17 +226,17 @@ func _init() -> void:
|
||||
|
||||
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
|
||||
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free),
|
||||
"刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free))
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
|
||||
"刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free))
|
||||
lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(),
|
||||
{"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
|
||||
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2,
|
||||
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
|
||||
|
||||
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/封印格跳过)
|
||||
var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301)
|
||||
var ob11 = orch_s.new(db, rng11)
|
||||
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}),
|
||||
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["sky_eye"]}),
|
||||
"天眼:装配起战(level 级充能 1)")
|
||||
var inv11 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
|
||||
@@ -265,7 +265,7 @@ func _init() -> void:
|
||||
# ⑫ 信标网络(封印不限相邻/封印价照付)
|
||||
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
|
||||
var ob12 = orch_s.new(db, rng12)
|
||||
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}),
|
||||
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["beacon_network"]}),
|
||||
"信标网络:装配起战(seal_range_free 注入)")
|
||||
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
|
||||
if far12 != Vector2i(-9, -9):
|
||||
@@ -277,7 +277,7 @@ func _init() -> void:
|
||||
"信标网络正常:封印可达豁免在册(封印仍按标准动作价计费=try_spend 照走)")
|
||||
var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312)
|
||||
var ob12b = orch_s.new(db, rng12b)
|
||||
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
|
||||
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
|
||||
Kit.check_quiet(ob12b.minesweeper.seal_range_free == false,
|
||||
"信标网络反例:未装配封印可达豁免=缺省 false(零装配零扰动)")
|
||||
|
||||
@@ -285,7 +285,7 @@ func _init() -> void:
|
||||
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
|
||||
var ob13 = orch_s.new(db, rng13)
|
||||
var lg13 = load("res://core/gain_journal.gd").new()
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
|
||||
"计数链:装配起战(GainJournal 携带)")
|
||||
var inv13 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
|
||||
@@ -24,7 +24,7 @@ func _init() -> void:
|
||||
|
||||
func _trigger_oob() -> void:
|
||||
# 越域请求(关卡序号 0 < 合法域 ≥1)——_fatal 内部 push fatal 诊断并 quit(1)(帧末退出)
|
||||
var got: Dictionary = _db.resolve_board_gen(0, 0)
|
||||
var got: Dictionary = _db.combat.board_gen.resolve_row(0, 0)
|
||||
if _db.diagnostics_count("fatal") >= 1:
|
||||
print("[fatal_oob_test] fatal 通道已触发(诊断在案)")
|
||||
quit(1) # 与 _fatal 的 quit(1) 同码双保险
|
||||
|
||||
@@ -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 单点契约的登记面)----
|
||||
|
||||
@@ -26,7 +26,7 @@ func _init() -> void:
|
||||
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
|
||||
|
||||
func _test_production_rows(db) -> void:
|
||||
var rows: Array = db.forge_routes()
|
||||
var rows: Array = db.crossrun.forge.route_rows()
|
||||
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
|
||||
var ids := []
|
||||
for row in rows:
|
||||
@@ -52,39 +52,44 @@ func _test_production_rows(db) -> void:
|
||||
initial.sort()
|
||||
Kit.check("初始持有恰 sword+light〔锻造工坊批 V1〕", initial == ["light", "sword"])
|
||||
# 默认两轴(契约 2 所有权移交——裸体态 normal/light)
|
||||
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.forge_default_attack_type() == "normal")
|
||||
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.forge_default_armor_type() == "light")
|
||||
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_attack_type == "normal")
|
||||
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_armor_type == "light")
|
||||
# 类型轴分族与并联双通道前置(4b-1 裁)
|
||||
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
|
||||
String(db.forge_route("spear").get("prereq_route", "")) == "sword"
|
||||
and String(db.forge_route("staff").get("prereq_route", "")) == "sword")
|
||||
String(db.crossrun.forge.route("spear").get("prereq_route", "")) == "sword"
|
||||
and String(db.crossrun.forge.route("staff").get("prereq_route", "")) == "sword")
|
||||
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
|
||||
String(db.forge_route("heavy").get("prereq_route", "")) == "light"
|
||||
and String(db.forge_route("ward").get("prereq_route", "")) == "light")
|
||||
String(db.crossrun.forge.route("heavy").get("prereq_route", "")) == "light"
|
||||
and String(db.crossrun.forge.route("ward").get("prereq_route", "")) == "light")
|
||||
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
|
||||
int(db.forge_route("spear").get("unlock_price", 0)) == 200
|
||||
and int(db.forge_route("staff").get("unlock_price", 0)) == 200
|
||||
and int(db.forge_route("heavy").get("unlock_price", 0)) == 200
|
||||
and int(db.forge_route("ward").get("unlock_price", 0)) == 200)
|
||||
int(db.crossrun.forge.route("spear").get("unlock_price", 0)) == 200
|
||||
and int(db.crossrun.forge.route("staff").get("unlock_price", 0)) == 200
|
||||
and int(db.crossrun.forge.route("heavy").get("unlock_price", 0)) == 200
|
||||
and int(db.crossrun.forge.route("ward").get("unlock_price", 0)) == 200)
|
||||
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
|
||||
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
|
||||
int(db.forge_route("light").get("base_ac", 0)) == 10
|
||||
and int(db.forge_route("heavy").get("base_ac", 0)) == 12
|
||||
and int(db.forge_route("ward").get("base_ac", 0)) == 9)
|
||||
int(db.crossrun.forge.route("light").get("base_ac", 0)) == 10
|
||||
and int(db.crossrun.forge.route("heavy").get("base_ac", 0)) == 12
|
||||
and int(db.crossrun.forge.route("ward").get("base_ac", 0)) == 9)
|
||||
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
|
||||
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
|
||||
String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward")
|
||||
Kit.check("forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕",
|
||||
String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear")
|
||||
String(db.crossrun.forge.armor_route_by_type("ward").get("route_id", "")) == "ward")
|
||||
var wpn_hit := {}
|
||||
for r in db.crossrun.forge.route_rows():
|
||||
if String(r.get("kind", "")) == "weapon" and String(r.get("type_axis", "")) == "pierce":
|
||||
wpn_hit = r
|
||||
break
|
||||
Kit.check("forge route_rows 按轴反查武器路线(pierce)=长枪锻造〔锻造工坊批 V1——访问器退役改直扫同判据〕",
|
||||
String(wpn_hit.get("route_id", "")) == "spear")
|
||||
# 缺行/未知轴 = 空字典合法态
|
||||
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
|
||||
db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty())
|
||||
db.crossrun.forge.route("blade").is_empty() and db.crossrun.forge.armor_route_by_type("plate").is_empty())
|
||||
|
||||
|
||||
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
|
||||
|
||||
func _test_price_ladder(db) -> void:
|
||||
var row: Dictionary = db.forge_route("sword")
|
||||
var row: Dictionary = db.crossrun.forge.route("sword")
|
||||
var prices := []
|
||||
for owned in range(5):
|
||||
prices.append(ForgeTableScript.level_price(row, owned))
|
||||
|
||||
@@ -41,9 +41,9 @@ func _table(rows: Array) -> Variant:
|
||||
# ---- 生产三行正向(V2)----
|
||||
|
||||
func _test_production(db) -> void:
|
||||
var r1: Dictionary = db.level_gen_params_row(1)
|
||||
var r2: Dictionary = db.level_gen_params_row(2)
|
||||
var r3: Dictionary = db.level_gen_params_row(3)
|
||||
var r1: Dictionary = db.run.level_gen_params.row_by_level_index(1)
|
||||
var r2: Dictionary = db.run.level_gen_params.row_by_level_index(2)
|
||||
var r3: Dictionary = db.run.level_gen_params.row_by_level_index(3)
|
||||
Kit.check("访问器读回 L1 行五旋钮齐备〔fv2〕",
|
||||
int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3)
|
||||
and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1)
|
||||
@@ -52,7 +52,7 @@ func _test_production(db) -> void:
|
||||
Kit.check("三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕",
|
||||
int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2
|
||||
and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8)
|
||||
Kit.check("生产表 validate 零错误〔fv2〕", db.level_gen_params.validate().is_empty())
|
||||
Kit.check("生产表 validate 零错误〔fv2〕", db.run.level_gen_params.validate().is_empty())
|
||||
|
||||
|
||||
# ---- 访问器缺行防御 ----
|
||||
|
||||
@@ -75,7 +75,7 @@ static func _errs_has_cross_field(errs: Array) -> bool:
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
var best := -1
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != level_index:
|
||||
continue
|
||||
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
|
||||
@@ -92,28 +92,28 @@ func _smoke_adventure_table() -> void:
|
||||
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null)
|
||||
if _db.adventure == null:
|
||||
return
|
||||
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.adventure.validate().is_empty())
|
||||
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.adventure.clear_heal_pct),
|
||||
int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100)
|
||||
for tier in _db.adventure.TIERS:
|
||||
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])],
|
||||
int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]))
|
||||
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.adventure.deck_total()) == 102)
|
||||
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty())
|
||||
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct),
|
||||
int(_db.run.adventure.clear_heal_pct) >= 0 and int(_db.run.adventure.clear_heal_pct) <= 100)
|
||||
for tier in _db.run.adventure.TIERS:
|
||||
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.run.adventure.TIER_QUOTAS[tier])],
|
||||
int(_db.run.adventure.tier_total(tier)) == int(_db.run.adventure.TIER_QUOTAS[tier]))
|
||||
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.run.adventure.deck_total()) == 102)
|
||||
Kit.check("数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2",
|
||||
int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
|
||||
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
|
||||
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
|
||||
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2)
|
||||
int(_db.run.adventure.curio_value("gp", "common")) == 20 and int(_db.run.adventure.curio_value("gp", "excellent")) == 50
|
||||
and int(_db.run.adventure.curio_value("attack", "common")) == 1 and int(_db.run.adventure.curio_value("attack", "excellent")) == 2
|
||||
and int(_db.run.adventure.curio_value("max_hp", "common")) == 5 and int(_db.run.adventure.curio_value("max_hp", "excellent")) == 10
|
||||
and int(_db.run.adventure.curio_value("armor", "common")) == 1 and int(_db.run.adventure.curio_value("armor", "excellent")) == 2)
|
||||
Kit.check("恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕",
|
||||
int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1)
|
||||
int(_db.run.adventure.curio_value("heal", "excellent")) == 6 and int(_db.run.adventure.curio_value("ap", "rare")) == 1)
|
||||
Kit.check("cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕",
|
||||
int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99)
|
||||
int(_db.run.adventure.curio_cap("gp")) == 99 and int(_db.run.adventure.curio_cap("attack")) == 99)
|
||||
Kit.check("同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕",
|
||||
Attunement.slots(_db, null) == 0)
|
||||
Kit.check("同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕",
|
||||
Attunement.slots(_db, _TreeSaveMock.new()) == 1)
|
||||
# 同调目录 12 件(V1/V2 断言落点;装配→战斗贯通与空装配基线的现行主语 = morning_order)
|
||||
var trows: Array = _db.treasure_attunement_rows()
|
||||
var trows: Array = _db.crossrun.treasure_items.attunement_rows()
|
||||
var tid_set := {}
|
||||
for r in trows:
|
||||
tid_set[String(r.get("item_id", ""))] = true
|
||||
@@ -160,11 +160,11 @@ func _smoke_adventure_table() -> void:
|
||||
alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2)
|
||||
var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191))
|
||||
Kit.check("装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕",
|
||||
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]})
|
||||
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "attunement": ["morning_order"]})
|
||||
and orch_l.attune_has("morning_order"))
|
||||
var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192))
|
||||
Kit.check("空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕",
|
||||
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
|
||||
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
|
||||
and not orch_e.attune_has("morning_order"))
|
||||
# V9 静态词表断言(棋盘事实不可变契约 3:12 行 triggers do.kind 与 effect_kinds
|
||||
# 全集零棋盘改写类词〔rewrite_board/remove_mines/cell_removal=改写族——现表零引用〕;
|
||||
@@ -223,14 +223,14 @@ func _smoke_enemy_bands() -> void:
|
||||
"3|boss|9|duergar_king", "3|boss|11|duergar_king", "3|boss|12|duergar_king", "3|boss|13|duergar_king",
|
||||
]
|
||||
var got_keys: Array[String] = []
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
|
||||
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
|
||||
Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕",
|
||||
_db.enemy.rows.size() == 28 and got_keys == expect_keys)
|
||||
_db.combat.enemy.rows.size() == 28 and got_keys == expect_keys)
|
||||
# ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行)
|
||||
var hob := {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if String(row.get("enemy_id", "")) == "hobgoblin":
|
||||
hob = row
|
||||
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕",
|
||||
@@ -242,7 +242,7 @@ func _smoke_enemy_bands() -> void:
|
||||
# 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行——
|
||||
# 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕)
|
||||
var boss_bands := {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if String(row.get("row_kind", "")) == "boss":
|
||||
var lk: int = int(row.get("level_index", -1))
|
||||
if not boss_bands.has(lk):
|
||||
@@ -269,47 +269,47 @@ func _smoke_enemy_bands() -> void:
|
||||
func _smoke_ac_swap_guard() -> void:
|
||||
print("--- 护甲等级掷点批·换装护栏 ---")
|
||||
# ① 三处无双轨
|
||||
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.status_effect_row("defense_up").is_empty())
|
||||
var ac_row: Dictionary = _db.status_effect_row("ac_up")
|
||||
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.combat.status_effects.row_by_id("defense_up").is_empty())
|
||||
var ac_row: Dictionary = _db.combat.status_effects.row_by_id("ac_up")
|
||||
Kit.check("ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)",
|
||||
not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \
|
||||
and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \
|
||||
and String(ac_row.get("stack_policy", "")) == "refresh")
|
||||
var ac_reward := {}
|
||||
for row in _db.reward.rows:
|
||||
for row in _db.run.reward.rows:
|
||||
if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive":
|
||||
ac_reward = row
|
||||
Kit.check("奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)",
|
||||
String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护")
|
||||
Kit.check("effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)",
|
||||
not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up"))
|
||||
not _db.combat.settlement.core.effect_pool_map.has("defense_up") and not _db.combat.settlement.core.effect_pool_map.has("ac_up"))
|
||||
# ② 免伤墙归零护栏(泛化——BUG-0003 免疫)
|
||||
var wall_ok := true
|
||||
var wall_detail := ""
|
||||
for eff_id in _db.settlement.core.effect_pool_map.keys():
|
||||
if String(_db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
|
||||
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
|
||||
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
|
||||
continue
|
||||
var row: Dictionary = _db.status_effect_row(eff_id)
|
||||
var row: Dictionary = _db.combat.status_effects.row_by_id(eff_id)
|
||||
if row.is_empty() or String(row.get("unit", "")) != "percent":
|
||||
continue
|
||||
var floor_v: int = int(row.get("magnitude", 0)) * int(row.get("max_stack", 1))
|
||||
if floor_v <= int(_db.settlement.core.pool_clamp_lo):
|
||||
if floor_v <= int(_db.combat.settlement.core.pool_clamp_lo):
|
||||
wall_ok = false
|
||||
wall_detail += " %s(%d)" % [eff_id, floor_v]
|
||||
Kit.check("hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
|
||||
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)], wall_ok)
|
||||
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.combat.settlement.core.pool_clamp_lo)], wall_ok)
|
||||
var hd_maps := 0
|
||||
for eff_id in _db.settlement.core.effect_pool_map.keys():
|
||||
if String(_db.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
|
||||
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
|
||||
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
|
||||
hd_maps += 1
|
||||
Kit.check("hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)", hd_maps == 0)
|
||||
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
|
||||
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class),
|
||||
int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30)
|
||||
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.combat.hero.armor_class),
|
||||
int(_db.combat.hero.armor_class) >= 0 and int(_db.combat.hero.armor_class) <= 30)
|
||||
var ac_band := true
|
||||
var ac_detail := ""
|
||||
var heavy_names := ["熊地精重击手", "灰矮人重装手"] # 逐行手设值 8/9,非精英编码位
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var ac: int = int(row.get("armor_class", -1))
|
||||
var dname := String(row.get("display_name", "?"))
|
||||
var boss_row: bool = String(row.get("enemy_id", "")).ends_with("_chief") or \
|
||||
@@ -325,7 +325,7 @@ func _smoke_ac_swap_guard() -> void:
|
||||
ac_detail if ac_detail != "" else "无"], ac_band)
|
||||
# ③b Boss 台阶断言(关卡即台阶——L1 8 < L2 10 < L3 12)+ 首遇位教学位低 AC(≤3)
|
||||
var boss_ac := {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
var eid := String(row.get("enemy_id", ""))
|
||||
if eid == "ogre_chief" or eid == "drider" or eid == "duergar_king":
|
||||
boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0))
|
||||
@@ -337,7 +337,7 @@ func _smoke_ac_swap_guard() -> void:
|
||||
# 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带)
|
||||
var l12_band := _first_encounter_band(1, "bugbear")
|
||||
var l12_row: Dictionary = {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
|
||||
and int(row.get("depth_band", -1)) == l12_band and String(row.get("enemy_id", "")) == "bugbear":
|
||||
l12_row = row
|
||||
@@ -350,7 +350,7 @@ func _smoke_ac_swap_guard() -> void:
|
||||
and int(l12_row.get("armor_class", -1)) == 2 \
|
||||
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2})
|
||||
var l1_totals := {}
|
||||
for row in _db.enemy.rows:
|
||||
for row in _db.combat.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != 1:
|
||||
continue
|
||||
var cnt_lo: int = int(row.get("unit_count", 1))
|
||||
@@ -370,12 +370,12 @@ func _smoke_ac_swap_guard() -> void:
|
||||
Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [
|
||||
curve_detail if curve_detail != "" else "无"], curve_ok)
|
||||
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
|
||||
var bounds: Array = _db.settlement.damage_roll.roll_tier_bounds
|
||||
var discounts: Array = _db.settlement.damage_roll.roll_tier_discounts
|
||||
var bounds: Array = _db.combat.settlement.damage_roll.roll_tier_bounds
|
||||
var discounts: Array = _db.combat.settlement.damage_roll.roll_tier_discounts
|
||||
Kit.check("掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)",
|
||||
int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \
|
||||
and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \
|
||||
and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll")
|
||||
int(_db.combat.settlement.damage_roll.roll_faces) == 20 and int(_db.combat.settlement.damage_roll.crit_roll) == 20 \
|
||||
and int(_db.combat.settlement.damage_roll.crit_multiplier) == 200 and int(_db.combat.settlement.damage_roll.damage_floor) == 1 \
|
||||
and int(_db.combat.settlement.damage_roll.attack_bonus) == 0 and String(_db.combat.settlement.damage_roll.armor_roll_stream) == "armor_roll")
|
||||
Kit.check("档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)",
|
||||
bounds == [3, 7] and discounts == [75, 50, 25])
|
||||
|
||||
@@ -385,24 +385,24 @@ func _smoke_ac_swap_guard() -> void:
|
||||
func _smoke_variation_framework() -> void:
|
||||
print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---")
|
||||
Kit.check("变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)",
|
||||
_db.variation.rows.size() == 6)
|
||||
_db.combat.variation.rows.size() == 6)
|
||||
Kit.check("变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)",
|
||||
_db.variation.validate().is_empty())
|
||||
_db.combat.variation.validate().is_empty())
|
||||
var pool1_ids: Array = []
|
||||
for r in _db.variant_pool(1, "dungeon"):
|
||||
for r in _db.combat.variation.variant_pool(1, "dungeon"):
|
||||
pool1_ids.append(String(r.variant_id))
|
||||
Kit.check("L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)", pool1_ids == ["thorn", "vine"])
|
||||
var pool2_ids: Array = []
|
||||
for r in _db.variant_pool(2, "underdark"):
|
||||
for r in _db.combat.variation.variant_pool(2, "underdark"):
|
||||
pool2_ids.append(String(r.variant_id))
|
||||
Kit.check("L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)", pool2_ids == ["magma", "ash"])
|
||||
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.variant_pool(1, "underdark").is_empty())
|
||||
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.combat.variation.variant_pool(1, "underdark").is_empty())
|
||||
var pool2d_ids: Array = []
|
||||
for r in _db.variant_pool(2, "dungeon"):
|
||||
for r in _db.combat.variation.variant_pool(2, "dungeon"):
|
||||
pool2d_ids.append(String(r.variant_id))
|
||||
Kit.check("board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)",
|
||||
pool2d_ids == ["thorn", "vine"])
|
||||
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.variation.row_by_id("nonexistent").is_empty())
|
||||
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.combat.variation.row_by_id("nonexistent").is_empty())
|
||||
# 跨字段互锁负样本(加载期致命——枚举语义一致性闸)
|
||||
var vt = load("res://data/schemas/variation_table.gd").new()
|
||||
vt.rows.assign([{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
|
||||
@@ -419,30 +419,30 @@ func _smoke_variation_framework() -> void:
|
||||
|
||||
func _smoke_difficulty_table() -> void:
|
||||
print("--- 难度曲线与Boss·DifficultyTable 结构位 ---")
|
||||
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.difficulty.rows.is_empty())
|
||||
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty())
|
||||
# ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样
|
||||
var enemy_system = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("spawn 受理", enemy_system.spawn(1, 0, int(_db.hero.max_hp)))
|
||||
Kit.check("spawn 受理", enemy_system.spawn(1, 0, int(_db.combat.hero.max_hp)))
|
||||
var unit = enemy_system.all_enemies()[0]
|
||||
var d0: Dictionary = _db.resolve_enemy_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
|
||||
var d0: Dictionary = _db.combat.enemy.resolve_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
|
||||
Kit.check("空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)],
|
||||
int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk))
|
||||
# ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk
|
||||
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
|
||||
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
|
||||
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn(1, 0, int(_db.hero.max_hp)))
|
||||
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn(1, 0, int(_db.combat.hero.max_hp)))
|
||||
var g2 = enemy_system_b.all_enemies()[0]
|
||||
Kit.check("hp 修饰 ×1.5(%d→%d,max_hp 同步)" % [int(d0.hp), int(g2.hp)],
|
||||
int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp))
|
||||
Kit.check("atk 修饰 ×2.0(%d→%d)" % [int(d0.atk), int(g2.atk)], int(g2.atk) == int(d0.atk) * 2)
|
||||
# ③ 越域 clamp:乘数 50 → clamp 10
|
||||
_db.difficulty.rows.clear()
|
||||
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
|
||||
_db.run.difficulty.rows.clear()
|
||||
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
|
||||
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
|
||||
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn(1, 0, int(_db.hero.max_hp)))
|
||||
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn(1, 0, int(_db.combat.hero.max_hp)))
|
||||
Kit.check("越域乘数 clamp 至 10(%d→%d)" % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)],
|
||||
int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10)
|
||||
_db.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
|
||||
_db.run.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
|
||||
|
||||
|
||||
# ---- ⑤ 格面资源九型 + gp_drop 流(黄金向量)----
|
||||
@@ -455,7 +455,7 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
var pos_weight := 0
|
||||
var gp_pos := -1
|
||||
var gp_neg := -1
|
||||
for row in _db.reward.rows:
|
||||
for row in _db.run.reward.rows:
|
||||
if String(row.get("pool_tag", "")) == "cell":
|
||||
cell_rows += 1
|
||||
if String(row.get("polarity", "")) == "positive":
|
||||
@@ -468,7 +468,7 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
cell_rows == 18)
|
||||
# 危险格负向直伤当量(负向≈正向 2 倍定价——真人反馈「三雷 3/4 血无后果感」的投值口径)
|
||||
var mine_power := {"heal_backlash": 6, "pierce_snare": 8, "magic_recoil": 8} # 危险格负向直伤当量(抬档值;依据见 numfix 报告)
|
||||
for row in _db.reward.rows:
|
||||
for row in _db.run.reward.rows:
|
||||
var rid := String(row.get("reward_id", ""))
|
||||
if mine_power.has(rid):
|
||||
Kit.check("危险格负向直伤 %s power=%d(卡⑧投值)" % [rid, int(mine_power[rid])],
|
||||
@@ -477,7 +477,7 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
# 合计即配置自身的和(改表自动跟随,不再硬编码合计值)。
|
||||
var pos_all_in_pool := true
|
||||
var neg_all_zero := true
|
||||
for row in _db.reward.rows:
|
||||
for row in _db.run.reward.rows:
|
||||
if String(row.get("pool_tag", "")) != "cell":
|
||||
continue
|
||||
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", 0)) < 1:
|
||||
@@ -506,11 +506,11 @@ func _smoke_meta() -> void:
|
||||
and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2)
|
||||
var attrs_ok := true
|
||||
for a in ["max_hp", "atk", "armor_class"]:
|
||||
attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty()
|
||||
attrs_ok = attrs_ok and not _conversion_row_of(_db.crossrun.meta.conversion, a).is_empty()
|
||||
Kit.check("conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)", attrs_ok)
|
||||
var boss_ok := true
|
||||
for li in [1, 2, 3]:
|
||||
var brow: Dictionary = _db.meta_boss_keystone_row(li)
|
||||
var brow: Dictionary = _db.crossrun.meta.boss_keystone_row(li)
|
||||
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
|
||||
Kit.check("boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)", boss_ok)
|
||||
|
||||
@@ -519,3 +519,10 @@ class _TreeSaveMock:
|
||||
# 核代码增补:最小 save 形态——attunement.slots 只读 tree_level("att_socketN")
|
||||
func tree_level(n: String) -> int:
|
||||
return 1 if n == "att_socket1" else 0
|
||||
|
||||
|
||||
func _conversion_row_of(rows: Array, attr: String) -> Dictionary:
|
||||
for r in rows:
|
||||
if String(r.get("attr", "")) == attr:
|
||||
return r
|
||||
return {}
|
||||
@@ -64,7 +64,7 @@ func _test_boss_name_uses_boss_rowset() -> void:
|
||||
if not loaded:
|
||||
return
|
||||
var boss_row := {}
|
||||
for row in db.enemy.rows:
|
||||
for row in db.combat.enemy.rows:
|
||||
if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss":
|
||||
boss_row = row
|
||||
break
|
||||
@@ -72,8 +72,8 @@ func _test_boss_name_uses_boss_rowset() -> void:
|
||||
if boss_row.is_empty():
|
||||
return
|
||||
var boss_depth: int = int(boss_row.depth_band)
|
||||
var boss_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "boss")
|
||||
var battle_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "battle")
|
||||
var boss_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "boss")
|
||||
var battle_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "battle")
|
||||
var boss_expect := String(boss_rows[0].get("display_name", ""))
|
||||
var battle_name := String(battle_rows[0].get("display_name", ""))
|
||||
var runner := _BossRunner.new()
|
||||
|
||||
@@ -96,7 +96,7 @@ func _sweep_source_names() -> void:
|
||||
row_view._tip_label.text.contains("来源:危险格"))
|
||||
row_view._hide_tip()
|
||||
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
|
||||
String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
|
||||
String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
|
||||
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db)
|
||||
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
|
||||
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))
|
||||
|
||||
@@ -27,7 +27,7 @@ func _init() -> void:
|
||||
return
|
||||
# board_type 词表自 board_gen 行集推导(禁测试侧写死词表)
|
||||
var types: Array = []
|
||||
for row in db.board_gen.rows:
|
||||
for row in db.combat.board_gen.rows:
|
||||
var bt := String(row.get("board_type", ""))
|
||||
if bt != "" and not types.has(bt):
|
||||
types.append(bt)
|
||||
|
||||
@@ -839,11 +839,11 @@ func _career_overlay_projection() -> void:
|
||||
sv.write_to_disk()
|
||||
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
|
||||
var data := {"total": sv.meta_state.career_state().adventure_total, "cleared": sv.meta_state.career_state().adventure_cleared,
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
|
||||
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.meta_state.keystone_balance()}
|
||||
for i in _db.level_count():
|
||||
for i in _db.run.level.level_count():
|
||||
var li: int = i + 1
|
||||
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
|
||||
data["clears"].append({"display_name": String(_db.run.level.row_by_level_index(li).get("display_name", "")),
|
||||
"count": sv.meta_state.career_state().clear_counts.get(str(li), 0)})
|
||||
if sv.meta_state.career_state().boss_first_kill.has(str(li)):
|
||||
data["boss_first_kills"] += 1
|
||||
@@ -884,10 +884,10 @@ func _career_overlay_projection() -> void:
|
||||
v5.load_from_disk() and v5.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0)
|
||||
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
|
||||
var data_v5 := {"total": v5.meta_state.career_state().adventure_total, "cleared": v5.meta_state.career_state().adventure_cleared,
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
|
||||
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.meta_state.keystone_balance()}
|
||||
for i in _db.level_count():
|
||||
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
|
||||
for i in _db.run.level.level_count():
|
||||
data_v5["clears"].append({"display_name": String(_db.run.level.row_by_level_index(i + 1).get("display_name", "")),
|
||||
"count": v5.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
|
||||
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
|
||||
co_v5.open_career(data_v5)
|
||||
|
||||
Reference in new issue
Block a user