fix(review-batch3): 类型收紧生产清零137处(P1-2三步+延伸)+autoload class_name(ConfigDb/RngServiceCore)与_init标注(P2-1)+validate_skill_pool_links/ShopTable标注(P2-4)+相位enum外String面(P2-2)+typed-rows探针实证.tres装载转换;smoke缝合20处rows.assign+str(bb)同语义;审计生产双归零(240→103/83→59全测试链),单测7套绿,冒烟576/0同基线

This commit is contained in:
DKChan committed 2026-09-29 07:01:29 +08:00
1 parent f5edb65d25
commit 104cea6461
46 files changed
+233 -218

No files matched your search

+29 -16
View File
@@ -13,19 +13,23 @@ const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active"
const PHASE_WAITING := "Waiting"
var db
# 相位轴(P6 状态机——code-review 2026-09-29 P2-2):内部 enum + match,外部 phase getter 保持
# String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约零变化)。
enum Phase { IDLE, ACTIVE, WAITING }
var db: ConfigDb
# 相位与账本只读出口(E5 封装收口 2026-09-13):外部读取经 getter-only 属性(架构文档
# "ap/rap/phase 只读" 的机制化),写入只经 notify_* / request_* 方法通道。
var _phase: String = PHASE_IDLE
var _phase: int = Phase.IDLE
var phase: String:
get: return _phase
get: return _phase_text(_phase)
var _ap := 0
var ap: int:
get: return _ap
var _rap := 0
var rap: int:
get: return _rap
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var last_round_end: Dictionary:
get: return _last_round_end
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
@@ -33,10 +37,19 @@ var _next_round_mode := "" # 调试观测量(同板续轮 /
var _pending_ap_delta := 0 # 下轮行动点修正排队(格面资源八型批 2026-09-20——action 型 ±1 排队,轮开始兑现后清零)
func _init(db_ref) -> void:
func _init(db_ref: ConfigDb) -> void:
db = db_ref
func _phase_text(p: int) -> String:
match p:
Phase.ACTIVE:
return PHASE_ACTIVE
Phase.WAITING:
return PHASE_WAITING
return PHASE_IDLE
func _cost(kind: String) -> int:
var ae = db.action_economy
match kind:
@@ -54,7 +67,7 @@ func _cost(kind: String) -> int:
# 通用扣减(排雷玩法 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
func try_spend(action_kind: String) -> bool:
if phase != PHASE_ACTIVE:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
return _spend_and_arbitrate(action_kind, false)
@@ -62,7 +75,7 @@ func try_spend(action_kind: String) -> bool:
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
func request_dig(chain_size: int, clears_map: bool) -> bool:
if phase != PHASE_ACTIVE:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
return _spend_and_arbitrate("dig", clears_map)
@@ -86,7 +99,7 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)
func request_stop() -> bool:
if phase != PHASE_ACTIVE:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
_rap += _ap
@@ -100,7 +113,7 @@ func request_stop() -> bool:
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
func request_refresh() -> bool:
if phase != PHASE_ACTIVE:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if _rap < db.action_economy.rap_refresh_cost:
@@ -111,7 +124,7 @@ func request_refresh() -> bool:
# 战斗结算 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
func notify_settlement_done(next_round_mode: String) -> bool:
if phase != PHASE_WAITING:
if _phase != Phase.WAITING:
_reject("phase")
return false
_settlement_consumed = true
@@ -130,25 +143,25 @@ func pending_ap_delta() -> int:
func notify_round_start() -> bool:
if phase == PHASE_IDLE:
if _phase == Phase.IDLE:
pass # 战斗首轮唯一出口
elif phase == PHASE_WAITING and _settlement_consumed:
elif _phase == Phase.WAITING and _settlement_consumed:
pass
else:
_reject("phase")
return false
_ap = maxi(0, db.action_economy.ap_per_round + _pending_ap_delta) # 每轮基线 + 排队修正(下限 0)
_pending_ap_delta = 0
_phase = PHASE_ACTIVE
_phase = Phase.ACTIVE
_settlement_consumed = false
return true
func notify_combat_end() -> bool:
if phase != PHASE_WAITING:
if _phase != Phase.WAITING:
_reject("phase")
return false
_phase = PHASE_IDLE
_phase = Phase.IDLE
_rap = 0 # RAP 战斗域清零(用户拍板;HUD框架可见反馈义务)
_ap = 0
return true
@@ -160,7 +173,7 @@ func _finish_round(path_tag: String, continuation_hint: String) -> void:
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
_phase = PHASE_WAITING
_phase = Phase.WAITING
func _reject(source: String) -> void:
+1 -1
View File
@@ -66,7 +66,7 @@ var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
func _init(db_ref, rng_ref) -> void:
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
+15 -15
View File
@@ -38,8 +38,8 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward"]
var db
var rng
var db: ConfigDb
var rng: RngServiceCore
# E1 封装收口(2026-09-13):协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/
# diagnostic_snapshot() 成文出口(呈现层链式穿透 settlement._enemy_system.hero_unit() 的根因收口)。
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
@@ -52,13 +52,13 @@ var _status_effects: StatusContainer
var _reward_economy: RewardEconomy
var _damage: DamageController # 伤害控制器(伤害管线单据化批 2026-09-24——单据唯一兑付管线)
var encounter: Dictionary = {}
var encounter: Dictionary[String, Variant] = {}
var round_index := 0
var battle_result := "" # "" / "victory" / "defeat" / "fatal_error"
var escrow := 0 # 战斗内 GP 缓冲(战斗域中间态——begin_battle 清零;gp_gain/gp_loss 兑付累记;随 gp_dropped 上抛即弃,不入钱包)
var _facts_reveals: Array = []
var _facts_flags: Array = []
var _facts_reveals: Array[Dictionary] = []
var _facts_flags: Array[Dictionary] = []
var _facts_path := ""
var _settling := false
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
@@ -71,7 +71,7 @@ var _perfect_hit_false := 0
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
func _init(db_ref, rng_ref) -> void:
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
_board_generator = BoardGenerator.new()
@@ -94,7 +94,7 @@ func begin_battle(enc: Dictionary) -> bool:
if round_index != 0:
push_warning("[战斗结算] begin_battle 重复受理")
return false
encounter = enc
encounter.assign(enc)
escrow = 0 # 战斗域零 GP 持久(GP+商店房实施批——每场战斗缓冲从零起算)
_damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费——口径同原 _defender_ac)
db.set_reload_gate(true)
@@ -150,7 +150,7 @@ func _install_new_board(during_round := false) -> bool:
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var eligible: Array = []
var eligible: Array[Dictionary] = []
for c in board.safe_cells():
if not r0.has(c):
eligible.append({"cell": c})
@@ -189,7 +189,7 @@ func _place_variants(board: BoardData) -> void:
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var candidates: Array = []
var candidates: Array[Vector2i] = []
for c in board.safe_cells():
if not r0.has(c):
candidates.append(c)
@@ -203,7 +203,7 @@ func _place_variants(board: BoardData) -> void:
var placed := {}
while placed.size() < total:
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.variation.pack_max)))
var free: Array = []
var free: Array[Vector2i] = []
for c in candidates:
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
@@ -217,7 +217,7 @@ func _place_variants(board: BoardData) -> void:
var pack: Array = [seed_c]
var pack_set := {seed_c: true}
while pack.size() < pack_size:
var fringe: Array = []
var fringe: Array[Vector2i] = []
for c in pack:
for n in board.neighbors8(c):
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
@@ -628,10 +628,10 @@ func _emit_composition_effects(side: String, ri: int) -> void:
# ---- 结算快照(playtest 数值分析数据面——每次结算全量状态;运行时零决策读取)----
func _emit_settlement_snapshot(verdict: String) -> void:
var enemies: Array = []
var enemies: Array[Dictionary] = []
for u in _enemy_system.query_alive("enemy"):
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "pending_skill": _enemy_system.pending_action(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))})
var buffs: Array = []
var buffs: Array[Dictionary] = []
for eff in _status_effects.query_active("hero"):
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
for eff in _status_effects.query_active("enemy"):
@@ -746,10 +746,10 @@ func _roll_gp_drop() -> int:
# (敌人技能池批批 2 定稿:beat/ring_size/ring_index 拆除 → pending_* 三字段 + 敌方增益 effects 投影;池与权重不公开;
# 八型批 2026-09-20:ambush_layers 拆除——伏击退役,警觉并入 effects 投影)hero_effects[](增益状态 query_active 投影)
func presentation_snapshot() -> Dictionary:
var enemies: Array = []
var enemies: Array[Dictionary] = []
var all: Array = _enemy_system.all_enemies()
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行,八型批 2026-09-20)
var enemy_effects: Array = []
var enemy_effects: Array[Dictionary] = []
for eff in _status_effects.query_active("enemy"):
var erow: Dictionary = db.status_effect_row(eff.effect_id)
if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]:
+4 -4
View File
@@ -49,7 +49,7 @@ func has_variant(c: Vector2i, vid: String) -> bool:
func neighbors8(c: Vector2i) -> Array:
var out: Array = []
var out: Array[Vector2i] = []
for dy in range(-1, 2):
for dx in range(-1, 2):
if dx == 0 and dy == 0:
@@ -61,7 +61,7 @@ func neighbors8(c: Vector2i) -> Array:
func spawn_cells() -> Array:
var out: Array = []
var out: Array[Vector2i] = []
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := Vector2i(spawn_center.x + dx, spawn_center.y + dy)
@@ -71,7 +71,7 @@ func spawn_cells() -> Array:
func all_cells() -> Array:
var out: Array = []
var out: Array[Vector2i] = []
for y in height:
for x in width:
out.append(Vector2i(x, y))
@@ -79,7 +79,7 @@ func all_cells() -> Array:
func safe_cells() -> Array:
var out: Array = []
var out: Array[Vector2i] = []
for c in all_cells():
if is_safe(c):
out.append(c)
+3 -3
View File
@@ -83,7 +83,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
var spawn := {}
for c in b.spawn_cells():
spawn[c] = true
var domain: Array = []
var domain: Array[Vector2i] = []
for c in b.all_cells():
if not spawn.has(c):
domain.append(c)
@@ -198,7 +198,7 @@ func measure_inferability(b: BoardData) -> Dictionary:
func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, safe_set: Dictionary) -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for c in r.keys():
if not safe_set.has(c):
continue # 只对已开安全格的数字建约束
@@ -273,7 +273,7 @@ func legality(b: BoardData, params: Dictionary, db) -> bool:
func _legality_faults(b: BoardData, params: Dictionary) -> Array:
var faults: Array = []
var faults: Array[String] = []
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(排雷玩法 修订义务)
for c in b.spawn_cells():
if b.is_mine(c):
+12 -11
View File
@@ -4,6 +4,7 @@
# ConfigDB autoload(数据配置)——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片注:headless --script 模式下由冒烟脚本显式实例化(数据配置 AC 前言);玩法代码禁止绕过本单点自建 load()。
class_name ConfigDb
extends Node
const SCHEMA_VERSION := 1
@@ -36,16 +37,16 @@ var reward: RewardTable
var settlement: SettlementTable
var level: LevelTable
var difficulty: DifficultyTable
# GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面,
# 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集)
var shop = null
# GP+商店房实施批(2026-09-21):ShopTable 类型化持用(schema class_name 已落籍——code-review 2026-09-29 P2-4 收口;
# 消费面一律走下方 shop_* 访问器,禁直读行集)
var shop: ShopTable = null
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
var meta: MetaTable
var adventure: AdventureTable
var is_ready := false
var _diagnostics: Array = [] # {severity, category, message}
var _warn_dedup: Dictionary = {} # R1 四元组去重注册表
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
@@ -170,7 +171,7 @@ func _cross_validate(loaded: Dictionary) -> void:
for lrow in lv.rows:
var li: int = int(lrow.level_index)
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
var node_lists: Array = []
var node_lists: Array[Array] = []
var tpls: Array = lrow.get("templates", [])
if tpls.is_empty():
node_lists.append(lrow.get("nodes", []))
@@ -199,8 +200,8 @@ func _cross_validate(loaded: Dictionary) -> void:
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
func validate_skill_pool_links(skill_table, enemy_table, status_table = null) -> Array:
var errs: Array = []
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
var errs: Array[Dictionary] = []
var skill_types := {}
for row in skill_table.rows:
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
@@ -267,7 +268,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Diction
if L < 1 or D < 0:
_fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
return {}
var level_keys: Array = []
var level_keys: Array[int] = []
for row in rows:
var lv := int(row.get("level_index", 0))
if not level_keys.has(lv):
@@ -280,7 +281,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Diction
var l_clamped := l_star < 0
if l_clamped:
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值)
var depth_keys: Array = []
var depth_keys: Array[int] = []
for row in rows:
if int(row.get("level_index", 0)) == l_star:
var dv := int(row.get("node_depth", 0))
@@ -328,7 +329,7 @@ func _ceil_key(sorted_keys: Array, request: int) -> int:
# board_type 词表——biome_tags 系旧观念残留退役)----
func variant_pool(level_index: int, board_type: String) -> Array:
var pool: Array = []
var pool: Array[Dictionary] = []
for row in variation.rows:
if int(row.get("unlock_level", 999)) > level_index:
continue
+2 -2
View File
@@ -48,14 +48,14 @@ const TARGET_FRONT_ALIVE := "front_alive_enemy"
const TARGET_HERO := "hero"
const TARGET_ALL_ALIVE := "all_alive_enemies"
var db
var db: ConfigDb
var rng
var units # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用)
var effects # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面)
var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight)
var policy := POLICY_SEGMENT # 结账时机开关(2026-09-24 用户拍板第二步拨 B=攒批段界统一结;回切 A 改回 POLICY_IMMEDIATE 即还原迁移前行为)
var _queue: Array = [] # 未结单据(B 模式攒批;A 模式恒空)
var _queue: Array[Dictionary] = [] # 未结单据(B 模式攒批;A 模式恒空)
var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(结账后可查——清图高光份数消费)
var _seq := 0 # 流水号
var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废)
+13 -13
View File
@@ -40,9 +40,9 @@ signal board_changed()
signal level_finished(payload: Dictionary) # {result, hp, node_index, node_id, node_type}
signal wallet_changed(event: String, gp: int, extra: Dictionary) # GP 钱包事件投影:gain{amount}/buy{product,price}/clear——试玩日志与商店覆盖层刷新源(GP+商店房实施批)
var db
var rng
var level_row: Dictionary = {}
var db: ConfigDb
var rng: RngServiceCore
var level_row: Dictionary[String, Variant] = {}
var node_index := -1
# ---- 账本载体(冒险结构实现批——漂移②清偿)----
# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本);
@@ -68,12 +68,12 @@ var hero_hp_pct := 0
var _running := false
var level_result := "" # "" / "victory" / "defeat"
var _awaiting_choice := false # 选路态(兑付确认后、pick 前为零自动推进的可见停点)
var _candidates: Array = [] # 当前选路候选(节点引用数组)
var visited: Array = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点)
var _candidates: Array[Dictionary] = [] # 当前选路候选(节点引用数组)
var visited: Array[Dictionary] = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点)
var _graph_mode := false # 图形态缓存(行集任一节点带 next;单链 = false)
func _init(db_ref, rng_ref) -> void:
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
@@ -142,7 +142,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
break
hero_hp = effective_max_hp()
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
level_row = _resolve_template_row(row, template_pick) # 池解析在播种后(node_map 抽签须已播种——EC7)
level_row.assign(_resolve_template_row(row, template_pick)) # 池解析在播种后(node_map 抽签须已播种——EC7)
_running = true
var entry := _entry_node()
if entry.is_empty():
@@ -177,7 +177,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
base_override = base_override_ref
_shop_purchased = purchased_ref
_rest_handler = rest_handler
level_row = _resolve_template_row(row, template_pick)
level_row.assign(_resolve_template_row(row, template_pick))
node_index = -1
level_result = ""
_awaiting_choice = false
@@ -286,11 +286,11 @@ func _out_ids(node: Dictionary) -> Array:
# 出边候选(节点引用数组——选路口/呈现层共用读面)
func _out_nodes(node: Dictionary) -> Array:
func _out_nodes(node: Dictionary) -> Array[Dictionary]:
var ids := {}
for n in nodes():
ids[_node_id(n)] = n
var out: Array = []
var out: Array[Dictionary] = []
for tid in _out_ids(node):
if ids.has(tid):
out.append(ids[tid])
@@ -304,7 +304,7 @@ func next_candidates() -> Array:
# 当前选路候选编号表(pick_next_node 的合法入参域——编号空间解析单一出口)
func next_candidate_ids() -> Array:
var out: Array = []
var out: Array[int] = []
for c in _candidates:
out.append(_node_id(c))
return out
@@ -326,7 +326,7 @@ func node_out_ids(node: Dictionary) -> Array:
# 已到节点编号序列(只读投影——编号空间解析单一出口)
func visited_ids() -> Array:
var out: Array = []
var out: Array[int] = []
for v in visited:
out.append(_node_id(v))
return out
@@ -493,7 +493,7 @@ func close_wallet() -> void:
# 商店覆盖层数据源(只读投影——现价/已购档数在此现算,呈现层禁复算定价公式)
func shop_products() -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for prow in db.shop_products():
out.append({
"product_id": String(prow.get("product_id", "")),
+6 -6
View File
@@ -16,11 +16,11 @@
class_name MetaProgression
extends RefCounted
var db
var save
var db: ConfigDb
var save: SaveManager
func _init(db_ref, save_ref) -> void:
func _init(db_ref: ConfigDb, save_ref: SaveManager) -> void:
db = db_ref
save = save_ref
@@ -83,7 +83,7 @@ func _workshop_progress(channel: String) -> int:
# 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress}
func workshop_status() -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for row in db.meta_workshop_items():
var iid := String(row.get("item_id", ""))
out.append({
@@ -98,7 +98,7 @@ func workshop_status() -> Array:
# 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次)
func refresh_workshop_unlocks() -> Array:
var new_unlocks: Array = []
var new_unlocks: Array[String] = []
for row in db.meta_workshop_items():
var iid := String(row.get("item_id", ""))
if save.item_unlocked(iid):
@@ -118,7 +118,7 @@ func refresh_workshop_unlocks() -> Array:
func settle_adventure(summary: Dictionary) -> Dictionary:
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
save.mark_first_adventure_done()
var drops: Array = []
var drops: Array[Dictionary] = []
for li in range(1, int(summary.get("levels_cleared", 0)) + 1):
var first: bool = not save.boss_first_kill_marked(li)
var row: Dictionary = db.meta_boss_keystone_row(li)
+8 -8
View File
@@ -15,18 +15,18 @@ signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "no_path", "out_of_catalog"] # flag_absent 随 flag_pull 退役(2026-09-19)
var db
var action_economy
var db: ConfigDb
var action_economy: ActionEconomy
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
var _board: BoardData = null
var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int}
var _flags := {} # Vector2i → true(棋盘单态;跨轮保留随图存续)
var _hero := Vector2i.ZERO
var _spawn: Dictionary = {}
var _spawn: Dictionary[Vector2i, bool] = {}
func _init(db_ref, action_economy_ref) -> void:
func _init(db_ref: ConfigDb, action_economy_ref: ActionEconomy) -> void:
db = db_ref
action_economy = action_economy_ref
@@ -125,7 +125,7 @@ func retire_board(during_round := false) -> Dictionary:
# 四不变式(诊断类别封闭枚举):inv1 出生区雷 / inv2a 出生区变异 / inv2b R₀ 变异 / inv3 数字恒真 / inv4 R₀=S
func _install_invariant_faults(b: BoardData) -> Array:
var faults: Array = []
var faults: Array[String] = []
var r0 := {}
for c in b.compute_r0():
r0[c] = true
@@ -171,7 +171,7 @@ func _initial_expansion_order(b: BoardData) -> Array:
# ---- 批次结算(唯一提交入口;单意图批 = 平凡形 ≡ 便捷 API)----
func settle_batch(intents: Array) -> Array:
var outcomes: Array = []
var outcomes: Array[Dictionary] = []
if intents.is_empty():
return outcomes
if action_economy.phase != action_economy.PHASE_ACTIVE or _board == null:
@@ -179,7 +179,7 @@ func settle_batch(intents: Array) -> Array:
outcomes.append(_outcome(false, "rejected", "phase", ""))
return outcomes
# F4 语义序仲裁:C_dig(真模拟清图)→ C_stop → C_other;类内稳定序 = 到达序
var indexed: Array = []
var indexed: Array[Dictionary] = []
for i in intents.size():
indexed.append({"i": i, "intent": intents[i], "cls": _classify(intents[i])})
indexed.sort_custom(func(a, b):
@@ -395,7 +395,7 @@ func compute_move_path(target: Vector2i) -> Array:
queue.append(n)
if not prev.has(target):
return []
var path: Array = []
var path: Array[Vector2i] = []
var cur := target
while cur != _hero:
path.push_front(cur)
+8 -8
View File
@@ -12,10 +12,10 @@
class_name RewardEconomy
extends RefCounted
var db
var rng
var db: ConfigDb
var rng: RngServiceCore
var _phase := "unrolled" # unrolled → ready → retired
var _eligible: Array = [] # [{cell}] 行主序规范序
var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序
var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载)
var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号)
var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行)
@@ -25,7 +25,7 @@ var _clear_granted := false
var _clear_snapshot := {}
func _init(db_ref, rng_ref) -> void:
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
@@ -43,7 +43,7 @@ func roll_board(eligible: Array) -> bool:
return false
# 行主序规范化 + 去重(排序责任不外推给 战斗结算)
var seen := {}
var list: Array = []
var list: Array[Dictionary] = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
@@ -175,7 +175,7 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary:
# ---- F6 当量供给(行动经济 条款①对照实物输入——全活行投影 + tier_min_buff)----
func equivalence_supply() -> Dictionary:
var live: Array = []
var live: Array[Dictionary] = []
var tier_min_buff := 99
for row in db.reward.rows:
if int(row.weight) <= 0:
@@ -206,7 +206,7 @@ func weighted_pick(rows: Array, r: int) -> String:
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array = []
var out: Array[Dictionary] = []
for row in db.reward.rows:
if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0:
out.append(row)
@@ -214,7 +214,7 @@ func _rollable_rows() -> Array:
func _pool_rows(pool_tag: String) -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for row in db.reward.rows:
if row.pool_tag == pool_tag and int(row.weight) > 0:
out.append(row)
+1
View File
@@ -11,6 +11,7 @@
# splitmix64 finalizer 变体终末混淆,见 _mix64 注记;并流前分盐黄金向量随批作废,新向量按单盐重登记(scope 0-3 与
# 旧 board_gen 向量逐位连续——单盐沿用原 board_gen 盐)。
# 取号语义:全新冒险首个 open_stream = 全局 scope 0;事件日志逐次落盘 user://logs/rng_log_<master_seed>.jsonl。
class_name RngServiceCore
extends Node
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
+4 -4
View File
@@ -33,8 +33,8 @@ const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合
var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径)
var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写)
var _data: Dictionary = {} # 内存数据模型(_init 即内置默认;load 后为档案内容)
var _migrations: Dictionary = {} # from_version → Callable(data)->data(升一版;生产链现为空——骨架先行)
var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容)
var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;生产链现为空——骨架先行)
func _init(path: String = "user://save.json") -> void:
@@ -44,7 +44,7 @@ func _init(path: String = "user://save.json") -> void:
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
func _default_data() -> Dictionary:
func _default_data() -> Dictionary[String, Variant]:
return {
"version": SAVE_FORMAT_VERSION,
"meta": {
@@ -222,7 +222,7 @@ func _corrupt_fallback(reason: String) -> bool:
# 迁移后/低版本档字段补齐(改版不弃档:缺新域按默认补齐,既有值保留)
func _normalize(data: Dictionary) -> Dictionary:
func _normalize(data: Dictionary) -> Dictionary[String, Variant]:
var merged := _default_data()
for key in data:
merged[key] = data[key]
+5 -5
View File
@@ -8,12 +8,12 @@
class_name StatusContainer
extends RefCounted
var db
var db: ConfigDb
var _open := false
var _instances: Array = [] # 插入序;元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display}
var _instances: Array[Dictionary] = [] # 插入序;元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display}
func _init(db_ref) -> void:
func _init(db_ref: ConfigDb) -> void:
db = db_ref
@@ -84,7 +84,7 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
# 只读投影(插入序稳定枚举;六归因字段 + display_name + 授予时快照)
func query_active(side: String) -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for inst in _instances:
var row: Dictionary = db.status_effect_row(inst.effect_id)
if row.is_empty():
@@ -119,7 +119,7 @@ func decay_round() -> int:
if not _open:
return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断
var k := 0
var survivors: Array = []
var survivors: Array[Dictionary] = []
for inst in _instances:
inst.remaining -= 1
if inst.remaining <= 0:
+8 -8
View File
@@ -20,18 +20,18 @@ extends RefCounted
# 消费方 = 伤害控制器(怪死 → 其未结单据作废〔死亡即不动手〕;主角死 → 兑付终止旗)。
signal unit_died(u: Dictionary)
var db
var db: ConfigDb
# 单位容器私有(E3 封装收口 2026-09-13):hero/enemies 原始数组不再外露——读取经 hero_unit()/
# all_enemies() 投影面(死亡残位查询包含在内),写入只经 apply_damage/heal/set_pending_action/commit_action/
# tick_cooldowns/spawn/despawn。
# 句柄语义不变:投影返回的 Dictionary 引用即单位句柄(同 query_alive 先例)。
var _hero: Dictionary = {} # {hp, max_hp, atk, is_hero: true}
var _enemies: Array = [] # 行主序生成序;元素 = 单位 Dictionary
var _hero: Dictionary[String, Variant] = {} # {hp, max_hp, atk, is_hero: true}
var _enemies: Array[Dictionary] = [] # 行主序生成序;元素 = 单位 Dictionary
var _spawned := false
var _despawned := false
func _init(db_ref) -> void:
func _init(db_ref: ConfigDb) -> void:
db = db_ref
@@ -69,7 +69,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
for i in range(count):
var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 0 else int(row.atk)
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条快照纪律)
var pool_snap: Array = []
var pool_snap: Array[Dictionary] = []
for entry in row.get("pool", []):
pool_snap.append({
"skill_id": String(entry.get("skill_id", "")),
@@ -103,7 +103,7 @@ func query_alive(side: String) -> Array:
return []
if side == "hero":
return [] if (_hero.is_empty() or not _hero.alive) else [_hero]
var out: Array = []
var out: Array[Dictionary] = []
for u in _enemies:
if u.alive:
out.append(u)
@@ -118,7 +118,7 @@ func rollable_skills(u: Dictionary) -> Array:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("rollable_skills 失效句柄")
return []
var out: Array = []
var out: Array[Dictionary] = []
for e in u.pool:
if int(e.cooldown_left) <= 0:
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
@@ -130,7 +130,7 @@ func special_skills(u: Dictionary) -> Array:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("special_skills 失效句柄")
return []
var out: Array = []
var out: Array[Dictionary] = []
for e in u.pool:
var srow: Dictionary = db.skill_row(String(e.skill_id))
if not srow.is_empty() and String(srow.type) != "basic":
+1 -1
View File
@@ -16,7 +16,7 @@ extends Resource
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if ap_per_round < 1 or ap_per_round > 32:
errs.append({"category": "domain", "message": "ap_per_round 越域 [1,32]: %d" % ap_per_round})
if dig_ap_cost < 1 or dig_ap_cost > 8:
+1 -1
View File
@@ -15,7 +15,7 @@ extends Resource
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if level_clear_hp_bonus < 0 or level_clear_hp_bonus > 999:
errs.append({"category": "domain", "message": "level_clear_hp_bonus 越域 [0,999]: %d" % level_clear_hp_bonus})
if keystone_per_level < 0 or keystone_per_level > 999:
+2 -2
View File
@@ -6,7 +6,7 @@
class_name BoardGenTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
const SOLVER_RULES := ["single", "single_subset"]
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚)
@@ -14,7 +14,7 @@ const BOARD_TYPE_VOCABULARY := ["dungeon", "underdark", "deep_underdark"] #
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "BoardGenTable 为空"})
return errs
+2 -2
View File
@@ -6,14 +6,14 @@
class_name DifficultyTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]:{level_index, node_depth, hp_mult, atk_mult}
@export var rows: Array[Dictionary] = [] # Array[Dictionary]:{level_index, node_depth, hp_mult, atk_mult}
const MULT_MIN := 0.1
const MULT_MAX := 10.0
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
var seen := {}
for row in rows:
var L: int = int(row.get("level_index", 0))
+4 -4
View File
@@ -9,7 +9,7 @@
class_name EnemyTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
# 护甲类型封闭枚举(攻击类型 × 护甲克制批——克制矩阵本体在 SettlementTable;
# 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
@@ -17,7 +17,7 @@ const ARMOR_TYPES := ["light", "ward", "heavy"]
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "EnemyTable 为空"})
return errs
@@ -108,14 +108,14 @@ func validate() -> Array:
# R1:进度键 floor + 行集恰一敌种(实体键无歧义,悬空致命由调用方处理)
func resolve_rowset(L: int, D: int) -> Dictionary:
var best: 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 = row
best.assign(row)
best_l = rl
best_d = rd
return best
+1 -1
View File
@@ -14,7 +14,7 @@ extends Resource
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if max_hp < 1 or max_hp > 999:
errs.append({"category": "domain", "message": "max_hp 越域 [1,999]: %d" % max_hp})
if atk < 1 or atk > 999:
+9 -9
View File
@@ -15,13 +15,13 @@
class_name LevelTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 商店房批 2026-09-21 四→五类(关卡节点地图 v1.1)
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "LevelTable 为空"})
return errs
@@ -40,7 +40,7 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L})
# 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验
# (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。
var node_lists: Array = [] # [ [上下文标签, nodes 数组], ... ]
var node_lists: Array[Array] = [] # [ [上下文标签, nodes 数组], ... ]
var templates: Array = row.get("templates", [])
if templates.is_empty():
node_lists.append(["", row.get("nodes", [])])
@@ -59,7 +59,7 @@ func validate() -> Array:
# 单节点链校验体(validate 抽出——row.nodes 与池内每张模板同跑一套;ctx = 消息上下文标签)
func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
var tag: String = "" if ctx.is_empty() else "〔%s〕" % ctx
if nodes.is_empty():
errs.append({"category": "domain", "message": "level %d%s 节点链为空" % [L, tag]})
@@ -102,9 +102,9 @@ func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array:
# 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 →
# 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。
func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array:
var errs: Array = []
var ids: Array = [] # 每节点 id(下标对齐)
var nexts: Array = [] # 每节点出边 id 数组
var errs: Array[Dictionary] = []
var ids: Array[int] = [] # 每节点 id(下标对齐)
var nexts: Array[Array] = [] # 每节点出边 id 数组
var id_to_idx := {}
for i in nodes.size():
var node: Dictionary = nodes[i]
@@ -116,7 +116,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") ->
if id_to_idx.has(nid):
errs.append({"category": "dup_key", "message": "level %d%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]})
id_to_idx[nid] = i
var nx: Array = []
var nx: Array[int] = []
if node.has("next"):
for v in node.get("next", []):
nx.append(int(v))
@@ -135,7 +135,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") ->
for i in nodes.size():
for tid in nexts[i]:
has_inbound[tid] = true
var entries: Array = []
var entries: Array[int] = []
for i in nodes.size():
if not has_inbound.has(ids[i]):
entries.append(i)
+5 -5
View File
@@ -16,10 +16,10 @@
class_name MetaTable
extends Resource
@export var forge_products: Array = [] # Array[Dictionary] 键 attr
@export var boss_keystone: Array = [] # Array[Dictionary] 键 level_index
@export var workshop_items: Array = [] # Array[Dictionary] 键 item_id
@export var conversion: Array = [] # Array[Dictionary] 键 attr
@export var forge_products: Array[Dictionary] = [] # Array[Dictionary] 键 attr
@export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
@export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id
@export var conversion: Array[Dictionary] = [] # Array[Dictionary] 键 attr
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"]
const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"]
@@ -30,7 +30,7 @@ const WORKSHOP_CHANNELS := ["total_clears"]
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命)
for family in ["forge_products", "boss_keystone", "workshop_items", "conversion"]:
if get(family).is_empty():
+3 -3
View File
@@ -10,7 +10,7 @@
class_name RewardTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"] # 九型封闭(格面资源九型批 2026-09-20 八→九,词表 v1.1)——清单外加载拒绝
const POLARITIES := ["positive", "negative"]
@@ -19,7 +19,7 @@ const CELL_TYPE_KEYS := ["attack", "defense", "heal", "blast", "weaken", "pierce
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "RewardTable 为空"})
return errs
@@ -106,7 +106,7 @@ func validate() -> Array:
func rows_by_pool(pool_tag: String) -> Array:
var out: Array = []
var out: Array[Dictionary] = []
for row in rows:
if row.get("pool_tag", "") == pool_tag:
out.append(row)
+6 -6
View File
@@ -9,8 +9,8 @@ extends Resource
@export var equivalence_ref_damage: int = 12 # [1,999] knob——当量参考敞口 D_ref
@export var p_perfect: int = 200 # {100,200} percent——完美排雷第二份兑付触发(载体域)
@export var segment_order: PackedStringArray = [] # 结构冻结——恒等拍板段序校验违例致命
@export var source_label_map: Dictionary = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var source_label_map: Dictionary[String, Variant] = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary[String, Variant] = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
@@ -19,7 +19,7 @@ extends Resource
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 原 damage_a_per_cell / damage_a_cap / damage_b_per_flag 与 charge_grants 充能奖励定义均退役(2026-09-19 充能退役批——攻击模型回归单分量)。
@export var armor_effectiveness: Dictionary = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
@export var armor_effectiveness: Dictionary[String, Variant] = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
# ---- 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 同批定稿)----
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何伤害落地全局下限 damage_floor(≥1)——
@@ -28,8 +28,8 @@ extends Resource
# 微调旋钮 = 档位边界(roll_tier_bounds)与档位值(roll_tier_discounts),不动 AC 值(armor_class 归单位表)。
@export var roll_faces: int = 20 # [2,1000] knob——掷点骰面(d20)
@export var attack_bonus: int = 0 # [0,999] 结构位——攻击者掷点加成,切片恒 0(元进度/武器附魔未来投值)
@export var roll_tier_bounds: Array = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25)
@export var roll_tier_bounds: Array[int] = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array[int] = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25)
@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额)
@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量)
@export var damage_floor: int = 1 # [1,999] knob——任何伤害落地全局下限·全来源("被减伤归零 v=0"行语义随批退役)
@@ -43,7 +43,7 @@ const P_PERFECT_DOMAIN := [100, 200]
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if equivalence_ref_damage < 1 or equivalence_ref_damage > 999:
errs.append({"category": "domain", "message": "equivalence_ref_damage 越域 [1,999]: %d" % equivalence_ref_damage})
if not P_PERFECT_DOMAIN.has(p_perfect):
+5 -5
View File
@@ -13,17 +13,17 @@
class_name ShopTable
extends Resource
@export var gp_drops: Array = [] # Array[Dictionary] 键 tier
@export var level_gp_bonus: Array = [] # Array[Dictionary] 键 level_index
@export var products: Array = [] # Array[Dictionary] 键 product_id
@export var price_level_bonus: Array = [] # Array[Dictionary] 键 level_index
@export var gp_drops: Array[Dictionary] = [] # Array[Dictionary] 键 tier
@export var level_gp_bonus: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
@export var products: Array[Dictionary] = [] # Array[Dictionary] 键 product_id
@export var price_level_bonus: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
const GP_TIERS := ["normal", "elite", "boss"]
const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"]
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
# 行族空护栏(四轴皆为 ConfigDB 消费面——缺族即致命)
for family in ["gp_drops", "level_gp_bonus", "products", "price_level_bonus"]:
if get(family).is_empty():
+2 -2
View File
@@ -8,7 +8,7 @@
class_name SkillTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
# 保底 M(provisional 归 难度曲线与Boss):连续 M 拍执行的都是普通攻击 → 下拍必出特殊技
# (特殊技内按权重、不受冷却限制——proposal §2.1 两道闸之二)。全局单旋钮(不分敌种)。
@export var pity_threshold: int = 4
@@ -20,7 +20,7 @@ const RIDER_MULT_MAX := 1.0 # rider_mult 域 (0, 1](
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "SkillTable 为空"})
return errs
+2 -2
View File
@@ -6,7 +6,7 @@
class_name StatusEffectTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
const TARGET_SIDES := ["hero", "enemy"]
const UNITS := ["percent", "flat", "ac"] # 2026-09-16 护甲等级掷点批增 ac——AC 阈值轴效果(ac_up),非池百分比非定点值
@@ -14,7 +14,7 @@ const STACK_POLICIES := ["stack", "refresh", "independent"]
func validate() -> Array:
var errs: Array = []
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "StatusEffectTable 为空"})
return errs
+2 -2
View File
@@ -11,7 +11,7 @@
class_name VariationTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
@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 用户拍板基数修正:百分比基数 = 候选池(安全格 − 初始展开区,即实际可挖格)而非总格数)
@@ -27,7 +27,7 @@ const PLACEMENTS := ["closed_only", "any"] # 布置约束
func validate() -> Array:
# 空清单是合法状态(地形变异 P0)——零行即零变体格,配置加载零错误、零警告
var errs: Array = []
var errs: Array[Dictionary] = []
var seen := {}
for row in rows:
var vid: String = row.get("variant_id", "")
+2 -2
View File
@@ -28,7 +28,7 @@ static func shop_line(gp: int) -> String:
# 选路口行(零自动推进停点——战斗胜利/兑付确认后出现)
static func route_line(candidates: Array, runner) -> String:
var names: Array = []
var names: Array[String] = []
for cand in candidates:
names.append("节点 %d" % runner.node_id_of(cand))
return "[color=#e0b860]── 岔路口:可选下一跳 %s ──[/color]" % " / ".join(names)
@@ -105,7 +105,7 @@ static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {
var line := "钥石入账:" + " · ".join(parts)
var unlocks: Array = detail.get("new_unlocks", [])
if not unlocks.is_empty():
var names: Array = []
var names: Array[String] = []
for iid in unlocks:
names.append(String(name_lookup.get(String(iid), String(iid))))
line += "\n工坊新解锁:" + "、".join(names)
+1 -1
View File
@@ -20,7 +20,7 @@ var _hero_override: Callable # 组合根生效值供给(→ {atk, m
var _seed_value: Callable # 组合根种子读取(LineEdit)
var _level_mode := false
var _level_battles: Array = []
var _level_battles: Array[int] = []
var _level_idx := 0
var _carry_hp := 0
var settlement = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
+3 -3
View File
@@ -41,7 +41,7 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
var atk: int = int(hero.atk)
# 修正 = attack_up 池有效值(hero_offense);来源角标 = 效果 display_name(快照 hero_effects 投影)
var mod_total := 0
var sources: Array = []
var sources: Array[String] = []
for eff in s.hero_effects:
if String(eff.effect_id) == "attack_up":
mod_total += int(eff.effective_value)
@@ -111,7 +111,7 @@ static func outcome_text(outcome: Dictionary) -> String:
# ---- 调整板批量模拟报告装配(文案归 presenter——视图只 set_text;code-review 2026-09-12 批) ----
static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String:
var lines: Array = []
var lines: Array[String] = []
lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct])
lines.append("通关率 [color=#80d080]%.0f%%[/color](%d/%d)" % [100.0 * float(wins) / float(maxi(1, n)), wins, n])
lines.append("整关承伤:均 %.1f · p90 %.0f(基础 HP %d)" % [_mean(dmg_all), _p90(dmg_all), base_hp])
@@ -121,7 +121,7 @@ static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_al
if death_nodes.is_empty():
lines.append("败北节点:—")
else:
var parts: Array = []
var parts: Array[String] = []
for k in death_nodes.keys():
parts.append("%s×%d" % [k, death_nodes[k]])
lines.append("败北节点:%s(0-3=战斗序,4=Boss)" % " ".join(parts))
+3 -3
View File
@@ -321,9 +321,9 @@ func _on_adventure_finished(summary: Dictionary) -> void:
stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾)
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
var detail: Dictionary = {}
var detail: Dictionary[String, Variant] = {}
if meta != null:
detail = meta.settle_adventure(summary)
detail.assign(meta.settle_adventure(summary))
var text := Narrator.adventure_summary_text(summary)
if not detail.is_empty():
text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup())
@@ -671,7 +671,7 @@ func _on_camp_closed() -> void:
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
func _camp_data() -> Dictionary:
var forge_rows: Array = []
var forge_rows: Array[Dictionary] = []
if meta != null:
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
+2 -2
View File
@@ -6,7 +6,7 @@
# 纯逻辑零节点构建——组合根持有本类并喂入 Control;不持久化核心状态。
extends RefCounted
var _list: Array = [] # 登记序 = z 遮挡序(低 → 高)
var _list: Array[Control] = [] # 登记序 = z 遮挡序(低 → 高)
var _occluded := {} # modal → 被其隐藏的上方层(关闭恢复)
@@ -15,7 +15,7 @@ func register(m: Control) -> void:
func open(m: Control) -> void:
var hidden: Array = []
var hidden: Array[Control] = []
var idx := _list.find(m)
for i in range(idx + 1, _list.size()):
var higher: Control = _list[i]
+2 -2
View File
@@ -17,8 +17,8 @@ const RING_SIZE := 200
var _file: FileAccess = null
var _path := ""
var _seed := 0
var _recent_steps: Array = [] # 环形缓冲:最近结算行
var _recent_actions: Array = [] # 环形缓冲:最近玩家动作
var _recent_steps: Array[Dictionary] = [] # 环形缓冲:最近结算行
var _recent_actions: Array[Dictionary] = [] # 环形缓冲:最近玩家动作
var _snap_count := 0
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
+4 -4
View File
@@ -30,10 +30,10 @@ func run(n: int) -> void:
panel.set_sim_running(true)
panel.set_sim_progress("模拟中:0/%d …" % n)
var wins := 0
var dmg_all: Array = []
var hp_left_win: Array = []
var hp_boss: Array = []
var rounds_all: Array = []
var dmg_all: Array[float] = []
var hp_left_win: Array[float] = []
var hp_boss: Array[float] = []
var rounds_all: Array[int] = []
var death_nodes := {}
for i in range(n):
var st = LevelAutoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
@@ -137,7 +137,7 @@ func _play_flag_recall(count: int) -> void:
# ---- 演出密度终稿(样张 v2 冻结批 T3——程序化 tween,确定性散布,零新帧资产) ----
var _crest_bricks: Array = [] # 纹章碎砖驻留面(close 时回收)
var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
var _fx_crest_shards := 0
var _fx_dust_motes := 0
var _fx_flag_dots := 0
+3 -3
View File
@@ -27,7 +27,7 @@ var _log: RichTextLabel
var _summary: Label
var _hint: Label
var _mode := "dig" # dig = 收起摘要 / settle = 实时流(模式归组合根驱动)
var _round_rows: Array = [] # 本结算轮行缓冲(上轮摘要构建源)
var _round_rows: Array[Dictionary] = [] # 本结算轮行缓冲(上轮摘要构建源)
var _pending_agg := {} # 聚合计数行 pending(type+batch_key → {row, count})
@@ -85,9 +85,9 @@ func finish_settlement_round() -> void:
_flush_pending_agg()
if _round_rows.is_empty():
return
var parts: Array = []
var parts: Array[String] = []
for bb in _round_rows:
parts.append(_strip_bb(String(bb)))
parts.append(_strip_bb(str(bb)))
var text := "上轮: " + " | ".join(parts)
if text.length() > 190:
text = text.substr(0, 190) + "…"
+1 -1
View File
@@ -31,7 +31,7 @@ var round_label: Label
var stage_center: Label
var enemy_box: Control # 站桩壳层伴随件容器(T3 语义改写:HBoxContainer → Control)
var log_view: BattleLogView
var _cards: Array = [] # StandeeBody(世界本体,敌)
var _cards: Array[Node2D] = [] # StandeeBody(世界本体,敌)
var _row: HBoxContainer # 文本半区行(stage_center 载体 + 世界布局基准)
var _companion_layer: Control # 壳层伴随件层(主角 slot_row 宿主;敌伴随件宿主 = enemy_box)
var _svp_container: SubViewportContainer
+3 -3
View File
@@ -17,11 +17,11 @@ var edge := 46.0: # 格边长(BoardView.set_cell_area 单点
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
# ── 接触带微光绘制面(2026-09-26 批:可挖域微光自"墙格整框"改"英雄格周界空间侧"——art-bible §3.2 v2.8;
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
var contact_bands: Array = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
var contact_corners: Array = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二)
var outline_sides: Array = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(轮廓加强批——程序侧结构件)
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(轮廓加强批——程序侧结构件)
# ── 高墙感 D 投影档(2026-09-28 挑档落码——研究件四变体用户裁 D=C+地面投影,ITB 同款高度线索)──
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
+1 -1
View File
@@ -127,7 +127,7 @@ const SHOVEL_FRAME_PX := 32.0 # 铲帧画布边长(缩放比与英雄帧
const SHOVEL_FRAME_S := 0.08 # ~80ms/帧(f0→f1→f2 共 ~240ms 即收)
const SHOVEL_GRIP := Vector2(18, 12) # 握把锚点(素材侧锁定的画布坐标——英雄持握手位对齐点)
var _shovel_texs: Array = [] # 惰性装载缓存(素材缺位 = 空表静默回退——素材不入 git)
var _shovel_texs: Array[Texture2D] = [] # 惰性装载缓存(素材缺位 = 空表静默回退——素材不入 git)
# 挖掘挥铲:hand = 英雄持握手位(BoardView.hero_hand_point 出口,fx 与棋盘同层同系)——
+3 -3
View File
@@ -102,7 +102,7 @@ var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微
var _breath_t := 0.0
var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit;rebuild 随清场复位)
var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀)
var _ripple_tws: Array = [] # R0 涟漪逐格 tween(rebuild 重入全杀)
var _ripple_tws: Array[Tween] = [] # R0 涟漪逐格 tween(rebuild 重入全杀)
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
var _hover := Vector2i(-9, -9)
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
@@ -768,8 +768,8 @@ func _apply_contact_bands() -> void:
if hu == null:
return
_contact_unit = hu
var bands: Array = []
var corners: Array = []
var bands: Array[int] = []
var corners: Array[int] = []
for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]:
if _diggable.has(_minesweeper.hero_pos() + d):
bands.append(0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)))
+3 -3
View File
@@ -21,8 +21,8 @@ const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class":
var _balance_label: Label
var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer
var _forge_rows: Array = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _workshop_rows: Array = [] # [{item_id, progress_l, status_l}]
var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
@@ -198,7 +198,7 @@ func _rebuild_workshop(data: Dictionary) -> void:
func _rebuild_recent(data: Dictionary) -> void:
var lines: Array = []
var lines: Array[String] = []
var recent: Array = data.get("recent", [])
if recent.is_empty():
lines.append(" (暂无冒险记录——完成或结束一次冒险后入账)")
+4 -4
View File
@@ -52,7 +52,7 @@ var _hp_bar: Control
var _attr_value: Label
var _bonus_row: HBoxContainer
var _bonus_value: Label
var _cand_ids: Array = [] # 本停点候选 id 集(图上命中合法集)
var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集)
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它)
var _hover_id := -1 # 悬停预承诺候选(-1 = 无)
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开)
@@ -230,7 +230,7 @@ func open_choices(cands: Array, runner) -> void:
_runner = runner
_hover_id = -1
_reset_abandon_confirm()
var visited: Array = []
var visited: Array[int] = []
if runner != null:
visited = runner.visited_ids()
if cands.size() > 1:
@@ -308,7 +308,7 @@ func _refresh_status() -> void:
ac_txt += " +%d" % ac_bonus
_attr_value.text = "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt]
var hp_bonus := int(_runner.bonus.get("max_hp", 0))
var parts: Array = []
var parts: Array[String] = []
if atk_bonus > 0:
parts.append("攻 +%d" % atk_bonus)
if hp_bonus > 0:
@@ -458,7 +458,7 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary:
func _assign_layers(nodes: Array, layer: Dictionary, slot: Dictionary) -> void:
var layer_counts := {}
var entry := _entry(nodes)
var queue: Array = []
var queue: Array[Dictionary] = []
if not entry.is_empty():
queue.append(entry)
layer[entry] = 0
+1 -1
View File
@@ -15,7 +15,7 @@ const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class":
var _gp_label: Label
var _cards_box: VBoxContainer
var _card_rows: Array = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流)
var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流)
func _init() -> void:
+32 -32
View File
@@ -152,8 +152,8 @@ func _smoke_node_map_templates() -> void:
"抽签确定性:同种子逐位同图(实测 %s)" % String(names[0]))
# ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档)
var lt = load("res://data/schemas/level_table.gd").new()
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30,
"templates": [tpl0]}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30,
"templates": [tpl0]}])
check(lt.validate().is_empty(), "N=1 池表通过图校验(回滚档形态合法)")
# ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异)
var rngF = load("res://core/rng_service.gd").new()
@@ -949,13 +949,13 @@ func _smoke_variation_framework() -> void:
check(_db.variation.row_by_id("nonexistent").is_empty(), "清单外 variant_id 返回空(读取通道在位)")
# 跨字段互锁负样本(加载期致命——枚举语义一致性闸)
var vt = load("res://data/schemas/variation_table.gd").new()
vt.rows = [{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
vt.rows.assign([{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
"effect": "flat_damage", "effect_value": 1, "trigger": "on_open", "placement": "any",
"board_types": [], "counter_items": []}]
"board_types": [], "counter_items": []}])
check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)")
vt.rows = [{"variant_id": "bad2", "display_name": "坏行乙", "unlock_level": 1, "scope": "region",
vt.rows.assign([{"variant_id": "bad2", "display_name": "坏行乙", "unlock_level": 1, "scope": "region",
"effect": "hide_number", "effect_value": 0, "trigger": "on_open", "placement": "any",
"board_types": [], "counter_items": []}]
"board_types": [], "counter_items": []}])
check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒")
@@ -2529,47 +2529,47 @@ func _smoke_skill_pool_death_void() -> void:
func _smoke_skill_pool_validation_reject() -> void:
var skill_table = load("res://data/schemas/skill_table.gd").new()
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}]
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}])
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"),
"加载校验拒绝:池条目 skill_id 悬空致命")
skill_table.rows = [{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}]
skill_table.rows.assign([{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}])
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
"加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)")
skill_table.rows = [
skill_table.rows.assign([
{"skill_id": "b1", "display_name": "b1", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 3},
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}]
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}]
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}])
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
"加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)")
skill_table.rows = [{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}]
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)")
skill_table.rows = [{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}]
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:普攻配冷却 ≠ 0(域校验锁死)")
skill_table.rows = [{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 30.0, "cooldown_beats": 1}]
skill_table.rows.assign([{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 30.0, "cooldown_beats": 1}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:damage_mult 越域 [1.1,16]")
var et2 = load("res://data/schemas/enemy_table.gd").new()
et2.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "a", "weight": 0}]}]
et2.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "a", "weight": 0}]}])
check(not et2.validate().is_empty(), "加载校验拒绝:池权重 ≤ 0")
et2.rows = [{"enemy_id": "x", "display_name": "x", "pool": []}]
et2.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": []}])
check(not et2.validate().is_empty(), "加载校验拒绝:空池(禁)")
# 新效应词汇批(2026-09-19):妨害 / 必中附加分量校验组
skill_table.rows = [
skill_table.rows.assign([
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4},
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}]
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}]
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}])
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
"加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)")
skill_table.rows = [{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}]
skill_table.rows.assign([{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:妨害技缺 buff_ref(效果引用)")
skill_table.rows = [{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}]
skill_table.rows.assign([{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:rider_mult 越域 (0,1]")
skill_table.rows = [{"skill_id": "b", "display_name": "b", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "rider_mult": 0.5, "cooldown_beats": 1}]
skill_table.rows.assign([{"skill_id": "b", "display_name": "b", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "rider_mult": 0.5, "cooldown_beats": 1}])
check(not skill_table.validate().is_empty(), "加载校验拒绝:非伤害技带必中附加分量")
# 施放类 side 守卫:妨害技引用敌方侧状态 → 拒绝(增益技引用主角侧同罪,本组以妨害反例覆盖)
skill_table.rows = [
skill_table.rows.assign([
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}]
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}]
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}])
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"),
"加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)")
@@ -2676,34 +2676,34 @@ func _smoke_level_graph() -> void:
var lt = load("res://data/schemas/level_table.gd").new()
var good := [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2]},
{"node_depth": 2, "type": "boss"}]
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": good}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": good}])
check(lt.validate().is_empty(), "图校验正例:两岔合流小图零错误")
var dup = JSON.parse_string(JSON.stringify(good))
dup[1].node_depth = 0
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dup}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dup}])
check(_has_err_category(lt.validate(), "dup_key"), "图校验拒绝:节点编号重复(关卡内唯一定位地址)")
var dual = JSON.parse_string(JSON.stringify(good))
dual.append({"node_depth": 9, "type": "battle", "next": [2]})
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dual}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dual}])
check(_has_err_category(lt.validate(), "structural"),
"图校验拒绝:第二无入边节点(入口唯一违例——入口节点数 = 3)")
var boss_out = JSON.parse_string(JSON.stringify(good))
boss_out[2].next = [1]
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": boss_out}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": boss_out}])
check(_has_err_category(lt.validate(), "structural"), "图校验拒绝:Boss 带出边(终点不可外出)")
var cyc = JSON.parse_string(JSON.stringify(good))
cyc[1].next = [0]
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": cyc}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": cyc}])
check(_has_err_category(lt.validate(), "structural"), "图校验拒绝:回边 1→0(连线只朝前进方向 ⇒ 无环)")
var dang = JSON.parse_string(JSON.stringify(good))
dang[0].next = [1, 7]
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dang}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dang}])
check(_has_err_category(lt.validate(), "dangling_next"), "图校验拒绝:出边指向不存在编号(悬空致命)")
var orphan = JSON.parse_string(JSON.stringify(good))
orphan[0].next = [1]
orphan[1].next = [2]
orphan.append({"node_depth": 5, "type": "battle", "next": [2]})
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": orphan}]
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": orphan}])
var orphan_errs: Array = lt.validate()
check(_has_err_category(orphan_errs, "structural"), "图校验拒绝:孤儿节点(从入口不可达 / 双入口同体违例)")
# ④ L2 选路推进:min / max 双路径全通(确定性路径轴——零随机流自开)