iter41-卡1:核心数据面结构收敛批:85键全清——三族拆域+config_db归位容器+存档拆件+meta瘦身+adventure拆段+spawn束化;行为零变化,8新件在阈

This commit is contained in:
DKChan committed 2026-10-29 01:33:50 +08:00
1 parent bda377eb16
commit a613a3cb90
157 files changed
+7453 -4288

No files matched your search

@@ -0,0 +1,15 @@
## 冒险表·已拿上限段(K4′ 拆段——curio_cap_* 9 槽随段件嵌套;段内槽名逐字不动;
## 概率与上限解耦〔奇物.md 池结构案D〕——概率位=档+令牌数、上限=本字段〔拿了才计、刷出不计〕;
## cap 满后该 kind 从候选视图剔除〔提前退役〕。99=实质无上限占位,实数归调参;专长 cap 在 feats.tres 行字段)。
class_name AdventureCapSegment
extends Resource
@export var curio_cap_attack: int = 99 # [1,99]
@export var curio_cap_max_hp: int = 99 # [1,99]
@export var curio_cap_armor: int = 99 # [1,99]
@export var curio_cap_gp: int = 99 # [1,99]
@export var curio_cap_ap: int = 99 # [1,99]
@export var curio_cap_heal: int = 99 # [1,99]
@export var curio_cap_refresh: int = 99 # [1,99]
@export var curio_cap_rap: int = 99 # [1,99]
@export var curio_cap_time_sand: int = 99 # [1,99]
@@ -0,0 +1,18 @@
## 冒险表·令牌配额段(K4′ 拆段——curio_quota_* 13 槽随段件嵌套;段内槽名逐字不动;
## 嵌套段单 .tres 保持——形态同 settlement_table 拆段既裁)。kind/tier 词表与配额结构锚留宿主。
class_name AdventureQuotaSegment
extends Resource
@export var curio_quota_attack_common: int = 18 # [0,99]
@export var curio_quota_attack_excellent: int = 5 # [0,99]
@export var curio_quota_max_hp_common: int = 17 # [0,99]
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
@export var curio_quota_armor_common: int = 17 # [0,99]
@export var curio_quota_armor_excellent: int = 5 # [0,99]
@export var curio_quota_gp_common: int = 18 # [0,99]
@export var curio_quota_gp_excellent: int = 5 # [0,99]
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(refresh 挤占 2 席位,总量 102 锚不变)
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(补给类成员;provisional 归曲线调参)
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档)
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(投放 provisional 归调参)
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(投放 provisional 归调参)
+40 -78
View File
@@ -45,49 +45,11 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 7, "legendary": 0}
@export var shop_refresh_cost_base: int = 20 # [1,999] knob(MC-1 provisional——GP 刷新基价)
@export var shop_refresh_cost_curve: float = 1.8 # [1.0,4.0] knob(MC-1 provisional——按已付费刷新数幂曲线,A 链同族)
# —— 令牌配额(kind × tier 计数;未列组合 = 0。四级档名 common/excellent/rare/legendary
# 承袭同一物理位序〔70/25/7/0〕;各 kind 现值见行注默认)——
@export var curio_quota_attack_common: int = 18 # [0,99]
@export var curio_quota_attack_excellent: int = 5 # [0,99]
@export var curio_quota_max_hp_common: int = 17 # [0,99]
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
@export var curio_quota_armor_common: int = 17 # [0,99]
@export var curio_quota_armor_excellent: int = 5 # [0,99]
@export var curio_quota_gp_common: int = 18 # [0,99]
@export var curio_quota_gp_excellent: int = 5 # [0,99]
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(refresh 挤占 2 席位,总量 102 锚不变)
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(补给类成员;provisional 归曲线调参)
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档)
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(投放 provisional 归调参)
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(投放 provisional 归调参)
# —— 令牌件值(小/大数值对——v2.2 令牌池口径定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)——
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
# —— 已拿上限 cap(概率与上限解耦〔奇物.md 池结构案D〕——案②令牌池+提前退役:
# 概率位=档+令牌数〔牌库物理张数〕、上限=本字段〔拿了才计、刷出不计〕;cap 满后该 kind 从候选
# 视图剔除〔提前退役〕。99=实质无上限占位,实数归调参;专长 cap 在 feats.tres 行字段〕)——
@export var curio_cap_attack: int = 99 # [1,99]
@export var curio_cap_max_hp: int = 99 # [1,99]
@export var curio_cap_armor: int = 99 # [1,99]
@export var curio_cap_gp: int = 99 # [1,99]
@export var curio_cap_ap: int = 99 # [1,99]
@export var curio_cap_heal: int = 99 # [1,99]
@export var curio_cap_refresh: int = 99 # [1,99]
@export var curio_cap_rap: int = 99 # [1,99]
@export var curio_cap_time_sand: int = 99 # [1,99]
# —— 列族三段件(K4′ 拆段——36 个列族标量槽随段件嵌套,段内槽名逐字不动;
# 嵌套段单 .tres 保持——形态同 settlement_table 拆段既裁)——
@export var quota_segment: AdventureQuotaSegment = AdventureQuotaSegment.new() # 令牌配额段(curio_quota_* 13)
@export var value_segment: AdventureValueSegment = AdventureValueSegment.new() # 令牌件值段(curio_value_* 13)
@export var cap_segment: AdventureCapSegment = AdventureCapSegment.new() # 已拿上限段(curio_cap_* 9)
# —— 专长获取专长凭证(非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
@@ -120,53 +82,53 @@ func curio_apply_route(kind: String) -> String:
## heal/refresh excellent、ap rare、凭证 kind rare 档)
func curio_quota(kind: String, tier: String) -> int:
match [kind, tier]:
["attack", TIER_COMMON]: return curio_quota_attack_common
["attack", TIER_EXCELLENT]: return curio_quota_attack_excellent
["max_hp", TIER_COMMON]: return curio_quota_max_hp_common
["max_hp", TIER_EXCELLENT]: return curio_quota_max_hp_excellent
["armor", TIER_COMMON]: return curio_quota_armor_common
["armor", TIER_EXCELLENT]: return curio_quota_armor_excellent
["gp", TIER_COMMON]: return curio_quota_gp_common
["gp", TIER_EXCELLENT]: return curio_quota_gp_excellent
["heal", TIER_EXCELLENT]: return curio_quota_heal_excellent
["refresh", TIER_EXCELLENT]: return curio_quota_refresh_excellent
["ap", TIER_RARE]: return curio_quota_ap_rare
["rap", TIER_RARE]: return curio_quota_rap_rare
["time_sand", TIER_RARE]: return curio_quota_time_sand_rare
["attack", TIER_COMMON]: return quota_segment.curio_quota_attack_common
["attack", TIER_EXCELLENT]: return quota_segment.curio_quota_attack_excellent
["max_hp", TIER_COMMON]: return quota_segment.curio_quota_max_hp_common
["max_hp", TIER_EXCELLENT]: return quota_segment.curio_quota_max_hp_excellent
["armor", TIER_COMMON]: return quota_segment.curio_quota_armor_common
["armor", TIER_EXCELLENT]: return quota_segment.curio_quota_armor_excellent
["gp", TIER_COMMON]: return quota_segment.curio_quota_gp_common
["gp", TIER_EXCELLENT]: return quota_segment.curio_quota_gp_excellent
["heal", TIER_EXCELLENT]: return quota_segment.curio_quota_heal_excellent
["refresh", TIER_EXCELLENT]: return quota_segment.curio_quota_refresh_excellent
["ap", TIER_RARE]: return quota_segment.curio_quota_ap_rare
["rap", TIER_RARE]: return quota_segment.curio_quota_rap_rare
["time_sand", TIER_RARE]: return quota_segment.curio_quota_time_sand_rare
return 0
## 令牌件值(kind × tier;四级构成随配额表)
func curio_value(kind: String, tier: String = TIER_COMMON) -> int:
match [kind, tier]:
["attack", TIER_COMMON]: return curio_value_attack_common
["attack", TIER_EXCELLENT]: return curio_value_attack_excellent
["max_hp", TIER_COMMON]: return curio_value_max_hp_common
["max_hp", TIER_EXCELLENT]: return curio_value_max_hp_excellent
["armor", TIER_COMMON]: return curio_value_armor_common
["armor", TIER_EXCELLENT]: return curio_value_armor_excellent
["gp", TIER_COMMON]: return curio_value_gp_common
["gp", TIER_EXCELLENT]: return curio_value_gp_excellent
["heal", TIER_EXCELLENT]: return curio_value_heal_excellent
["refresh", TIER_EXCELLENT]: return curio_value_refresh_excellent
["ap", TIER_RARE]: return curio_value_ap_rare
["rap", TIER_RARE]: return curio_value_rap_rare
["time_sand", TIER_RARE]: return curio_value_time_sand_rare
["attack", TIER_COMMON]: return value_segment.curio_value_attack_common
["attack", TIER_EXCELLENT]: return value_segment.curio_value_attack_excellent
["max_hp", TIER_COMMON]: return value_segment.curio_value_max_hp_common
["max_hp", TIER_EXCELLENT]: return value_segment.curio_value_max_hp_excellent
["armor", TIER_COMMON]: return value_segment.curio_value_armor_common
["armor", TIER_EXCELLENT]: return value_segment.curio_value_armor_excellent
["gp", TIER_COMMON]: return value_segment.curio_value_gp_common
["gp", TIER_EXCELLENT]: return value_segment.curio_value_gp_excellent
["heal", TIER_EXCELLENT]: return value_segment.curio_value_heal_excellent
["refresh", TIER_EXCELLENT]: return value_segment.curio_value_refresh_excellent
["ap", TIER_RARE]: return value_segment.curio_value_ap_rare
["rap", TIER_RARE]: return value_segment.curio_value_rap_rare
["time_sand", TIER_RARE]: return value_segment.curio_value_time_sand_rare
return 0
## 已拿上限读数(案②——kind 维度;专长 cap 走 feats.tres 行字段不走本面)
func curio_cap(kind: String) -> int:
match kind:
"attack": return curio_cap_attack
"max_hp": return curio_cap_max_hp
"armor": return curio_cap_armor
"gp": return curio_cap_gp
"ap": return curio_cap_ap
"heal": return curio_cap_heal
"refresh": return curio_cap_refresh
"rap": return curio_cap_rap
"time_sand": return curio_cap_time_sand
"attack": return cap_segment.curio_cap_attack
"max_hp": return cap_segment.curio_cap_max_hp
"armor": return cap_segment.curio_cap_armor
"gp": return cap_segment.curio_cap_gp
"ap": return cap_segment.curio_cap_ap
"heal": return cap_segment.curio_cap_heal
"refresh": return cap_segment.curio_cap_refresh
"rap": return cap_segment.curio_cap_rap
"time_sand": return cap_segment.curio_cap_time_sand
return 99
@@ -0,0 +1,18 @@
## 冒险表·令牌件值段(K4′ 拆段——curio_value_* 13 槽随段件嵌套;段内槽名逐字不动;
## 小/大数值对——v2.2 令牌池口径定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)。
class_name AdventureValueSegment
extends Resource
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
+93 -56
View File
@@ -19,63 +19,100 @@ func validate() -> Array:
var levels := {}
var anchor_ok := {}
for row in rows:
var L: int = int(row.get("level_index", 0))
var D: int = int(row.get("node_depth", -1))
var key := "%d_%d" % [L, D]
if levels.has(key):
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
levels[key] = true
if L < 1:
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
if D < 0:
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
var w: int = int(row.get("board_width", 0))
var h: int = int(row.get("board_height", 0))
if w < 8 or w > 64 or h < 8 or h > 64:
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
var d: float = float(row.get("mine_density", 0.0))
if d <= 0.0 or d > 0.5:
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
var t: float = float(row.get("inferability_target", 0.0))
if t < TARGET_ABS_FLOOR or t > 1.0:
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
if not SOLVER_RULES.has(row.get("solver_rules", "")):
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
if row.get("solver_rules", "") != "single_subset":
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
var ms: int = int(row.get("min_safe_count", 0))
if ms < 1 or ms > 999:
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
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)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
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 可行性
if w * h - n_mines < ms:
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
# F2c 成本可行性
if att * w * h > COST_BUDGET:
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
var bt: String = str(row.get("board_type", ""))
if not BOARD_TYPE_VOCABULARY.has(bt):
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
# 初始展开收窄两参数(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
var sr: int = int(row.get("shave_ring_radius", 1))
if sr < 0 or sr > 2:
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
var r0_cap: int = int(row.get("r0_max_cap", 0))
if r0_cap > 0:
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
if r0_cap < BoardData.SPAWN_AREA_CELLS:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
if r0_cap > (w * h) / 2:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
errs.append_array(_validate_row(row, levels, anchor_ok))
errs.append_array(_validate_row_constraints(row))
errs.append_array(_validate_shave_params(row))
for L in anchor_ok.keys():
if not anchor_ok[L]:
errs.append({"category": "anchor", "message": "level %d 缺 depth-0 锚行(EC15)" % L})
return errs
## 行域:键唯一与进度/尺寸/密度/推理目标/solver 规则冻结(levels/anchor_ok 引用累计)
func _validate_row(row: Dictionary, levels: Dictionary, anchor_ok: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var L: int = int(row.get("level_index", 0))
var D: int = int(row.get("node_depth", -1))
var key := "%d_%d" % [L, D]
if levels.has(key):
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
levels[key] = true
if L < 1:
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
if D < 0:
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
var w: int = int(row.get("board_width", 0))
var h: int = int(row.get("board_height", 0))
if w < 8 or w > 64 or h < 8 or h > 64:
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
var d: float = float(row.get("mine_density", 0.0))
if d <= 0.0 or d > 0.5:
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
var t: float = float(row.get("inferability_target", 0.0))
if t < TARGET_ABS_FLOOR or t > 1.0:
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
if not SOLVER_RULES.has(row.get("solver_rules", "")):
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
if row.get("solver_rules", "") != "single_subset":
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
return errs
## 约束域:min_safe/生成尝试预算/布雷数下限与可行性/成本护栏/图类型词表
func _validate_row_constraints(row: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var L: int = int(row.get("level_index", 0))
var D: int = int(row.get("node_depth", -1))
var w: int = int(row.get("board_width", 0))
var h: int = int(row.get("board_height", 0))
var d: float = float(row.get("mine_density", 0.0))
var ms: int = int(row.get("min_safe_count", 0))
if ms < 1 or ms > 999:
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
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)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
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 可行性
if w * h - n_mines < ms:
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
# F2c 成本可行性
if att * w * h > COST_BUDGET:
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
var bt: String = str(row.get("board_type", ""))
if not BOARD_TYPE_VOCABULARY.has(bt):
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
return errs
## 初始展开收窄域(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var L: int = int(row.get("level_index", 0))
var D: int = int(row.get("node_depth", -1))
var w: int = int(row.get("board_width", 0))
var h: int = int(row.get("board_height", 0))
var sr: int = int(row.get("shave_ring_radius", 1))
if sr < 0 or sr > 2:
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
var r0_cap: int = int(row.get("r0_max_cap", 0))
if r0_cap > 0:
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
if r0_cap < BoardData.SPAWN_AREA_CELLS:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
if r0_cap > (w * h) / 2:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 进度行解析委托(消费面 db.combat.board_gen.resolve_row——R1 ceil/clamp 语义归 ConfigDbResolver)
func resolve_row(L: int, D: int) -> Dictionary:
return _resolver.combat.board_gen.resolve_row(L, D)
+8
View File
@@ -28,3 +28,11 @@ func validate() -> Array:
if v < MULT_MIN or v > MULT_MAX:
errs.append({"category": "mult_oor_warning", "message": "%s (%s) 越域 [0.1,10] 使用时 clamp: %f" % [key, k, v]})
return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 难度修饰解析委托(消费面 db.run.difficulty.resolve_modifiers——clamp 语义归 ConfigDbResolver)
func resolve_modifiers(L: int, D: int) -> Dictionary:
return _resolver.run.difficulty.resolve_modifiers(L, D)
+142 -85
View File
@@ -44,98 +44,147 @@ func validate() -> Array:
var anchor_ok := {}
var boss_ok := {}
for row in rows:
var L: int = int(row.get("level_index", 0))
var rk: String = String(row.get("row_kind", ""))
var band: int = int(row.get("depth_band", -1))
var eid: String = row.get("enemy_id", "")
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
if seen.has(key):
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
seen[key] = true
if L < 1 or band < 0:
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
if not ROW_KINDS.has(rk):
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
if eid == "":
errs.append({"category": "domain", "message": "enemy_id 空"})
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 < 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 > 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})
var atk: int = int(row.get("atk", -1))
if atk < 0 or atk > 999:
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
# atk_min/atk_max(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
# 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
var has_amin: bool = row.has("atk_min")
var has_amax: bool = row.has("atk_max")
if has_amin != has_amax:
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
elif has_amin:
var amin: int = int(row.get("atk_min", 0))
var amax: int = int(row.get("atk_max", 0))
if amin < 0 or amax > 999 or amin > amax:
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
if atk != int((amin + amax) / 2.0):
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
# 池模板(单表结构校验;跨表构成纪律在 ConfigDB):
# 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
# 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
var pool: Array[Variant] = row.get("pool", [])
if pool.is_empty():
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
var pool_ids := {}
for entry_v in pool:
if not (entry_v is Dictionary):
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
continue
var entry: Dictionary[Variant, Variant] = entry_v
var sid: String = String(entry.get("skill_id", ""))
if sid == "":
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
elif pool_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
pool_ids[sid] = true
var w: int = int(entry.get("weight", 0))
if w <= 0 or w > 9999:
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
# armor_type(可省略——缺省 light 向后兼容)
var at: String = String(row.get("armor_type", "light"))
if not ARMOR_TYPES.has(at):
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
# attack_type(可省略,缺省 normal 向后兼容;写法沿用 armor_type 同款)
var att: String = String(row.get("attack_type", "normal"))
if not ATTACK_TYPES.has(att):
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
# armor_class(可省略——缺省 0 向后兼容;防守方掷点判定阈值基数)
var ac: int = int(row.get("armor_class", 0))
if ac < 0 or ac > 999:
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
errs.append_array(_validate_row_base(row, seen))
var eid2 := String(row.get("enemy_id", ""))
errs.append_array(_validate_row_numbers(row, eid2))
errs.append_array(_validate_row_atk_range(row, eid2))
errs.append_array(_validate_row_pool(row, eid2))
errs.append_array(_validate_row_types(row, eid2))
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
# 同带键多敌种混编合法;同带键同敌种第二行仍由
# dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。
var rowset_key := "%d_%s_%d" % [L, rk, band]
var rowset_key := "%d_%s_%d" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]
if rowset_key != last_rowset_key:
if closed_rowset.has(rowset_key):
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]})
errs.append_array(_validate_rowset_closed(rowset_key, closed_rowset, row))
closed_rowset[rowset_key] = true
last_rowset_key = rowset_key
if rk == "battle" and band == 0:
anchor_ok[L] = true
elif rk == "boss":
boss_ok[L] = true
if not anchor_ok.has(L):
anchor_ok[L] = false
var L2: int = int(row.get("level_index", 0))
var rk2: String = String(row.get("row_kind", ""))
if rk2 == "battle" and int(row.get("depth_band", -1)) == 0:
anchor_ok[L2] = true
elif rk2 == "boss":
boss_ok[L2] = true
if not anchor_ok.has(L2):
anchor_ok[L2] = false
errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok))
return errs
## 行头基面域:键提取/查重与登记/进度键越域/row_kind 枚举/enemy_id 非空/display_name
func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var L: int = int(row.get("level_index", 0))
var rk: String = String(row.get("row_kind", ""))
var band: int = int(row.get("depth_band", -1))
var eid: String = row.get("enemy_id", "")
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
if seen.has(key):
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
seen[key] = true
if L < 1 or band < 0:
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
if not ROW_KINDS.has(rk):
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
if eid == "":
errs.append({"category": "domain", "message": "enemy_id 空"})
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
return errs
## 数值域:unit_count(含可省略上限)/hp/atk 基值
func _validate_row_numbers(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var uc: int = int(row.get("unit_count", 0))
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 > 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})
var atk: int = int(row.get("atk", -1))
if atk < 0 or atk > 999:
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
return errs
## atk 区间域(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
## 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var atk: int = int(row.get("atk", -1))
var has_amin: bool = row.has("atk_min")
var has_amax: bool = row.has("atk_max")
if has_amin != has_amax:
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
elif has_amin:
var amin: int = int(row.get("atk_min", 0))
var amax: int = int(row.get("atk_max", 0))
if amin < 0 or amax > 999 or amin > amax:
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
if atk != int((amin + amax) / 2.0):
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
return errs
## 池模板域(单表结构校验;跨表构成纪律在 ConfigDB):
## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var pool: Array[Variant] = row.get("pool", [])
if pool.is_empty():
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
var pool_ids := {}
for entry_v in pool:
if not (entry_v is Dictionary):
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
continue
var entry: Dictionary[Variant, Variant] = entry_v
var sid: String = String(entry.get("skill_id", ""))
if sid == "":
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
elif pool_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
pool_ids[sid] = true
var w: int = int(entry.get("weight", 0))
if w <= 0 or w > 9999:
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
return errs
## 攻防类型域(均可省略——armor_type 缺省 light/attack_type 缺省 normal/armor_class 缺省 0 向后兼容)
func _validate_row_types(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var at: String = String(row.get("armor_type", "light"))
if not ARMOR_TYPES.has(at):
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
var att: String = String(row.get("attack_type", "normal"))
if not ATTACK_TYPES.has(att):
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
# armor_class(防守方掷点判定阈值基数)
var ac: int = int(row.get("armor_class", 0))
if ac < 0 or ac > 999:
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
return errs
## 行序违例域:当前行集键曾出现且 ≠ 上一行行集键 = 同带键行被隔断(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)
func _validate_rowset_closed(rowset_key: String, closed_rowset: Dictionary, row: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
if closed_rowset.has(rowset_key):
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]})
return errs
## 锚行尾段:每 level 必有 (battle, band 0) 锚行与 Boss 行集
func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
for L in anchor_ok.keys():
if not anchor_ok[L]:
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L})
@@ -158,3 +207,11 @@ static func band_of(band_keys: Array, D: int) -> int:
# 进度键解析单点收口(全仓统走
# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧),
# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移)
## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver)
func resolve_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
return _resolver.combat.enemy.resolve_rowset(L, D, row_kind)
+118 -60
View File
@@ -45,66 +45,101 @@ func validate() -> Array:
for row in routes: # 预扫:route_id→kind 全集(前置反查需前向引用——轻甲行在长枪行之后)
kind_by_id[String(row.get("route_id", ""))] = String(row.get("kind", ""))
for row in routes:
var rid := String(row.get("route_id", ""))
if not ROUTE_IDS.has(rid):
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
if seen.has(rid):
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
seen[rid] = true
var kind := String(row.get("kind", ""))
if not ["weapon", "armor"].has(kind):
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
var head_out := _validate_route_head(row, seen, type_seen)
errs.append_array(head_out["errs"])
if head_out["skip"]:
continue
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
var axis := String(row.get("type_axis", ""))
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
if not domain.has(axis):
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
var type_key := "%s|%s" % [kind, axis]
if type_seen.has(type_key):
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
type_seen[type_key] = true
# 等级/增量/价格/基础 AC 值域
var cap_v: int = int(row.get("cap", -1))
if cap_v < 1 or cap_v > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
var gain: int = int(row.get("per_level_gain", -1))
if gain < 0 or gain > 9:
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
var base_ac: int = int(row.get("base_ac", -1))
if kind == "armor":
if base_ac < 0 or base_ac > 999:
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
elif base_ac != 0:
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
var base_price: int = int(row.get("base_price", -1))
if base_price < 1 or base_price > 999:
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
var ratio: int = int(row.get("price_ratio_pct", -1))
if ratio < 100 or ratio > 500:
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
var unlock_price: int = int(row.get("unlock_price", -1))
if unlock_price < 0 or unlock_price > 999:
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
# 解锁通道(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置
var initial: bool = bool(row.get("initial_owned", false))
var prereq := String(row.get("prereq_route", ""))
if initial:
if prereq != "":
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
if unlock_price != 0:
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
initial_ids[rid] = kind
else:
if unlock_price < 1:
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
if not ROUTE_IDS.has(prereq):
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
elif prereq == rid:
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
# 初始持有恰两行且一武器一甲(长剑/轻甲)
var rid := String(row.get("route_id", ""))
errs.append_array(_validate_route_values(row, rid))
errs.append_array(_validate_route_unlock(row, rid, kind_by_id, initial_ids))
errs.append_array(_validate_initial_coverage(initial_ids))
return errs
## 行头域:route_id 词表与查重/kind 出域(skip=跳过行剩余判据)/类型轴域与每型恰一路线
func _validate_route_head(row: Dictionary, seen: Dictionary, type_seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var skip := false
var rid := String(row.get("route_id", ""))
if not ROUTE_IDS.has(rid):
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
if seen.has(rid):
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
seen[rid] = true
var kind := String(row.get("kind", ""))
if not ["weapon", "armor"].has(kind):
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
skip = true
return {"errs": errs, "skip": skip}
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
var axis := String(row.get("type_axis", ""))
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
if not domain.has(axis):
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
var type_key := "%s|%s" % [kind, axis]
if type_seen.has(type_key):
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
type_seen[type_key] = true
return {"errs": errs, "skip": skip}
## 数值域:等级/增量/价格/基础 AC 值域(甲型基础 AC 只属护甲路线)
func _validate_route_values(row: Dictionary, rid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var kind := String(row.get("kind", ""))
var cap_v: int = int(row.get("cap", -1))
if cap_v < 1 or cap_v > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
var gain: int = int(row.get("per_level_gain", -1))
if gain < 0 or gain > 9:
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
var base_ac: int = int(row.get("base_ac", -1))
if kind == "armor":
if base_ac < 0 or base_ac > 999:
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
elif base_ac != 0:
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
var base_price: int = int(row.get("base_price", -1))
if base_price < 1 or base_price > 999:
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
var ratio: int = int(row.get("price_ratio_pct", -1))
if ratio < 100 or ratio > 500:
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
var unlock_price: int = int(row.get("unlock_price", -1))
if unlock_price < 0 or unlock_price > 999:
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
return errs
## 解锁通道域(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置(武器←武器/甲←甲)
func _validate_route_unlock(row: Dictionary, rid: String, kind_by_id: Dictionary, initial_ids: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var kind := String(row.get("kind", ""))
var unlock_price: int = int(row.get("unlock_price", -1))
var initial: bool = bool(row.get("initial_owned", false))
var prereq := String(row.get("prereq_route", ""))
if initial:
if prereq != "":
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
if unlock_price != 0:
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
initial_ids[rid] = kind
else:
if unlock_price < 1:
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
if not ROUTE_IDS.has(prereq):
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
elif prereq == rid:
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
return errs
## 初始持有覆盖域(两尾段):初始持有恰两行且一武器一甲(长剑/轻甲)+
## 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
func _validate_initial_coverage(initial_ids: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var init_weapons := 0
var init_armors := 0
for k in initial_ids.keys():
@@ -114,7 +149,6 @@ func validate() -> Array:
init_armors += 1
if init_weapons != 1 or init_armors != 1:
errs.append({"category": "coverage", "message": "初始持有须恰一武器一甲(现 sword/light): 武器 %d / 甲 %d" % [init_weapons, init_armors]})
# 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
if init_weapons == 1 and init_armors == 1:
for row in routes:
if not bool(row.get("initial_owned", false)):
@@ -124,3 +158,27 @@ func validate() -> Array:
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
return errs
# ---- 行族访问器(自 ConfigDB 归位;消费面 db.crossrun.forge.<m> 直连)——
# routes 属性在册故行族读取命名 route_rows(避开成员名冲突)----
## 六路线行族(副本——调用方禁改)
func route_rows() -> Array:
return routes.duplicate()
## 路线行单点(键 route_id;缺行=空字典)
func route(route_id: String) -> Dictionary:
for row in routes:
if String(row.get("route_id", "")) == route_id:
return row
return {}
## 按类型轴反查护甲路线(armor_type → 路线行——入战 AC 三层之「甲型基础 AC」取数口)
func armor_route_by_type(armor_type: String) -> Dictionary:
for row in routes:
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) == armor_type:
return row
return {}
+69 -44
View File
@@ -1,5 +1,5 @@
## 关卡节点图参数档 schema(节点图生成器核心批 卡① T1):五旋钮+战斗保底+岔口去路数的行式数据表。
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.level_gen_params_row() 读取;生成器只吃参数档、不认关卡号(契约 6)。
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.run.level_gen_params.row_by_level_index() 读取;生成器只吃参数档、不认关卡号(契约 6)。
## 数值全 provisional(定值批落终值);域校验沿关卡节点图生成.md v1.1 调参表已锁条款——层深 L1≥6 递增增幅变大、
## 配比四类合计 1、出口 L1 恒 1/L2-L3 1-3、岔口去路数恒 2/3、战斗保底≥0。
class_name LevelGenParams
@@ -17,50 +17,13 @@ func validate() -> Array:
var seen := {}
var layers_by_level := {}
for row in rows:
var L: int = int(row.get("level_index", 0))
var key := "level %d" % L
if seen.has(L):
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
seen[L] = true
if L < 1:
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
var ks := (row as Dictionary).keys()
ks.sort()
var expect := ROW_KEYS.duplicate()
expect.sort()
if ks != expect:
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
var head := _validate_row_head(row, seen)
errs.append_array(head["errs"])
if not head["ok"]:
continue
var layers: int = int(row.get("layers", 0))
if layers < 6:
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
layers_by_level[L] = layers
var bd: float = float(row.get("branch_density", -1.0))
if bd < 0.0 or bd > 1.0:
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
var ratio_sum := 0.0
for rk in RATIO_KEYS:
var v: float = float(row.get(rk, -1.0))
if v < 0.0 or v > 1.0:
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
ratio_sum += v
if not is_equal_approx(ratio_sum, 1.0):
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
var entry_count: int = int(row.get("entry_count", 0))
if entry_count < 1:
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
var exit_count: int = int(row.get("exit_count", 0))
if exit_count < 1 or exit_count > 3:
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
if L == 1 and exit_count != 1:
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
var bf: int = int(row.get("battle_floor", -1))
if bf < 0:
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
var fmin: int = int(row.get("fork_ways_min", 0))
var fmax: int = int(row.get("fork_ways_max", 0))
if fmin != 2 or fmax != 3:
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
errs.append_array(_validate_row_shape(row, head["L"], head["key"], layers_by_level))
errs.append_array(_validate_row_counts(row, head["L"], head["key"]))
# 层深递进尾段(L1<L2<L3 且增幅变大——层深 L1≥6 递增增幅变大已锁)
if layers_by_level.has(1) and layers_by_level.has(2) and layers_by_level.has(3):
var d12: int = int(layers_by_level[2]) - int(layers_by_level[1])
var d23: int = int(layers_by_level[3]) - int(layers_by_level[2])
@@ -72,6 +35,68 @@ func validate() -> Array:
return errs
## 行头域:level_index 查重/关卡序号越域/字段键集闭集(ok=跳过行剩余判据)
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var L: int = int(row.get("level_index", 0))
var key := "level %d" % L
if seen.has(L):
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
seen[L] = true
if L < 1:
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
var ks := (row as Dictionary).keys()
ks.sort()
var expect := ROW_KEYS.duplicate()
expect.sort()
if ks != expect:
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
return {"errs": errs, "L": L, "key": key, "ok": false}
return {"errs": errs, "L": L, "key": key, "ok": true}
## 行形态域:层深下限(layers_by_level 引用登记——层深递进尾段取数)/分支密度/配比四类合计 1
func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level: Dictionary) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var layers: int = int(row.get("layers", 0))
if layers < 6:
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
layers_by_level[L] = layers
var bd: float = float(row.get("branch_density", -1.0))
if bd < 0.0 or bd > 1.0:
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
var ratio_sum := 0.0
for rk in RATIO_KEYS:
var v: float = float(row.get(rk, -1.0))
if v < 0.0 or v > 1.0:
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
ratio_sum += v
if not is_equal_approx(ratio_sum, 1.0):
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
return errs
## 行计数域:入口/出口(L1 恒 1)/战斗保底/岔口去路数锁值
func _validate_row_counts(row: Dictionary, L: int, key: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var entry_count: int = int(row.get("entry_count", 0))
if entry_count < 1:
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
var exit_count: int = int(row.get("exit_count", 0))
if exit_count < 1 or exit_count > 3:
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
if L == 1 and exit_count != 1:
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
var bf: int = int(row.get("battle_floor", -1))
if bf < 0:
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
var fmin: int = int(row.get("fork_ways_min", 0))
var fmax: int = int(row.get("fork_ways_max", 0))
if fmin != 2 or fmax != 3:
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
return errs
# 参数档单点读取(键 = level_index,缺行返回空字典——沿 level_row() 体例)
func row_by_level_index(level_index: int) -> Dictionary:
for row in rows:
+5
View File
@@ -170,3 +170,8 @@ static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> v
if (outs[nid] as Array).has(cur) and not seen.has(int(nid)):
seen[int(nid)] = true
queue.append(int(nid))
## 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径)
func level_count() -> int:
return rows.size()
+40 -5
View File
@@ -18,7 +18,15 @@ func validate() -> Array:
for family in ["boss_keystone", "conversion", "attune_refresh_upgrade"]:
if get(family).is_empty():
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
errs.append_array(_validate_boss_keystone())
errs.append_array(_validate_conversion())
errs.append_array(_validate_attune_refresh())
return errs
## boss_keystone 行族域:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
func _validate_boss_keystone() -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var seen_li := {}
for row in boss_keystone:
var li: int = int(row.get("level_index", 0))
@@ -36,9 +44,13 @@ func validate() -> Array:
if li_at != i + 1:
errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]})
break
# 无 workshop_items 行族(解锁混合制——条件谓词挂 treasure_items 行内;
# 本表不校验秘宝解锁,单点=TreasureItemTable.validate)
# conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
return errs
## conversion 行族域(属性换算):attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
## (无 workshop_items 行族——解锁混合制:条件谓词挂 treasure_items 行内;本表不校验秘宝解锁,单点=TreasureItemTable.validate)
func _validate_conversion() -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var seen_conv := {}
for row in conversion:
var cattr: String = row.get("attr", "")
@@ -54,7 +66,12 @@ func validate() -> Array:
for conv_key in FORGE_ATTRS:
if not seen_conv.has(conv_key):
errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key})
# attune_refresh_upgrade:level 唯一 + 连续从 1 + charges/cost 域
return errs
## attune_refresh_upgrade 行族域(Y 刷新升级):level 唯一 + 连续从 1 + charges/cost 域
func _validate_attune_refresh() -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var seen_al := {}
for row in attune_refresh_upgrade:
var al: int = int(row.get("level", -1))
@@ -73,3 +90,21 @@ func validate() -> Array:
errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]})
break
return errs
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.crossrun.meta.<m> 直连)----
## Boss 钥石两档行单点(键 level_index;缺行=空字典)
func boss_keystone_row(level_index: int) -> Dictionary:
for row in boss_keystone:
if int(row.get("level_index", -1)) == level_index:
return row
return {}
## Y 刷新升级行单点(键 = 等级,缺行返回空字典=未购/已满级合法态)
func attune_refresh_row(level: int) -> Dictionary:
for row in attune_refresh_upgrade:
if int(row.get("level", -1)) == level:
return row
return {}
+94 -53
View File
@@ -39,57 +39,105 @@ func validate() -> Array:
errs.append({"category": "dup_key", "message": "node_id 重复: %s" % nid})
continue
seen[nid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % nid})
if not VEINS.has(String(row.get("vein", ""))):
errs.append({"category": "enum", "message": "%s vein 出域" % nid})
var kind := String(row.get("kind", ""))
if not KINDS.has(kind):
errs.append({"category": "enum", "message": "%s kind 出域" % nid})
if not CURRENCIES.has(String(row.get("currency", ""))):
errs.append({"category": "enum", "message": "%s currency 出域" % nid})
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]})
var cap := int(row.get("cap", 1))
if cap < 1 or cap > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]})
if kind in ["unlock", "socket", "fork", "easter"] and cap != 1:
errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]})
var price := int(row.get("price", 0))
if price < 0 or price > 9999:
errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]})
errs.append_array(_validate_row_head(row, nid))
errs.append_array(_validate_row_numbers(row, nid))
errs.append_array(_validate_row_effects(row, nid))
errs.append_array(_validate_row_fork(row, nid, fork_seen))
errs.append_array(_validate_requires_dangling(rows, seen))
errs.append_array(_validate_requires_level(rows, seen))
# 彩蛋 ≤3
var easter := 0
for row in rows:
if String(row.get("kind", "")) == "easter":
easter += 1
if easter > 3:
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(上限 3)" % easter})
return errs
## 行头域:命名与枚举判据(display_name/vein/kind/currency/行键白名单)
func _validate_row_head(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % nid})
if not VEINS.has(String(row.get("vein", ""))):
errs.append({"category": "enum", "message": "%s vein 出域" % nid})
var kind := String(row.get("kind", ""))
if not KINDS.has(kind):
errs.append({"category": "enum", "message": "%s kind 出域" % nid})
if not CURRENCIES.has(String(row.get("currency", ""))):
errs.append({"category": "enum", "message": "%s currency 出域" % nid})
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]})
return errs
## 数值域:cap 越域/kind×cap 交叉/price 越域
func _validate_row_numbers(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
var kind := String(row.get("kind", ""))
var cap := int(row.get("cap", 1))
if cap < 1 or cap > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]})
if kind in ["unlock", "socket", "fork", "easter"] and cap != 1:
errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]})
var price := int(row.get("price", 0))
if price < 0 or price > 9999:
errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]})
return errs
## 效果域:effects 键词表封闭与局外树层级纪律(archetype 禁入)
func _validate_row_effects(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
for ek in fx.keys():
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
if fx.has("archetype"):
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
return errs
## Fork 结构域:一节点双态结构判据(fork_group 词表/组唯一/主 effects 须空/a/b 双键规则面)
func _validate_row_fork(row: Dictionary, nid: String, fork_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var kind := String(row.get("kind", ""))
if kind == "fork":
var fg := String(row.get("fork_group", ""))
if not FORK_GROUPS.has(fg):
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
if fork_seen.has(fg):
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
fork_seen[fg] = true
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
for ek in fx.keys():
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
if fx.has("archetype"):
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
# Fork 一节点双态结构:fork 行必带 fork_group(每组恰一节点);主 effects 须空,效果只放
# a/b 双键且键 ⊆ RULE_KEYS(A/B 只放规则面——层级纪律)
if kind == "fork":
var fg := String(row.get("fork_group", ""))
if not FORK_GROUPS.has(fg):
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
if fork_seen.has(fg):
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
fork_seen[fg] = true
if not fx.is_empty():
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
for skey in ["effects_a", "effects_b"]:
var sd: Dictionary[Variant, Variant] = row.get(skey, {}) as Dictionary
if sd.is_empty():
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
for k in sd.keys():
if not RULE_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
# requires 悬空引用
if not fx.is_empty():
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
for skey in ["effects_a", "effects_b"]:
var sd: Dictionary[Variant, Variant] = row.get(skey, {}) as Dictionary
if sd.is_empty():
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
for k in sd.keys():
if not RULE_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
return errs
## requires 悬空引用域
func _validate_requires_dangling(rows: Array, seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
for row in rows:
for req in (row.get("requires", []) as Array):
if not seen.has(String(req)):
errs.append({"category": "dangling", "message": "%s requires 悬空: %s" % [String(row.get("node_id", "")), String(req)]})
# requires_level 等级阈值前置(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
return errs
## requires_level 等级阈值前置域(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
func _validate_requires_level(rows: Array, seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
for row in rows:
var nid2: String = String(row.get("node_id", ""))
var rl_v: Variant = row.get("requires_level", {})
@@ -101,13 +149,6 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "%s requires_level 阈值须 ≥1: %s=%d" % [nid2, String(req_id), int(rl_v[req_id])]})
elif row.has("requires_level"):
errs.append({"category": "domain", "message": "%s requires_level 须为 Dictionary {node_id: n}" % nid2})
# 彩蛋 ≤3
var easter := 0
for row in rows:
if String(row.get("kind", "")) == "easter":
easter += 1
if easter > 3:
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(上限 3)" % easter})
return errs
+114 -69
View File
@@ -30,84 +30,129 @@ func validate() -> Array:
if seen.has(rid):
errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid})
seen[rid] = true
var ctype: String = row.get("cell_type", "")
if not CELL_TYPES.has(ctype):
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
var pol: String = row.get("polarity", "")
if not POLARITIES.has(pol):
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
var atk_type: String = String(row.get("attack_type", ""))
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
# aux 投放行专属校验:极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
# 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable)
if ctype == TREASURE_ITEM_TYPE:
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
# effect_id 空/power 0/weight 0 不入池)
if pol == "negative":
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
continue
if String(row.get("effect_id", "")) == "":
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
if int(row.get("power", 0)) != 0 or row.has("power_min"):
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
var pool: String = row.get("pool_tag", "")
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
if not POOL_TAGS.has(pool) and not pool_reserved:
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
elif pool_reserved:
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
var power: int = int(row.get("power", 0))
var weight: int = int(row.get("weight", -1))
var tier: int = int(row.get("tier", 0))
if weight < 0 or weight > 999:
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
if tier < 1 or tier > 8:
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
var stacks: int = int(row.get("stacks", 1))
if stacks < 1 or stacks > 16:
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
# 跨字段:effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
# flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级)。
var eff: String = row.get("effect_id", "")
if power < 0 or power > 999:
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
if eff == "" and power < 1:
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
# 区间配对(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
var has_min: bool = row.has("power_min")
var has_max: bool = row.has("power_max")
if has_min != has_max:
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
elif has_min:
var pmin: int = int(row.get("power_min", 0))
var pmax: int = int(row.get("power_max", 0))
if pmin < 1 or pmax > 999 or pmin > pmax:
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
if eff != "":
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
if power != int((pmin + pmax) / 2.0):
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
errs.append_array(_validate_row_axes(row, rid))
var aux_out := _validate_row_aux(row, rid)
errs.append_array(aux_out["errs"])
if aux_out["skip"]:
continue
errs.append_array(_validate_row_pool_tag(row, rid))
errs.append_array(_validate_row_values(row, rid))
errs.append_array(_validate_row_power_range(row, rid))
# 字典完整性 / 池护栏累计(cell 池行)
if pool == "cell" and CELL_TYPES.has(ctype) and POLARITIES.has(pol):
cell_dict["%s|%s" % [ctype, pol]] = true
if pol == "positive" and weight > 0:
if String(row.get("pool_tag", "")) == "cell" and CELL_TYPES.has(String(row.get("cell_type", ""))) and POLARITIES.has(String(row.get("polarity", ""))):
cell_dict["%s|%s" % [String(row.get("cell_type", "")), String(row.get("polarity", ""))]] = true
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", -1)) > 0:
cell_positive_live += 1
if pool == "clear_default" and eff == "" and power >= 1:
if String(row.get("pool_tag", "")) == "clear_default" and String(row.get("effect_id", "")) == "" and int(row.get("power", 0)) >= 1:
clear_value_ok = true
# ① 字典完整性:五型 × 正负两行齐备(缺任一即致命)
errs.append_array(_validate_table_closure(cell_dict, cell_positive_live, clear_value_ok))
return errs
## 轴域:cell_type 五型封闭/polarity 两极/attack_type 可省略枚举
func _validate_row_axes(row: Dictionary, rid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var ctype: String = row.get("cell_type", "")
if not CELL_TYPES.has(ctype):
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
var pol: String = row.get("polarity", "")
if not POLARITIES.has(pol):
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
var atk_type: String = String(row.get("attack_type", ""))
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
return errs
## aux 投放行域(掉落投放行):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
## 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable);
## 负占位行返回 skip=true(跳过行剩余判据——原 continue 语义)
func _validate_row_aux(row: Dictionary, rid: String) -> Dictionary:
var errs: Array[Dictionary] = []
var skip := false
if String(row.get("cell_type", "")) == TREASURE_ITEM_TYPE:
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
# effect_id 空/power 0/weight 0 不入池)
if String(row.get("polarity", "")) == "negative":
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
skip = true
return {"errs": errs, "skip": skip}
if String(row.get("effect_id", "")) == "":
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
if int(row.get("power", 0)) != 0 or row.has("power_min"):
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
return {"errs": errs, "skip": skip}
## 池标签域:pool_tag 词表与 clear_<biome> 命名空间预留拒绝/display_name 非空
func _validate_row_pool_tag(row: Dictionary, rid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var pool: String = row.get("pool_tag", "")
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
if not POOL_TAGS.has(pool) and not pool_reserved:
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
elif pool_reserved:
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
return errs
## 数值域:weight/tier/stacks/power 越域与 flat 行 power ≥ 1
## (effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
## flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级))
func _validate_row_values(row: Dictionary, rid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var power: int = int(row.get("power", 0))
var weight: int = int(row.get("weight", -1))
var tier: int = int(row.get("tier", 0))
if weight < 0 or weight > 999:
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
if tier < 1 or tier > 8:
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
var stacks: int = int(row.get("stacks", 1))
if stacks < 1 or stacks > 16:
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
var eff: String = row.get("effect_id", "")
if power < 0 or power > 999:
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
if eff == "" and power < 1:
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
return errs
## 区间配对域(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
func _validate_row_power_range(row: Dictionary, rid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var power: int = int(row.get("power", 0))
var eff: String = row.get("effect_id", "")
var has_min: bool = row.has("power_min")
var has_max: bool = row.has("power_max")
if has_min != has_max:
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
elif has_min:
var pmin: int = int(row.get("power_min", 0))
var pmax: int = int(row.get("power_max", 0))
if pmin < 1 or pmax > 999 or pmin > pmax:
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
if eff != "":
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
if power != int((pmin + pmax) / 2.0):
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
return errs
## 表闭合域(三尾段):①九型字典完整性(类型×极性齐备)②池空护栏 ③clear_default 恒正当量
func _validate_table_closure(cell_dict: Dictionary, cell_positive_live: int, clear_value_ok: bool) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
for ctype in CELL_TYPE_KEYS:
for pol in POLARITIES:
if not cell_dict.has("%s|%s" % [ctype, pol]):
errs.append({"category": "cross_field", "message": "九型字典缺行: %s %s——(类型, 极性) 齐备为加载前提" % [ctype, pol]})
# ② 池空护栏:cell 池 ≥1 正向行 weight>0(摇号池非空)
if cell_positive_live < 1:
errs.append({"category": "cross_field", "message": "cell 池零活正向行(weight>0)——摇号池空"})
# ③ clear_default 恒正当量(行动经济 条款④——清图奖励 > 0 的行级兑现口径)
if not clear_value_ok:
errs.append({"category": "cross_field", "message": "clear_default 池无正当量活行(清图奖励 > 0 违例)"})
return errs
+52
View File
@@ -133,3 +133,55 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void:
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
if spend_discount <= 0.0 or spend_discount > 1.0:
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop.<m> 直连)----
## GP 掉落行单点(键 tier;缺行=空字典——掉落按 0 兑付的兜底在调用方)
func gp_drop_row(tier: String) -> Dictionary:
for row in gp_drops:
if String(row.get("tier", "")) == tier:
return row
return {}
## 关卡 GP 加成单点(键 level_index;缺行=0)
func level_gp_bonus_at(level_index: int) -> int:
for row in level_gp_bonus:
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
## 价格关卡加成单点(键 level_index;缺行=0)
func price_level_bonus_at(level_index: int) -> int:
for row in price_level_bonus:
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
## 商品行族({product_id, display_name, attr, flat_value, base_price, price_increment})
func product_rows() -> Array:
return products
## 商品行单点(键 product_id;缺行=空字典)
func product_row(product_id: String) -> Dictionary:
for row in products:
if String(row.get("product_id", "")) == product_id:
return row
return {}
## 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
func pool_rows() -> Array:
return pool_shelf
## 货架行单点(键 kind_id;缺行=空字典)
func pool_row(kind_id: String) -> Dictionary:
for row in pool_shelf:
if String(row.get("kind_id", "")) == kind_id:
return row
return {}
+92 -57
View File
@@ -31,73 +31,108 @@ func validate() -> Array:
var seen := {}
var has_basic := false
for row in rows:
var sid: String = row.get("skill_id", "")
if sid == "":
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
var head := _validate_row_head(row, seen)
errs.append_array(head["errs"])
if not head["ok"]:
continue
if seen.has(sid):
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
seen[sid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
var type: String = row.get("type", "")
if not SKILL_TYPES.has(type):
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
continue
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
if type == "basic" and row.has("trigger"):
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
# 冷却域(全类型):施放后连续 N 拍不可摇中
var cd: int = int(row.get("cooldown_beats", -1))
if cd < 0 or cd > 99:
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
var sid: String = head["sid"]
var type: String = head["type"]
_validate_common_fields(sid, row, type, errs)
if type == "basic":
has_basic = true
_validate_basic_row(sid, row, cd, errs)
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
_validate_basic_row(sid, row, int(row.get("cooldown_beats", -1)), errs)
if type == "damage":
if not row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
else:
var m: float = float(row.damage_mult)
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
if row.has("buff_ref") or row.has("buff_duration"):
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
if row.has("rider_mult"):
var rm: float = float(row.rider_mult)
if rm <= 0.0 or rm > RIDER_MULT_MAX:
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
_validate_damage_row(sid, row, errs)
if type == "buff":
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
var bd: int = int(row.get("buff_duration", 0))
if bd < 1 or bd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
# 妨害技:0 伤害施放动作,对主角施加负面状态——
# 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
_validate_buff_row(sid, row, errs)
if type == "debuff":
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
var dbd: int = int(row.get("buff_duration", 0))
if dbd < 1 or dbd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
_validate_debuff_row(sid, row, errs)
_validate_table_invariants(has_basic, errs)
return errs
## 行头域:skill_id 非空与查重/display_name/type 枚举(ok=跳过行剩余判据)
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var sid: String = row.get("skill_id", "")
if sid == "":
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
return {"errs": errs, "sid": sid, "type": "", "ok": false}
if seen.has(sid):
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
seen[sid] = true
return {"errs": errs, "sid": sid, "type": "", "ok": false}
seen[sid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
var type: String = row.get("type", "")
if not SKILL_TYPES.has(type):
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
return {"errs": errs, "sid": sid, "type": type, "ok": false}
return {"errs": errs, "sid": sid, "type": type, "ok": true}
## 公共域(全类型):trigger 触发条件校验/普攻行禁配 trigger/冷却域
func _validate_common_fields(sid: String, row: Dictionary, type: String, errs: Array[Dictionary]) -> void:
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
if type == "basic" and row.has("trigger"):
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
# 冷却域(全类型):施放后连续 N 拍不可摇中
var cd: int = int(row.get("cooldown_beats", -1))
if cd < 0 or cd > 99:
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
## 伤害技域:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
## 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
func _validate_damage_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
if not row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
else:
var m: float = float(row.damage_mult)
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
if row.has("buff_ref") or row.has("buff_duration"):
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
if row.has("rider_mult"):
var rm: float = float(row.rider_mult)
if rm <= 0.0 or rm > RIDER_MULT_MAX:
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
## 增益技域:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
func _validate_buff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
var bd: int = int(row.get("buff_duration", 0))
if bd < 1 or bd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
## 妨害技域:0 伤害施放动作,对主角施加负面状态——
## 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
func _validate_debuff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
var dbd: int = int(row.get("buff_duration", 0))
if dbd < 1 or dbd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
## 表不变量域:普攻行存在性(池构成纪律的结构前提)与保底旋钮值域
func _validate_table_invariants(has_basic: bool, errs: Array[Dictionary]) -> void:
if not has_basic:
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
if pity_threshold < 1 or pity_threshold > 99:
errs.append({"category": "domain", "message": "pity_threshold 越域 [1,99]: %d" % pity_threshold})
return errs
# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq
+43 -23
View File
@@ -18,30 +18,12 @@ func validate() -> Array:
return errs
var seen := {}
for row in rows:
var eid: String = row.get("effect_id", "")
if eid == "":
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
var head := _validate_row_head(row, seen)
errs.append_array(head["errs"])
if not head["ok"]:
continue
if seen.has(eid):
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
seen[eid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
if not TARGET_SIDES.has(row.get("target_side", "")):
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
var unit: String = row.get("unit", "")
if not UNITS.has(unit):
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
if not STACK_POLICIES.has(row.get("stack_policy", "")):
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
var m: int = int(row.get("magnitude", 0))
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 / ac ∈ [-20,20] 且 ≠0(AC 阈值轴——幅度域独立,防呆上限 20)
if unit == "percent" and (m == 0 or m < -100 or m > 100):
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
if unit == "flat" and m < 1:
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
if unit == "ac" and (m == 0 or m < -20 or m > 20):
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
var eid: String = head["eid"]
errs.append_array(_validate_magnitude_domain(row, eid, head["unit"]))
var dur: int = int(row.get("duration_settlements", 0))
if dur < 1 or dur > 99:
errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]})
@@ -51,6 +33,44 @@ func validate() -> Array:
return errs
## 行头域:effect_id 非空与查重/display_name/target_side/unit/stack_policy 词表
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var eid: String = row.get("effect_id", "")
if eid == "":
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
if seen.has(eid):
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
seen[eid] = true
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
seen[eid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
if not TARGET_SIDES.has(row.get("target_side", "")):
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
var unit: String = row.get("unit", "")
if not UNITS.has(unit):
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
if not STACK_POLICIES.has(row.get("stack_policy", "")):
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
return {"errs": errs, "eid": eid, "unit": unit, "ok": true}
## 幅度量纲域(三段量纲判据——percent/flat/ac 各自值域;
## ac = AC 阈值轴效果,非池百分比非定点值——幅度域独立,防呆上限 20)
func _validate_magnitude_domain(row: Dictionary, eid: String, unit: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var m: int = int(row.get("magnitude", 0))
if unit == "percent" and (m == 0 or m < -100 or m > 100):
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
if unit == "flat" and m < 1:
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
if unit == "ac" and (m == 0 or m < -20 or m > 20):
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
return errs
func row_by_id(effect_id: String) -> Dictionary:
for row in rows:
if row.get("effect_id", "") == effect_id:
+185 -126
View File
@@ -73,136 +73,195 @@ func validate() -> Array:
errs.append({"category": "dup_key", "message": "item_id 重复: %s" % iid})
continue
seen[iid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
var retired: bool = bool(row.get("retired", false))
var deferred: bool = bool(row.get("deferred", false))
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
var tags: Array[Variant] = row.get("dimension_tags", [])
if tags.is_empty():
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
for tag in tags:
if not DIMENSIONS.has(String(tag)):
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
var kinds: Array[Variant] = row.get("effect_kinds", [])
var target: String = String(row.get("target", "none"))
if not TARGETS.has(target):
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
var phase: String = String(row.get("use_phase", "active_dig"))
if not USE_PHASES.has(phase):
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
# triggers 事件订阅行(被动件形态;与 effect_kinds 互斥校验
# 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
# (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
if row.has("triggers"):
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
var form: String = String(row.get("form", ""))
if not FORMS.has(form):
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
if retired or deferred:
errs.append_array(_validate_row_head(row, iid))
if bool(row.get("retired", false)) or bool(row.get("deferred", false)):
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
continue
# ---- 活行校验 ----
if kinds.is_empty() and not row.has("triggers"):
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
var kind_seen := {}
for k in kinds:
var kind: String = String(k)
if not EFFECT_KINDS.has(kind):
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
if kind_seen.has(kind):
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
kind_seen[kind] = true
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
for rv in REVEAL_KINDS:
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
if target == "board_wide" and kind_seen.has("mine_reveal"):
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
# magnitude:MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
var mag: int = int(row.get("magnitude", 0))
if mag < 0 or mag > 999:
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
var mag_consuming := false
for k in kind_seen.keys():
if MAGNITUDE_KINDS.has(String(k)):
mag_consuming = true
if mag_consuming and mag < 1:
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
errs.append_array(_validate_row_active(row, iid, kind_seen))
var form: String = String(row.get("form", ""))
if form != "":
if int(row.get("slot_cost", 1)) != 1:
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
var tier: String = String(row.get("tier", ""))
if not TIERS.has(tier):
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
var charges: int = int(row.get("charges_per_adventure", 0))
if form == "passive" and charges != 0:
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges < 0:
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges == 0:
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
var has_gate := kind_seen.has("ap_charge_safe")
if not has_gate and row.has("triggers"):
for tr_v in (row.get("triggers", []) as Array):
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
has_gate = true
break
if not has_gate:
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
var creset: String = String(row.get("charge_reset", "adventure"))
if not CHARGE_RESETS.has(creset):
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
var price: int = int(row.get("unlock_keystone_cost", 0))
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
if price != 0 and ureq_count > 0:
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
if price < 0 or price > 999:
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
var eff_rounds: int = int(row.get("effective_rounds", 0))
if eff_rounds < 0 or eff_rounds > 999:
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
var charge_n: int = int(row.get("charge_n", 0))
if charge_n < 0 or charge_n > 999:
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
if kind_seen.has("ap_charge_safe") and charge_n < 1:
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
# unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举
var ureqs: Array[Variant] = row.get("unlock_requires", [])
var ukeys := {}
for req in ureqs:
var ukey: String = String(req.get("key", ""))
if not UNLOCK_COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
if ukeys.has(ukey):
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
ukeys[ukey] = true
var uth: int = int(req.get("threshold", 0))
if uth < 1 or uth > 999:
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
errs.append_array(_validate_row_attunement(row, iid, kind_seen))
errs.append_array(_validate_row_unlock_costs(row, iid))
errs.append_array(_validate_row_effect_numbers(row, iid, kind_seen))
errs.append_array(_validate_row_unlock_requires(row, iid))
return errs
## 行头域:display_name/维度标签/target/use_phase/行键白名单/triggers 订阅形态/form 词表
func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
var tags: Array[Variant] = row.get("dimension_tags", [])
if tags.is_empty():
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
for tag in tags:
if not DIMENSIONS.has(String(tag)):
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
var target: String = String(row.get("target", "none"))
if not TARGETS.has(target):
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
var phase: String = String(row.get("use_phase", "active_dig"))
if not USE_PHASES.has(phase):
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
errs.append_array(_validate_row_triggers(row, iid))
var form: String = String(row.get("form", ""))
if not FORMS.has(form):
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
return errs
## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验
## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if row.has("triggers"):
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
return errs
## 活行域:effect_kinds 非空与词表/真视窄域与 board_wide 承重护栏/magnitude 语义(kind_seen 引用填充——同调域消费)
func _validate_row_active(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var kinds: Array[Variant] = row.get("effect_kinds", [])
var target: String = String(row.get("target", "none"))
if kinds.is_empty() and not row.has("triggers"):
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
for k in kinds:
var kind: String = String(k)
if not EFFECT_KINDS.has(kind):
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
if kind_seen.has(kind):
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
kind_seen[kind] = true
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
for rv in REVEAL_KINDS:
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
if target == "board_wide" and kind_seen.has("mine_reveal"):
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
errs.append_array(_validate_row_magnitude(row, iid, kind_seen))
return errs
## N 值语义域:magnitude 越域与 MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
func _validate_row_magnitude(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var mag: int = int(row.get("magnitude", 0))
if mag < 0 or mag > 999:
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
var mag_consuming := false
for k in kind_seen.keys():
if MAGNITUDE_KINDS.has(String(k)):
mag_consuming = true
if mag_consuming and mag < 1:
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
return errs
## 同调充能与档位域(form 非空——装配面;过渡行 form="" 不校验):槽位统一/档位必填/充能值域与积点制受理闸/充能重置周期
func _validate_row_attunement(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
if int(row.get("slot_cost", 1)) != 1:
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
var tier: String = String(row.get("tier", ""))
if not TIERS.has(tier):
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
var charges: int = int(row.get("charges_per_adventure", 0))
var form: String = String(row.get("form", ""))
if form == "passive" and charges != 0:
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges < 0:
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges == 0:
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
errs.append_array(_validate_row_charge_gate(row, iid, kind_seen))
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
var creset: String = String(row.get("charge_reset", "adventure"))
if not CHARGE_RESETS.has(creset):
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
return errs
## 积点制受理闸域:charges=0 的 active_charged 行必须带使用闸(ap_charge_safe 滑耗或 counter_spend 受理闸)
func _validate_row_charge_gate(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var has_gate := kind_seen.has("ap_charge_safe")
if not has_gate and row.has("triggers"):
for tr_v in (row.get("triggers", []) as Array):
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
has_gate = true
break
if not has_gate:
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
return errs
## 解锁互斥域(form 非空行,契约 6):钥石购买价与条件谓词互斥——每件单解锁方式,混合不做
func _validate_row_unlock_costs(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
var price: int = int(row.get("unlock_keystone_cost", 0))
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
if price != 0 and ureq_count > 0:
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
if price < 0 or price > 999:
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
return errs
## 效果数值域(form 非空行):effective_rounds 与 charge_n 的值域及 kind 交叉
func _validate_row_effect_numbers(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var eff_rounds: int = int(row.get("effective_rounds", 0))
if eff_rounds < 0 or eff_rounds > 999:
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
var charge_n: int = int(row.get("charge_n", 0))
if charge_n < 0 or charge_n > 999:
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
if kind_seen.has("ap_charge_safe") and charge_n < 1:
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
return errs
## 谓词行域:unlock_requires 计数键封闭枚举与阈值(空 = 已解锁——首批全解锁合法态,底稿 §2.2)
func _validate_row_unlock_requires(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
var ureqs: Array[Variant] = row.get("unlock_requires", [])
var ukeys := {}
for req in ureqs:
var ukey: String = String(req.get("key", ""))
if not UNLOCK_COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
if ukeys.has(ukey):
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
ukeys[ukey] = true
var uth: int = int(req.get("threshold", 0))
if uth < 1 or uth > 999:
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
return errs
+109 -49
View File
@@ -26,57 +26,82 @@ func validate() -> Array:
var errs: Array[Dictionary] = []
var seen := {}
for row in rows:
var vid: String = row.get("variant_id", "")
if vid == "":
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
var head := _validate_row(row, seen)
errs.append_array(head["errs"])
if not head["ok"]:
continue
if seen.has(vid):
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
var vid: String = head["vid"]
errs.append_array(_validate_effect_interlocks(row, vid))
errs.append_array(_validate_global_interlocks(row, vid))
return errs
## 行域:variant_id 非空与查重/display_name/unlock_level/四词表(scope/effect/trigger/placement)
func _validate_row(row: Dictionary, seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var vid: String = row.get("variant_id", "")
if vid == "":
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
return {"errs": errs, "vid": vid, "ok": false}
if seen.has(vid):
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
seen[vid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
var ul: int = int(row.get("unlock_level", 0))
if ul < 1 or ul > 999:
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
if not SCOPES.has(row.get("scope", "")):
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
if not EFFECTS.has(row.get("effect", "")):
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
if not TRIGGERS.has(row.get("trigger", "")):
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
if not PLACEMENTS.has(row.get("placement", "")):
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
# 跨字段互锁(加载期致命——枚举语义一致性立闸):
# 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数
if row.get("effect", "") == "flat_damage":
if row.get("trigger", "") != "on_open":
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
if row.get("placement", "") != "closed_only":
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
var ev: int = int(row.get("effect_value", 0))
if ev < 1 or ev > 9:
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
# 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
# 信息遮蔽型只允许落安全格(遮蔽对象是数字;危险格无数字可遮)——placement=any 时由读取方
# 呈现面按 kind=safe 自然短路,此处不设布置约束
# 全局作用域型互锁(resource_face ⟺ scope=global 三条 + 新值域;
# 判定键=effect/scope 词值,既有六 region 行(flat_damage/hide_number)不入本组——零误伤)
if row.get("effect", "") == "resource_face":
if row.get("scope", "") != "global":
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
# resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
var gev: int = int(row.get("effect_value", 0))
if gev < 0 or gev > 50:
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
if row.get("scope", "") == "global":
if row.get("effect", "") != "resource_face":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
if row.get("placement", "") != "any":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
if row.get("trigger", "") != "always":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
return {"errs": errs, "vid": vid, "ok": false}
seen[vid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
var ul: int = int(row.get("unlock_level", 0))
if ul < 1 or ul > 999:
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
if not SCOPES.has(row.get("scope", "")):
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
if not EFFECTS.has(row.get("effect", "")):
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
if not TRIGGERS.has(row.get("trigger", "")):
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
if not PLACEMENTS.has(row.get("placement", "")):
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
return {"errs": errs, "vid": vid, "ok": true}
## 效果互锁域(加载期致命——枚举语义一致性立闸):
## 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数;
## 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
## (信息遮蔽型只允许落安全格——遮蔽对象是数字,危险格无数字可遮;placement=any 时由读取方
## 呈现面按 kind=safe 自然短路,此处不设布置约束)
func _validate_effect_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
if row.get("effect", "") == "flat_damage":
if row.get("trigger", "") != "on_open":
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
if row.get("placement", "") != "closed_only":
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
var ev: int = int(row.get("effect_value", 0))
if ev < 1 or ev > 9:
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
return errs
## 全局作用域互锁域(resource_face ⟺ scope=global;判定键=effect/scope 词值,
## 既有六 region 行(flat_damage/hide_number)不入本组——零误伤);
## resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
func _validate_global_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
if row.get("effect", "") == "resource_face":
if row.get("scope", "") != "global":
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
var gev: int = int(row.get("effect_value", 0))
if gev < 0 or gev > 50:
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
if row.get("scope", "") == "global":
if row.get("effect", "") != "resource_face":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
if row.get("placement", "") != "any":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
if row.get("trigger", "") != "always":
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
return errs
@@ -85,3 +110,38 @@ func row_by_id(variant_id: String) -> Dictionary:
if row.get("variant_id", "") == variant_id:
return row
return {}
# ---- 候选池访问器(自 ConfigDB 归位;消费面 db.combat.variation.<m> 直连)----
# 门控第三维 = 节点身份(全局作用域变异):scope=global 行仅 boss/elite 节点入池,
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
## 候选池门控(∅ board_types = 通配;字段名对齐 board_type 词表,无 biome_tags)
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
var pool: Array[Dictionary] = []
for row in rows:
if String(row.get("scope", "region")) == "global" \
and node_type != "boss" and node_type != "elite":
continue
if int(row.get("unlock_level", 999)) > level_index:
continue
var tags: Array[Variant] = row.get("board_types", [])
if tags.is_empty():
pool.append(row)
continue
if tags.has(board_type):
pool.append(row)
return pool
## 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
## boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
## 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
if node_type != "boss" and node_type != "elite":
return {}
for row in variant_pool(level_index, board_type, node_type):
if String(row.get("scope", "region")) == "global":
return row
return {}
+20 -24
View File
@@ -1,11 +1,12 @@
[gd_resource type="Resource" script_class="AdventureTable" load_steps=2 format=3]
[gd_resource type="Resource" script_class="AdventureTable" load_steps=5 format=3]
[ext_resource type="Script" path="res://data/schemas/adventure_table.gd" id="1"]
[ext_resource type="Script" path="res://data/schemas/adventure_quota_segment.gd" id="2"]
[ext_resource type="Script" path="res://data/schemas/adventure_value_segment.gd" id="3"]
[ext_resource type="Script" path="res://data/schemas/adventure_cap_segment.gd" id="4"]
[resource]
script = ExtResource("1")
clear_heal_pct = 20
curio_candidate_count = 3
[sub_resource type="Resource" id="AdventureTable_quota"]
script = ExtResource("2")
curio_quota_attack_common = 18
curio_quota_attack_excellent = 5
curio_quota_max_hp_common = 17
@@ -17,6 +18,9 @@ curio_quota_gp_excellent = 5
curio_quota_heal_excellent = 3
curio_quota_refresh_excellent = 2
curio_quota_ap_rare = 3
[sub_resource type="Resource" id="AdventureTable_value"]
script = ExtResource("3")
curio_value_attack_common = 1
curio_value_attack_excellent = 2
curio_value_max_hp_common = 5
@@ -28,26 +32,18 @@ curio_value_gp_excellent = 50
curio_value_heal_excellent = 6
curio_value_refresh_excellent = 1
curio_value_ap_rare = 1
[sub_resource type="Resource" id="AdventureTable_cap"]
script = ExtResource("4")
[resource]
script = ExtResource("1")
clear_heal_pct = 20
curio_candidate_count = 3
quota_segment = SubResource("AdventureTable_quota")
value_segment = SubResource("AdventureTable_value")
cap_segment = SubResource("AdventureTable_cap")
feat_ticket_chance = 20
feat_ticket_quota = 2
feat_ticket_limit = 1
feat_ticket_pity_step = 5
; 迭代 20 T2 池结构案D 换代(2026-10-11,奇物.md 池结构与抽取 v1):四级档位 70/25/7/0 = 102 枚——
; 稀有档投放三 kind=AP 件 3/储备金袋 2/时沙 2(ap_rare 显式载于本表,与 schema 缺省同值);
; 概率与上限解耦(案②令牌池+提前退役):概率位=档+令牌数、已拿上限=curio_cap_*(拿了才计);
; 曝光槽=三选一 2 格全池+1 格优秀档以上子池(子池空回退全池);凭证不进子池。
; 前史 TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
; 洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭;
; 种类次数上限退役(池消耗即上限;货架库存同账本=牌库剩余令牌数)。
; 档位构成(现行):普通〔小GP 18/小增伤 18/小护甲 17/小生命 17〕+ 优秀〔大GP/大增伤/大护甲/大生命 各 5+恢复 3+刷新件 2〕
; + 稀有〔AP 件 3/储备金袋 2/时沙 2〕。
; 数值对:小GP 20/大GP 50、小增伤 +1/大 +2、小护甲 +1/大 +2、小生命 +5/大 +10、恢复 +6(当即回血)、AP +1。
; 恢复件 = 新入池种类(优秀档)。
; 退役字段:bonus_weight_*(E-S 权重族)/ bonus_cap_*(种类次数上限族)/ BONUS_CAP_UNLIMITED 哨兵——
; 洗牌改令牌 Fisher-Yates(不放回抽 3,消费 reward_pick 封闭流)。
; 迭代 11 第②批 商店共享池注记:货架 = 令牌级抽签(同 kind 去重每 kind 至多一卡、价格公式 tier-blind
; ceil(基价×b^(关序−1)×A^visit) 维持批②裁定、购买消费在架令牌本体)。
; 末关的关卡完成不发三选一与回血(奖励只朝下一战生效)。
; 全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss;档位配额结构锚 70/25/7/0——
; schema TIER_QUOTAS,validate 硬断言,改总量须重裁)。