168 lines
8.9 KiB
GDScript
168 lines
8.9 KiB
GDScript
## 棋盘安装与变异布置域件(B1 域·BoardInstaller):生成受理、全局作用域变异决议、装板受理、
|
||
## 资源摇号资格域组装(roll_board 恰一次——install 后、挖掘前)、region 变异决议与连通片布置。
|
||
## 棋盘设置顺序 = 生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置(布置在初始展开与
|
||
## 资源格之后,变异天然不落已开区)。宿主侧装板后状态复位(封印/完美计数清零、板序推进、
|
||
## board_changed 发射、秘宝叠加层退役)留宿主薄壳——本件零复位职责。零 UI 零 await;
|
||
## 随机只经注入 RngServiceCore 的 board_gen/variant_place 两封闭流(流名与开流时点不变,
|
||
## 生成器只消费传入流——同种子同输入逐位复现)。
|
||
class_name BoardInstaller
|
||
extends RefCounted
|
||
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore
|
||
var minesweeper: MinesweeperRules
|
||
var reward_economy: RewardEconomy
|
||
var encounter: Dictionary[String, Variant] # 宿主 encounter 同对象引用(assign 原地变异——跨战活引用)
|
||
var fatal_cb: Callable # 协作件违约→宿主 _fatal 封印(单点接线——BoardGenerator 同款)
|
||
var generator: BoardGenerator # 件内自建自接线(原宿主 _board_generator 成员随件迁移)
|
||
|
||
|
||
func _init(bundle: Dictionary) -> void:
|
||
for k in ["db", "rng", "minesweeper", "reward_economy", "encounter", "fatal_cb"]:
|
||
assert(bundle.has(k), "[装板域件] 引用束缺键: %s" % k)
|
||
db = bundle.db
|
||
rng = bundle.rng
|
||
minesweeper = bundle.minesweeper
|
||
reward_economy = bundle.reward_economy
|
||
encounter = bundle.encounter
|
||
fatal_cb = bundle.fatal_cb
|
||
generator = BoardGenerator.new()
|
||
generator.fatal_handler = fatal_cb # 协作件违约→宿主 _fatal 封印(单点接线)
|
||
|
||
|
||
func _fatal(msg: String) -> void:
|
||
if fatal_cb.is_valid():
|
||
fatal_cb.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
|
||
else:
|
||
push_error(msg)
|
||
assert(false, msg)
|
||
|
||
|
||
# 装板单入口(生成 → 全局变异决议 → 装板受理 → 资源摇号 → 变异布置);
|
||
# 宿主态复位归宿主薄壳(_install_and_reset)——本件返回即装板完成、复位未行。
|
||
func install(during_round := false) -> bool:
|
||
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
|
||
var board: BoardData = generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||
if board == null:
|
||
return false
|
||
# 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant)
|
||
_resolve_global_variant(board)
|
||
var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round)
|
||
if not res.accepted:
|
||
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
|
||
return false
|
||
# 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池)
|
||
var r0 := {}
|
||
for c in board.compute_r0():
|
||
r0[c] = true
|
||
var eligible: Array[Dictionary] = []
|
||
for c in board.safe_cells():
|
||
if not r0.has(c):
|
||
eligible.append({"cell": c})
|
||
for c in board.mines.keys():
|
||
if not r0.has(c):
|
||
eligible.append({"cell": c})
|
||
if not reward_economy.roll_board(eligible):
|
||
_fatal("地图战斗资源 roll_board 被拒")
|
||
return false
|
||
# 地形变异 布置段(棋盘设置顺序第 4 步)——布置在初始展开与资源格之后,变异天然不落已开区
|
||
_place_variants(board)
|
||
return true
|
||
|
||
|
||
# ---- 全局作用域变异 决议(装板序内 roll_board 之前)----
|
||
# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在
|
||
# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。
|
||
# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走
|
||
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
|
||
func _resolve_global_variant(board: BoardData) -> void:
|
||
board.board_variants.clear()
|
||
reward_economy.roller.board_skew = 0
|
||
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
|
||
board.board_variants = [String(row.variant_id)]
|
||
reward_economy.roller.board_skew = int(row.get("effect_value", 0))
|
||
|
||
|
||
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
|
||
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
|
||
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
|
||
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
|
||
# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
|
||
# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) +
|
||
# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例,
|
||
# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。
|
||
# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。
|
||
func _place_variants(board: BoardData) -> void:
|
||
if not board.board_variants.is_empty():
|
||
return
|
||
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
|
||
if pool.is_empty():
|
||
return
|
||
var r0 := {}
|
||
for c in board.compute_r0():
|
||
r0[c] = true
|
||
var candidates: Array[Vector2i] = []
|
||
for c in board.safe_cells():
|
||
if not r0.has(c):
|
||
candidates.append(c)
|
||
if candidates.is_empty():
|
||
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.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)
|
||
|
||
|
||
# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费)
|
||
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(int(db.combat.variation.pack_min), int(db.combat.variation.pack_max)))
|
||
var free: Array[Vector2i] = []
|
||
for c in candidates:
|
||
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
|
||
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
|
||
if placed.has(c) or _adjacent_to_packs(board, placed, c):
|
||
continue
|
||
free.append(c)
|
||
if free.is_empty():
|
||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
|
||
placed[seed_c] = true
|
||
var pack: Array[Variant] = [seed_c]
|
||
var pack_set := {seed_c: true}
|
||
while pack.size() < pack_size:
|
||
var fringe := _variants_fringe(board, pack, candidates, placed, pack_set)
|
||
if fringe.is_empty():
|
||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
|
||
placed[nxt] = true
|
||
pack_set[nxt] = true
|
||
pack.append(nxt)
|
||
for c in placed.keys():
|
||
board.variants[c] = [String(vrow.variant_id)]
|
||
|
||
|
||
# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接)
|
||
func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]:
|
||
var fringe: Array[Vector2i] = []
|
||
for c in pack:
|
||
for n in board.neighbors8(c):
|
||
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
|
||
and not _adjacent_to_packs(board, placed, n, pack_set):
|
||
fringe.append(n)
|
||
return fringe
|
||
|
||
|
||
# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max
|
||
func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool:
|
||
for m in board.neighbors8(c):
|
||
if placed.has(m) and not own_pack.has(m):
|
||
return true
|
||
return false
|