卡①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=无条件技)