T5 core/data结构段: config_db拆R1解析器+跨表校验器两类(门面38消费方签名零变化)+三选一双渠道参数化(_apply_bonus_pick共享/守卫收AuxItems.can_accept/替换子交互保守分轨注记)+rest回血单点+行主序键单点+魔法数结构化(bonus_candidate_count默认3/出生区9/数量域常量——值零改动)+level_runner Phase enum(外部读面零变化)+表访问收口(aux走访问器/行集直扫三处收口/删clamped_pool壳与死变量erow/_minesweeper_ref包装)+tune_sim反向依赖解除(构造注入缺省null兜底)+类型注批量;闸=冲烟636/0+run_all 15套+探针全绿+MC对拍200局逐位一致
This commit is contained in:
1 parent
59f0c70ca3
commit
d1460327a4
21 files changed
+491
-342
No files matched your search
@@ -53,8 +53,8 @@ signal round_started(round_index: int)
|
||||
signal board_changed()
|
||||
signal wallet_changed(event: String, gp: int, extra: Dictionary)
|
||||
|
||||
var db
|
||||
var rng
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var state := "idle" # idle / running / settling(冒险结构.md 三态)
|
||||
var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享)
|
||||
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 统一冒险加算区(商店购买与三选一属性件同一本账;四轴 +ap——迭代 10 奖励重构)
|
||||
@@ -107,7 +107,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
keystone = 0
|
||||
summary = {}
|
||||
_reset_bonus_state()
|
||||
levels_total = db.level.rows.size()
|
||||
levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役)
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
@@ -148,7 +148,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
purchased = (bp.get("shop_purchased", {}) as Dictionary).duplicate(true)
|
||||
keystone = int(bp.get("keystone", 0))
|
||||
summary = {}
|
||||
levels_total = db.level.rows.size()
|
||||
levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_pending_next_level = false
|
||||
_reset_bonus_state()
|
||||
@@ -221,10 +221,10 @@ func runner() -> LevelRunner:
|
||||
return _runner
|
||||
|
||||
|
||||
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶;与 LevelRunner.rest_heal_amount
|
||||
# 同式,同一公式两个落点是临时态,注释指认,统一归并归后续批)
|
||||
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = LevelRunner.rest_heal
|
||||
#(2026-10-02 修复批——同式双落点字面量退役,与 LevelRunner.rest_heal_amount 同一单点)
|
||||
func apply_rest(pct: int, hp: int, max_hp: int) -> int:
|
||||
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
|
||||
return LevelRunner.rest_heal(pct, hp, max_hp)
|
||||
|
||||
|
||||
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
|
||||
@@ -255,6 +255,19 @@ func pick_bonus(id: String) -> bool:
|
||||
return false
|
||||
var value: int = int(db.adventure.bonus_value(id))
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
if id == "aux_item":
|
||||
return _pick_aux_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨)
|
||||
if not _apply_bonus_pick(id, value):
|
||||
push_error("[冒险结构] pick_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
_finish_bonus_pick(id, value, hp_before)
|
||||
return true
|
||||
|
||||
|
||||
# 三选一件兑付共享体(2026-10-02 修复批——通关/奖励节点双渠道 match 双胞胎收口):
|
||||
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包。道具件不入此体——两渠道持有容器
|
||||
# 语义不同(跨关携带 vs 关卡域库存),分派回调用方渠道处理;未知件返回 false 由调用方按渠道名报错。
|
||||
func _apply_bonus_pick(id: String, value: int) -> bool:
|
||||
match id:
|
||||
"attack":
|
||||
bonus["atk"] = int(bonus.get("atk", 0)) + value
|
||||
@@ -271,37 +284,24 @@ func pick_bonus(id: String) -> bool:
|
||||
"gp":
|
||||
if _runner != null:
|
||||
_runner.grant_bonus_gp(value)
|
||||
"aux_item":
|
||||
return _pick_aux_item(value)
|
||||
_:
|
||||
push_error("[冒险结构] pick_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
_finish_bonus_pick(id, value, hp_before)
|
||||
return true
|
||||
|
||||
|
||||
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)。
|
||||
# 掷定前置(追加批二):具体道具随候选抽定(_last_draw_aux / _bonus_pending.aux_detail)——本函数消费预掷值,
|
||||
# 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。
|
||||
# 持有容器 = 跨关携带 _bonus_aux_carry(与节点渠道的关卡域库存分轨——容器差异保守不合,注记在案)。
|
||||
func _pick_aux_item(count: int) -> bool:
|
||||
var pool: Array = _bonus_aux_pool()
|
||||
if pool.is_empty():
|
||||
var picked: Array = _draw_pending_aux(_bonus_pending, count, true)
|
||||
if picked.is_empty():
|
||||
push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
|
||||
return false
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
var picked: Array[String] = []
|
||||
var detail: Array = (_bonus_pending.get("aux_detail", []) as Array).duplicate()
|
||||
if detail.is_empty():
|
||||
detail = _last_draw_aux.duplicate()
|
||||
for i in maxi(1, count):
|
||||
if i < detail.size():
|
||||
picked.append(String(detail[i]))
|
||||
else:
|
||||
picked.append(_roll_bonus_aux_item(pool))
|
||||
var slots := maxi(0, int(db.adventure.transitional_sync_slots))
|
||||
if _bonus_aux_carry.size() + picked.size() <= slots:
|
||||
if AuxItems.can_accept(picked.size(), _bonus_aux_carry.size(), db): # 同调位守卫单点(2026-10-02)
|
||||
for iid in picked:
|
||||
_bonus_aux_carry.append(iid)
|
||||
_bonus_aux_carry.append(String(iid))
|
||||
_finish_bonus_pick("aux_item", picked.size(), hp_before)
|
||||
return true
|
||||
# 满位替换子交互:现有持有件 + 新件 N+1 选 N(丢旧收新 / 放弃新件两路,终态恒 ≤ 同调位)
|
||||
@@ -312,8 +312,9 @@ func _pick_aux_item(count: int) -> bool:
|
||||
|
||||
|
||||
# 替换子交互受理(discard_index = -1 放弃新件不获取;否则丢该件腾位收新)——终态恒 ≤ 同调位
|
||||
# 持有容器 = 跨关携带(序号寻址);与节点渠道(库存句柄寻址)的丢弃/授予/收尾三步保守不合。
|
||||
func resolve_aux_replace(discard_index: int) -> bool:
|
||||
if _bonus_pending.is_empty() or String(_bonus_pending.get("chosen", "")) != "aux_item":
|
||||
if not _replace_in_transit(_bonus_pending):
|
||||
push_error("[冒险结构] resolve_aux_replace 拒绝——无替换在途")
|
||||
return false
|
||||
var incoming: Array = _bonus_pending.get("incoming", []) as Array
|
||||
@@ -368,55 +369,33 @@ func apply_node_bonus(id: String) -> bool:
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——件不在候选: %s" % id)
|
||||
return false
|
||||
var value: int = int(db.adventure.bonus_value(id))
|
||||
match id:
|
||||
"attack":
|
||||
bonus["atk"] = int(bonus.get("atk", 0)) + value
|
||||
_bonus_taken["attack"] = int(_bonus_taken.get("attack", 0)) + 1
|
||||
"max_hp":
|
||||
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value
|
||||
_bonus_taken["max_hp"] = int(_bonus_taken.get("max_hp", 0)) + 1
|
||||
"armor":
|
||||
bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value
|
||||
_bonus_taken["armor"] = int(_bonus_taken.get("armor", 0)) + 1
|
||||
"ap":
|
||||
bonus["ap"] = int(bonus.get("ap", 0)) + value
|
||||
_bonus_taken["ap"] = int(_bonus_taken.get("ap", 0)) + 1
|
||||
"gp":
|
||||
if _runner != null:
|
||||
_runner.grant_bonus_gp(value)
|
||||
"aux_item":
|
||||
return _apply_node_aux(value)
|
||||
_:
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
if id == "aux_item":
|
||||
return _apply_node_aux(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨)
|
||||
if not _apply_bonus_pick(id, value):
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
_finish_node_bonus(id, value)
|
||||
return true
|
||||
|
||||
|
||||
# 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用);
|
||||
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨)。
|
||||
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨,容器语义保守不合)。
|
||||
# 掷定前置:消费 _node_bonus_pending.aux_detail 预掷值(缺省回退当场掷定)。
|
||||
func _apply_node_aux(count: int) -> bool:
|
||||
var pool: Array = _bonus_aux_pool()
|
||||
if pool.is_empty():
|
||||
var picked: Array = _draw_pending_aux(_node_bonus_pending, count, false)
|
||||
if picked.is_empty():
|
||||
push_error("[冒险结构] apply_node_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
|
||||
return false
|
||||
var picked: Array[String] = []
|
||||
var detail: Array = (_node_bonus_pending.get("aux_detail", []) as Array).duplicate()
|
||||
for i in maxi(1, count):
|
||||
if i < detail.size():
|
||||
picked.append(String(detail[i]))
|
||||
else:
|
||||
picked.append(_roll_bonus_aux_item(pool))
|
||||
var slots := maxi(0, int(db.adventure.transitional_sync_slots))
|
||||
var records: Array = []
|
||||
if _runner != null and _runner.aux_inventory != null:
|
||||
records = _runner.aux_inventory.holding_records()
|
||||
var holding := records.size() + _bonus_aux_carry.size()
|
||||
if holding + picked.size() <= slots:
|
||||
if AuxItems.can_accept(picked.size(), holding, db): # 同调位守卫单点(2026-10-02)
|
||||
if _runner != null:
|
||||
for iid in picked:
|
||||
_runner.grant_aux_item(iid, "reward_node")
|
||||
var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node")
|
||||
if rec.is_empty():
|
||||
push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid))
|
||||
_finish_node_bonus("aux_item", picked.size())
|
||||
return true
|
||||
_node_bonus_pending["chosen"] = "aux_item"
|
||||
@@ -426,7 +405,7 @@ func _apply_node_aux(count: int) -> bool:
|
||||
|
||||
|
||||
func _resolve_node_replace(discard_handle: int) -> bool:
|
||||
if _node_bonus_pending.is_empty() or String(_node_bonus_pending.get("chosen", "")) != "aux_item":
|
||||
if not _replace_in_transit(_node_bonus_pending):
|
||||
push_error("[冒险结构] resolve_node_replace 拒绝——无替换在途")
|
||||
return false
|
||||
var incoming: Array = _node_bonus_pending.get("incoming", []) as Array
|
||||
@@ -441,6 +420,32 @@ func _resolve_node_replace(discard_handle: int) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=aux_item)
|
||||
func _replace_in_transit(pending: Dictionary) -> bool:
|
||||
return not pending.is_empty() and String(pending.get("chosen", "")) == "aux_item"
|
||||
|
||||
|
||||
# 双渠道道具件掷定共享体(2026-10-02 修复批——_pick_aux_item/_apply_node_aux 复制债收口一步):
|
||||
# 解锁道具池空集守卫 + N 件掷定(先消费停点预掷 aux_detail,不足部分当场经 reward_pick 封闭流补掷——
|
||||
# 取数序与拆分前逐位一致)。返回空数组 = 池空集(调用方按渠道名报错)。
|
||||
# 保守不合处:通关渠道允许回退 _last_draw_aux(allow_detail_fallback)、节点渠道不回退——掷定回退
|
||||
# 语义渠道各异,参数化保留差异;持有容器与兑付收尾仍归调用方渠道。
|
||||
func _draw_pending_aux(pending: Dictionary, count: int, allow_detail_fallback: bool) -> Array:
|
||||
var pool: Array = _bonus_aux_pool()
|
||||
if pool.is_empty():
|
||||
return []
|
||||
var picked: Array = []
|
||||
var detail: Array = (pending.get("aux_detail", []) as Array).duplicate()
|
||||
if detail.is_empty() and allow_detail_fallback:
|
||||
detail = _last_draw_aux.duplicate()
|
||||
for i in maxi(1, count):
|
||||
if i < detail.size():
|
||||
picked.append(String(detail[i]))
|
||||
else:
|
||||
picked.append(_roll_bonus_aux_item(pool))
|
||||
return picked
|
||||
|
||||
|
||||
# 替换受理分发(单入口——节点停点在途优先按库存句柄,否则按通关停点的携带序号)
|
||||
func resolve_bonus_replace(discard: int) -> bool:
|
||||
if not _node_bonus_pending.is_empty() and _node_bonus_pending.has("incoming"):
|
||||
@@ -479,7 +484,7 @@ func _draw_bonus_candidates() -> Array:
|
||||
keyed.append({"id": id, "key": pow(rg.randf(), 1.0 / w)})
|
||||
keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"]))
|
||||
var out: Array = []
|
||||
for i in mini(3, keyed.size()):
|
||||
for i in mini(int(db.adventure.bonus_candidate_count), keyed.size()): # 候选数读表(2026-10-02 魔法数结构化——默认 3 = 现行行为)
|
||||
var cid := String(keyed[i]["id"])
|
||||
out.append(cid)
|
||||
if cid == "aux_item":
|
||||
|
||||
+15
-3
@@ -73,6 +73,15 @@ func holding_count() -> int:
|
||||
return n
|
||||
|
||||
|
||||
# 同调位守卫谓词(2026-10-02 修复批——四处手抄收口单点:战斗结算授予路由 / 关卡授予 / 三选一双渠道):
|
||||
# holding = 调用方渠道的当前实际持有数(库存渠道传 holding_count()、通关渠道传跨关携带数、
|
||||
# 节点渠道传库存+携带合计);读数源 = db.adventure.transitional_sync_slots(负值钳 0 口径同前)。
|
||||
# 返回再入 n 件是否仍在位内。挂 static:谓词纯算术不依赖实例态——通关渠道(跨关携带)没有
|
||||
# AuxItems 实例可用,实例方法形态不成立;db 参数不标类型(同 _db 桩表注入纪律)。
|
||||
static func can_accept(n: int, holding: int, db) -> bool:
|
||||
return holding + n <= maxi(0, int(db.adventure.transitional_sync_slots))
|
||||
|
||||
|
||||
# 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
|
||||
func holding_records() -> Array:
|
||||
var out: Array = []
|
||||
@@ -105,10 +114,13 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool:
|
||||
|
||||
|
||||
func _item_row(item_id: String) -> Dictionary:
|
||||
if _db != null and _db.aux_items != null:
|
||||
return _db.aux_items.row_by_id(item_id)
|
||||
return {}
|
||||
if _db == null:
|
||||
return {}
|
||||
if _db.has_method("aux_item_row"):
|
||||
return _db.aux_item_row(item_id) # 表访问收口(2026-10-02——正式面走 ConfigDB 访问器)
|
||||
return _db.aux_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役)
|
||||
|
||||
# 不标具体类型原因:单元测试注入 StubDB 桩表(aux_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注
|
||||
var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表)
|
||||
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ var action_economy: ActionEconomy
|
||||
var _enemy_system: UnitContainer
|
||||
var _status_effects: StatusContainer
|
||||
var _reward_economy: RewardEconomy
|
||||
var aux_inventory = null # AuxItems(迭代 7 T3——LevelRunner 注入的关卡域库存引用;
|
||||
# 每战新建本类,库存不归本类所有——注入面)
|
||||
var aux_inventory: AuxItems # 迭代 7 T3——LevelRunner 注入的关卡域库存引用;
|
||||
# 每战新建本类,库存不归本类所有——注入面
|
||||
var _damage: DamageController # 伤害控制器(伤害管线单据化批 2026-09-24——单据唯一兑付管线)
|
||||
|
||||
var encounter: Dictionary[String, Variant] = {}
|
||||
@@ -126,7 +126,6 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk):
|
||||
_fatal("敌人系统 spawn 被拒")
|
||||
return false
|
||||
var erow: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
@@ -375,7 +374,8 @@ func _grant_aux(snap: Dictionary, polarity: String, source_class: String, cell:
|
||||
# 同调位持有上限(迭代 10 奖励重构批过渡承载——transitional_sync_slots,provisional 1):
|
||||
# 战斗内渠道遇满位沉没不入(战斗域零交互纪律——替换交互归停点渠道;正主 = 同调位谓词表,
|
||||
# 全渠道统一收口随魔法道具同调实施批)。逐行归因纪律:沉没行照发(「获得」的零兑付形态)。
|
||||
if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)):
|
||||
# 谓词单点 = AuxItems.can_accept(2026-10-02 修复批——四处手抄收口)。
|
||||
if not AuxItems.can_accept(1, aux_inventory.holding_count(), db):
|
||||
_emit_row("effect", source_class, cell, ri, "同调位已满,%s 沉没" % String(db.aux_item_row(iid).get("display_name", iid)), 0,
|
||||
{"effect_id": "", "aux_sink": iid, "sink_reason": "sync_slots_full"}, "")
|
||||
return false
|
||||
@@ -396,7 +396,7 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary:
|
||||
return {"ok": false, "reason": "库存未接线"}
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
|
||||
var board = _minesweeper_ref()
|
||||
var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_aux 直引公有成员)
|
||||
var v: Dictionary = aux_inventory.validate_use(handle, target,
|
||||
Vector2(board.width, board.height),
|
||||
func(c: Vector2i): return not board.is_revealed(c))
|
||||
@@ -419,10 +419,6 @@ func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
_reward_economy.set_aux_unlocked(ids)
|
||||
|
||||
|
||||
func _minesweeper_ref() -> Variant:
|
||||
return minesweeper
|
||||
|
||||
|
||||
func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool:
|
||||
# 迭代 7 T3——aux 投放行授予路由(魔法道具 GDD:授予路由唯一调用方=战斗结算):正路由(挖开/标中)
|
||||
# → 道具入关卡域库存(来源可归因);负路由(雷格/标错旗摇中 aux 身份——雷安同池)→ 零兑付
|
||||
@@ -672,18 +668,14 @@ func _combat_segment(ri: int) -> void:
|
||||
|
||||
# ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)----
|
||||
# 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点);
|
||||
# 本类仅保留两处薄委托——_defender_ac(快照面消费)/ _clamped_pool(冒烟白盒断言直呼面 smoke_all 沿用)。
|
||||
# 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除
|
||||
# (冒烟白盒断言消费面改直引 settlement._damage.clamped_pool——并行卡同批改 smoke_all)。
|
||||
|
||||
# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照)
|
||||
func _defender_ac(unit: Dictionary) -> int:
|
||||
return _damage.defender_ac(unit)
|
||||
|
||||
|
||||
# 池内加算 → clamp——实现归 伤害控制器 clamped_pool(本委托供冒烟白盒断言 smoke_all 直呼兼容)
|
||||
func _clamped_pool(pool: String) -> int:
|
||||
return _damage.clamped_pool(pool)
|
||||
|
||||
|
||||
func _pool_effective(effect_id: String) -> int:
|
||||
var side := "hero"
|
||||
var row: Dictionary = db.status_effect_row(effect_id)
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
class_name BoardData
|
||||
extends RefCounted
|
||||
|
||||
# 出生 3×3 区格数(2026-10-02 修复批 T2 魔法数结构化——W×H−9 布雷域基的双落点收口单点:
|
||||
# 棋盘生成 _attempt/_finalize 与 BoardGenTable 布雷数下限校验共用;与 spawn_cells() 的 3×3 几何同源)
|
||||
const SPAWN_AREA_CELLS := 9
|
||||
|
||||
var width: int = 0
|
||||
var height: int = 0
|
||||
var spawn_center := Vector2i.ZERO
|
||||
@@ -24,6 +28,12 @@ func in_bounds(c: Vector2i) -> bool:
|
||||
return c.x >= 0 and c.y >= 0 and c.x < width and c.y < height
|
||||
|
||||
|
||||
# 行主序排序键(2026-10-02 修复批——排雷玩法 旗判定批与 地图战斗资源 资格规范化两处手抄收口单点):
|
||||
# y×1000+x 字典序 = 行主序;板宽域 [8,64] 远小于 1000,x 不跨行、键值不碰撞。
|
||||
static func row_major_key(c: Vector2i) -> int:
|
||||
return c.y * 1000 + c.x
|
||||
|
||||
|
||||
func index_of(c: Vector2i) -> int:
|
||||
return c.y * width + c.x
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ const METRIC_VERSION := 1
|
||||
|
||||
# ==================== 生成入口 ====================
|
||||
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
|
||||
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表(2026-10-02 注)
|
||||
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData:
|
||||
var row: Dictionary = db.resolve_board_gen(L, D)
|
||||
if row.is_empty():
|
||||
@@ -80,6 +81,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
b.level_index = params.L
|
||||
b.node_depth = params.D
|
||||
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"
|
||||
# (−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS——2026-10-02 修复批)
|
||||
var spawn := {}
|
||||
for c in b.spawn_cells():
|
||||
spawn[c] = true
|
||||
@@ -87,7 +89,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
for c in b.all_cells():
|
||||
if not spawn.has(c):
|
||||
domain.append(c)
|
||||
var n_mines: int = int(round(params.density * float(params.w * params.h - 9)))
|
||||
var n_mines: int = int(round(params.density * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
|
||||
# 部分 Fisher-Yates(确定性,消费 board_gen 流)
|
||||
for i in range(0, mini(n_mines, domain.size())):
|
||||
var j := rng_stream.randi_range(i, domain.size() - 1)
|
||||
@@ -314,7 +316,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
|
||||
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db) -> void:
|
||||
var m := measure_inferability(b)
|
||||
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
|
||||
var n_target: int = int(round(float(params.density) * float(params.w * params.h - 9)))
|
||||
var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if b.mines.size() > n_target:
|
||||
_fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target])
|
||||
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed)
|
||||
|
||||
+31
-204
@@ -48,10 +48,15 @@ var adventure: AdventureTable
|
||||
|
||||
var is_ready := false
|
||||
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
|
||||
var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表
|
||||
var _resolver: ConfigDbResolver # R1 难度行解析协作类(2026-10-02 修复批 T5 拆分——门面公有签名零变化)
|
||||
var _validator := ConfigDbCrossValidator.new() # 跨表校验协作类(无状态——声明期即建)
|
||||
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_resolver = ConfigDbResolver.new(self) # 协作类持门面引用(诊断/致命通道回呼)
|
||||
|
||||
|
||||
func push_diag(severity: String, category: String, message: String) -> void:
|
||||
_diagnostics.append({"severity": severity, "category": category, "message": message})
|
||||
if severity == "warning":
|
||||
@@ -73,7 +78,7 @@ func _fatal(category: String, message: String) -> void:
|
||||
|
||||
func reset_diagnostics() -> void:
|
||||
_diagnostics.clear()
|
||||
_warn_dedup.clear()
|
||||
_resolver.reset_warn_dedup() # R1 告警去重注册表(协作类持钥——清空时点与拆分前一致)
|
||||
|
||||
|
||||
func diagnostics() -> Array:
|
||||
@@ -95,7 +100,7 @@ func _ready() -> void:
|
||||
func load_tables(root: String, collect_only: bool) -> bool:
|
||||
is_ready = false
|
||||
_diagnostics.clear()
|
||||
_warn_dedup.clear()
|
||||
_resolver.reset_warn_dedup()
|
||||
var loaded := {}
|
||||
for key in TABLE_MANIFEST.keys():
|
||||
var path := root.path_join(TABLE_MANIFEST[key])
|
||||
@@ -134,222 +139,31 @@ func load_tables(root: String, collect_only: bool) -> bool:
|
||||
return not has_errors
|
||||
|
||||
|
||||
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出:
|
||||
# ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。
|
||||
func _cross_validate(loaded: Dictionary) -> void:
|
||||
var st: StatusEffectTable = loaded.get("status_effects")
|
||||
var rt: RewardTable = loaded.get("reward")
|
||||
var st_tab: SettlementTable = loaded.get("settlement")
|
||||
if st == null or rt == null or st_tab == null:
|
||||
return # 单表校验已报错,不连锁
|
||||
var effect_ids := {}
|
||||
for row in st.rows:
|
||||
effect_ids[row.get("effect_id", "")] = true
|
||||
# ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
# GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
|
||||
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
|
||||
push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
|
||||
# ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id,
|
||||
# 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
# 后仍投放(连带错误),同拦。
|
||||
var aux_live := {}
|
||||
var aux_tab = loaded.get("aux_items")
|
||||
if aux_tab != null:
|
||||
for arow in aux_tab.active_rows():
|
||||
aux_live[String(arow.get("item_id", ""))] = true
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE:
|
||||
continue
|
||||
var aux_eff: String = String(row.get("effect_id", ""))
|
||||
if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验
|
||||
continue
|
||||
if not aux_live.has(aux_eff):
|
||||
push_diag("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"])
|
||||
# ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter}
|
||||
# = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减
|
||||
# ①c F2 投放维度覆盖校验随战斗掉落渠道退役(2026-10-01 用户拍板甲——挖格不掉道具,魔法道具
|
||||
# 唯一来路=三选一共享池〔节点/通关〕):旧校验=「掉落池五维度各 ≥1 正权重活行」,载体已随
|
||||
# reward.tres aux 投放四行删除而消失;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。静态覆盖保证的重立(如按维度钉共享池抽签面)归同调实施批/渠道设计批。
|
||||
# ② SettlementTable.effect_pool_map 悬空
|
||||
for eff in st_tab.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键)
|
||||
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.source_label_map.has(s):
|
||||
push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行——
|
||||
# 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦)
|
||||
var lv: LevelTable = loaded.get("level")
|
||||
var bg: BoardGenTable = loaded.get("board_gen")
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
if lv != null and bg != null and en != null:
|
||||
var bg_keys := {}
|
||||
for row in bg.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in en.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
for lrow in lv.rows:
|
||||
var li: int = int(lrow.level_index)
|
||||
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
|
||||
var node_lists: Array[Array] = []
|
||||
var tpls: Array = lrow.get("templates", [])
|
||||
if tpls.is_empty():
|
||||
node_lists.append(lrow.get("nodes", []))
|
||||
else:
|
||||
for tpl in tpls:
|
||||
node_lists.append(tpl.get("nodes", []))
|
||||
for nodes_arr in node_lists:
|
||||
for node in nodes_arr:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
if not bg_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
|
||||
push_diag("error", e.category, e.message)
|
||||
_validator.cross_validate(loaded, push_diag)
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
|
||||
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
|
||||
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
|
||||
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
|
||||
# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类)
|
||||
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skill_types := {}
|
||||
for row in skill_table.rows:
|
||||
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
|
||||
# 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
|
||||
for row in skill_table.rows:
|
||||
var st: String = String(row.get("type", ""))
|
||||
if st != "buff" and st != "debuff":
|
||||
continue
|
||||
var want_side: String = "enemy" if st == "buff" else "hero"
|
||||
var eff: String = String(row.get("buff_ref", ""))
|
||||
var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {}
|
||||
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
|
||||
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var basic_n := 0
|
||||
var buff_n := 0
|
||||
var debuff_n := 0
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
continue
|
||||
match skill_types[sid]:
|
||||
"basic":
|
||||
basic_n += 1
|
||||
"buff":
|
||||
buff_n += 1
|
||||
"debuff":
|
||||
debuff_n += 1
|
||||
if basic_n != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
|
||||
if buff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
|
||||
if debuff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
|
||||
return errs
|
||||
return _validator.validate_skill_pool_links(skill_table, enemy_table, status_table)
|
||||
|
||||
|
||||
# ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)----
|
||||
|
||||
# 解析本体自 2026-10-02 修复批迁出:ConfigDbResolver(core/config_db_resolver.gd)承载——
|
||||
# 含 _resolve_progress/_ceil_key 与 R1 告警去重注册表;门面同名方法委托,越域 Fatal 语义逐位一致。
|
||||
func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(board_gen.rows, L, D, "board_gen")
|
||||
return _resolver.resolve_board_gen(L, D)
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(enemy.rows, L, D, "enemy")
|
||||
return _resolver.resolve_enemy_rowset(L, D)
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
if difficulty == null:
|
||||
return {}
|
||||
for row in difficulty.rows:
|
||||
if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D:
|
||||
return {
|
||||
"hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0),
|
||||
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary:
|
||||
if L < 1 or D < 0:
|
||||
_fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
|
||||
return {}
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
if level_keys.is_empty():
|
||||
_fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id)
|
||||
return {}
|
||||
level_keys.sort()
|
||||
var l_star := _ceil_key(level_keys, L)
|
||||
var l_clamped := l_star < 0
|
||||
if l_clamped:
|
||||
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值)
|
||||
var depth_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star:
|
||||
var dv := int(row.get("node_depth", 0))
|
||||
if not depth_keys.has(dv):
|
||||
depth_keys.append(dv)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := ""
|
||||
if l_clamped and d_clamped:
|
||||
missing = "both_clamp"
|
||||
elif l_clamped:
|
||||
missing = "level_clamp"
|
||||
elif d_clamped:
|
||||
missing = "depth_clamp"
|
||||
elif l_star != L and d_star != D:
|
||||
missing = "both_ceil"
|
||||
elif l_star != L:
|
||||
missing = "level_ceil"
|
||||
elif d_star != D:
|
||||
missing = "depth_ceil"
|
||||
if missing != "":
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal
|
||||
func _ceil_key(sorted_keys: Array, request: int) -> int:
|
||||
for k in sorted_keys:
|
||||
if int(k) >= request:
|
||||
return int(k)
|
||||
return -1
|
||||
return _resolver.resolve_difficulty_modifiers(L, D)
|
||||
|
||||
|
||||
# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐
|
||||
@@ -373,6 +187,19 @@ func status_effect_row(effect_id: String) -> Dictionary:
|
||||
return status_effects.row_by_id(effect_id)
|
||||
|
||||
|
||||
# ---- 关卡行集访问器(2026-10-02 修复批 T5 表访问收口——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 {}
|
||||
|
||||
|
||||
# 序列总关数(AdventureRunner levels_total / MetaProgression 通关累加域的取数口径)
|
||||
func level_count() -> int:
|
||||
return level.rows.size() if level != null else 0
|
||||
|
||||
|
||||
# ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)----
|
||||
# ── 魔法道具访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)──
|
||||
# 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-10-02(code-quality 修复批 T5——ConfigDb 拆分:跨表校验协作类,自 config_db.gd 迁出)
|
||||
# 职责单一化(Rule 6 校验双通道的跨表一侧):_cross_validate 全域(悬空引用/池护栏/节点精确行/
|
||||
# 技能池构成)+ validate_skill_pool_links(冒烟直验独立入口)。ConfigDb autoload 门面保持——
|
||||
# load_tables 调用点与冒烟直验经门面同名方法,公有签名零变化。
|
||||
# 诊断通道注入:跨表校验的行级错误经 push_diag: Callable 外发(门面传 Callable(self, "push_diag"))——
|
||||
# 本类零 ConfigDb 依赖(无状态纯校验器,装载序上先于成员赋值执行的前提保持)。
|
||||
# 迁移注记:validate_skill_pool_links 的 status_table 显式入参纪律照抄(装载通道传 loaded 表,
|
||||
# 冒烟直验传门面 status_effects;null 时 side 守卫跳过)——返回错误列表不进诊断通道,消费面归调用方。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚)
|
||||
class_name ConfigDbCrossValidator
|
||||
extends RefCounted
|
||||
|
||||
|
||||
func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var st: StatusEffectTable = loaded.get("status_effects")
|
||||
var rt: RewardTable = loaded.get("reward")
|
||||
var st_tab: SettlementTable = loaded.get("settlement")
|
||||
if st == null or rt == null or st_tab == null:
|
||||
return # 单表校验已报错,不连锁
|
||||
var effect_ids := {}
|
||||
for row in st.rows:
|
||||
effect_ids[row.get("effect_id", "")] = true
|
||||
# ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
# GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
|
||||
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
|
||||
push_diag.call("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
|
||||
# ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id,
|
||||
# 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
# 后仍投放(连带错误),同拦。
|
||||
var aux_live := {}
|
||||
var aux_tab = loaded.get("aux_items")
|
||||
if aux_tab != null:
|
||||
for arow in aux_tab.active_rows():
|
||||
aux_live[String(arow.get("item_id", ""))] = true
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE:
|
||||
continue
|
||||
var aux_eff: String = String(row.get("effect_id", ""))
|
||||
if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验
|
||||
continue
|
||||
if not aux_live.has(aux_eff):
|
||||
push_diag.call("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"])
|
||||
# ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter}
|
||||
# = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减
|
||||
# ①c F2 投放维度覆盖校验随战斗掉落渠道退役(2026-10-01 用户拍板甲——挖格不掉道具,魔法道具
|
||||
# 唯一来路=三选一共享池〔节点/通关〕):旧校验=「掉落池五维度各 ≥1 正权重活行」,载体已随
|
||||
# reward.tres aux 投放四行删除而消失;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。静态覆盖保证的重立(如按维度钉共享池抽签面)归同调实施批/渠道设计批。
|
||||
# ② SettlementTable.effect_pool_map 悬空
|
||||
for eff in st_tab.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag.call("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键)
|
||||
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.source_label_map.has(s):
|
||||
push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行——
|
||||
# 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦)
|
||||
var lv: LevelTable = loaded.get("level")
|
||||
var bg: BoardGenTable = loaded.get("board_gen")
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
if lv != null and bg != null and en != null:
|
||||
var bg_keys := {}
|
||||
for row in bg.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in en.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
for lrow in lv.rows:
|
||||
var li: int = int(lrow.level_index)
|
||||
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
|
||||
var node_lists: Array[Array] = []
|
||||
var tpls: Array = lrow.get("templates", [])
|
||||
if tpls.is_empty():
|
||||
node_lists.append(lrow.get("nodes", []))
|
||||
else:
|
||||
for tpl in tpls:
|
||||
node_lists.append(tpl.get("nodes", []))
|
||||
for nodes_arr in node_lists:
|
||||
for node in nodes_arr:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
if not bg_keys.has(key):
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
|
||||
push_diag.call("error", e.category, e.message)
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
|
||||
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
|
||||
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
|
||||
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
|
||||
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skill_types := {}
|
||||
for row in skill_table.rows:
|
||||
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
|
||||
# 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
|
||||
for row in skill_table.rows:
|
||||
var st: String = String(row.get("type", ""))
|
||||
if st != "buff" and st != "debuff":
|
||||
continue
|
||||
var want_side: String = "enemy" if st == "buff" else "hero"
|
||||
var eff: String = String(row.get("buff_ref", ""))
|
||||
var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {}
|
||||
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
|
||||
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var basic_n := 0
|
||||
var buff_n := 0
|
||||
var debuff_n := 0
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
continue
|
||||
match skill_types[sid]:
|
||||
"basic":
|
||||
basic_n += 1
|
||||
"buff":
|
||||
buff_n += 1
|
||||
"debuff":
|
||||
debuff_n += 1
|
||||
if basic_n != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
|
||||
if buff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
|
||||
if debuff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://d4dfucylnemk6
|
||||
@@ -0,0 +1,108 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-10-02(code-quality 修复批 T5——ConfigDb 拆分:R1 难度行解析协作类,自 config_db.gd 迁出)
|
||||
# 职责单一化(R1 解析域):resolve_board_gen / resolve_enemy_rowset / resolve_difficulty_modifiers
|
||||
# 及其私有算术(_resolve_progress / _ceil_key)与 R1 专属的告警去重注册表。ConfigDb autoload 门面
|
||||
# 保持——全部消费方(战斗结算/敌人系统/棋盘生成/呈现层/冒烟)经门面同名方法取数,公有签名零变化。
|
||||
# 诊断通道归门面:push_diag / _fatal 经 _db 引用回呼(协作类同仓内部通道——不另设公共出口,
|
||||
# 行为与拆分前逐位一致,含越域 Fatal 退出语义)。
|
||||
# 迁移注记:_warn_dedup 去重注册表随解析逻辑同迁(唯一消费方 = _resolve_progress)——
|
||||
# 门面 load_tables / reset_diagnostics 经 reset_warn_dedup() 同步清空,清空时点零变化。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚)
|
||||
class_name ConfigDbResolver
|
||||
extends RefCounted
|
||||
|
||||
var _db: ConfigDb # 门面引用(表读取 + 诊断/致命通道——_init 注入)
|
||||
var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表(自 ConfigDb 迁入)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
_db = db_ref
|
||||
|
||||
|
||||
# 去重注册表清空(门面 load_tables / reset_diagnostics 调用——与拆分前清空时点逐位一致)
|
||||
func reset_warn_dedup() -> void:
|
||||
_warn_dedup.clear()
|
||||
|
||||
|
||||
# ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)----
|
||||
|
||||
func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(_db.board_gen.rows, L, D, "board_gen")
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(_db.enemy.rows, L, D, "enemy")
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
if _db.difficulty == null:
|
||||
return {}
|
||||
for row in _db.difficulty.rows:
|
||||
if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D:
|
||||
return {
|
||||
"hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0),
|
||||
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary:
|
||||
if L < 1 or D < 0:
|
||||
_db._fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
|
||||
return {}
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
if level_keys.is_empty():
|
||||
_db._fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id)
|
||||
return {}
|
||||
level_keys.sort()
|
||||
var l_star := _ceil_key(level_keys, L)
|
||||
var l_clamped := l_star < 0
|
||||
if l_clamped:
|
||||
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值)
|
||||
var depth_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star:
|
||||
var dv := int(row.get("node_depth", 0))
|
||||
if not depth_keys.has(dv):
|
||||
depth_keys.append(dv)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := ""
|
||||
if l_clamped and d_clamped:
|
||||
missing = "both_clamp"
|
||||
elif l_clamped:
|
||||
missing = "level_clamp"
|
||||
elif d_clamped:
|
||||
missing = "depth_clamp"
|
||||
elif l_star != L and d_star != D:
|
||||
missing = "both_ceil"
|
||||
elif l_star != L:
|
||||
missing = "level_ceil"
|
||||
elif d_star != D:
|
||||
missing = "depth_ceil"
|
||||
if missing != "":
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_db.push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal
|
||||
func _ceil_key(sorted_keys: Array, request: int) -> int:
|
||||
for k in sorted_keys:
|
||||
if int(k) >= request:
|
||||
return int(k)
|
||||
return -1
|
||||
@@ -0,0 +1 @@
|
||||
uid://bv7guty0wowk
|
||||
@@ -49,9 +49,9 @@ const TARGET_HERO := "hero"
|
||||
const TARGET_ALL_ALIVE := "all_alive_enemies"
|
||||
|
||||
var db: ConfigDb
|
||||
var rng
|
||||
var units # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用)
|
||||
var effects # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面)
|
||||
var rng: RngServiceCore # 注入成员类型注(2026-10-02 修复批——单测经真实 RngService 构造,标注安全)
|
||||
var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用)
|
||||
var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面)
|
||||
var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight)
|
||||
|
||||
var policy := POLICY_SEGMENT # 结账时机开关(2026-09-24 用户拍板第二步拨 B=攒批段界统一结;回切 A 改回 POLICY_IMMEDIATE 即还原迁移前行为)
|
||||
|
||||
+45
-34
@@ -61,16 +61,19 @@ var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区
|
||||
var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零)
|
||||
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
||||
var aux_inventory = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续,
|
||||
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
|
||||
var aux_inventory: AuxItems = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续,
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放)
|
||||
# 关卡末清空+快照计数入 level_finished 载荷)
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
var hero_hp_pct := 0
|
||||
var _running := false
|
||||
var level_result := "" # "" / "victory" / "defeat"
|
||||
var _awaiting_choice := false # 选路态(兑付确认后、pick 前为零自动推进的可见停点)
|
||||
# ---- 关卡相位轴(2026-10-02 修复批——布尔旗收拢:_running/_awaiting_choice 两旗 → 显式 Phase enum;
|
||||
# 对齐 行动经济 Phase 先例(action_economy.gd:内部 enum + 守卫换算,外部读面契约零变化)——
|
||||
# level_result:String 与 awaiting_choice() 被 AdventureRunner/呈现层消费的语义逐字不变)----
|
||||
enum Phase { IDLE, RUNNING, AWAITING_CHOICE, ENDED }
|
||||
var _phase: int = Phase.IDLE
|
||||
var level_result := "" # "" / "victory" / "defeat"(外部契约面——语义零变化)
|
||||
var _candidates: Array[Dictionary] = [] # 当前选路候选(节点引用数组)
|
||||
var visited: Array[Dictionary] = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点)
|
||||
var _graph_mode := false # 图形态缓存(行集任一节点带 next;单链 = false)
|
||||
@@ -81,6 +84,11 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
rng = rng_ref
|
||||
|
||||
|
||||
# 进行中谓词(原 _running 语义——RUNNING 或 AWAITING_CHOICE 两相位合计;内部守卫专用)
|
||||
func _is_running() -> bool:
|
||||
return _phase == Phase.RUNNING or _phase == Phase.AWAITING_CHOICE
|
||||
|
||||
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)----
|
||||
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
@@ -135,7 +143,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
return false
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
_awaiting_choice = false
|
||||
_phase = Phase.IDLE
|
||||
_candidates = []
|
||||
visited = []
|
||||
_graph_mode = false
|
||||
@@ -152,11 +160,11 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
hero_hp = effective_max_hp()
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
|
||||
level_row.assign(_resolve_template_row(row, template_pick)) # 池解析在播种后(node_map 抽签须已播种——EC7)
|
||||
_running = true
|
||||
_phase = Phase.RUNNING
|
||||
var entry := _entry_node()
|
||||
if entry.is_empty():
|
||||
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
return false
|
||||
return _enter_node(entry)
|
||||
|
||||
@@ -189,7 +197,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
level_row.assign(_resolve_template_row(row, template_pick))
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
_awaiting_choice = false
|
||||
_phase = Phase.IDLE
|
||||
_candidates = []
|
||||
visited = []
|
||||
_graph_mode = false
|
||||
@@ -197,7 +205,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
if node.has("next"):
|
||||
_graph_mode = true
|
||||
break
|
||||
_running = true
|
||||
_phase = Phase.RUNNING
|
||||
# 断点续玩路径(迭代 8 卡①——defer_entry 之外的第三种停点入口):直接重建到停点所在节点,
|
||||
# 零 RNG 消耗(模板已随 template_pick 强制、节点状态纯重建不 _enter_node——当场战斗从头打)。
|
||||
# resume_state = {"node_id": int(-1 = 入口停点), "visited_ids": Array[int]};空字典 = 既有路径逐位不变。
|
||||
@@ -207,10 +215,10 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
var entry := _entry_node()
|
||||
if entry.is_empty():
|
||||
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
return false
|
||||
if defer_entry:
|
||||
_awaiting_choice = true # 入口停点(node_index 保持 -1——进节点前无 current_node)
|
||||
_phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1——进节点前无 current_node)
|
||||
_candidates = [entry]
|
||||
return true
|
||||
return _enter_node(entry)
|
||||
@@ -219,7 +227,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
# 入口停点呈现(停点前移批 2026-09-23——defer_entry=true 的第二步):发出入口候选供呈现层弹
|
||||
# 选路层;非停点态(缺省自动进入路径)拒绝。零 RNG 消耗——呈现与行进取号解耦。
|
||||
func present_entry_choice() -> bool:
|
||||
if not _running or not _awaiting_choice or _candidates.is_empty() or node_index != -1:
|
||||
if _phase != Phase.AWAITING_CHOICE or _candidates.is_empty() or node_index != -1:
|
||||
push_error("[关卡节点地图] present_entry_choice 拒绝——非入口停点态(defer_entry 未开或已 pick)")
|
||||
return false
|
||||
choices_presented.emit(_candidates)
|
||||
@@ -237,9 +245,9 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool:
|
||||
var entry := _entry_node()
|
||||
if entry.is_empty():
|
||||
push_error("[关卡节点地图] 断点恢复失败——入口节点缺失")
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
return false
|
||||
_awaiting_choice = true
|
||||
_phase = Phase.AWAITING_CHOICE
|
||||
_candidates = [entry]
|
||||
return true
|
||||
var stop: Dictionary = {}
|
||||
@@ -249,7 +257,7 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool:
|
||||
break
|
||||
if stop.is_empty():
|
||||
push_error("[关卡节点地图] 断点恢复失败——停点节点编号 %d 不在本关图" % node_id)
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
return false
|
||||
node_index = nodes().find(stop)
|
||||
visited = []
|
||||
@@ -258,11 +266,11 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool:
|
||||
if _node_id(node) == int(vid):
|
||||
visited.append(node)
|
||||
break
|
||||
_awaiting_choice = true
|
||||
_phase = Phase.AWAITING_CHOICE
|
||||
_candidates = _out_nodes(stop)
|
||||
if _candidates.is_empty():
|
||||
push_error("[关卡节点地图] 断点恢复失败——停点节点 %d 无出边(非合法停点)" % node_id)
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -270,7 +278,7 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool:
|
||||
# 停点呈现(断点续玩路径——present_entry_choice 的中途节点版):信号顺序契约同入口
|
||||
# (呈现层须先经 level_started 重取 runner 实例)。零 RNG 消耗。
|
||||
func present_resumed_choice() -> bool:
|
||||
if not _running or not _awaiting_choice or _candidates.is_empty() or node_index < 0:
|
||||
if _phase != Phase.AWAITING_CHOICE or _candidates.is_empty() or node_index < 0:
|
||||
push_error("[关卡节点地图] present_resumed_choice 拒绝——非中途停点态")
|
||||
return false
|
||||
choices_presented.emit(_candidates)
|
||||
@@ -286,10 +294,7 @@ func restore_aux_items(items: Array) -> void:
|
||||
|
||||
|
||||
func _find_level_row(level_index: int) -> Dictionary:
|
||||
for row in db.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
return row
|
||||
return {}
|
||||
return db.level_row(level_index) # 表访问收口(2026-10-02——行集直扫归 LevelTable/ConfigDB 访问器)
|
||||
|
||||
|
||||
# ---- 模板池解析(关卡节点图生成 v0.4——M-3 迭代 5 T5)----
|
||||
@@ -411,11 +416,11 @@ func visited_ids() -> Array:
|
||||
|
||||
|
||||
func awaiting_choice() -> bool:
|
||||
return _awaiting_choice
|
||||
return _phase == Phase.AWAITING_CHOICE
|
||||
|
||||
|
||||
func _enter_node(node: Dictionary) -> bool:
|
||||
if not _running:
|
||||
if not _is_running():
|
||||
return false
|
||||
node_index = nodes().find(node)
|
||||
visited.append(node)
|
||||
@@ -490,7 +495,7 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
var hp_left := hero_hp if result == "victory" else 0
|
||||
if not _external:
|
||||
close_wallet() # 单关模式:关卡收束即冒险收束(GP 清零);冒险模式归 AdventureRunner._settle
|
||||
_running = false
|
||||
_phase = Phase.ENDED
|
||||
_settlement = null
|
||||
var aux_cleared := 0
|
||||
if aux_inventory != null:
|
||||
@@ -505,12 +510,17 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
_present_choices()
|
||||
|
||||
|
||||
# 恢复数学单一路径(漂移③清偿——同一公式两个落点:本体内置默认 + AdventureRunner.apply_rest,
|
||||
# 统一归并归后续批):floor(上限 × 比例 ÷ 100) 后封顶缺口;冒险结构 GDD 公式逐位一致
|
||||
func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int:
|
||||
# 恢复数学单一路径(漂移③清偿;2026-10-02 修复批——同式双落点收口:算术宿主 = 本静态方法,
|
||||
# AdventureRunner.apply_rest 同式委托,字面量双写退役)。floor(上限 × 比例 ÷ 100) 后封顶缺口;
|
||||
# 冒险结构 GDD 公式逐位一致
|
||||
static func rest_heal(pct: int, hp: int, max_hp: int) -> int:
|
||||
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
|
||||
|
||||
|
||||
func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int:
|
||||
return rest_heal(pct, hp, max_hp) # 公有成文出口保持(start_level 内置恢复执行体经 Callable 引用)
|
||||
|
||||
|
||||
func _apply_rest() -> int:
|
||||
var pct: int = int(level_row.get("rest_heal_pct", 0))
|
||||
# 接口③治疗封顶回报:单关模式 = 内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest
|
||||
@@ -528,7 +538,7 @@ func _present_choices() -> bool:
|
||||
if cands.is_empty():
|
||||
push_error("[关卡节点地图] 当前节点无出边且非 Boss(图合法性校验应已拦截)")
|
||||
return false
|
||||
_awaiting_choice = true
|
||||
_phase = Phase.AWAITING_CHOICE
|
||||
_candidates = cands
|
||||
choices_presented.emit(cands)
|
||||
return true
|
||||
@@ -536,11 +546,11 @@ func _present_choices() -> bool:
|
||||
|
||||
# 玩家选下一跳(候选外编号拒绝;Boss 恒无出边——选路只在中间节点发生)
|
||||
func pick_next_node(node_id: int) -> bool:
|
||||
if not _awaiting_choice:
|
||||
if _phase != Phase.AWAITING_CHOICE:
|
||||
return false
|
||||
for cand in _candidates:
|
||||
if _node_id(cand) == node_id:
|
||||
_awaiting_choice = false
|
||||
_phase = Phase.RUNNING
|
||||
_candidates = []
|
||||
return _enter_node(cand)
|
||||
return false
|
||||
@@ -563,7 +573,7 @@ func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
func grant_aux_item(item_id: String, source: String) -> Dictionary:
|
||||
if aux_inventory == null:
|
||||
aux_inventory = AuxItems.new(db)
|
||||
if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)):
|
||||
if not AuxItems.can_accept(1, aux_inventory.holding_count(), db): # 同调位守卫单点(2026-10-02)
|
||||
push_warning("[关卡节点地图] 同调位已满,授予拒绝: %s(%s)" % [item_id, source])
|
||||
return {}
|
||||
return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
|
||||
@@ -642,7 +652,8 @@ func buy_product(product_id: String) -> bool:
|
||||
return false
|
||||
var price := current_price(prow)
|
||||
if gp < price:
|
||||
return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝)
|
||||
push_warning("[关卡节点地图] 购买拒绝——余额不足: %s(需 %d,钱包 %d)" % [product_id, price, gp])
|
||||
return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝;2026-10-02 补静默留痕)
|
||||
gp -= price
|
||||
var flat := int(prow.get("flat_value", 0))
|
||||
bonus[attr] = int(bonus[attr]) + flat
|
||||
|
||||
@@ -74,7 +74,7 @@ func _workshop_progress(channel: String) -> int:
|
||||
match channel:
|
||||
"total_clears":
|
||||
var total := 0
|
||||
for i in db.level.rows.size():
|
||||
for i in db.level_count(): # 表访问收口(2026-10-02——行集直扫退役)
|
||||
total += save.clear_count(i + 1)
|
||||
return total
|
||||
_:
|
||||
|
||||
@@ -416,9 +416,10 @@ func _do_stop() -> Dictionary:
|
||||
# ---- 轮末旗判定 + 棋盘路径 round_end 外发 ----
|
||||
|
||||
func _emit_flags_batch() -> void:
|
||||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 战斗结算)
|
||||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性——排序键单点 BoardData.row_major_key,
|
||||
# 2026-10-02 修复批);被拔的不判;诚实重复上报(去重归 战斗结算)
|
||||
var cells: Array = _flags.keys()
|
||||
cells.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
|
||||
cells.sort_custom(func(a, b): return BoardData.row_major_key(a) < BoardData.row_major_key(b))
|
||||
for c in cells:
|
||||
flag_judged.emit({"cell": c, "hit": _board.is_mine(c)})
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func roll_board(eligible: Array) -> bool:
|
||||
if eligible.is_empty():
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
|
||||
return false
|
||||
# 行主序规范化 + 去重(排序责任不外推给 战斗结算)
|
||||
# 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key——2026-10-02 修复批)
|
||||
var seen := {}
|
||||
var list: Array[Dictionary] = []
|
||||
for e in eligible:
|
||||
@@ -51,7 +51,7 @@ func roll_board(eligible: Array) -> bool:
|
||||
continue
|
||||
seen[c] = true
|
||||
list.append({"cell": c})
|
||||
list.sort_custom(func(a, b): return a.cell.y * 1000 + a.cell.x < b.cell.y * 1000 + b.cell.x)
|
||||
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
|
||||
_eligible = list
|
||||
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
|
||||
_power_roll.clear()
|
||||
|
||||
@@ -62,8 +62,9 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
var hp_mult: float = float(mod.get("hp_mult", 1.0))
|
||||
var atk_mult: float = float(mod.get("atk_mult", 1.0))
|
||||
# unit_count_override ≥1 = 战斗结算 遭遇骰结果(敌人系统 零随机纪律——收到即确定值;-1 = 行值定值路径)
|
||||
# 数量域常量单点 = EnemyTable.UNIT_COUNT_MIN/MAX(2026-10-02 修复批——与表校验同源)
|
||||
var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count)
|
||||
if count < 1 or count > 3:
|
||||
if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX:
|
||||
push_warning("[敌人系统] 实例化数量越域: %d" % count)
|
||||
return false
|
||||
for i in range(count):
|
||||
|
||||
@@ -18,6 +18,10 @@ const BONUS_CAP_UNLIMITED := -1
|
||||
# —— 通关部分回血(固定型,非末关通关兑付)——
|
||||
@export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满
|
||||
|
||||
# —— 三选一候选数(2026-10-02 修复批 T2 魔法数结构化:自 adventure_runner 抽签字面量 3 收口进表;
|
||||
# 默认 3 = 现行行为零改动——加权洗牌不放回掷 N 的 N,改值须重扫三选一体验与牌库剔除节奏)——
|
||||
@export var bonus_candidate_count: int = 3 # [1,6] knob——候选数上界受 BONUS_IDS 词表封闭约束
|
||||
|
||||
# —— 三选一件值(兑付面)——
|
||||
@export var bonus_attack_flat: int = 1 # [0,999] knob——攻击轴累加(覆盖值通道)
|
||||
@export var bonus_max_hp_flat: int = 5 # [0,999] knob——生命上限轴累加(不连带当前生命)
|
||||
@@ -93,6 +97,8 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "bonus_cap(%s) 越域 [0,99]: %d" % [id, c]})
|
||||
if bonus_aux_item_count < 1 or bonus_aux_item_count > 9:
|
||||
errs.append({"category": "domain", "message": "bonus_aux_item_count 越域 [1,9]: %d" % bonus_aux_item_count})
|
||||
if bonus_candidate_count < 1 or bonus_candidate_count > BONUS_IDS.size():
|
||||
errs.append({"category": "domain", "message": "bonus_candidate_count 越域 [1,%d]: %d" % [BONUS_IDS.size(), bonus_candidate_count]})
|
||||
if transitional_sync_slots < 0 or transitional_sync_slots > 9:
|
||||
errs.append({"category": "domain", "message": "transitional_sync_slots 越域 [0,9]: %d" % transitional_sync_slots})
|
||||
return errs
|
||||
@@ -51,8 +51,8 @@ func validate() -> Array:
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝
|
||||
var n_mines := int(round(d * float(w * h - 9)))
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS——2026-10-02 修复批)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
|
||||
@@ -15,6 +15,11 @@ extends Resource
|
||||
# 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
|
||||
const ARMOR_TYPES := ["light", "ward", "heavy"]
|
||||
|
||||
# 实例化数量域(2026-10-02 修复批 T2 魔法数结构化——数量域 [1,3] 双落点收口单点:
|
||||
# 本表 validate 与 单位容器 spawn 守卫共用;遭遇骰结果与行值 unit_count/unit_count_max 同域)
|
||||
const UNIT_COUNT_MIN := 1
|
||||
const UNIT_COUNT_MAX := 3
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
@@ -39,13 +44,13 @@ func validate() -> Array:
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < 1 or uc > 3:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid})
|
||||
if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]})
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > 3:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),3]" % [eid, ucm, uc]})
|
||||
if ucm < uc or ucm > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
|
||||
@@ -20,6 +20,14 @@ extends Resource
|
||||
const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 商店房批 2026-09-21 四→五类(关卡节点地图 v1.1)
|
||||
|
||||
|
||||
# 行单点读取(2026-10-02 修复批 T5 表访问收口——LevelRunner._find_level_row 直扫收口;键 = level_index)
|
||||
func row_by_level_index(level_index: int) -> Dictionary:
|
||||
for row in rows:
|
||||
if int(row.level_index) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
if rows.is_empty():
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1/1.2 模拟编排独立成类)
|
||||
# 调整板批量模拟控制器:逐次驱动 LevelAutoplay(与 headless 扫描同口径)+ 进度/报告 setter 注入面板。
|
||||
# 调整板批量模拟控制器:逐次驱动 关卡自动策略(与 headless 扫描同口径)+ 进度/报告 setter 注入面板。
|
||||
# 职责边界:零核心写——表基线复位由组合根在移交前完成(_apply_hero_override(0,0)),
|
||||
# 加成施加 = LevelAutoplay 内部 runner.set_hero_override 单路径;报告文案装配归 HudPresenter。
|
||||
# 加成施加 = 自动策略内部 runner.set_hero_override 单路径;报告文案装配归 HudPresenter。
|
||||
# 组合根只转发 simulate_requested 信号。await 依赖面板所在场景树(进度逐帧刷新)。
|
||||
# 反向依赖解除(2026-10-02 修复批 T6):presentation → smoke 的 preload 编译期依赖拆除——
|
||||
# 驱动实例由调用方构造注入(_driver,须供 run_level(...) 与 smoke/level_autoplay 同款签名);
|
||||
# 构造参数缺省 null = 内部自建旧行为(运行时 load 兜底,同种子同口径零漂移)——main 侧接线归主控后续卡。
|
||||
extends RefCounted
|
||||
|
||||
const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放)
|
||||
const LevelAutoplay := preload("res://smoke/level_autoplay.gd") # 与 headless 扫描同口径的自动策略(P3:preload 编译期路径校验)
|
||||
const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量)
|
||||
|
||||
var db
|
||||
var panel # TunePanel setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report
|
||||
var _base_hp := 0 # 报告中的"基础 HP"口径注记值
|
||||
var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底)
|
||||
|
||||
|
||||
func _init(db_ref, panel_ref, base_hp: int) -> void:
|
||||
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
|
||||
db = db_ref
|
||||
panel = panel_ref
|
||||
_base_hp = base_hp
|
||||
_driver = driver
|
||||
|
||||
|
||||
# 驱动解析:注入优先;缺省回退运行时 load(旧行为——编译期反向依赖已解除,运行时口径与拆分前逐位一致)
|
||||
func _autoplay():
|
||||
if _driver != null:
|
||||
return _driver
|
||||
return load("res://smoke/level_autoplay.gd") # 与 headless 扫描同口径的自动策略(P3 兜底路径)
|
||||
|
||||
|
||||
func run(n: int) -> void:
|
||||
# 共享日志会话(2026-09-12 用户要求:模拟运行与真人试玩同日志合规——操作/轮次/战斗/通关全量 JSONL;
|
||||
# 一文件多局,run 行携带种子切分局次)
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
var logger = PlaytestLogger.new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(SEED_BASE)
|
||||
print("[tune_sim] 模拟日志: %s" % log_path)
|
||||
@@ -35,8 +47,9 @@ func run(n: int) -> void:
|
||||
var hp_boss: Array[float] = []
|
||||
var rounds_all: Array[int] = []
|
||||
var death_nodes := {}
|
||||
var autoplay = _autoplay()
|
||||
for i in range(n):
|
||||
var st = LevelAutoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
dmg_all.append(st.hero_dmg)
|
||||
rounds_all.append(st.rounds)
|
||||
if st.result == "victory":
|
||||
|
||||
Reference in new issue
Block a user