Files
gplan-2/game/data/schemas/forge_table.gd
T
DKChan b605022001 feat(迭代27 T5): 波五a 类型闸拉起+存量收口——审计器 0 存量+warning 配置在位
P1-6 类型闸(须 T1-T4 全落后——本波按执行顺序锁殿后):
- tools/audit_type_hints.py 新机器闸(V7): R1 无类型容器注解/R2 有值返回缺返回类型/
  R3 参数缺类型三规则+行级 # type-hint: ignore 显式白名单通道;产品码 core/data/presentation
- 存量收口: 368 处显式 Array/Dictionary 非参数化注解→机械参数化(Array[Variant]/
  Dictionary[Variant, Variant] 零行为面);13 处参数真类型(SaveManagerCore/ConfigDb/
  BattleOrchestrator/MinesweeperRules/LevelRunner/SettingsStore 等)
- 白名单豁免四处: board_generator generate/_attempt/legality/_finalize 的 db 参——
  测量面 _BoardRowStub 冒烟夹具鸭子型注入(typed 参拒 stub)
- project.godot 拉起 gdscript/warnings/untyped_declaration=1(V7 源码断言在 smoke _smoke_type_gate)
- static_scan 新 S9 类型闸规则;顺修历史 cd 错位(9-24 布局迁移后 /.. 落 game/ 致守卫
  恒跳过静默空转——修复后 S1-S4/S9 首次实跑全绿)+S1/S2 增 ADR-0007-exempt 标记豁免通道
  (main _seed_value 引擎熵源=文档化豁免面)

验收: 冒烟 1081/0(+V7×2)+run_all 27 套+static_scan 全绿+probe click+render golden+对拍三变体全同
2026-10-15 01:05:17 +08:00

132 lines
8.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
# 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[Variant] = 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