iter41-卡1:核心数据面结构收敛批:85键全清——三族拆域+config_db归位容器+存档拆件+meta瘦身+adventure拆段+spawn束化;行为零变化,8新件在阈
This commit is contained in:
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 归调参)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 {}
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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 硬断言,改总量须重裁)。
|
||||
Reference in new issue
Block a user