148 lines
8.1 KiB
GDScript
148 lines
8.1 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 地形变异表 schema:逐 variant_id 行的类型化读取(空清单=零变异合法态;垂直切片验证:完整战斗逐行归因)。
|
||
## 数据驱动扩展契约:新增类型=表加行+必要时扩枚举词表,不改读取方。
|
||
## 跨字段互锁(效果↔触发↔布置↔作用域)违例加载期致命(fail-fast)。
|
||
class_name VariationTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
# 布置参数(表头字段;数量公式 = round(候选池格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus,
|
||
# 连通片生长、单片 ≤ pack_max——图规模百分比 × 关卡递增,全连通、单片不超 4;
|
||
# 百分比基数 = 候选池(安全格 − 初始展开区,即实际可挖格)而非总格数)
|
||
@export var percent_of_cells: int = 5
|
||
@export var per_level_bonus: int = 2
|
||
@export var pack_max: int = 4
|
||
|
||
const SCOPES := ["region", "global"]
|
||
# resource_face = 资源面偏斜(全局作用域变异——整图修摇号分布:生存族按 effect_value%
|
||
# 下调、进攻族等比上浮重归一,不动任何格子数据;effect_value 语义随行域切换,不适用 flat_damage [1,9])
|
||
const EFFECTS := ["none", "flat_damage", "hide_number", "resource_face"] # 效果词表:直伤 / 数字遮蔽 / 资源面偏斜(可扩充)
|
||
const TRIGGERS := ["on_open", "on_number_shown", "always"] # 触发词表:被打开即触发 / 打开且显示数字时触发 / 整图恒在无触发时机(可扩充)
|
||
const PLACEMENTS := ["closed_only", "any"] # 布置约束词表:只落布置时未开格 / 任意格(可扩充)
|
||
|
||
|
||
func validate() -> Array:
|
||
# 空清单是合法状态(地形变异 P0)——零行即零变体格,配置加载零错误、零警告
|
||
var errs: Array[Dictionary] = []
|
||
var seen := {}
|
||
for row in rows:
|
||
var head := _validate_row(row, seen)
|
||
errs.append_array(head["errs"])
|
||
if not head["ok"]:
|
||
continue
|
||
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
|
||
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
|
||
|
||
|
||
func row_by_id(variant_id: String) -> Dictionary:
|
||
for row in rows:
|
||
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 {}
|