Files
gplan-2/game/data/schemas/forge_table.gd
T
DKChan df666ddf30 feat(迭代23-T1): 锻造工坊配置表落位——ForgeTable 新表(默认两轴常驻持有+六路线行族)+ConfigDB 访问器+V1/V11 单测 41 断言
- 新表 forge.tres(schema forge_table.gd):default_attack_type/armor_type 默认值(契约 2 所有权移交——hero.tres 两轴随后批删除)+routes 六行(sword/spear/staff/light/heavy/ward)
- provisional 沿需求卡片 T1 锚表:cap 5/每级增量 +1/基础价 20×150%^n(20/30/45/67/101)/解锁价 200/甲型基础 AC 10-12-9/初始持有 sword+light/并联双通道前置 spear·staff←sword、heavy·ward←light
- schema validate 22 类正反(行 id 唯一/类型轴分族枚举/恰六行/初始持有恰一武器一甲/默认轴=初始路线类型轴/前置类型相符…)
- ConfigDB:TABLE_MANIFEST 登 forge+类型化持用+forge_route/forge_routes/forge_default_*/forge_weapon_route_by_type/forge_armor_route_by_type 访问器
- 新单测 forge_table_test 41 断言(V1 生产面+22 类负向/V11 行字段闭集+效果类型词表 31 钉现/V2 价格梯);run_all 22→23 套
- 事实订正(L2):卡片 F11「效果类型闭集 24」系 2026-10-07 旧快照——现行 31(iter20 +7/iter22 +1),本批零扩签钉 31
- 轻闸:冒烟 898/0 零扰动
2026-10-12 23:23:16 +08:00

