76 lines
4.9 KiB
GDScript
76 lines
4.9 KiB
GDScript
# 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"]
|
||
const EFFECTS := ["none", "flat_damage", "hide_number"] # 效果词表:直伤 / 数字遮蔽(可扩充)
|
||
const TRIGGERS := ["on_open", "on_number_shown"] # 触发词表:被打开即触发 / 打开且显示数字时触发(可扩充)
|
||
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 自然短路,此处不设布置约束
|
||
return errs
|
||
|
||
|
||
func row_by_id(variant_id: String) -> Dictionary:
|
||
for row in rows:
|
||
if row.get("variant_id", "") == variant_id:
|
||
return row
|
||
return {}
|