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

94 lines
6.5 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
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# 数据配置 VariationTable schema(地形变异 首批类型定稿批 2026-09-19;行 = Dictionary,键 variant_id)。
# 字段集(2026-09-19 用户裁定:枚举抽象,不写死类型):effect/effect_value/trigger/placement 四字段
# 把"变异是什么、在哪出现、何时起作用"全部数据化——新增类型 = 清单加行 + 必要时扩充枚举词表,不改读取方。
# biome_tags 更名 board_types(用户裁定:门控匹配对象即 board_type 词表,biome 系旧观念残留不沿用);
# counter_items 字段位保留(反制道具绑定,归 Alpha 反制批回填,切片期恒空)。
# info_mask/cell_constraint 两旧字段随枚举化退役(hide_number 并入 effect 词表,safe_nonzero 由
# 布置域结构保障——初始展开区天然排除,见 battle_orchestrator._place_variants)。
class_name VariationTable
extends Resource
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
# 布置参数(表头字段;数量公式 = round(候选池格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus,
# 连通片生长、单片 ≤ pack_max——2026-09-19 用户拍板:图规模百分比 × 关卡递增,全连通、单片不超 4;
# 2026-09-21 用户拍板基数修正:百分比基数 = 候选池(安全格 − 初始展开区,即实际可挖格)而非总格数)
@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 = 资源面偏斜(全局作用域变异首发批 2026-10-04——整图修摇号分布:生存族按 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 vid: String = row.get("variant_id", "")
if vid == "":
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
continue
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")]})
# 跨字段互锁(加载期致命——枚举语义一致性随首批类型定稿批 2026-09-19 立闸):
# 伤害型 = 挖开即伤 ⇒ 触发 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 自然短路,此处不设布置约束
# 全局作用域型互锁(2026-10-04 首发批——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
func row_by_id(variant_id: String) -> Dictionary:
for row in rows:
if row.get("variant_id", "") == variant_id:
return row
return {}