122 lines
6.4 KiB
GDScript
122 lines
6.4 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||
# Date: 2026-09-10
|
||
# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
|
||
# HeroTable 单行全局(Rule 4 豁免)。依据 design/gdd/systems/战斗循环/敌人系统.md Schema 节。
|
||
# 敌人技能池批(2026-09-16 用户拍板,设计权威 docs/proposal-skill-pool.md v1.0):pattern 行为环数组与
|
||
# heavy_multiplier 退役,改池模板 pool = [{skill_id, weight}](定义层——实例池快照与冷却状态在 单位容器)。
|
||
# 池构成纪律(每池恰一行普攻、增益 ≤1、skill_id 悬空致命)需跨表视野,校验在 ConfigDB 跨表通道。
|
||
class_name EnemyTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
|
||
# 护甲类型封闭枚举(攻击类型 × 护甲克制批——克制矩阵本体在 SettlementTable;
|
||
# 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
|
||
const ARMOR_TYPES := ["light", "ward", "heavy"]
|
||
|
||
|
||
func validate() -> Array:
|
||
var errs: Array[Dictionary] = []
|
||
if rows.is_empty():
|
||
errs.append({"category": "empty_table", "message": "EnemyTable 为空"})
|
||
return errs
|
||
var seen := {}
|
||
var rowset_species := {}
|
||
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 eid: String = row.get("enemy_id", "")
|
||
var key := "%d_%d_%s" % [L, D, eid]
|
||
if seen.has(key):
|
||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||
seen[key] = true
|
||
if L < 1 or D < 0:
|
||
errs.append({"category": "domain", "message": "进度键越域 (%d,%d)" % [L, D]})
|
||
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 < 1 or uc > 3:
|
||
errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid})
|
||
# 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 > 3:
|
||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),3]" % [eid, ucm, uc]})
|
||
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(可省略配对——2026-09-14 敌方攻击区间批):遭遇骰同流 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 = 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 = 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]})
|
||
# armor_class(可省略——缺省 0 向后兼容;2026-09-16 护甲等级掷点批:防守方掷点判定阈值基数)
|
||
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]})
|
||
# 每行集恰一敌种(行集内 enemy_id 唯一)
|
||
var rk := "%d_%d" % [L, D]
|
||
if rowset_species.has(rk):
|
||
errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集出现第二敌种(每行集恰一敌种违例)" % [L, D]})
|
||
rowset_species[rk] = eid
|
||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||
for L in anchor_ok.keys():
|
||
if not anchor_ok[L]:
|
||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 depth-0 锚行" % L})
|
||
return errs
|
||
|
||
|
||
# R1:进度键 floor + 行集恰一敌种(实体键无歧义,悬空致命由调用方处理)
|
||
func resolve_rowset(L: int, D: int) -> Dictionary:
|
||
var best: Dictionary[String, Variant] = {}
|
||
var best_l := -1
|
||
var best_d := -1
|
||
for row in rows:
|
||
var rl: int = int(row.get("level_index", 0))
|
||
var rd: int = int(row.get("node_depth", 0))
|
||
if rl <= L and rd <= D and (rl > best_l or (rl == best_l and rd > best_d)):
|
||
best.assign(row)
|
||
best_l = rl
|
||
best_d = rd
|
||
return best
|