卡①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:
1 parent
2bd2625194
commit
070a5340b3
13 files changed
+136
-88
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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() # 还原空表(后续冒烟/关卡断言基线)
|
||||
|
||||
Reference in new issue
Block a user