卡①T9(核心数据面结构收敛批): 散条组——K5 action_economy(删_next_round_mode死存储+_ap_exhausted_last派生合并(ap_exhausted_last_round改_last_round_end.path_tag派生读,同源同刻等值)+last_round_end属性→方法,消费方minesweeper_rules:495改址)/reward_economy roll_board规范化域提取(_normalize_eligible开流前零RNG)+_skewed_cell_rows两域(_skew_families分族+_apply_skew权重装配,守恒assert随段迁移)/minesweeper_rules _do_dig拆_reveal_chain(cell_revealed发射序即链序整体随段搬迁)/K6′ unit_container spawn签名束化(10→1 req Dictionary,束键封闭词表缺省逐字)+体内拆_spawn_hero_unit/_spawn_rowset/_row_instance_counts+调用点24处9文件改束形(orchestrator+测试8,断言标签字面量零触碰);gdstyle 92→83键NEW=0

This commit is contained in:
DKChan committed 2026-10-28 08:46:22 +08:00
1 parent 2bd2625194
commit 070a5340b3
13 files changed
+136 -88

No files matched your search

+9 -8
View File
@@ -30,10 +30,7 @@ var _rap := 0
var rap: int:
get: return _rap
var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var last_round_end: Dictionary[Variant, Variant]:
get: return _last_round_end
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态:转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、
# 深挖对价=dig_paid counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本
@@ -44,7 +41,6 @@ var charge_layers: int:
var _fx: EffectEngine = null
var _ctx: EffectContext = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
var _ap_exhausted_last := false # 上轮 AP 耗尽封印(一滴不剩 round_start when 面)
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
@@ -216,7 +212,6 @@ func notify_settlement_done(next_round_mode: String) -> bool:
_reject("phase")
return false
_settlement_consumed = true
_next_round_mode = next_round_mode
return true
@@ -272,13 +267,19 @@ func _finish_round(path_tag: String, continuation_hint: String) -> void:
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
_ap_exhausted_last = path_tag == "ap_exhausted" # 一滴不剩 when 面(轮始发射读数)
_phase = Phase.WAITING
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)
# 上轮组包读数({path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源;
# 属性→方法 K5 换形,消费方 minesweeper_rules 单点改址)
func last_round_end() -> Dictionary[String, String]:
return _last_round_end
# 上轮 AP 是否耗尽(round_start 发射 payload 读数面;轮始兑现后保持到本轮末)——
# 派生读(同源同刻:_last_round_end 组包点即派生原点,K5 派生合并——存储成员退役)
func ap_exhausted_last_round() -> bool:
return _ap_exhausted_last
return _last_round_end.get("path_tag", "") == "ap_exhausted"
func _reject(source: String) -> void:
+1 -3
View File
@@ -302,9 +302,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
return false
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_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)
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
_fatal("敌人系统 spawn 被拒")
return false
return true
+13 -9
View File
@@ -304,15 +304,22 @@ func _do_dig(c: Vector2i) -> Dictionary:
if was_sealed:
_seals.erase(c)
var reveal_order: Array[Variant] = _chain_reveal_order(c, eff_seals)
_reveal_chain(c, reveal_order, was_sealed)
if action_economy.phase != action_economy.PHASE_ACTIVE:
_emit_round_end_seals_and_payload()
return _outcome(true, "executed", "", "")
## 连锁揭示域:首格+连锁新格揭示(BFS 规范序——cell_revealed 发射序即链序,整体随段搬迁)。
## 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带,
## 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
## dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印)
func _reveal_chain(c: Vector2i, reveal_order: Array, was_sealed: bool) -> void:
for rc in reveal_order:
if _revealed.has(rc):
continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|)
var is_mine := _board.is_mine(rc)
var channel := "dig" if rc == c else "chain"
# 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带,
# 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
# dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印)
if is_mine:
if _board.is_mine(rc):
_revealed[rc] = {"kind": "mine", "digit": -1}
cell_revealed.emit({
"cell": rc, "kind": "mine", "digit": -1,
@@ -326,9 +333,6 @@ func _do_dig(c: Vector2i) -> Dictionary:
"variant_ids": _board.variant_ids(rc), "channel": channel, "resource": true,
"dug_under_seal": was_sealed and rc == c,
})
if action_economy.phase != action_economy.PHASE_ACTIVE:
_emit_round_end_seals_and_payload()
return _outcome(true, "executed", "", "")
func _board_face_dig(c: Vector2i) -> String:
@@ -492,7 +496,7 @@ func _emit_seals_batch() -> void:
func _emit_round_end_seals_and_payload() -> void:
_emit_seals_batch()
var payload: Dictionary[Variant, Variant] = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约)
var payload: Dictionary[Variant, Variant] = action_economy.last_round_end() # 逐字段取自 行动经济(读取权 = 接口契约;属性→方法 K5)
round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")})
+36 -20
View File
@@ -40,18 +40,8 @@ func roll_board(eligible: Array) -> bool:
if eligible.is_empty():
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
return false
# 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
var seen := {}
var list: Array[Dictionary] = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c})
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
_eligible = list
_eligible = _normalize_eligible(eligible)
var list: Array[Dictionary] = _eligible
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
_power_roll.clear()
_clear_power = -1
@@ -85,6 +75,21 @@ func roll_board(eligible: Array) -> bool:
return true
## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
var seen := {}
var list: Array[Dictionary] = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c})
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
return list
# ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)----
# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段)
@@ -217,6 +222,20 @@ func _skewed_cell_rows() -> Array:
if board_skew <= 0:
return _rollable_rows()
var base: Array[Variant] = _rollable_rows()
var families := _skew_families(base)
if families["down"].is_empty() or families["up"].is_empty():
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200)
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops))
var out := _apply_skew(base, families["down"], families["up"], drops, gains)
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
return out
## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰)
func _skew_families(base: Array) -> Dictionary:
var down: Array[Variant] = []
var up: Array[Variant] = []
for row in base:
@@ -225,13 +244,11 @@ func _skewed_cell_rows() -> Array:
down.append(row)
elif SKEW_UP_KINDS.has(k):
up.append(row)
if down.is_empty() or up.is_empty():
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
var v := board_skew
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
var drops := _largest_remainder_split(down, v, 100, (2 * _pool_weight(down) * v + 100) / 200)
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
var gains := _largest_remainder_split(up, drops_total(drops), _pool_weight(up), drops_total(drops))
return {"down": down, "up": up}
## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移)
func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array:
var di := 0
var ui := 0
var out: Array[Variant] = []
@@ -248,7 +265,6 @@ func _skewed_cell_rows() -> Array:
var skewed: Dictionary[Variant, Variant] = row.duplicate()
skewed["weight"] = w
out.append(skewed)
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
return out
+54 -25
View File
@@ -33,42 +33,58 @@ func _init(db_ref: ConfigDb) -> void:
# (下标=行集行序,长度短于行集时缺省 -1 = 该行行值路径;空数组 = 全行定值)。
# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和)
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [],
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [],
hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool:
# spawn 引用束(K6′ 签名束化——10 形参收单 Dictionary 束):束键封闭词表——
# 必载 level_index/node_depth/hero_hp;可选 unit_count_overrides=[]/enemy_atk_overrides=[]/
# hero_atk_override=-1/hero_max_hp_override=-1/hero_attack_type=""/hero_armor_type=""/
# row_kind="battle"(缺省语义与现行形参缺省逐字同值)。
# enemy_atk_override:遭遇骰同流的 spawn 期整战持久 roll 结果
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult)。敌人系统 零随机纪律保持。
# 行集受理:unit_count_overrides / enemy_atk_overrides 为**逐行数组**
# (下标=行集行序,长度短于行集时缺省 -1 = 该行行值路径;空数组 = 全行定值)。
# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和)
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
func spawn(req: Dictionary) -> bool:
if _spawned:
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
return false
# 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序)
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(level_index, node_depth, row_kind)
var rowset: Array[Variant] = db.combat.enemy.resolve_rowset(int(req.level_index), int(req.node_depth), String(req.get("row_kind", "battle")))
if rowset.is_empty():
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind])
push_error("[敌人系统] 遭遇行解析失败/越域 (%s,%s,%s)" % [str(req.get("level_index")), str(req.get("node_depth")), String(req.get("row_kind", "battle"))])
assert(false, "遭遇行悬空(R1 前置守卫)")
return false
if not _spawn_hero_unit(int(req.hero_hp), int(req.get("hero_atk_override", -1)), int(req.get("hero_max_hp_override", -1)), String(req.get("hero_attack_type", "")), String(req.get("hero_armor_type", ""))):
return false
var mod: Dictionary[Variant, Variant] = db.run.difficulty.resolve_modifiers(int(req.level_index), int(req.node_depth))
if not _spawn_rowset(rowset, req.get("unit_count_overrides", []), req.get("enemy_atk_overrides", []), float(mod.get("hp_mult", 1.0)), float(mod.get("atk_mult", 1.0))):
return false
_spawned = true
_despawned = false
return true
## 英雄单位构建域:生效上限与类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light)。
## max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁
## (恢复点/治疗包的满血守卫与治疗上限随之失效)
func _spawn_hero_unit(hero_hp: int, atk_override: int, max_hp_override: int, hero_attack_type: String, hero_armor_type: String) -> bool:
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.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.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))
# 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值;
# 行 i 数量 = overrides[i](≥1 时)否则该行 unit_count 行值)
var per_row_counts: Array[int] = []
_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} # 护甲/攻击类型轴快照(甲效矩阵消费面)
return true
## 行集实例化域(难度曲线修饰施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为〔AC-10〕;
## 混编行集内全部敌种同乘——(L,D) 精确匹配一次取值,F9 同乘不改变异,敌人系统 v0.15)
func _spawn_rowset(rowset: Array, unit_count_overrides: Array, enemy_atk_overrides: Array, hp_mult: float, atk_mult: float) -> bool:
_enemies.clear()
var per_row_counts := _row_instance_counts(rowset, unit_count_overrides)
if per_row_counts.is_empty():
return false
var total_instances := 0
for row_i in range(rowset.size()):
var row_i_dict: Dictionary[Variant, Variant] = rowset[row_i]
var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1
var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count)
if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX:
push_warning("[敌人系统] 行 %d 实例化数量越域(单种数域 [%d,%d]): %d" % [row_i, EnemyTable.UNIT_COUNT_MIN, EnemyTable.UNIT_COUNT_MAX, count])
return false
per_row_counts.append(count)
for count in per_row_counts:
total_instances += count
# 行集实例总数守卫(行集构成纪律③——各行骰后数量求和 ≤4;骰后值加载期不可判,此处 fail-fast)
if total_instances > EnemyTable.ROWSET_INSTANCE_MAX:
@@ -99,11 +115,24 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
"is_hero": false,
})
handle += 1
_spawned = true
_despawned = false
return true
## 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值;
## 行 i 数量 = overrides[i](≥1 时)否则该行 unit_count 行值)。越域行 = 空数组(受理拒绝信号)
func _row_instance_counts(rowset: Array, unit_count_overrides: Array) -> Array[int]:
var per_row_counts: Array[int] = []
for row_i in range(rowset.size()):
var row_i_dict: Dictionary[Variant, Variant] = rowset[row_i]
var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1
var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count)
if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX:
push_warning("[敌人系统] 行 %d 实例化数量越域(单种数域 [%d,%d]): %d" % [row_i, EnemyTable.UNIT_COUNT_MIN, EnemyTable.UNIT_COUNT_MAX, count])
return []
per_row_counts.append(count)
return per_row_counts
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条
# 快照纪律。触发条件:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
@@ -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.combat.hero.max_hp))
var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok}
@@ -343,13 +343,13 @@ 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.combat.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 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.combat.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
enemy_system_b.spawn({"level_index": 1, "node_depth": 2, "hero_hp": 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.combat.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
enemy_system_c.spawn({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": "boss"}) and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
var am: Dictionary = _db.combat.settlement.damage_roll.armor_effectiveness
Kit.check("普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)",
@@ -661,13 +661,13 @@ 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.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
es.spawn({"level_index": 1, "node_depth": int(depth), "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": _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.combat.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
es2.spawn({"level_index": 2, "node_depth": int(depth), "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": _row_kind_of(2, int(depth))})
for u in es2.all_enemies():
if es2.rollable_skills(u).is_empty():
rollable_ok = false
@@ -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.combat.hero.max_hp), [], -1, -1, [], "", "", kind)
var ok: bool = ec.spawn({"level_index": L, "node_depth": D, "hero_hp": int(db.combat.hero.max_hp), "row_kind": 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.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
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.combat.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u2: Dictionary = ec2.all_enemies()[0]
ec.apply_damage(u, 3)
ec2.apply_damage(u2, 3)
@@ -52,7 +52,7 @@ func _init() -> void:
# ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)----
var ec = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn")
Kit.check_quiet(ec.spawn({"level_index": 2, "node_depth": 9, "hero_hp": 30, "hero_atk_override": 8, "hero_max_hp_override": 30, "row_kind": "boss"}), "V2 前置:卓尔 Boss spawn")
var u = ec.all_enemies()[0]
Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)")
var r_full: Array = ec.rollable_skills(u)
@@ -67,7 +67,7 @@ func _init() -> void:
# ---- V3 保底不逼条件不满足技(保底集合取 special_skills 已含排除——编排级:
# 无条件特殊技 A w1+条件假技 B w9999+普攻;保底命中摇出的是 A,撤销过滤态同种子必中 B)----
var ec_v3 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v3.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V3 前置:地精 spawn")
Kit.check_quiet(ec_v3.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V3 前置:地精 spawn")
var g3 = ec_v3.all_enemies()[0]
g3["pool"] = [
{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
@@ -96,7 +96,7 @@ func _init() -> void:
Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"),
"V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕")
var ec8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)")
Kit.check_quiet(ec8.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V4 前置:地精 spawn(hp 基线 8)")
var g = ec8.all_enemies()[0]
g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
{"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0,
@@ -160,7 +160,7 @@ func _init() -> void:
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
var ec_v8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn")
Kit.check_quiet(ec_v8.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V8 前置:地精 spawn")
var g8 = ec_v8.all_enemies()[0]
var has_dg := false
for e in (g8.get("pool", []) as Array):
@@ -176,7 +176,7 @@ func _init() -> void:
# ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)----
var ec_v9 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn")
Kit.check_quiet(ec_v9.spawn({"level_index": 3, "node_depth": 1, "hero_hp": 30, "hero_atk_override": 8, "hero_max_hp_override": 30}), "V9 前置:钩爪恐魔 spawn")
var h9 = ec_v9.all_enemies()[0]
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"),
"V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕")
@@ -189,7 +189,7 @@ func _init() -> void:
# ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)----
var ec_v10 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn")
Kit.check_quiet(ec_v10.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V10 前置:地精 spawn")
var g10 = ec_v10.all_enemies()[0]
var stray := 0
for e in (g10.get("pool", []) as Array):
@@ -264,7 +264,7 @@ func _smoke_table_migration() -> void:
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.combat.hero.max_hp))
var spawned: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕",
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.combat.hero.display_name))
@@ -703,7 +703,7 @@ func _smoke_level_graph() -> void:
_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.combat.hero.max_hp)))
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn({"level_index": 1, "node_depth": 5, "hero_hp": 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")
@@ -711,7 +711,7 @@ func _smoke_level_graph() -> void:
_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.combat.hero.max_hp)))
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn({"level_index": 1, "node_depth": 6, "hero_hp": 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)
@@ -396,7 +396,7 @@ func _smoke_forge_axes() -> void:
_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.combat.hero.max_hp)))
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 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,7 +527,7 @@ 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.combat.hero.max_hp), [], -1, -1, [], "magic", "heavy")
uc2.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_attack_type": "magic", "hero_armor_type": "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——树零攻防来源)
@@ -422,7 +422,7 @@ func _smoke_difficulty_table() -> void:
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.combat.hero.max_hp)))
Kit.check("spawn 受理", enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
var unit = enemy_system.all_enemies()[0]
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)],
@@ -430,7 +430,7 @@ func _smoke_difficulty_table() -> void:
# ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk
_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.combat.hero.max_hp)))
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 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))
@@ -439,7 +439,7 @@ func _smoke_difficulty_table() -> void:
_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.combat.hero.max_hp)))
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 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.run.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)