132 lines
8.4 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
# Date: 2026-10-12(迭代 23 锻造工坊实装批 T1——设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6)
# 锻造工坊配置表(ForgeTable):两根类型轴默认值(常驻持有——契约 2)+ 六条锻造路线行族(三武器+三甲)。
# 所有权切分(契约 1 红线):攻击唯一入口=武器路线、AC 唯一入口=甲路线——树零攻防来源,本表是攻防
# 数值成长的唯一数据面。等级语义(Q1 用户裁):未解锁/未锻造=0 加值(裸体态,类型=默认轴值);
# 解锁并锻造第一级=「获得装备」=1 级=首个 +1;逐级递进(2 级=+2 量级)。
# 数值全 provisional(需求卡片 T1 锚表投值——终值归 M4 数值批):cap 5/每级增量统一 +1(v1.2 用户裁)/
# 基础价 20/公比 150%(各级价格 20/30/45/67/101)/一次性解锁价 200/甲型基础 AC 10-12-9(现役结构值)。
# 表运行期只读(铁律):永久强化只写存档(forge_routes 域),本表零写面。
class_name ForgeTable
extends Resource
# 路线 id 词表(代拍板立项:甲路线 id=甲型值本身——ward=魔法护甲,3a 裁统一命名)
const WEAPON_IDS := ["sword", "spear", "staff"]
const ARMOR_IDS := ["light", "heavy", "ward"]
const ROUTE_IDS := ["sword", "spear", "staff", "light", "heavy", "ward"]
# 两根类型轴枚举域(hero.tres 静态表两轴随本批删除——词表归本表常驻持有)
const ATTACK_TYPES := ["normal", "pierce", "magic"]
const ARMOR_TYPES := ["light", "ward", "heavy"]
# 行字段闭集(V11:满级特效本期不做——行族无特效参数位;键集出域即拒)
const ROW_KEYS := ["route_id", "display_name", "kind", "type_axis", "cap", "per_level_gain",
"base_ac", "base_price", "price_ratio_pct", "initial_owned", "prereq_route", "unlock_price"]
@export var default_attack_type: String = "normal" # 攻击类型轴默认值(常驻持有——与柜台解锁状态解耦:未点亮=无交互入口但默认值照常注入)
@export var default_armor_type: String = "light" # 甲型轴默认值(常驻持有)
@export var routes: Array[Dictionary] = [] # 六路线行族(键 route_id)
# 各级价格(几何递增:基础价 × 150%^已购级数——锚表 20/30/45/67/101;1.5^n 二进制精确,无浮点尘)
static func level_price(row: Dictionary, owned_levels: int) -> int:
var base := int(row.get("base_price", 0))
var ratio := float(int(row.get("price_ratio_pct", 100))) / 100.0
return int(floor(float(base) * pow(ratio, maxi(0, owned_levels))))
func validate() -> Array:
var errs: Array[Dictionary] = []
# 默认两轴枚举域
if not ATTACK_TYPES.has(default_attack_type):
errs.append({"category": "enum", "message": "default_attack_type 出域 %s: %s" % [ATTACK_TYPES, default_attack_type]})
if not ARMOR_TYPES.has(default_armor_type):
errs.append({"category": "enum", "message": "default_armor_type 出域 %s: %s" % [ARMOR_TYPES, default_armor_type]})
# 行族规模恰六(三武器+三甲——需求卡片 §1①)
if routes.size() != 6:
errs.append({"category": "structural", "message": "锻造路线行族规模 %d ≠ 6(三武器+三甲)" % routes.size()})
var seen := {}
var type_seen := {}
var kind_by_id := {}
var initial_ids := {}
for row in routes: # 预扫:route_id→kind 全集(前置反查需前向引用——轻甲行在长枪行之后)
kind_by_id[String(row.get("route_id", ""))] = String(row.get("kind", ""))
for row in routes:
var rid := String(row.get("route_id", ""))
if not ROUTE_IDS.has(rid):
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
if seen.has(rid):
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
seen[rid] = true
var kind := String(row.get("kind", ""))
if not ["weapon", "armor"].has(kind):
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
continue
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
var axis := String(row.get("type_axis", ""))
var domain: Array = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
if not domain.has(axis):
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
var type_key := "%s|%s" % [kind, axis]
if type_seen.has(type_key):
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
type_seen[type_key] = true
# 等级/增量/价格/基础 AC 值域
var cap_v: int = int(row.get("cap", -1))
if cap_v < 1 or cap_v > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
var gain: int = int(row.get("per_level_gain", -1))
if gain < 0 or gain > 9:
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
var base_ac: int = int(row.get("base_ac", -1))
if kind == "armor":
if base_ac < 0 or base_ac > 999:
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
elif base_ac != 0:
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
var base_price: int = int(row.get("base_price", -1))
if base_price < 1 or base_price > 999:
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
var ratio: int = int(row.get("price_ratio_pct", -1))
if ratio < 100 or ratio > 500:
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
var unlock_price: int = int(row.get("unlock_price", -1))
if unlock_price < 0 or unlock_price > 999:
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
# 解锁通道(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置
var initial: bool = bool(row.get("initial_owned", false))
var prereq := String(row.get("prereq_route", ""))
if initial:
if prereq != "":
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
if unlock_price != 0:
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
initial_ids[rid] = kind
else:
if unlock_price < 1:
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
if not ROUTE_IDS.has(prereq):
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
elif prereq == rid:
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
# 初始持有恰两行且一武器一甲(长剑/轻甲——语义继承自已退役属性锻造行)
var init_weapons := 0
var init_armors := 0
for k in initial_ids.keys():
if String(initial_ids[k]) == "weapon":
init_weapons += 1
elif String(initial_ids[k]) == "armor":
init_armors += 1
if init_weapons != 1 or init_armors != 1:
errs.append({"category": "coverage", "message": "初始持有须恰一武器一甲(现 sword/light): 武器 %d / 甲 %d" % [init_weapons, init_armors]})
# 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
if init_weapons == 1 and init_armors == 1:
for row in routes:
if not bool(row.get("initial_owned", false)):
continue
if String(row.get("kind", "")) == "weapon" and String(row.get("type_axis", "")) != default_attack_type:
errs.append({"category": "cross_field", "message": "default_attack_type 须=初始武器路线类型轴(裸体态普通): %s vs %s" % [default_attack_type, String(row.get("type_axis", ""))]})
if String(row.get("kind", "")) == "armor" and String(row.get("type_axis", "")) != default_armor_type:
errs.append({"category": "cross_field", "message": "default_armor_type 须=初始甲路线类型轴(裸体态轻甲): %s vs %s" % [default_armor_type, String(row.get("type_axis", ""))]})
return errs