Files
gplan-2/game/data/schemas/variation_table.gd
T

148 lines
8.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 {}