feat(tree): 迭代21天赋树实装批 T1-T6 主体——12节点换代+六键接线+营地四柜台(2026-10-12 U0)

T1 schema 换代:MetaTreeTable FORK_GROUPS 4→3(fg_mastery 退役)/RULE_KEYS 2→6(sense/economy 四键扩签)/
  fork 单行双态(effects_a/b+name_a/b、每组恰一校验)/row 字段退役(行门槛主语=节点等级 level_gate)/
  requires 只留点亮前置(R1 裁撤案执行);meta_tree.tres 33→12 行重排(脊柱2/锻造2含彩蛋/钥石2/同调6,
  att_socket1 改根节点无前置,easter_ley 摘 atk/armor_class 分量=契约9)
T2 购买与彩蛋面:tree_can_buy easter 拒购+行门槛换轴;tree_buy_fork=解锁默认A;tree_toggle_fork 零币切态;
  tree_rule_keys 按激活态 effects_a/b 投影;_check_easter_egg 特化判定(两线拉满+socket全解锁);营地树行板双态钮
T3 六键接线:EVENTS+flag_missed/FACTS+battle_index(L2 扩签两处);rebuild 六键合成订阅
  (posture 窗=battle_index&&round_index 双闸/sense 两组=counter 闸+记账双行 domain:adventure 限次2次 provisional);
  level_runner battle_index 注入(visited battle 型计数);_judge_flag 去重闸后 flag_missed 发射(契约12);
  聚=_roll_gp_drop 入账前×1.5 provisional(escrow 不乘);散=_spend_discounted ×80%(current_price/pool_price
  双计价点,刷新价不折=§11②定义边界);_start_current_level 限次按关清零(_reset_tree_limited_counters 可测口)
T4 注入换代:hero_override=树淬炼flat+FORGE_ROUTE_BONUS预留0(命中不注入=契约6);Attunement.slots 判定源切
  树点亮数(谓词表退役);tree_buy(ks_refresh) 同账路由 buy_attune_refresh(V15 双入口)
T5 旧档废弃与删除:save_manager tree_struct=2 结构版本标记+_normalize 白名单重建+树域废弃重置(禁迁移折算);
  migrate_tree/buy_forge/forge_flat/forge_level/forge_price/meta_forge_row/forge_products 行族/
  attunement_growth 表+访问器/生涯页三投影/career forge_total 全删;treasure_item_table UNLOCK_COUNTER_KEYS 内联;
  verifier_batch 花费策略换代树节点;MC slots 画像常量3
T6 营地统一:四柜台并排横滚(树淬炼主界面/锻造工坊钥匙门+零交互占位面板/秘宝匣钥匙门+内容迁入/图鉴常开);
  暂停层锻造段连带删;camp data forge 投影删
存量红修复:_flush_pending_reveals 三处空 source_class 补 fx_reveal+类级豁免(a57489e9 落地件,冒烟51行)
测试随批:smoke 树段全重写(V1/V2/V3/V8/V9/V16)+_smoke_tree_rules V10 行为段(窗/限次/守侧五场景编排rig/聚散
  同种子对拍)+单元 tree_rules_v11_test(六键合成行闭集校验)+run_all 22 套;ui_probe/ui_click_probe 换代树点击路径
轻闸全绿:冒烟886/0+run_all 22套+probe --click 全绿
This commit is contained in:
DKChan committed 2026-10-12 07:54:38 +08:00
1 parent 8093ff7ab2
commit 96fc61c701
30 files changed
+907 -1198

No files matched your search

+9
View File
@@ -1053,8 +1053,17 @@ func abandon() -> bool:
# ---- 关卡推进(私有——序列编排单一出口)----
# 树限次计数按关重置(迭代 21 T3——危险直觉锋/守两组「每关限次」账本键:跨战持久由 adventure
# 域导出/回灌承载,此处随关清零闭合生命周期四态;键集=效果引擎树合成行声明的 counter_id;
# 独立成口=V10 断言面直呼)
func _reset_tree_limited_counters() -> void:
for cid in ["tree_sense_sharp_used", "tree_sense_guard_used"]:
adventure_counters.erase(cid)
func _start_current_level() -> bool:
_runner = LevelRunner.new(db, rng)
_reset_tree_limited_counters() # 树限次计数按关重置(V10 生命周期四态闭合——断言面直呼本口)
# core↔core 接线只许方法 Callable(ADR-0001 S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环)
_runner.node_entered.connect(_forward_node_entered)
_runner.choices_presented.connect(_forward_choices_presented)
+13 -21
View File
@@ -1,27 +1,19 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 同调位读数是否由「通关次数 + Boss 击杀数」经 attunement_growth.tres 谓词表
# 实时求值(Σ达成 grant clamp slot_cap),迭代 10 过渡键 transitional_sync_slots 得以退役(P0)
# Date: 2026-10-03(迭代 11 第①批 秘宝同调实施批 T3——需求卡片 v1.1 §六 T3;设计权威底稿 proposal §2)
# 同调位求值服务:计数源组装(save 账本 → 计数字典)与实时求值,纯函数零副作用。
# 读数源切换(迭代 10 欠账③):装配面消费方(营地装配覆盖层 T4/冒险开始装配注入 T6)一律经本服务取
# Slots;过渡键 adventure.transitional_sync_slots 的在途读取面(treasure_items 持有上限 gate/战斗内沉没/
# 停点替换交互/main 同调位呈现)系过渡渠道承载——T7 退役面切换时随渠道一并消亡(该批读数源切换的
# 正主落点,06 欠账③销账注记随 T7/T9)。过渡期切早 = off 态 MC 对 81000 锚添新扰动(fresh 档计数全零
# → Slots=0 ≠ 过渡键 1),违「attune=off 逐位一致」🔴 契约——故本服务先立、在途面 T7 切。
# attune=off 纪律:零 await、零 RNG、零写副作用(🔴 契约 3 的服务侧承载)。
# 同调位求值服务(迭代 21 天赋树实装批 T4 换代——槽位收编 Q2 裁甲:槽位数量=树上同调位节点点亮数,
# 旧「按击杀 Boss 次数/通关次数自动涨」谓词表判定链随批删除〔attunement_growth.tres 与表访问器
# 随 T5 清退;boss_kill_count/clear_count 计数域保留——各有非槽位硬消费,见卡片 T0⑥〕)。
# 纯函数零副作用;attune=off 纪律:零 await、零 RNG、零写副作用。
class_name Attunement
extends RefCounted
# 计数源组装(save = SaveManager 或 null——模拟/冒烟未注入档,按全零计数求值)
static func counters_from_save(save) -> Dictionary:
if save == null:
return {}
return save.attunement_counters()
# 同调位实时求值(营地进冒险前求值一次 + 装配覆盖层实时呈现共用同一口径)
# 同调位实时求值(营地进冒险前求值一次 + 装配覆盖层实时呈现共用同一口径):
# 槽位数量 = att_socket1..3 树点亮数(tree_level>=1),未点亮=0。
# db 参数保留(调用面两处签名不变——当前判定源零表依赖,参数弃置)。
static func slots(db, save) -> int:
if db == null or db.attunement_growth == null:
if save == null:
return 0
return db.attunement_growth.slots(counters_from_save(save))
var n := 0
for i in range(1, 4):
if save.tree_level("att_socket%d" % i) >= 1:
n += 1
return n
+24 -6
View File
@@ -38,8 +38,10 @@ signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态
# 判定行 verdict · 高光行 highlight
# 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
# fx_reveal=专长揭示掷格(落空行无真实格〔迭代 21 存量红修复——a57489e9 落地件三处空来源
# 触发行来源断言,51 行/冒烟,两轮确定性复现〕)
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal"]
var db: ConfigDb
var rng: RngServiceCore
@@ -154,7 +156,7 @@ func _flush_pending_reveals(ri: int) -> void:
if not minesweeper.is_revealed(c) and not board.is_mine(c):
cands.append(c)
if cands.is_empty():
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "")
_emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "")
continue
var rg: RandomNumberGenerator = rng.open_stream("fx_reveal")
var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)]
@@ -162,12 +164,12 @@ func _flush_pending_reveals(ri: int) -> void:
var ok: bool = minesweeper.fx_reveal(cell)
_fx_revealing = false
if not ok:
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "")
_emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "")
continue
var snap = _reward_economy.query_full_snapshot(cell)
if snap is Dictionary:
_grant_routed(snap, "positive", "fx_reveal", cell, ri, "fx_reveal")
_emit_row("fx_reveal", "", cell, ri, "专长揭示", 0, {}, "fx_reveal")
_emit_row("fx_reveal", "fx_reveal", cell, ri, "专长揭示", 0, {}, "fx_reveal")
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
@@ -183,6 +185,8 @@ func _fx_fact(fact: String):
return n
"round_index":
return round_index
"battle_index":
return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
"flags_count":
var hits_n := 0
for fevt in _facts_flags:
@@ -1129,6 +1133,14 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
if _perfect_hit_false == 1:
hl = "perfect_broken" # 本板完美谓词已破的归因锚
_emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl)
# 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在
# 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
# 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行
# adventure 域计数器闸)
for h in _fx_emit("flag_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
var snap = _reward_economy.grant(cell)
if not (snap is Dictionary):
return
@@ -1467,7 +1479,13 @@ func _roll_gp_drop() -> int:
else:
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
return roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0))) + escrow
var drop := roll + db.shop_level_gp_bonus(int(encounter.get("level_index", 0)))
# 财富之道·聚(迭代 21 T3——战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
# 店内 escrow 不乘〔范围=仅战斗掉落渠道,卡片§11②裁决〕;本函数返回值=结算框「GP 增量」行
# 显示源,落在入账前=显示即实得;乘数 provisional 150%,值归 M4 数值批)
if (encounter.get("tree_rules", []) as Array).has("economy_hoard"):
drop = int(round(float(drop) * 1.5))
return drop + escrow
# ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口——
+3 -18
View File
@@ -23,10 +23,9 @@ const TABLE_MANIFEST := {
"difficulty": "difficulty.tres",
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
"meta_tree": "meta_tree.tres", # 迭代 12 T8 双层 BD 天赋树(MetaTreeTable 定义表 Rule 4 豁免——R1 甲融合案 33 节点)
"meta_tree": "meta_tree.tres", # 迭代 12 T8 建域、迭代 21 T1 换代 12 节点(MetaTreeTable 定义表 Rule 4 豁免——天赋树.md v0.8 结构事实源)
"treasure_items": "treasure_items.tres", # 迭代 7 T1 秘宝内容批(TreasureItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
"attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免)
"feats": "feats.tres", # 迭代 12 T1 双层 BD 规则改写型特性基建(FeatTable 定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
}
@@ -49,7 +48,6 @@ var meta: MetaTable
var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tree_node 访问器)
var treasure_items: TreasureItemTable # 迭代 7 T1 类型化持用(消费面走 treasure_* 访问器)
var adventure: AdventureTable
var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器)
var feats: FeatTable # 迭代 12 T1 类型化持用(消费面走 feat_* 访问器)
var is_ready := false
@@ -141,7 +139,6 @@ func load_tables(root: String, collect_only: bool) -> bool:
meta = loaded.get("meta")
treasure_items = loaded.get("treasure_items")
adventure = loaded.get("adventure")
attunement_growth = loaded.get("attunement_growth")
feats = loaded.get("feats")
meta_tree = loaded.get("meta_tree")
is_ready = not has_errors
@@ -277,13 +274,8 @@ func treasure_live_ids() -> Dictionary:
return out
# ── 同调轴访问器(迭代 11 第①批 T1——消费面一律走访问器,禁直读行集)──
func attunement_slot_cap() -> int:
return attunement_growth.slot_cap if attunement_growth != null else 0
func attunement_milestones() -> Array:
return attunement_growth.milestones if attunement_growth != null else []
# ── 同调轴访问器已删除(迭代 21 T5——attunement_growth 谓词表随槽位收编退役:槽位数量=树同调位
# 节点点亮数,判定源=Attunement.slots;boss_kill_count/clear_count 计数域保留——非槽位硬消费在册)──
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
@@ -361,13 +353,6 @@ func _shop_rows(family: String) -> Array:
# workshop_items {item_id, display_name, clear_threshold} ·
# conversion {attr, per_level_shift_pct}。
func meta_forge_row(attr: String) -> Dictionary:
for row in _meta_rows("forge_products"):
if String(row.get("attr", "")) == attr:
return row
return {}
func meta_boss_keystone_row(level_index: int) -> Dictionary:
for row in _meta_rows("boss_keystone"):
if int(row.get("level_index", -1)) == level_index:
+37 -5
View File
@@ -17,10 +17,12 @@ class_name EffectEngine
extends RefCounted
## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕
## +battle_won 战斗胜利〔战地包扎〕;原 16 发射点不动)
## +battle_won 战斗胜利〔战地包扎〕;迭代 21 天赋树批 T3 扩签 18→19:+flag_missed 标错判定
## 〔危险直觉·守挂点——_judge_flag 去重闸后发射,契约 12 触发主语=当回合标错动作;
## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;原发射点不动)
const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle",
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won"]
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "flag_missed"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
@@ -53,7 +55,8 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter",
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_flag",
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct"]
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct",
"battle_index"] # 迭代 21 T3 扩签——关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面)
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
@@ -113,17 +116,46 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
continue
for tr in (row.get("triggers", []) as Array):
_push_trigger("attune", iid, tr, row)
# 树规则合成订阅(T5——规则键效果走同面:非数据行,词表同闭集)
# 树规则合成订阅(迭代 21 T3 换代——六键按 v0.8 三组双态):posture 两键=「每关首轮窗」语义
# 〔battle_index==1 且 round_index==1;battle_index=关卡域战斗序由 enc 携带,原「每战首轮」
# 实态随窗语义换代〕;sense 两组=每关限次〔adventure 域计数器 lt 上限闸 + 命中即 counter_add
# 记账双行——同事件双行同预评估态见闸同记,序=装载序确定;生命周期四态:跨战持久=domain
# adventure 导出/回灌、按关重置=AdventureRunner._start_current_level 清零、新冒险清零=账本
# 归零、断点保留=断点快照随键〕;economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的
# tree_rules 成员判定(编排器/关卡 runner 读 enc 键集),不入事件面。数值全 provisional 归 M4。
for key_v in tree_rules:
var rk := String(key_v)
match rk:
"posture_first_strike":
_push_trigger("tree", rk, {"on": "round_start",
"when": [{"fact": "round_index", "op": "eq", "value": 1}],
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
"posture_bulwark":
_push_trigger("tree", rk, {"on": "damage_taken",
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "shield", "value": 1}}, {})
"sense_sharp":
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
_push_trigger("tree", rk, {"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "damage_mult", "value": 50}}, {})
_push_trigger("tree", rk, {"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_sharp_used",
"value": 1, "domain": "adventure"}}, {})
"sense_guard":
# 守=标错返还行动点·每关限次(契约 12 失误补偿——flag_missed 由 _judge_flag 去重闸后
# 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立)
_push_trigger("tree", rk, {"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
_push_trigger("tree", rk, {"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_guard_used",
"value": 1, "domain": "adventure"}}, {})
# 计数器账本重建(T6——battle 域清零照旧;adventure 域由 counters_seed 回灌〔rebuild 后调用〕)
_counters = {}
+27 -4
View File
@@ -483,6 +483,7 @@ func _start_battle(node: Dictionary) -> bool:
"attune_charges": _attune_charges,
"feats": _feats,
"tree_rules": _tree_rules,
"battle_index": _battle_index(),
"bonus_ref": bonus,
"adventure_counters": _adventure_counters_ref,
"voucher_rap": _voucher_rap,
@@ -501,6 +502,17 @@ func _forward_step_row(row: Dictionary) -> void:
step_row.emit(row)
# battle_index=第几场战斗(关卡域 1 基序——visited 已含当前节点〔_enter_node :437 先追加后启动〕;
# 按已到节点 type=battle 计数,与图形态 node_depth/单链数组下标两编号空间解耦〔混编碰撞免疫〕;
# 开局姿态组「每关首轮窗」条件面——迭代 21 T3)
func _battle_index() -> int:
var n := 0
for v in visited:
if String((v as Dictionary).get("type", "")) == "battle":
n += 1
return n
func _forward_round_started(ri: int) -> void:
round_started.emit(ri)
@@ -724,9 +736,20 @@ func shop_products() -> Array:
# 现价 = base_price + price_level_bonus(level_index) + price_increment × 该商品已购档数
# (散折价随批:economy_spend 激活时商品标价 ×80%——见 _spend_discounted)
func current_price(prow: Dictionary) -> int:
return int(prow.get("base_price", 0)) + db.shop_price_level_bonus(int(level_row.level_index)) \
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", "")))
return _spend_discounted(int(prow.get("base_price", 0)) + db.shop_price_level_bonus(int(level_row.level_index)) \
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", ""))))
# 散折价单点(迭代 21 T3——财富之道·散 economy_spend 激活时商品购买价 ×80% provisional,
# floor 取整偏向玩家、下限 1 防 1 元商品折成 0;范围=商品标价定义边界〔legacy 商品面 current_price
# + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折〔卡片§11②裁决〕;
# 折率归 M4 数值批)
func _spend_discounted(price: int) -> int:
if _tree_rules.has("economy_spend"):
return maxi(1, int(floor(float(price) * 0.8)))
return price
func purchased_tier(product_id: String) -> int:
@@ -842,12 +865,12 @@ func pool_price(kind: String) -> int:
var row: Dictionary = db.shop_pool_row(kind)
if row.is_empty():
if kind == "relic":
return int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))
return _spend_discounted(int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys)))))
return 999999
var base := float(maxi(1, int(row.get("base_price", 1))))
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \
* pow(db.shop_price_a(), float(_shop_visit_buys))
return int(ceil(price_f))
return _spend_discounted(int(ceil(price_f)))
# 货架只读投影(呈现层数据源——kind/显示名/件值/现价/库存余量齐备,UI 零复算)。
+67 -116
View File
@@ -29,41 +29,10 @@ func keystone_balance() -> int:
return save.keystone_balance()
# ---- 属性锻造(钥石直购、可重复升级——几何递增定价)----
func forge_level(attr: String) -> int:
return save.forge_level(attr)
# 每级强化 flat 值 × 当前级(注入面的算料——投值语言锁定 flat 整数)
func forge_flat(attr: String) -> int:
return forge_level(attr) * int(db.meta_forge_row(attr).get("flat_value", 0))
# 下一级价格:tier = 当前级,迭代 floor(p × ratio/100) 恰 tier 次(tier 0 = base_price)
func forge_price(attr: String) -> int:
var row: Dictionary = db.meta_forge_row(attr)
if row.is_empty():
return 0
var p := int(row.get("base_price", 0))
var ratio := int(row.get("price_ratio_pct", 100))
for _i in forge_level(attr):
p = int(floor(float(p) * float(ratio) / 100.0))
return p
# 购买:余额不足 / attr 出域 false 零副作用;成功 = 消费 + 升级 + 写盘
func buy_forge(attr: String) -> bool:
if db.meta_forge_row(attr).is_empty():
return false
if not save.spend_keystone(forge_price(attr)):
return false
if not save.set_forge_level(attr, forge_level(attr) + 1):
return false
return save.write_to_disk()
# ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)----
# 〔属性锻造柜台退役——迭代 21 T4/T5:buy_forge/forge_flat/forge_level/forge_price 与 meta_forge
# 表族随攻防所有权切分删除;生命唯一入口=树脊柱淬炼、攻/AC 唯一入口=锻造工坊路线(契约 9);
# 注入面 hero_override 换树淬炼 flat+锻造路线加值位(预留 0)公式。〕
# 解锁判定量单点(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展,禁与渠道实现耦合)。
# 契约:渠道由 meta.tres workshop_items.channel 声明(缺省 total_clears,词表 MetaTable.WORKSHOP_CHANNELS
@@ -142,15 +111,19 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
}
# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档+树四线之三条)----
# ---- 跨局注入(冒险开始参数面)----
# 迭代 21 T4 注入公式换代:基础值 + 树生命/命中淬炼 flat(命中线在账未注入——契约 6,消费面归
# 数值批)+ 锻造路线加值位(本批预留 0——锻造工坊实装批按当前武器/甲路线等级填充)+ 局内加算区
# (bonus 区域不经本面)。旧属性柜台 forge_flat 项退役移除(V7/V12);攻/AC 树来源随所有权切分
# 砍除(契约 9——树零攻防来源,现表零 atk/armor_class 行,tree_stat_flat 二轴恒 0 保留通式)。
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
# 迭代 12 T8:树四线 flat 并入(脊柱 stat+锻造脉+彩蛋——同一覆盖值通道;hit 线消费面=
# 命中折扣档归数值批,本批不入 override 三字段通道——披露)。
const FORGE_ROUTE_BONUS := {"atk": 0, "max_hp": 0, "armor_class": 0} # 锻造路线加值位(预留 0——锻造工坊实装批填充)
func hero_override() -> Dictionary:
return {
"atk": forge_flat("atk") + tree_stat_flat("atk"),
"max_hp": forge_flat("max_hp") + tree_stat_flat("max_hp"),
"armor_class": forge_flat("armor_class") + tree_stat_flat("armor_class"),
"atk": int(FORGE_ROUTE_BONUS.get("atk", 0)) + tree_stat_flat("atk"),
"max_hp": int(FORGE_ROUTE_BONUS.get("max_hp", 0)) + tree_stat_flat("max_hp"),
"armor_class": int(FORGE_ROUTE_BONUS.get("armor_class", 0)) + tree_stat_flat("armor_class"),
}
@@ -164,29 +137,37 @@ func tree_spent_total() -> int:
return total
# 节点可购校验(row-gating + 显式前置 + 币 + cap;free 节点=迁移/彩蛋不经购买)
# 节点可购校验(行门槛 + 显式前置 + 币 + cap;free 节点=钥匙不经购买,彩蛋=觉醒不经购买)
func tree_can_buy(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty():
return false
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
if String(row.get("kind", "")) == "easter":
return false
var lvl := save.tree_level(node_id)
if lvl >= int(row.get("cap", 1)):
return false
var row_num := int(row.get("row", 0))
if row_num > 0 and tree_spent_total() < db.meta_tree.row_gate(row_num):
# 刷新升级节点:cap 主语=Y 账本(单一事实源——树上入口与装配段同 cap 同价,防双账漂移)
if node_id == "ks_refresh" and db.meta_attune_refresh_row(save.attune_refresh_level() + 1).is_empty():
return false
# 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1)——迭代 21 主语平移,row 字段退役)
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat":
if tree_spent_total() < db.meta_tree.level_gate(lvl + 1):
return false
for req in (row.get("requires", []) as Array):
if save.tree_level(String(req)) < 1:
return false
var fg := String(row.get("fork_group", ""))
if fg != "" and save.fork_side(fg) != "":
return false # 同组已激活一侧——互斥(切另侧走 tree_toggle_fork 免费切换)
return true
# 节点购买(营地树 UI 唯一写口):树全钥石单币(2026-10-07 用户裁甲——原 T8 双币骨架披露销账;
# 表内 currency 字段保留为呈现元数据)。free=迁移点亮/彩蛋觉醒零币(price 0,零额消费无副作用)。
# 节点购买(营地树 UI 唯一写口):树全钥石单币(2026-10-07 用户裁甲)。free=工作间钥匙零币点亮
# (price 0,零额消费无副作用);彩蛋不经购买(can_buy 已拒——觉醒唯一入口=_check_easter_egg)。
# 刷新升级节点走同账路由(Q1 裁乙——树上+秘宝匣装配段双入口写同一 attune_refresh_level 账本,
# 价格/上限单一事实源=Y 行表值 keystone_cost;V15)。
func tree_buy(node_id: String) -> bool:
if node_id == "ks_refresh":
return buy_attune_refresh()
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or not tree_can_buy(node_id):
return false
@@ -198,21 +179,19 @@ func tree_buy(node_id: String) -> bool:
return save.write_to_disk()
# Fork 激活/切换(营地树 UI——同组换侧零币=免费切换裁定;未激活组首次激活=消费购买面 tree_buy
# 已购该侧后此处激活;同侧重复=零操作)
# Fork 切换(营地树 UI——一节点双态:购买解锁组默认 A 态,换态走本口零币;同态重复=零操作;
# 未解锁组切换=拒绝——解锁唯一入口=购买面 tree_buy_fork)
func tree_toggle_fork(group: String, side: String) -> bool:
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
return false
var cur := save.fork_side(group)
if cur == side:
return false # 同侧零操作
if cur == "":
return false # 未激活组走购买面(tree_buy 激活侧节点后由激活写入——见下)
if cur == "" or cur == side:
return false # 未解锁走购买面;同态零操作
save.set_fork_side(group, side)
return save.write_to_disk()
# Fork 侧节点购买入口(购节点即激活该侧——组内互斥由 can_buy 拦截另侧)
# Fork 节点购买入口(一节点双态——购=解锁开关并置默认 A 态;重复购买由 cap 经 can_buy 拦截)
func tree_buy_fork(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or String(row.get("kind", "")) != "fork":
@@ -223,7 +202,7 @@ func tree_buy_fork(node_id: String) -> bool:
if not save.spend_keystone(price):
return false
save.set_tree_level(node_id, 1)
save.set_fork_side(String(row.get("fork_group", "")), String(row.get("fork_side", "")))
save.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
return save.write_to_disk()
@@ -244,10 +223,11 @@ func tree_rule_keys() -> Array:
for row in db.tree_rows():
if String(row.get("kind", "")) != "fork":
continue
var fg := String(row.get("fork_group", ""))
if String(sides.get(fg, "")) != String(row.get("fork_side", "")):
continue
for k in (row.get("effects", {}) as Dictionary).keys():
var active := String(sides.get(String(row.get("fork_group", "")), ""))
if active == "":
continue # 未解锁组零规则输出
var fx: Dictionary = row.get("effects_" + active, {}) as Dictionary
for k in fx.keys():
if not out.has(String(k)):
out.append(String(k))
return out
@@ -265,9 +245,9 @@ func tree_camp_rows() -> Array:
"vein": String(row.get("vein", "")),
"kind": String(row.get("kind", "")),
"cap": int(row.get("cap", 1)),
"level": save.tree_level(String(row.get("node_id", ""))),
"level": save.attune_refresh_level() if String(row.get("node_id", "")) == "ks_refresh" else save.tree_level(String(row.get("node_id", ""))), # 刷新升级行主语=Y 账本(同账路由 V15)
"price": int(row.get("price", 0)),
"currency": String(row.get("currency", "gp")),
"currency": String(row.get("currency", "keystone")),
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
})
return out
@@ -276,72 +256,43 @@ func tree_camp_rows() -> Array:
func tree_fork_groups() -> Array:
var out: Array = []
var sides: Dictionary = save.fork_sides()
for fg in db.meta_tree.FORK_GROUPS:
var group_rows: Array = []
for row in db.tree_rows():
if String(row.get("fork_group", "")) == String(fg):
group_rows.append(row)
if group_rows.is_empty():
for row in db.tree_rows():
if String(row.get("kind", "")) != "fork":
continue
var sides_out: Array = []
for row in group_rows:
sides_out.append({
"side": String(row.get("fork_side", "")),
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
})
var fg := String(row.get("fork_group", ""))
out.append({
"group": String(fg),
"display_name": String(group_rows[0].get("display_name", String(fg))).split("·")[0],
"active_side": String(sides.get(String(fg), "")),
"sides": sides_out,
"group": fg,
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"active_side": String(sides.get(fg, "")),
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
"price": int(row.get("price", 0)),
"sides": [
{"side": "a", "display_name": String(row.get("name_a", "")), "effects": row.get("effects_a", {}) as Dictionary},
{"side": "b", "display_name": String(row.get("name_b", "")), "effects": row.get("effects_b", {}) as Dictionary},
],
})
return out
# 彩蛋觉醒检查(锻造脉三节点全满→easter_ley 免费点亮——发现率监控 T10 呈报)
# 彩蛋觉醒检查(特化判定——生命/命中两线拉满 且 同调位 1/2/3 全解锁 → easter_ley 免费点亮;
# 迭代 21 T2 换代:不再依赖 requires 点亮形态〔R1 裁撤〕,发现率监控呈报面沿袭)
func _check_easter_egg() -> void:
var row: Dictionary = db.tree_node("easter_ley")
if row.is_empty() or save.tree_level("easter_ley") > 0:
if save.tree_level("easter_ley") > 0:
return
for req in (row.get("requires", []) as Array):
var req_row: Dictionary = db.tree_node(String(req))
if save.tree_level(String(req)) < int(req_row.get("cap", 1)):
for nid in ["sp_hp", "sp_hit"]:
var row: Dictionary = db.tree_node(nid)
if row.is_empty() or save.tree_level(nid) < int(row.get("cap", 1)):
return
for i in range(1, 4):
if save.tree_level("att_socket%d" % i) < 1:
return
save.set_tree_level("easter_ley", 1)
# 三体系迁移(迭代 12 T9——v4→v5 旧域只读→树域点亮;幂等=重复迁移零变化):
# forge_levels 等级→锻造脉节点树级原样映射(钥石付价不变零亏损——「宁多勿少」以零亏损满足);
# item_unlocks 任一真→ks_workshop 点亮;同调槽位解锁态→socket 点亮(谓词表 Slots 求值≥i)。
func migrate_tree() -> Dictionary:
var migrated := {"forge": 0, "unlock": 0, "socket": 0}
for pair in [["forge_atk", "atk"], ["forge_hp", "max_hp"], ["forge_ac", "armor_class"]]:
var lvl := save.forge_level(String(pair[1]))
if lvl > 0 and save.tree_level(String(pair[0])) < lvl:
save.set_tree_level(String(pair[0]), lvl)
migrated["forge"] += 1
var any_unlock := false
for iid in db.treasure_live_ids().keys():
if save.item_unlocked(String(iid)):
any_unlock = true
break
if not any_unlock:
for row in db.treasure_attunement_rows():
if save.item_unlocked(String(row.get("item_id", ""))):
any_unlock = true
break
if any_unlock and save.tree_level("ks_workshop") < 1:
save.set_tree_level("ks_workshop", 1)
migrated["unlock"] += 1
var slots := Attunement.slots(db, save)
for i in range(1, slots + 1):
if save.tree_level("att_socket%d" % i) < 1:
save.set_tree_level("att_socket%d" % i, 1)
migrated["socket"] += 1
save.write_to_disk()
return migrated
# 三体系迁移函数已删除(迭代 21 T5——旧档破坏性废弃〔Q3 用户裁:开发阶段无迁移〕;换代前结构
# 由 SaveManager._normalize 的 tree_struct 标记检测废弃重置,禁迁移禁折算)。
# ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 需求卡片 P1 甲)----
+30 -35
View File
@@ -8,12 +8,19 @@
# RngService,可独立实例化独立验证〔存档序列化.md 验收标准〕)。
#
# 域清单与 owner 分工(内容 owner 定口径,本类照单承载、不替任何人定义进度):
# keystone_balance / forge_levels / item_unlocks / level_clear_counts / boss_first_kill
# —— 内容 owner = 元进度.md(营地三柜台:钥石余额/属性锻造/道具工坊/通关次数/首杀标记)
# keystone_balance / item_unlocks / level_clear_counts / boss_first_kill
# —— 内容 owner = 元进度.md(钥石余额/道具工坊/通关次数/首杀标记;属性锻造柜台已退役
# 〔迭代 21 攻防所有权切分——forge_levels 域随批清退,生命唯一入口=树脊柱、攻/AC 唯一入口
# =锻造工坊路线,见 meta_progression.gd 注入面〕)
# boss_kill_count / attunement_equipped / attune_refresh_level
# —— 内容 owner = 秘宝.md(同调位账本,M-4 迭代 11 第①批 T2):Boss 击杀总数〔含重复〕=
# attunement_growth 谓词表计数源;装配关系 = 局外进度事实(元进度域同族——非契约 2 反面清单的
# 「局内道具库存」,后者=冒险进行中瞬态);attune_refresh_level = Y 刷新升级购买等级
# 生涯页终身计数与迭代 22 解锁条件消费面〔原 attunement_growth 谓词表计数源职责随迭代 21
# T4 槽位收编退役——槽位数量=树同调位节点点亮数〕;装配关系 = 局外进度事实(元进度域同族
# ——非契约 2 反面清单的「局内道具库存」,后者=冒险进行中瞬态);attune_refresh_level = Y 刷新
# 升级购买等级
# tree_nodes / fork_sides / tree_struct —— 内容 owner = 天赋树.md(迭代 12 T9 建域、迭代 21 T5
# 换代承载:tree_nodes=node_id→已购等级;fork_sides=fork_group→激活态;tree_struct=树结构
# 版本标记〔缺失=换代前结构→加载时树域破坏性废弃重置——开发阶段无迁移,Q3 用户裁〕)
# breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归
# AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例
# recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径)
@@ -42,7 +49,8 @@ extends RefCounted
const SAVE_FORMAT_VERSION := 6 # registry schema_version:v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;需求卡片「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台)
const TREE_STRUCT_VERSION := 2 # 树结构版本标记(迭代 21 T5——12 节点 v0.8 结构;加载检测缺失
# 或更低 = 换代前结构 → 树域破坏性废弃重置〔契约 4:禁迁移禁折算〕)
var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径)
var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写)
@@ -85,12 +93,9 @@ func _migrate_v3_v4(data: Dictionary) -> Dictionary:
return data
# v4→v5:天赋树域+三体系迁移(迭代 12 T9——R1 甲融合案;差额补偿原则=宁多勿少):
# ①tree_nodes/fork_sides 两新域(缺键默认补齐);②forge_levels 已购等级→锻造脉节点等效点亮
# (原样映射零亏损——钥石付价不变);③item_unlocks 已解锁→ks_workshop Unlock 节点点亮;
# ④attunement 槽位解锁态→同调 socket 点亮(已拥有道具/装配关系不动——装配交互不变裁定);
# ⑤迁移幂等由 MetaProgression.migrate_tree 侧承载(重复迁移零变化——T10 断言面)。
# 旧三域只读保留一版周期(forge_levels/item_unlocks 读取面维持——回滚窗口)。
# v4→v5:天赋树域(迭代 12 T9 建域——meta 三新域 tree_nodes/fork_sides/migration_note,缺键默认
# 补齐;迭代 21 T5 换代:原「三体系迁移」映射体随 migrate_tree 一并删除——开发阶段破坏性废弃
# 〔Q3 用户裁〕,换代前结构由 _normalize 的 tree_struct 标记检测废弃重置,本函数只剩缺键补齐)。
func _migrate_v4_v5(data: Dictionary) -> Dictionary:
var meta: Dictionary = data.get("meta", {}) as Dictionary
if not meta.has("tree_nodes"):
@@ -116,15 +121,15 @@ func _default_data() -> Dictionary[String, Variant]:
"version": SAVE_FORMAT_VERSION,
"meta": {
"keystone_balance": 0,
"forge_levels": {"max_hp": 0, "atk": 0, "armor_class": 0},
"item_unlocks": {},
"level_clear_counts": {"1": 0, "2": 0, "3": 0},
"boss_first_kill": {},
"boss_kill_count": 0,
"attunement_equipped": [],
"attune_refresh_level": 0,
"tree_nodes": {}, # 天赋树已购等级(node_id→int——迭代 12 T9 树域)
"fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载)
"tree_nodes": {}, # 天赋树已购等级(node_id→int——迭代 12 T9 建域)
"fork_sides": {}, # Fork 激活态(fork_group→"a"|"b"——一节点双态切换承载,迭代 21 收编语义)
"tree_struct": TREE_STRUCT_VERSION, # 树结构版本标记(迭代 21 T5——缺失=换代前结构,加载废弃重置)
"recent_summaries": [],
"first_adventure_done": false,
"adventure_total": 0, # 生涯终身计数·冒险总局数(迭代 18 卡④——开局即计含放弃与失败,收束唯一入账)
@@ -229,18 +234,6 @@ func spend_keystone(n: int) -> bool:
return true
func forge_level(attr: String) -> int:
return int(_data["meta"]["forge_levels"].get(attr, 0))
func set_forge_level(attr: String, n: int) -> bool:
if not FORGE_ATTRS.has(attr):
push_error("[存档序列化] 属性锻造键出域拒绝:%s(合法键 %s)" % [attr, str(FORGE_ATTRS)])
return false
_data["meta"]["forge_levels"][attr] = n
return true
func item_unlocked(item_id: String) -> bool:
return _data["meta"]["item_unlocks"].has(item_id)
@@ -403,22 +396,24 @@ func _corrupt_fallback(reason: String) -> bool:
return true
# 迁移后/低版本档字段补齐(改版不弃档:缺新域按默认补齐,既有值保留)
# 迁移后/低版本档字段补齐 + 退役域清退(改版不弃档:缺新域按默认补齐,既有值保留;
# 退役域按默认模型键集白名单收口——换代退役键〔forge_levels/migration_note 等树域旧件〕不流入
# 内存不回写盘,开发阶段破坏性废弃口径〔迭代 21 T5,Q3 用户裁:禁迁移禁折算〕)。
# 树结构检查先于缺键补齐:换代前结构(tree_struct 标记缺失或更低)→ 树域废弃重置——
# tree_nodes/fork_sides 清空、钥石余额等他域零触碰(V16 断言面)。
func _normalize(data: Dictionary) -> Dictionary[String, Variant]:
var merged := _default_data()
var def_meta: Dictionary = merged["meta"] # 补齐源引用(先于 data 覆盖取出——data 自带 meta 时 merged["meta"] 被换走,此引用仍指默认值)
for key in data:
merged[key] = data[key]
var meta: Dictionary = merged.get("meta", {})
if int(meta.get("tree_struct", 0)) < TREE_STRUCT_VERSION:
meta["tree_nodes"] = {}
meta["fork_sides"] = {}
var clean_meta := {}
for key in def_meta:
if not meta.has(key):
meta[key] = def_meta[key]
var forge: Dictionary = meta.get("forge_levels", {})
for sk in def_meta["forge_levels"]:
if not forge.has(sk):
forge[sk] = def_meta["forge_levels"][sk]
meta["forge_levels"] = forge
merged["meta"] = meta
clean_meta[key] = meta.get(key, def_meta[key])
merged["meta"] = clean_meta
return merged
@@ -1,81 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 同调位增长是否由「通关次数 + Boss 击杀数」经数据驱动的里程碑谓词表实时求值,
# 试玩后调增长曲线 = 改表不动码(proposal-magic-item-attunement-2026-09-22 §2.1 用户点名要求)
# Date: 2026-10-03(迭代 11 第①批 秘宝同调实施批 T1——需求卡片 v1.1 §六 T1;设计权威底稿 proposal §2)
# 数据配置 AttunementGrowthTable schema(秘宝 回填 owner)。
# 单行全局常量表(slot_cap + milestones 行族——AdventureTable 单行全局先例,Rule 4 豁免)。
# 行 = Dictionary:{id: String 唯一, grant: int, requires: [{key: String, threshold: int}]}——
# requires 为 (计数键, 阈值) 对的 AND 列表,全部满足即授予 grant 槽;
# 同调位读数唯一口径 = Σ已达成 grant clamp slot_cap(slots())。
# 承重契约(需求卡片 §三 7 + 底稿 §2.1):
# 计数键封闭枚举(COUNTER_KEYS——预留可插拔位 COUNTER_KEYS_RESERVED 本期不实现、加行即扩展不破坏
# schema);阈值 [1,999];Σgrant ≤ slot_cap(超出部分永不可达,禁);数值全 provisional 归
# 难度曲线与Boss——本表只锁形态,试玩调平衡的全部动作落在表上(改阈值/换计数键/slot_cap 加行)。
# OR/复合需求出现时升级谓词形态走需求卡片修订(底稿 §2.1 同款约束)。
class_name AttunementGrowthTable
extends Resource
@export var slot_cap: int = 3
@export var milestones: Array = [] # Array[Dictionary] 键 id/grant/requires
# 计数键封闭枚举(底稿 §2.1——save_file 逐域记账的键词表;T2 落 boss_kill_count 记账)
const COUNTER_KEYS := ["clear_count", "boss_kill_count"]
# 预留可插拔位(本期不实现,schema 枚举不收——底稿 §2.1 明列四件)
const COUNTER_KEYS_RESERVED := ["boss_firstkill_count", "adventure_count", "no_damage_clear_count", "run_depth_best"]
func validate() -> Array:
var errs: Array = []
if slot_cap < 0 or slot_cap > 9:
errs.append({"category": "domain", "message": "slot_cap 越域 [0,9]: %d" % slot_cap})
var seen := {}
var grant_sum := 0
for row in milestones:
var mid: String = String(row.get("id", ""))
if mid == "":
errs.append({"category": "domain", "message": "milestone id 空"})
continue
if seen.has(mid):
errs.append({"category": "dup_key", "message": "milestone id 重复: %s" % mid})
continue
seen[mid] = true
var grant: int = int(row.get("grant", 0))
if grant < 1 or grant > maxi(1, slot_cap):
errs.append({"category": "domain", "message": "%s grant 越域 [1,slot_cap=%d]: %d" % [mid, slot_cap, grant]})
grant_sum += maxi(0, grant)
var reqs: Array = row.get("requires", [])
if reqs.is_empty():
errs.append({"category": "domain", "message": "%s requires 空(无条件授予非法——里程碑必有谓词)" % mid})
var key_seen := {}
for req in reqs:
var key: String = String(req.get("key", ""))
if not COUNTER_KEYS.has(key):
errs.append({"category": "enum", "message": "%s 计数键出域 %s: %s" % [mid, str(COUNTER_KEYS), key]})
if key_seen.has(key):
errs.append({"category": "dup_key", "message": "%s 计数键重复: %s" % [mid, key]})
key_seen[key] = true
var th: int = int(req.get("threshold", 0))
if th < 1 or th > 999:
errs.append({"category": "domain", "message": "%s 阈值越域 [1,999]: %d" % [mid, th]})
if grant_sum > slot_cap:
errs.append({"category": "cross_field", "message": "Σgrant(%d) > slot_cap(%d)——超出部分永不可达,schema 禁" % [grant_sum, slot_cap]})
return errs
# 实时求值:Σ已达成 grant(谓词 = requires 全键达标;counters = {计数键: int},缺键按 0 计)
func granted_slots(counters: Dictionary) -> int:
var total := 0
for row in milestones:
var ok := true
for req in row.get("requires", []):
if int(counters.get(String(req.get("key", "")), 0)) < int(req.get("threshold", 0)):
ok = false
break
if ok:
total += maxi(0, int(row.get("grant", 0)))
return total
# 同调位读数唯一口径:Σ达成 grant clamp slot_cap(求值服务/装配覆盖层/运行期上限共读此口径)
func slots(counters: Dictionary) -> int:
return mini(granted_slots(counters), slot_cap)
@@ -1 +0,0 @@
uid://tfgsbs87xakg
+7 -32
View File
@@ -2,29 +2,28 @@
# Validation Question: 玩家能否在营地用钥石换永久强化、且强化在下一局开局生效——存档变厚成为玩家可感的现实
#(元进度 GDD 验收标准,本批 = 其实现批)
# Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2)
# 数据配置 MetaTable schema(元进度/永久强化 行族四轴)。
# 数据配置 MetaTable schema(元进度行族)。
# 行族四轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集):
# forge_products = 属性锻造商品行(键 attr:{max_hp, atk, armor_class} 恰三唯一——注入契约三字段必齐;
# 投值语言锁定 flat 整数;几何定价 tier 级价格 = 迭代 p = floor(p × price_ratio_pct/100),tier 0 = base_price)。
# boss_keystone = Boss 钥石两档行(level_index 连续从 1;first_kill ≥ repeat_kill——首杀全量、
# 重复杀固定小额恒定不递减,两档数值归元进度设计批配读数)。
# workshop_items = 道具工坊行(item_id = 秘宝六件词表封闭恰全;首版解锁渠道 = 总通关次数
# 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 秘宝重设计批,本表只有解锁状态面)。
# conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]——
# conversion = 区间平移换算率行(attr 与三属性轴词表一致恰三;per_level_shift_pct ∈ [0,100]——
# registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。
# attune_refresh_upgrade = Y 刷新升级行族(迭代 11 第①批 T4——P1 甲:每冒险开局给 Y 次、冒险周期池、
# 用完即无;level 连续从 1,等级 L → refresh_charges=Y 表值,钥石直购 keystone_cost;数值全 provisional)。
# 〔forge_products 属性锻造行族已退役——迭代 21 T5 攻防所有权切分:生命唯一入口=树脊柱、攻/AC
# 唯一入口=锻造工坊路线;行族、访问器与购买面随批删除,历史投值记载归 git 考古。〕
# 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。
class_name MetaTable
extends Resource
@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
@export var attune_refresh_upgrade: Array[Dictionary] = [] # Array[Dictionary] 键 level——Y 刷新升级行族(迭代 11 第①批 T4)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"]
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 三属性轴词表(conversion 行键域——原 forge 词表名沿用作轴名)
const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"]
# 工坊解锁渠道词表(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展。
# 现行唯一合法值 total_clears = 总通关次数;扩新渠道 = 词表加值 + MetaProgression._workshop_progress
@@ -34,34 +33,10 @@ const WORKSHOP_CHANNELS := ["total_clears"]
func validate() -> Array:
var errs: Array[Dictionary] = []
# 行族空护栏(五族皆为 ConfigDB 消费面——缺族即致命)
for family in ["forge_products", "boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]:
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命)
for family in ["boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]:
if get(family).is_empty():
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
# forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400]
var seen_attr := {}
for row in forge_products:
var attr: String = row.get("attr", "")
if not FORGE_ATTRS.has(attr):
errs.append({"category": "enum", "message": "forge_products attr 出域 {max_hp, atk, armor_class}: %s" % attr})
if seen_attr.has(attr):
errs.append({"category": "dup_key", "message": "forge_products attr 重复: %s" % attr})
seen_attr[attr] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "forge_products %s display_name 空" % attr})
var flat: int = int(row.get("flat_value", 0))
if flat < 1 or flat > 999:
errs.append({"category": "domain", "message": "forge_products %s flat_value 越域 [1,999]: %d" % [attr, flat]})
var base: int = int(row.get("base_price", 0))
if base < 1 or base > 999:
errs.append({"category": "domain", "message": "forge_products %s base_price 越域 [1,999]: %d" % [attr, base]})
var ratio: int = int(row.get("price_ratio_pct", -1))
if ratio < 100 or ratio > 400:
errs.append({"category": "domain", "message": "forge_products %s price_ratio_pct 越域 [100,400]: %d" % [attr, ratio]})
if not forge_products.is_empty():
for attr_key in FORGE_ATTRS:
if not seen_attr.has(attr_key):
errs.append({"category": "coverage", "message": "forge_products 缺属性键: %s(注入契约三字段必齐——恰三)" % attr_key})
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
var seen_li := {}
for row in boss_keystone:
+52 -39
View File
@@ -1,23 +1,27 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 三旧体系(锻造/钥石/同调)收敛为一棵营地天赋树后,双币消费、row-gating、
# A/B 免费切换、彩蛋觉醒能否机械承载(P0——迭代 12 批次二 T8,R1 甲融合案)
# Date: 2026-10-04(双层 BD 实施批 T8;设计权威 双层BD体系-整合定稿-2026-10-03 §1.4)
# 数据配置 MetaTreeTable schema(元进度 回填 owner 批末落文;行 = Dictionary 键 node_id,Rule 4 豁免
# 定义表——FeatTable 先例)。
# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱 row-gating,钥石买深度〔2026-10-07
# 用户裁甲——树全钥石单币,原双币铁律骨架披露销账〕;forge=锻造脉
# 数值专精;keystone=钥石脉内容解锁;attune=同调脉槽位与规则)/ kind ∈ KINDS(stat=四线数值/
# rule=全局规则/unlock=内容解锁/socket=同调位/fork=A/B 互斥对/easter=彩蛋觉醒)/
# currency ∈ {keystone, free}(树全钥石单币——2026-10-07 用户裁甲;free=迁移点亮与彩蛋)/
# price [0,9999] / cap [1,9](等级上限;fork/unlock 恒 1)/
# row(脊柱行号 1..N——row-gating:行 N 可购条件 = 全树已购等级总和 ≥ SPINE_ROW_GATE×(N-1))/
# requires Array[node_id](显式前置——父开子脉内链)/ effects Dictionary(stat 四线 flat:
# atk/max_hp/hit/armor_class;rule 键封闭 RULE_KEYS)/ fork_group/fork_side(a/b——同组互斥、
# 营地免费切换)。
# 🔴 层级纪律(需求卡片红线 2):局外树只放全局规则件——effects 禁流派标签(relic 特性永不入树,
# archetype 键出域=加载致命);规则改写型特性只走局内随机渠道。
# A/B 只放规则面(定稿 1.3——全树互斥面 ≤10-15% 首期 4 组);两侧「不同强非强弱」MC 验差 <~3pp(T10)。
# 彩蛋 ≤3(定稿 1.4——点满某脉某排免费觉醒;发现率 <5% 改灰色预告——T10 追认单呈报)。
# 数据配置 MetaTreeTable schema(迭代 21 天赋树实装批 T1 换代——设计权威
# design/gdd/systems/跨局循环/天赋树.md v0.8,2026-10-11 三轮换代终态)。
# 结构:12 节点 = 脊柱 2(生命淬炼/命中淬炼——一线一节点 cap4,2026-10-11 用户裁:一个内容一个
# 节点、等级在节点内)+ 锻造脉 2(锻造工坊钥匙 free 无前置 + 彩蛋地脉共鸣——彩蛋含在此 2 内)
# + 钥石脉 2(秘宝匣/刷新升级)+ 同调脉 6(同调位 1/2/3 + Fork 3 双态节点)。
# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱;forge=锻造脉;keystone=钥石脉内容
# 解锁;attune=同调脉)/ kind ∈ KINDS(stat=数值线/rule=全局规则/unlock=内容解锁/socket=同调位/
# fork=一节点双态开关/easter=彩蛋觉醒)/ currency ∈ {keystone, free}(树全钥石单币——2026-10-07
# 用户裁甲;free=工作间钥匙与彩蛋)/ price [0,9999] / cap [1,9](等级上限;unlock/socket/fork/
# easter 恒 1)/ requires Array[node_id](点亮前置——前置节点 tree_level>=1 即满足;
# 「等级阈值前置」新形态经 R1 裁撤〔2026-10-12 卡片 v1.7 默认案执行〕,schema 只保留点亮前置)/
# effects Dictionary(stat 线 flat:max_hp/hit;rule 键封闭 RULE_KEYS)。
# Fork 一节点双态(2026-10-11 用户裁,原 A/B 双行结构收编):fork_group ∈ FORK_GROUPS 每组恰一节点;
# 行持 effects_a/effects_b 双效果键(A/B 只放规则面——键 ⊆ RULE_KEYS)+ name_a/name_b 双态呈现名;
# 购买解锁默认 A 态、营地零币切换(tree_toggle_fork),效果按激活态(save.fork_sides)取值。
# 行门槛(一线一节点换代):脊柱节点第 N 级可购需全树已购总等级 ≥ SPINE_ROW_GATE×(N−1)(总等级口径
# =tree_nodes 全部节点等级之和,免费点亮与 Fork 解锁计入——2026-10-11 歧义扫描裁甲);
# row 字段随层行退役。
# 🔴 层级纪律(需求卡片红线 2):局外树只放全局规则件——effects 禁流派标签(archetype 键出域=加载
# 致命);规则改写型特性只走局内随机渠道,永不入树。
# 彩蛋 ≤3(schema 校验);觉醒判定=MetaProgression._check_easter_egg 特化(生命/命中两线拉满+同调位
# 全解锁),不依赖 requires 点亮形态。
# 数值全 provisional 归 M4 数值批(契约 7);两侧「不同强非强弱」MC 验差 <~3pp(V19)。
class_name MetaTreeTable
extends Resource
@@ -27,21 +31,23 @@ const VEINS := ["spine", "forge", "keystone", "attune"]
const KINDS := ["stat", "rule", "unlock", "socket", "fork", "easter"]
const CURRENCIES := ["keystone", "free"] # 树全钥石单币(2026-10-07 用户裁甲——原 gp 枚举值退役,字段保留为呈现元数据)
const STAT_KEYS := ["atk", "max_hp", "hit", "armor_class"]
## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;首期两键=T8 Fork 面真实接线)
const RULE_KEYS := ["posture_first_strike", "posture_bulwark"]
## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;六键=三组 Fork 双态真实接线,
## 迭代 21 T1 扩签:posture 两键现役沿袭,sense/economy 四键随危险直觉/财富之道两组建效面新增)
const RULE_KEYS := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"]
const KEY_WHITELIST := ["node_id", "display_name", "vein", "kind", "currency", "price", "cap",
"row", "requires", "effects", "fork_group", "fork_side", "note"]
## 脊柱行门槛(row-gating:行 N 可购需全树已购总等级 ≥ GATE×(N−1)——「投入点数累计解锁新行」)
"requires", "effects", "effects_a", "effects_b", "name_a", "name_b", "fork_group", "note"]
## 脊柱行门槛(一线一节点:节点第 N 级可购需全树总等级 ≥ GATE×(N−1)——「投入点数累计解锁深度」)
const SPINE_ROW_GATE := 4
## A/B 组词表(同组恰两侧 a/b——互斥对;营地免费切换承载面)
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy", "fg_mastery"]
## Fork 双态组词表(每组恰一节点——互斥由单态承载;营地零币切换承载面;
## fg_mastery 克制专精整组砍除——2026-10-11 裁,类型表达归锻造工坊)
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"]
func validate() -> Array:
var errs: Array = []
if rows.is_empty():
return errs # 空表合法态(T8 建表先行——内容随迁移批落)
return errs # 空表合法态(建表先行——内容随换代批落)
var seen := {}
var fork_seen := {}
for row in rows:
@@ -76,22 +82,28 @@ func validate() -> Array:
var fx: Dictionary = row.get("effects", {}) as Dictionary
for ek in fx.keys():
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
errs.append({"category": "zero_flat", "message": "%s effects 键出域(四线/rule 词表封闭): %s" % [nid, String(ek)]})
errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
if fx.has("archetype"):
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
# A/B 互斥对结构:fork 行必带 fork_group+side;同组恰 a/b 各一
# Fork 一节点双态结构:fork 行必带 fork_group(每组恰一节点);主 effects 须空,效果只放
# a/b 双键且键 ⊆ RULE_KEYS(A/B 只放规则面——层级纪律)
if kind == "fork":
var fg := String(row.get("fork_group", ""))
if not FORK_GROUPS.has(fg):
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
var side := String(row.get("fork_side", ""))
if side != "a" and side != "b":
errs.append({"category": "enum", "message": "%s fork_side 出域: %s" % [nid, side]})
var fk := fg + "|" + side
if fork_seen.has(fk):
errs.append({"category": "dup_key", "message": "%s fork 对重复: %s" % [nid, fk]})
fork_seen[fk] = true
if fork_seen.has(fg):
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
fork_seen[fg] = true
if not fx.is_empty():
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
for skey in ["effects_a", "effects_b"]:
var sd: Dictionary = row.get(skey, {}) as Dictionary
if sd.is_empty():
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
for k in sd.keys():
if not RULE_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
# requires 悬空引用
for row in rows:
for req in (row.get("requires", []) as Array):
@@ -103,7 +115,7 @@ func validate() -> Array:
if String(row.get("kind", "")) == "easter":
easter += 1
if easter > 3:
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(定稿 §1.4 上限)" % easter})
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(上限 3)" % easter})
return errs
@@ -114,6 +126,7 @@ func row_by_id(node_id: String) -> Dictionary:
return {}
## row-gating 门槛(行 N 可购需全树总等级 ≥ GATE×(N−1);非脊柱节点跟随其 vein 父行——requires 主导)
func row_gate(row_num: int) -> int:
return SPINE_ROW_GATE * maxi(0, row_num - 1)
## 行门槛(一线一节点:脊柱节点第 N 级可购需全树总等级 ≥ GATE×(N−1);原 row-gating 行号主语
## 随层行退役平移到节点等级轴)
func level_gate(level: int) -> int:
return SPINE_ROW_GATE * maxi(0, level - 1)
+8 -3
View File
@@ -22,7 +22,8 @@
# form ∈ {"", "passive", "active_charged"}(空 = 切片期过渡形态行——掉落/停点渠道现行承载,T7 退役;
# passive = 装配整局生效;active_charged = 每冒险充能、挖掘期发起);
# charges_per_adventure(active_charged ≥1 / passive 恒 0);slot_cost(同调行恒 1——底稿裁决 5,schema 强制);
# unlock_requires 谓词行 [{key ∈ AttunementGrowthTable.COUNTER_KEYS, threshold ∈ [1,999]}](空 = 已解锁——
# unlock_requires 谓词行 [{key ∈ 计数键封闭枚举〔原 AttunementGrowthTable.COUNTER_KEYS,随迭代 21
# T5 谓词表退役就地内联为 UNLOCK_COUNTER_KEYS〕, threshold ∈ [1,999]}](空 = 已解锁——
# 首批全解锁合法态,底稿 §2.2 用户裁;正式解锁渠道归 元进度 道具工坊柜台);
# effective_rounds(前 N 轮限时件轮数——P4 甲表值化;仅 ap_rounds_front 行消费);
# charge_n(充能进度阈值 N——仅 ap_charge_safe 行消费)。
@@ -34,6 +35,10 @@ extends Resource
@export var rows: Array = [] # Array[Dictionary] 键 item_id
# 计数键封闭枚举(原 AttunementGrowthTable.COUNTER_KEYS——迭代 21 T5 谓词表退役后就地内联;
# boss_kill_count/clear_count 计数域保留〔各有非槽位硬消费,卡片 T0⑥〕)
const UNLOCK_COUNTER_KEYS := ["clear_count", "boss_kill_count"]
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
# 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
# cell_removal = 爆破工兵包中立移除格(T7 原位换装时启用——T6 影响评估前置);mine_hit_halve 系固定
@@ -164,8 +169,8 @@ func validate() -> Array:
var ukeys := {}
for req in ureqs:
var ukey: String = String(req.get("key", ""))
if not AttunementGrowthTable.COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(AttunementGrowthTable.COUNTER_KEYS), ukey]})
if not UNLOCK_COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
if ukeys.has(ukey):
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
ukeys[ukey] = true
-34
View File
@@ -1,34 +0,0 @@
[gd_resource type="Resource" script_class="AttunementGrowthTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/attunement_growth_table.gd" id="1"]
; 迭代 11 第①批 同调实施批 T1(2026-10-03)——同调位里程碑谓词表。
; 数值全 provisional 归 难度曲线与Boss(需求卡片 §三 7);行形态 = 底稿 §2.1 示意形态。
[resource]
script = ExtResource("1")
slot_cap = 3
milestones = [{
"id": "m1",
"grant": 1,
"requires": [{
"key": "clear_count",
"threshold": 1
}]
}, {
"id": "m2",
"grant": 1,
"requires": [{
"key": "boss_kill_count",
"threshold": 3
}]
}, {
"id": "m3",
"grant": 1,
"requires": [{
"key": "clear_count",
"threshold": 3
}, {
"key": "boss_kill_count",
"threshold": 6
}]
}]
+4 -21
View File
@@ -4,25 +4,6 @@
[resource]
script = ExtResource("1")
forge_products = [{
"attr": "max_hp",
"display_name": "生命锻造",
"flat_value": 3,
"base_price": 20,
"price_ratio_pct": 150
}, {
"attr": "atk",
"display_name": "攻击锻造",
"flat_value": 1,
"base_price": 20,
"price_ratio_pct": 150
}, {
"attr": "armor_class",
"display_name": "护甲锻造",
"flat_value": 1,
"base_price": 20,
"price_ratio_pct": 150
}]
boss_keystone = [{
"level_index": 1,
"first_kill": 3,
@@ -89,8 +70,10 @@ attune_refresh_upgrade = [{
; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2):
; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。
; forge flat 档与局内 GP 商店同族指数值档位非货币(血+3/攻+1/甲+1),base_price 现值 20(2026-09-22 对齿批投值)/ ratio 150 三行同;boss 两档 L1 5/1 · L2 8/2 · L3 12/3
; 〔2026-10-07 天赋树货币甲案修复批订正——原句「GP 商店同族…base 10」陈旧:GP 字样误导(锻造消费货币=钥石,meta_progression.gd:59 spend_keystone),base 20 经 2026-09-22 对齿批改值〕
; 〔forge_products 属性锻造行族已退役——迭代 21 T5 攻防所有权切分:生命唯一入口=树脊柱淬炼、
; 攻/AC 唯一入口=锻造工坊路线(契约 9 红线),meta_forge_row 访问器与 buy_forge 购买面随批删除;
; 历史投值记载(2026-09-22 对齿批 base 20/ratio 150、2026-09-23 标定批 6/9/1)系 conversion 行
; 与 ForgeGDD 考古用,行族本体不再在表。〕boss 两档 L1 5/1 · L2 8/2 · L3 12/3
; (首杀全量、重复杀固定小额恒定不递减);工坊六件阈值 1/1/2/2/3/3 按总通关次数(首版只挂通关次数渠道,
; 钥石直购价随定价批后补);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按
; 解锁后参数重测)。
+72 -319
View File
@@ -5,242 +5,63 @@
[resource]
script = ExtResource("1")
rows = [{
"node_id": "sp_hp_r1",
"display_name": "生命淬炼·1 层",
"node_id": "sp_hp",
"display_name": "生命淬炼",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 10,
"cap": 3,
"row": 1,
"cap": 4,
"requires": [],
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r1",
"display_name": "攻击淬炼·1 层",
"node_id": "sp_hit",
"display_name": "命中淬炼",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r1",
"display_name": "命中淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 10,
"cap": 3,
"row": 1,
"cap": 4,
"requires": [],
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r1",
"display_name": "护甲淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r2",
"display_name": "生命淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r2",
"display_name": "攻击淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r2",
"display_name": "命中淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r2",
"display_name": "护甲淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r3",
"display_name": "生命淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r3",
"display_name": "攻击淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r3",
"display_name": "命中淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r3",
"display_name": "护甲淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r4",
"display_name": "生命淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r4",
"display_name": "攻击淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r4",
"display_name": "命中淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r4",
"display_name": "护甲淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "keystone",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"armor_class": 1
}
}, {
"node_id": "forge_atk",
"display_name": "武器锻造",
"node_id": "forge_workshop",
"display_name": "锻造工坊",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_atk_r1"],
"effects": {
"atk": 1
}
"kind": "unlock",
"currency": "free",
"price": 0,
"cap": 1,
"requires": [],
"note": "工作间钥匙——无前置直接可点亮(2026-10-11 攻防所有权切分裁);点亮展开锻造工坊柜台"
}, {
"node_id": "forge_hp",
"display_name": "护身锻造",
"node_id": "easter_ley",
"display_name": "地脉共鸣",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_hp_r1"],
"kind": "easter",
"currency": "free",
"price": 0,
"cap": 1,
"requires": [],
"note": "彩蛋觉醒——判定归 _check_easter_egg 特化(生命/命中两线拉满+同调位全解锁),不经购买;🔴 effects 只持生命/命中两键(契约 9 攻防所有权切分——攻/护分量已摘除)",
"effects": {
"max_hp": 1
}
}, {
"node_id": "forge_ac",
"display_name": "甲胄锻造",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_ac_r1"],
"effects": {
"armor_class": 1
"max_hp": 5,
"hit": 1
}
}, {
"node_id": "ks_workshop",
"display_name": "道具工坊",
"display_name": "秘宝匣",
"vein": "keystone",
"kind": "unlock",
"currency": "free",
"price": 0,
"cap": 1,
"requires": ["sp_hp_r1"]
"requires": ["sp_hp"],
"note": "工作间钥匙——前置=脊柱生命第 1 级(v0.8 节点总表);点亮展开秘宝匣柜台(解锁+同调装配)"
}, {
"node_id": "ks_refresh",
"display_name": "刷新升级",
@@ -249,7 +70,8 @@ rows = [{
"currency": "keystone",
"price": 30,
"cap": 2,
"requires": ["ks_workshop"]
"requires": ["ks_workshop"],
"note": "树上刷新升级——购买写 attune_refresh_level 同账(迭代 21 T4 接线,双入口并存 Q1 裁乙)"
}, {
"node_id": "att_socket1",
"display_name": "同调位 1",
@@ -258,7 +80,8 @@ rows = [{
"currency": "keystone",
"price": 20,
"cap": 1,
"requires": ["sp_atk_r1"]
"requires": [],
"note": "同调脉根节点——无前置(v0.8 节点总表换代,原 sp_atk_r1 前置随攻防切分砍除)"
}, {
"node_id": "att_socket2",
"display_name": "同调位 2",
@@ -278,140 +101,70 @@ rows = [{
"cap": 1,
"requires": ["att_socket2"]
}, {
"node_id": "fork_posture_a",
"display_name": "先手压制",
"node_id": "fork_posture",
"display_name": "开局姿态",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_posture",
"fork_side": "a"
"name_a": "先手压制",
"name_b": "稳健拓荒",
"effects_a": {
"posture_first_strike": 1
},
"effects_b": {
"posture_bulwark": 1
}
}, {
"node_id": "fork_posture_b",
"display_name": "稳健拓荒",
"node_id": "fork_sense",
"display_name": "危险直觉",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_posture",
"fork_side": "b"
}, {
"node_id": "fork_sense_a",
"display_name": "雷感直觉·锋",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_sense",
"fork_side": "a"
"name_a": "锋",
"name_b": "守",
"effects_a": {
"sense_sharp": 1
},
"effects_b": {
"sense_guard": 1
}
}, {
"node_id": "fork_sense_b",
"display_name": "雷感直觉·守",
"node_id": "fork_economy",
"display_name": "财富之道",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_sense",
"fork_side": "b"
}, {
"node_id": "fork_economy_a",
"display_name": "经济人格·聚",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_economy",
"fork_side": "a"
}, {
"node_id": "fork_economy_b",
"display_name": "经济人格·散",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
"name_a": "聚",
"name_b": "散",
"effects_a": {
"economy_hoard": 1
},
"fork_group": "fg_economy",
"fork_side": "b"
}, {
"node_id": "fork_mastery_a",
"display_name": "克制专精·穿刺",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_mastery",
"fork_side": "a"
}, {
"node_id": "fork_mastery_b",
"display_name": "克制专精·魔法",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_mastery",
"fork_side": "b"
}, {
"node_id": "easter_ley",
"display_name": "地脉共鸣",
"vein": "forge",
"kind": "easter",
"currency": "free",
"price": 0,
"cap": 1,
"requires": ["forge_atk", "forge_hp", "forge_ac"],
"effects": {
"atk": 1,
"max_hp": 5,
"hit": 1,
"armor_class": 1
"effects_b": {
"economy_spend": 1
}
}]
; 双层 BD 实施批 T8(2026-10-04,R1 甲融合案——整合定稿 §1.4 一树双币三脉一脊+装配区+彩蛋):
; 脊柱 4 行×4 线(生命/攻击/命中/护甲——钥石买深度 cap3 价 10×row〔2026-10-07 用户裁甲 gp→keystone〕;
; row-gating=全树已购总等级
; ≥ 4×(row−1));锻造脉 3 节点收编 forge_levels(钥石付价=现行迁移零痛);钥石脉=工坊解锁收编
; (free 迁移点亮)+刷新升级移植;同调脉 socket 3(钥石=规则面首站)+Fork 4 对(A/B 集中同调脉
; 营地免费切换——effects 首期两 rule 键真实接线余组 stat 面 provisional);彩蛋 1(锻造满排觉醒)。
; 迭代 21 天赋树实装批 T1 换代(2026-10-12,设计权威 design/gdd/systems/跨局循环/天赋树.md v0.8):
; 12 节点 = 脊柱 2(生命淬炼/命中淬炼——一线一节点 cap4,2026-10-11 用户裁,原 4 层×4 线 16 行收编;
; 攻/护淬炼线随攻防所有权切分砍除——攻唯一入口=锻造工坊武器路线、AC 唯一入口=甲路线〔契约 9 红线〕)
; + 锻造脉 2(forge_workshop 钥匙 free 无前置 + easter_ley 彩蛋——effects 只持生命/命中两键,
; :402/:405 旧攻/护分量已摘除)+ 钥石脉 2(秘宝匣 ks_workshop〔display 旧名「道具工坊」退役〕/
; 刷新升级 ks_refresh)+ 同调脉 6(同调位 1/2/3——socket1 改根节点无前置 + Fork 3 单行双态节点,
; 原 8 行 A/B 双行结构与 fg_mastery 克制专精组退役)。
; 行门槛=一线一节点主语:脊柱节点第 N 级需全树总等级 ≥4×(N−1)(SPINE_ROW_GATE=4 公式平移,
; row 字段退役);requires=点亮前置(R1 裁撤案执行,schema 无等级阈值形态)。
; 🔴 层级纪律:局外树只放全局规则件——零 archetype 键(schema 校验强制);规则改写型特性只走局内随机渠道。
; 数值全 provisional 归 M-4 数值批;A/B 两侧差 <~3pp 验收=T10 MC。
; 2026-10-07 用户裁甲:树全钥石单币落定(脊柱 16 行 currency gp→keystone;T8「GP 余额域缺位」双币骨架披露销账——GP 局内清零不入存档永不互兑条款不动,元进度.md:55)。数值 provisional 归 M4。
; 数值全 provisional 归 M4 数值批(契约 7——价格沿旧量纲锚:脊柱 10=旧 r1 价、socket 20/40/60、
; Fork 25、刷新 30 沿旧;生命 +5/级、命中 +1/级沿旧量纲);A/B 两侧差 <~3pp 验收=V19 MC。
+7 -25
View File
@@ -61,26 +61,15 @@ func on_camp_closed() -> void:
_refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
# 营地快照投影(纯读——工坊六行/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归
# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役)
func camp_data() -> Dictionary:
var meta = _meta_get.call()
var forge_rows: Array[Dictionary] = []
if meta != null:
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
forge_rows.append({
"attr": attr,
"display_name": String(row.get("display_name", "")),
"level": meta.forge_level(attr),
"flat": int(row.get("flat_value", 0)),
"price": meta.forge_price(attr),
})
return {
"balance": meta.keystone_balance() if meta != null else 0,
"forge": forge_rows,
"workshop": meta.workshop_status() if meta != null else [],
"attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影
"tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影
"tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行)
"fork_groups": meta.tree_fork_groups() if meta != null else [],
"recent": _save_get.call().recent_summaries() if _save_get.call() != null else [],
}
@@ -90,17 +79,10 @@ func on_camp_buy(attr: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
if attr == "attune_refresh":
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1)
if not meta.buy_attune_refresh():
toast.show_toast("钥石不足——买不起这次升级", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
return
if not meta.buy_forge(attr):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
toast.show_toast("钥石不足——买不起这件强化", "warn")
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;双入口之一 Q1 裁乙——树上 ks_refresh 同账;
# 按钮禁用为常态防线,兜底拒绝可见 pattern 1。属性锻造面已退役——buy_forge 分支随批删除)
if attr != "attune_refresh" or not meta.buy_attune_refresh():
toast.show_toast("购买被拒", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
+5 -19
View File
@@ -1013,20 +1013,11 @@ func _on_settings_closed() -> void:
# ---- 局内暂停层·只读(迭代 17 卡③——入口角标在两屏装配段〔组 B〕;开合与数据喂入归组合根)----
# 暂停层快照投影(R4 数据只读同源:每次开层实时读 MetaProgression 访问器现值拼装——
# 快照仅服务单次开层生命周期(open_pause 重建各行),跨开层零持有;零写调用)
# 快照仅服务单次开层生命周期(open_pause 重建各行),跨开层零持有;零写调用。
# 属性锻造三行投影已删——迭代 21 柜台退役连带〔暂停层锻造段同步删除〕)
func _pause_data() -> Dictionary:
var forge_rows: Array[Dictionary] = []
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
forge_rows.append({
"attr": attr,
"display_name": String(row.get("display_name", "")),
"level": meta.forge_level(attr),
"flat": int(row.get("flat_value", 0)),
})
return {
"balance": meta.keystone_balance(),
"forge": forge_rows,
"workshop": meta.workshop_status(),
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
@@ -1058,7 +1049,9 @@ func _on_career_closed() -> void:
# 生涯页快照投影(只读同源——每次开层实时读 SaveManager 计数访问器 + MetaProgression 访问器
# 现值拼装(暂停层 _pause_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用)
# 现值拼装(暂停层 _pause_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用。
# 账号成长态三投影已删〔forge_levels_total/workshop/tree——2026-10-12 用户裁:生涯页不统计
# 天赋树与账号成长态,只统计终身计数〕)
func _career_data() -> Dictionary:
var clears: Array[Dictionary] = []
var boss_first := 0
@@ -1068,9 +1061,6 @@ func _career_data() -> Dictionary:
"count": save_manager.clear_count(li)})
if save_manager.boss_first_kill_marked(li):
boss_first += 1
var forge_total := 0
for attr in MetaTable.FORGE_ATTRS:
forge_total += meta.forge_level(attr)
return {
"total": save_manager.adventure_total(),
"cleared": save_manager.adventure_cleared(),
@@ -1079,10 +1069,6 @@ func _career_data() -> Dictionary:
"boss_first_total": db.level_count(),
"boss_kills": save_manager.boss_kill_count(),
"keystone": meta.keystone_balance(),
"forge_levels_total": forge_total,
"workshop": meta.workshop_status(),
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
}
+143 -144
View File
@@ -1,9 +1,14 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-21(元进度实现批——营地覆盖层呈现;2026-09-30 迭代 9 卡⑭ 视觉终稿——对照营地屏
# 方向稿 artwork/mockups/v3-draft-2026-09-29/4-camp.png 重排:石纹全屏底 + 顶栏铭牌/钥石徽 +
# 三柜台列(属性锻造/道具工坊/奖励格图鉴,头带深棕+米色板身+内嵌行板,暗底→亮板双色墨制)+
# 底栏关闭钮/操作注记;最近冒险收底栏条带。信号面/投影契约零变化:buy_requested(attr)/closed、
# open_camp(data)/refresh_camp(data)、_forge_rows 行结构 {attr,level_l,price_l,btn}(探针依赖)。
# Date: 2026-09-21(元进度实现批——营地覆盖层呈现;2026-10-12 迭代 21 T6 营地统一重排——
# 四柜台并排〔2026-10-11 锻造工坊裁决会话 A② 裁〕:树淬炼柜台=树脊柱界面形态=主界面 /
# 锻造工坊柜台〔钥匙门:forge_workshop 点亮前锁定态,点亮=工作间占位面板〕/ 秘宝匣柜台
# 〔钥匙门:ks_workshop 点亮前锁定态,点亮=现工坊内容整体迁入——六行+同调装配段+Y 升级行〕/
# 奖励格图鉴柜台常开独立位;宽度不足横向滚动条。旧属性锻造柜台退役〔攻防所有权切分——
# 生命唯一入口=树脊柱、攻/AC 唯一入口=锻造工坊路线〕)。
# 占位面板硬要求(2026-10-11 用户令):锻造工坊工作间占位面板打开/关闭零副作用——零写档、
# 零报错、面板内无任何可交互购买路径(纯文案标签,视觉终稿归 HUD 营地视觉批)。
# 信号面/投影契约:buy_requested(attr)〔现仅 Y 刷新升级行消费——装配段双入口之一 Q1 裁乙〕/closed、
# attune_toggled/tree_buy_requested/tree_fork_toggled、open_camp(data)/refresh_camp(data)。
# 挂接方式:开合与开始层互斥仿设置面先例(开始覆盖层非模态且树序在后——打开本层前由组合根
# 隐藏开始层,规范 2.3 遮挡纪律)。
# 高内聚边界:纯视图 setter 面——账本快照经组合根喂入(MetaProgression 只读投影),购买/关闭
@@ -15,24 +20,26 @@ signal buy_requested(attr: String)
signal closed
signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement)
signal tree_buy_requested(node_id: String) # 天赋树购买意图(迭代 12 T9——写口唯一归 MetaProgression.tree_buy)
signal tree_fork_toggled(group: String, side: String) # A/B 免费切换意图(同组换侧零币)
signal tree_fork_toggled(group: String, side: String) # 双态切换意图(一节点双态——零币切态)
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批)
# 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批)
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
const BADGE_PANEL_COL := UiKit.CAMP_BADGE_PANEL
var _balance_label: Label
var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer
var _tree_holder: VBoxContainer # 天赋树面板(迭代 12 T9 程序占位——行板复用行板结构)
var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮)
var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费)
var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费)
var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮)
var _forge_workbench: VBoxContainer # 锻造工坊工作间占位面板(点亮后展开——零交互纯文案)
var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见)
var _ws_lock_l: Label # 秘宝匣柜台锁定态行
var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行——迭代 11 第①批 T4 结构随迁)
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
var _attune_header_l: Label # 同调装配段(迭代 11 第①批 T4 工坊扩容——Slots 点亮呈现)
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
var _refresh_l: Label # Y 刷新升级行(P1 甲——每冒险开局给 Y 次、冒险周期池)
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账 Q1 裁乙)
var _refresh_btn: Button
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
@@ -69,64 +76,92 @@ func _init() -> void:
top.add_child(plaque)
var plaque_col := VBoxContainer.new()
plaque.add_child(plaque_col)
var title := UiKit.label("营地 · 永久强化",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
var title := UiKit.label("营地 · 天赋树",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
plaque_col.add_child(title)
var sub := UiKit.label("跨局成长账本——钥石换永久属性",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——STONE 对铭牌板 3.59:1 越带,换灰石深化 5.98:1
var sub := UiKit.label("营地本身即树——钥石逐节点点亮,属性/规则/工作间都在树上",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——STONE 对铭牌板 3.59:1 越带,换灰石深化 5.98:1
plaque_col.add_child(sub)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(spacer)
# M3(UI 落码批):钥石徽章烛光黄底+深墨字(demo ②——旧暗底 CAMP_BADGE_PANEL 退役;
# BADGE_PANEL_COL 常量保留他用不动)。Q10 甲(2026-10-06):本地拼装迁 UiKit.keystone_badge
# 工厂调用(badge 单点化——暂停层同款消费);形制参数逐项同原装配,观感零变化
# M3(UI 落码批):钥石徽章烛光黄底+深墨字(demo ②)。Q10 甲(2026-10-06):UiKit.keystone_badge 工厂单点
var badge := UiKit.keystone_badge("钥石 0")
top.add_child(badge)
_balance_label = badge.value_label
# ── 三柜台列(方向稿同构:头带深棕+米色板身)──
# 三柜等宽分配(2026-10-06 用户实测缺陷修——原定宽 356+行标签按文字全宽算 min,进度行文案
# 「累计通关 28/2 次·能力封存中」随档变长把面板撑爆,三柜总宽溢出 1248 可用宽=右柜裁出屏;
# 改=EXPAND_FILL 等比分屏宽+行标签截断兜底,min 恒 ≤ 定宽)
# ── 四柜台并排(A② 裁——迭代 21 T6 程序占位重排,视觉终稿归 HUD 营地视觉批)──
# 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高)
var camp_scroll := ScrollContainer.new()
camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
col.add_child(camp_scroll)
var mid := HBoxContainer.new()
mid.add_theme_constant_override("separation", 18)
mid.alignment = BoxContainer.ALIGNMENT_CENTER
mid.add_theme_constant_override("separation", 14)
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
col.add_child(mid)
_forge_holder = VBoxContainer.new()
_forge_holder.add_theme_constant_override("separation", 6)
var forge_panel := _counter_panel("属性锻造", UiKit.TEX_ICON_FORGE, _forge_holder, 356)
camp_scroll.add_child(mid)
# 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮)
_tree_holder = VBoxContainer.new()
_tree_holder.add_theme_constant_override("separation", 6)
var tree_scroll := ScrollContainer.new()
tree_scroll.custom_minimum_size = Vector2(300, 336)
tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
tree_scroll.add_child(_tree_holder)
var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300)
tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mid.add_child(tree_panel)
# 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间占位面板,零交互)
_forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
_forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_forge_lock_l.custom_minimum_size = Vector2(280, 0)
_forge_workbench = VBoxContainer.new()
_forge_workbench.add_theme_constant_override("separation", 6)
var wb_hint := UiKit.label("工作间两区并列:武器锻造区 / 护甲锻造区",UiKit.FS_HINT, true, UiKit.INK)
wb_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
wb_hint.custom_minimum_size = Vector2(280, 0)
_forge_workbench.add_child(wb_hint)
var wb_body := UiKit.label("武器/护甲路线升级随「锻造工坊实装批」开放——本批为占位面板,无任何购买入口。",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
wb_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
wb_body.custom_minimum_size = Vector2(280, 0)
_forge_workbench.add_child(wb_body)
var forge_holder := VBoxContainer.new()
forge_holder.add_theme_constant_override("separation", 6)
forge_holder.add_child(_forge_lock_l)
forge_holder.add_child(_forge_workbench)
var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300)
forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mid.add_child(forge_panel)
# 迭代 11 第①批 T4 工坊扩容(P2 甲程序占位——同调装配段+Y 升级行入工坊柜台,行板复用;行数增长以
# 柜台内滚承载,冻结三柜台版式与另两柜台零触碰,视觉终稿归营地批)
var ws_scroll := ScrollContainer.new()
ws_scroll.custom_minimum_size = Vector2(340, 336)
ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL # 2026-10-06 用户实测修——纵向撑满面板余高(固定 336=行断流但面板下半空着,滚动条提前出现)
ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
# 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行)
_ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
_ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_ws_lock_l.custom_minimum_size = Vector2(280, 0)
_ws_scroll = ScrollContainer.new()
_ws_scroll.custom_minimum_size = Vector2(300, 336)
_ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_workshop_holder = VBoxContainer.new()
_workshop_holder.add_theme_constant_override("separation", 6)
ws_scroll.add_child(_workshop_holder)
var ws_panel := _counter_panel("道具工坊", UiKit.TEX_ICON_WORKSHOP, ws_scroll, 356)
_ws_scroll.add_child(_workshop_holder)
var ws_holder := VBoxContainer.new()
ws_holder.add_theme_constant_override("separation", 6)
ws_holder.add_child(_ws_lock_l)
ws_holder.add_child(_ws_scroll)
var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300)
ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mid.add_child(ws_panel)
# 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha)
var atlas_holder := VBoxContainer.new()
var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356)
var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300)
atlas_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mid.add_child(atlas_panel)
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)",UiKit.FS_HINT, false, UiKit.STONE)
lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
lock_l.custom_minimum_size = Vector2(300, 0)
lock_l.custom_minimum_size = Vector2(280, 0)
atlas_holder.add_child(lock_l)
# 天赋树面板(迭代 12 T9——程序占位:图鉴柜台下段承载树行〔脉分组行板+购买/切换钮〕;
# 「三柜台→一树」字面重构归营地视觉批披露,本批功能重构程序占位先行)
_tree_holder = VBoxContainer.new()
_tree_holder.add_theme_constant_override("separation", 6)
var tree_scroll := ScrollContainer.new()
tree_scroll.custom_minimum_size = Vector2(300, 200)
tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL # 2026-10-06 同修——树段内滚撑满图鉴柜余高
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
tree_scroll.add_child(_tree_holder)
atlas_holder.add_child(tree_scroll)
# ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ──
var close_btn := UiKit.button("关闭营地(回开始界面)")
@@ -137,7 +172,7 @@ func _init() -> void:
var spacer2 := Control.new()
spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bottom.add_child(spacer2)
var caption := UiKit.label("每购得一件永久强化,对应柜台亮起一格暖光 · 价格为钥石",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
bottom.add_child(caption)
col.add_child(bottom)
_recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
@@ -163,7 +198,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_
head.add_theme_constant_override("separation", 6)
band.add_child(head)
if icon_path != "":
var ic := UiKit.icon_rect(icon_path, 32) # P6 甲②(2026-10-06):柜台头带 64 源 0.5×(原 20 违 §8.2);缺位 null → 纯文本回退
var ic := UiKit.icon_rect(icon_path, 32) # P6 甲②(2026-10-06):柜台头带 64 源 0.5×;缺位 null → 纯文本回退
if ic != null:
head.add_child(ic)
var l := UiKit.label(title_text,UiKit.FS_BODY, true, UiKit.WARM)
@@ -174,15 +209,17 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)----
# 天赋树段重建(迭代 12 T9 程序占位——脉分组行板:节点名/等级/价/购买钮 + Fork 组侧切换钮;
# 天赋树段重建(迭代 21 T6——树淬炼柜台主界面:脉分组行板:节点名/等级/价/购买钮 + Fork 双态切换钮;
# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy,
# currency}...] + fork_groups [{group, active_side, sides:[{node_id, side, display_name, owned}]}])
# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}])
func _rebuild_tree(data: Dictionary) -> void:
for c in _tree_holder.get_children():
c.queue_free()
_tree_buy_btns.clear()
_tree_fork_btns.clear()
var tree: Array = data.get("tree", []) as Array
var forks: Array = data.get("fork_groups", []) as Array
var vein_names := {"spine": "·主干脊柱·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"}
var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"}
var last_vein := ""
for row in tree:
var vein := String(row.get("vein", ""))
@@ -199,59 +236,70 @@ func _rebuild_tree(data: Dictionary) -> void:
name_l.clip_text = true
hb.add_child(name_l)
if String(row.get("kind", "")) == "fork":
continue # Fork 行去 UI 化——由 fork_groups 段承载切换钮
continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮
var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准(树全钥石单币 2026-10-07 裁甲)
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
btn.disabled = not bool(row.get("can_buy", false))
hb.add_child(btn)
_tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn})
_tree_holder.add_child(hb)
for fg in forks:
var group := String(fg.get("group", ""))
var active := String(fg.get("active_side", ""))
var owned := bool(fg.get("owned", false))
var fb := HBoxContainer.new()
fb.add_theme_constant_override("separation", 6)
var gl := UiKit.label("%s[激活:%s]" % [String(fg.get("display_name", group)), active if active != "" else "未激活"], 13, false, UiKit.INK_LABEL)
var state_txt := "未解锁"
if owned:
state_txt = "激活:%s" % active
var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL)
fb.add_child(gl)
for side_row in (fg.get("sides", []) as Array):
var sd := String(side_row.get("side", ""))
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
sb.disabled = not bool(side_row.get("owned", false))
fb.add_child(sb)
if not owned:
var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免
bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", ""))))
bb.disabled = not bool(fg.get("can_buy", false))
fb.add_child(bb)
_tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb})
else:
for side_row in (fg.get("sides", []) as Array):
var sd := String(side_row.get("side", ""))
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
fb.add_child(sb)
_tree_fork_btns.append({"group": group, "side": sd, "btn": sb})
_tree_holder.add_child(fb)
# 打开营地(全量重建三柜台行 + 同调装配段 + 天赋树段 + 最近冒险——数据 = 组合根 _camp_data() 投影)
# 工作间钥匙门(迭代 21 T6——锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行;
# 图鉴常开无门槛。判据读树投影行 level——零核心访问,纯视图)
func _refresh_key_gates(data: Dictionary) -> void:
var lvl_by_id := {}
for row in (data.get("tree", []) as Array):
lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0))
var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1
var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1
_forge_lock_l.visible = not forge_open
_forge_workbench.visible = forge_open
_ws_lock_l.visible = not ws_open
_ws_scroll.visible = ws_open
# 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建——
# 数据 = 组合根 _camp_data() 投影)
func open_camp(data: Dictionary) -> void:
_rebuild_forge(data)
_rebuild_workshop(data)
_rebuild_attune(data)
_rebuild_tree(data)
_rebuild_workshop(data) # 段尾链 _rebuild_attune(同调装配段+Y 升级行随秘宝匣柜台内容)
_rebuild_recent(data)
refresh_camp(data)
visible = true
# 状态重刷(购买后调用——余额/价格/禁用态/工坊状态就地改文案,零重建)
# 状态重刷(购买后调用——余额/树行板/钥匙门/工坊状态就地重建或改文案;树行板随刷重建=按钮
# 禁用态与门槛/cap 同步,12 行重建开销可忽略)
func refresh_camp(data: Dictionary) -> void:
var balance := int(data.get("balance", 0))
_balance_label.text = "钥石 %d" % balance
var forge_by_attr := {}
for row in data.get("forge", []):
forge_by_attr[String(row.get("attr", ""))] = row
for card in _forge_rows:
var row: Dictionary = forge_by_attr.get(String(card.attr), {})
if row.is_empty():
continue
card.level_l.text = "Lv %d" % int(row.get("level", 0))
card.price_l.text = "%d 钥石" % int(row.get("price", 0))
# M3:满级态文案(demo ②——已满级替代价签与按钮动词;数据无 maxed 字段时零影响)
if bool(row.get("maxed", false)):
card.btn.text = "已满级"
card.btn.disabled = true
else:
card.btn.text = "锻造 +1 级"
card.btn.disabled = balance < int(row.get("price", 0)) # 余额不足禁用置灰
_rebuild_tree(data)
_refresh_key_gates(data)
var ws_by_id := {}
for row in data.get("workshop", []):
ws_by_id[String(row.get("item_id", ""))] = row
@@ -299,62 +347,15 @@ func refresh_camp(data: Dictionary) -> void:
_refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
func _rebuild_forge(data: Dictionary) -> void:
_forge_rows.clear()
for child in _forge_holder.get_children():
child.queue_free()
for row in data.get("forge", []):
_forge_rows.append(_build_forge_row(_forge_holder, row))
func _build_forge_row(holder: VBoxContainer, row: Dictionary) -> Dictionary:
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 8) # 2026-10-06 营地溢出修(12→8——行 min 压回定宽内)
card.add_child(line)
var attr := String(row.get("attr", ""))
# M3(UI 落码批):行首 20px 语义图标(图标+形状+颜色三通道——demo ②;子串映射容错 attr 键名演进)
var icon_key := ""
var attr_l := attr.to_lower()
if attr_l.contains("attack"):
icon_key = UiKit.TEX_ICON_SWORD
elif attr_l.contains("armor") or attr_l.contains("ac"):
icon_key = UiKit.TEX_ICON_SHIELD
elif attr_l.contains("hp") or attr_l.contains("life"):
icon_key = UiKit.TEX_ICON_HEART
elif attr_l.contains("dig"):
icon_key = UiKit.TEX_ICON_SHOVEL
if icon_key != "":
var ic := UiKit.icon_rect(icon_key, 20)
if ic != null:
line.add_child(ic)
var name_l := UiKit.label("%s %+d/级" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(112, 0)
name_l.clip_text = true # 2026-10-06 营地溢出修——文字全宽不入 min(三柜等宽分配前提)
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(name_l)
var level_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.INK_ON_DEEP)
# M3:Lv 章化(木底小章——demo lvtag;Label normal 槽挂样式盒)
level_l.add_theme_stylebox_override("normal", UiKit.panel_flat(UiKit.CAMP_HEADER_BAND, Color(0, 0, 0, 0),UiKit.RADIUS_OBJECT, 0, Vector4(6, 0, 6, 0)))
level_l.custom_minimum_size = Vector2(44, 0)
line.add_child(level_l)
var price_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——STONE 对行板 4.02:1 越带,换灰石深化 6.70:1
price_l.custom_minimum_size = Vector2(64, 0)
price_l.clip_text = true
price_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(price_l)
var btn := UiKit.button("锻造 +1 级", 0.0) # 柜台行板紧凑钮豁免宽度基准
btn.pressed.connect(_emit_buy_requested.bind(attr))
line.add_child(btn)
holder.add_child(card)
return {"attr": attr, "level_l": level_l, "price_l": price_l, "btn": btn}
func _rebuild_workshop(data: Dictionary) -> void:
_workshop_rows.clear()
for child in _workshop_holder.get_children():
child.queue_free()
_attune_nodes.clear()
_attune_btns.clear()
_attune_header_l = null
_refresh_l = null
_refresh_btn = null
for row in data.get("workshop", []):
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
@@ -366,8 +367,8 @@ func _rebuild_workshop(data: Dictionary) -> void:
name_l.clip_text = true
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(name_l)
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——同行价格墨同案(STONE 对行板 4.02:1 越带→6.70:1)
progress_l.custom_minimum_size = Vector2(150, 0) # 2026-10-06 修——110 截掉阈值数字(「累计通关 28 / 2 次」≈154px)
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——同行价格墨同案
progress_l.custom_minimum_size = Vector2(150, 0)
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(progress_l)
@@ -379,14 +380,12 @@ func _rebuild_workshop(data: Dictionary) -> void:
line.add_child(status_l)
_workshop_holder.add_child(card)
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l})
_rebuild_attune(data)
# 同调装配段重建(迭代 11 第①批 T4——工坊柜台尾部追加;Slots 点亮+九件行板+Y 升级行,程序占位面)
# 同调装配段重建(迭代 11 第①批 T4——秘宝匣柜台尾部段〔迭代 21 T6 随柜台迁入〕;Slots 点亮+
# 九件行板+Y 升级行,程序占位面)
func _rebuild_attune(data: Dictionary) -> void:
for n in _attune_nodes:
n.queue_free()
_attune_nodes.clear()
_attune_btns.clear()
var att: Dictionary = data.get("attune", {})
_attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK)
_workshop_holder.add_child(_attune_header_l)
+7 -43
View File
@@ -1,10 +1,11 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 生涯覆盖层·只读全屏页(迭代 18 卡④ 生涯界面实装批·期一 2026-10-07——需求卡片 v1.2 裁点全甲;
# 设计权威 design/gdd/systems/跨局循环/生涯.md v0.1 期一范围)。
# 裁决落形:P1/P2 甲=汇总段六类九项(冒险总局数/通关局数/各关通关次数/Boss 首杀/Boss 总击杀/
# 钥石余额/属性锻造/道具工坊/天赋树——逐行只读);P3 甲=登记 modal_stack 营地同档(设置层旁、
# 确认弹窗之前——登记序=树序=z 序,组合根 addChild 同位);P4 甲=零跳转(页内零「前往他处」
# 入口,仅只读展示+关闭)+局内不放生涯入口(开始覆盖层「营地与系统」组唯一入口)。
# 裁决落形:P1/P2 甲=汇总段两类六项(冒险总局数/通关局数/各关通关次数/Boss 首杀/Boss 总击杀/
# 钥石余额——逐行只读;原「三柜台解锁完成度概览」三投影〔属性锻造/道具工坊/天赋树〕整行删
# ——2026-10-12 用户裁:生涯页不统计天赋树与账号成长态,只统计终身计数);P3 甲=登记 modal_stack
# 营地同档(设置层旁、确认弹窗之前——登记序=树序=z 序,组合根 addChild 同位);P4 甲=零跳转
# (页内零「前往他处」入口,仅只读展示+关闭)+局内不放生涯入口(开始覆盖层「营地与系统」组唯一入口)。
# 期二/期三内容(历史回看/图鉴/单局明细)零触碰——页内无任何期二占位件。
# 只读契约(需求卡片契约 3/4):数据快照由组合根 open_career 喂入(每次开层实时读 SaveManager
# 计数访问器 + MetaProgression 访问器重建——暂停层 R4 同构,快照仅单次开层生命周期,跨开层
@@ -23,9 +24,6 @@ var _clears_label: Label # 各关通关次数(一行三关
var _boss_first_label: Label # Boss 首杀(已首杀关数/总关数)
var _boss_kills_label: Label # Boss 总击杀(含重复)
var _keystone_badge: UiKit.KeystoneBadge # 钥石余额(badge 工厂件——暂停层同形制)
var _forge_label: Label # 属性锻造(已购等级合计)
var _workshop_label: Label # 道具工坊(已解锁件数/总件数)
var _tree_label: Label # 天赋树已购概览(聚合单条——暂停层同构)
func _init() -> void:
@@ -57,8 +55,8 @@ func _init() -> void:
_boss_first_label = _row(box, "Boss 首杀")
_boss_kills_label = _row(box, "Boss 总击杀")
# 组二·资源与成长(钥石/三柜台解锁概览——现役只读投影)
_section_head(box, "资源与成长")
# 组二·资源(钥石余额——终身计数域同段只读投影;账号成长态概览三投影已删——2026-10-12 用户裁)
_section_head(box, "资源")
var bal_row := HBoxContainer.new()
bal_row.add_theme_constant_override("separation", 8)
box.add_child(bal_row)
@@ -66,10 +64,6 @@ func _init() -> void:
bal_row.add_child(bal_name)
_keystone_badge = UiKit.keystone_badge("钥石 0")
bal_row.add_child(_keystone_badge)
_forge_label = _row(box, "属性锻造")
_workshop_label = _row(box, "道具工坊")
_tree_label = _row(box, "天赋树")
_tree_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律)
@@ -86,39 +80,9 @@ func open_career(data: Dictionary) -> void:
_boss_first_label.text = "%d/%d 关" % [int(data.get("boss_first_kills", 0)), int(data.get("boss_first_total", 0))]
_boss_kills_label.text = "%d 次" % int(data.get("boss_kills", 0))
_keystone_badge.set_text("钥石 %d" % int(data.get("keystone", 0)))
_forge_label.text = "已购 %d 级(三属性合计)" % int(data.get("forge_levels_total", 0))
var ws: Array = data.get("workshop", []) as Array
var unlocked := 0
for row in ws:
if bool(row.get("unlocked", false)):
unlocked += 1
_workshop_label.text = "已解锁 %d/%d 件" % [unlocked, ws.size()] if not ws.is_empty() else "—"
_tree_label.text = _tree_summary(data)
visible = true
# 天赋树聚合文案(暂停层 _rebuild_tree 同构——已购节点等级+Fork 激活侧;零已购=空态句)
func _tree_summary(data: Dictionary) -> String:
var owned: Array[String] = []
for row in (data.get("tree", []) as Array):
var lvl := int(row.get("level", 0))
if lvl > 0:
owned.append("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, int(row.get("cap", 1))])
for fg in (data.get("fork_groups", []) as Array):
var active := String(fg.get("active_side", ""))
if active == "":
continue
var side_name := active
for sd in (fg.get("sides", []) as Array):
if String(sd.get("side", "")) == active:
side_name = String(sd.get("display_name", active))
break
owned.append("%s·激活 %s" % [String(fg.get("display_name", "")), side_name])
if owned.is_empty():
return "尚未购买任何节点"
return "已购 %d 项:%s" % [owned.size(), " · ".join(owned)]
func _emit_closed() -> void:
closed.emit()
+5 -29
View File
@@ -2,9 +2,10 @@
# 局内暂停层·只读(迭代 17 卡③ 暂停层只读入口批 2026-10-06——需求卡片 v1.1;事实源=06-待办清单
# 「解锁界面与钥石呈现的交互形态(残余:暂停层只读入口)」)。
# 裁决落形:P1 丁=入口为战斗屏 HUD / 关卡地图屏常驻角标(入口装配在 ui_assembler/两屏,本类不含入口);
# P2 甲=登记 modal_stack 设置层同档(确认弹窗之前)带 dim 遮罩;P3 甲=展示钥石余额+三柜台进度
# (属性锻造三行已购等级 / 道具工坊六件解锁态 / 天赋树已购概览——层内行序与分组复用营地屏同序);
# P4 甲=进出零动效直开直关(无 tween 无 AnimationPlayer——R5)。
# P2 甲=登记 modal_stack 设置层同档(确认弹窗之前)带 dim 遮罩;P3 甲=展示钥石余额+进度概览
# (道具工坊六件解锁态 / 天赋树已购概览——层内行序与分组复用营地屏同序;属性锻造三行段随
# 迭代 21 柜台退役连带删除——数据源 forge_levels 域已清退);P4 甲=进出零动效直开直关
# (无 tween 无 AnimationPlayer——R5)。
# 只读契约:R4 数据只读同源——账面快照由组合根 open_pause 喂入(每次开层实时读 MetaProgression
# 访问器重建,快照仅用单次开层生命周期,跨开层零持有);本类零核心访问、零写通路——唯一信号
# closed(关闭意图,组合根经模态栈单口收口)。R1 公共件单点:全部构成件经 UiKit 工厂产出
@@ -15,7 +16,6 @@ extends Control
signal closed
var _balance_badge: UiKit.KeystoneBadge # 钥石余额徽(badge 工厂件——Q10 甲)
var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer
var _tree_label: Label
@@ -40,9 +40,6 @@ func _init() -> void:
bal_row.add_child(_balance_badge)
box.add_child(HSeparator.new())
_section_head(box, "属性锻造 · 已购等级")
_forge_holder = _section_holder(box)
_section_head(box, "道具工坊 · 解锁态")
_workshop_holder = _section_holder(box)
@@ -79,32 +76,11 @@ func _section_holder(box: VBoxContainer) -> VBoxContainer:
func open_pause(data: Dictionary) -> void:
_balance_badge.set_text("钥石 %d" % int(data.get("balance", 0)))
_rebuild_forge(data)
_rebuild_workshop(data)
_rebuild_tree(data)
visible = true
# 属性锻造三行(营地屏同序同文案族——名称+每级 flat / 已购等级;只读零价格零按钮)
func _rebuild_forge(data: Dictionary) -> void:
for c in _forge_holder.get_children():
c.queue_free()
for row in (data.get("forge", []) as Array):
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 8)
var name_l := UiKit.label("%s %+d/级" % [String(row.get("display_name", "")), int(row.get("flat", 0))],UiKit.FS_HINT, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(150, 0)
name_l.clip_text = true
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(name_l)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
line.add_child(spacer)
var lvl_l := UiKit.label("Lv %d" % int(row.get("level", 0)),UiKit.FS_HINT, true, UiKit.INK_TITLE)
line.add_child(lvl_l)
_forge_holder.add_child(line)
# 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数)
func _rebuild_workshop(data: Dictionary) -> void:
for c in _workshop_holder.get_children():
@@ -157,7 +133,7 @@ func _rebuild_tree(data: Dictionary) -> void:
break
owned.append("%s·激活 %s" % [String(fg.get("display_name", "")), side_name])
if owned.is_empty():
_tree_label.text = "尚未购买任何节点(天赋树在营地柜台三)"
_tree_label.text = "尚未购买任何节点(天赋树在营地主柜台)"
else:
_tree_label.text = "已购 %d 项:%s" % [owned.size(), " · ".join(owned)]
+6 -4
View File
@@ -69,10 +69,12 @@ func _initialize() -> void:
return
var attune_profile := {}
if attune_mode == "on":
# 贪心画像:中间进度计数(通关 3/Boss 击杀 6)经谓词表求值 Slots;装配序 = 表序 passive 优先至
# Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能],provisional 画像归调参批);
# Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,刷新消费归玩家策略面(钓鱼归玩家)。
var slots: int = db.attunement_growth.slots({"clear_count": 3, "boss_kill_count": 6})
# 贪心画像:Slots=3(迭代 21 V14 换代——槽位判定源=树同调位节点点亮数,模拟画像=三节点
# 全点亮常量;原「中间进度计数经谓词表求值」随 attunement_growth 谓词表退役);装配序 =
# 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能],
# provisional 画像归调参批);Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,
# 刷新消费归玩家策略面(钓鱼归玩家)。
var slots: int = 3
var passives: Array = []
var actives: Array = []
for r in db.treasure_attunement_rows():
+246 -151
View File
@@ -116,6 +116,7 @@ func _run_core_smoke(db, rng) -> void:
_smoke_save()
_smoke_meta()
_smoke_tree()
_smoke_tree_rules()
_smoke_node_map_templates()
@@ -229,9 +230,8 @@ func _smoke_adventure_table() -> void:
"恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕")
check(int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99,
"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕")
check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3
and Attunement.slots(_db, null) == 0,
"同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)")
check(Attunement.slots(_db, null) == 0,
"同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕")
# 迭代 19 卡④——提灯载体行(fog_lantern):新行加载/schema/同调目录投影+装配→战斗贯通(核心层半边;
# 呈现半边归 ui_input_probe case⑮ 载体级段——测试三形态契约 9)
var lantern_row: Dictionary = _db.treasure_items.row_by_id("fog_lantern")
@@ -4327,19 +4327,19 @@ func _smoke_nine_types_gp_stream() -> void:
# ---- 元进度·MetaTable 表结构(元进度实现批 T1——行值全 provisional 归元进度设计批配读数;
# 注入契约三字段必齐 = forge/conversion 覆盖恰三,Boss 两档 first ≥ repeat)----
# conversion 覆盖恰三,Boss 两档 first ≥ repeat;forge_products 行族已退役)----
func _smoke_meta() -> void:
print("--- 元进度·MetaTable 表结构 ---")
var meta_tbl = _db.meta
check(meta_tbl != null and meta_tbl.validate().is_empty(),
"MetaTable 非空 + validate 零错(四行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)")
check(meta_tbl.forge_products.size() == 3 and meta_tbl.boss_keystone.size() == 3
check(meta_tbl.boss_keystone.size() == 3
and meta_tbl.workshop_items.size() == 6 and meta_tbl.conversion.size() == 3,
"MetaTable 四行族规模 3/3/6/3(forge/Boss/换算恰三 · 工坊恰六)")
"MetaTable 行族规模 3/6/3(Boss/工坊恰六/换算恰三——forge_products 行族已随迭代 21 T5 退役)")
var attrs_ok := true
for a in ["max_hp", "atk", "armor_class"]:
attrs_ok = attrs_ok and not _db.meta_forge_row(a).is_empty() and not _db.meta_conversion_row(a).is_empty()
check(attrs_ok, "forge/conversion attr 词表封闭恰全(注入契约三字段齐——meta_* 访问器可达)")
attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty()
check(attrs_ok, "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)")
var boss_ok := true
for li in [1, 2, 3]:
var brow: Dictionary = _db.meta_boss_keystone_row(li)
@@ -4360,41 +4360,20 @@ func _pause_overlay_projection() -> void:
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已购态夹具:余额/锻造两档非零 + 工坊首件解锁——投影可辨非全零白板
# 已购态夹具:余额非零 + 工坊首件解锁——投影可辨非全零白板(属性锻造段已随柜台退役删除)
sv.add_keystone(37)
sv.set_forge_level("atk", 2)
sv.set_forge_level("max_hp", 1)
var ws_meta: Array = _db.meta_workshop_items()
if not ws_meta.is_empty():
sv.unlock_item(String(ws_meta[0].get("item_id", "")))
sv.write_to_disk()
# 投影拼装(main._pause_data 同构)+ 视图喂入
var data := {"balance": mp.keystone_balance(), "forge": [], "workshop": mp.workshop_status(),
var data := {"balance": mp.keystone_balance(), "workshop": mp.workshop_status(),
"tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()}
for attr in MetaTable.FORGE_ATTRS:
var frow: Dictionary = _db.meta_forge_row(attr)
data["forge"].append({"attr": attr, "display_name": String(frow.get("display_name", "")),
"level": mp.forge_level(attr), "flat": int(frow.get("flat_value", 0))})
var po = load("res://presentation/views/pause_overlay.gd").new()
po.open_pause(data)
check(po.visible, "暂停层开层可见(open_pause 直开——P4 甲零动效路径)")
check(String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance(),
"余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text)
var forge_ok: bool = po._forge_holder.get_child_count() == MetaTable.FORGE_ATTRS.size()
var fi := 0
for attr in MetaTable.FORGE_ATTRS:
if fi >= po._forge_holder.get_child_count():
forge_ok = false
break
var lbls: Array = []
for ch in po._forge_holder.get_child(fi).get_children():
if ch is Label:
lbls.append(ch)
forge_ok = forge_ok and lbls.size() == 2 \
and String(lbls[0].text).begins_with(String(_db.meta_forge_row(attr).get("display_name", ""))) \
and String(lbls[1].text) == "Lv %d" % mp.forge_level(attr)
fi += 1
check(forge_ok, "锻造三行投影逐行 = display_name + forge_level() 读数(已购夹具 atk Lv2 / max_hp Lv1 在列)")
var ws: Array = mp.workshop_status()
var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size()
for i in mini(ws.size(), po._workshop_holder.get_child_count()):
@@ -4421,7 +4400,7 @@ func _pause_overlay_projection() -> void:
if int(trow.get("level", 0)) > 0:
owned += 1
var tree_txt := String(po._tree_label.text)
check(tree_txt == "尚未购买任何节点(天赋树在营地柜台三)" or tree_txt.begins_with("已购 %d 项:" % owned),
check(tree_txt == "尚未购买任何节点(天赋树在营地主柜台)" or tree_txt.begins_with("已购 %d 项:" % owned),
"天赋树概览聚合文案与已购节点数一致(零已购=空态句)")
po.visible = false
po.free()
@@ -4435,36 +4414,24 @@ func _career_overlay_projection() -> void:
print("--- 生涯页·只读投影一致性(隔离档 smoke_meta_career/career_v5)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[9])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已玩态夹具:两局收束(一通一止)+ 钥石/锻造/工坊首件/树首节点非零——投影可辨非全零白板
# 已玩态夹具:两局收束(一通一止)+ 钥石非零——投影可辨非全零白板
# (账号成长态三投影已删——2026-10-12 用户裁:生涯页不统计天赋树与账号成长态,只统计终身计数)
sv.add_keystone(45)
sv.set_forge_level("atk", 2)
sv.set_forge_level("max_hp", 1)
var ws0: Array = _db.meta_workshop_items()
if not ws0.is_empty():
sv.unlock_item(String(ws0[0].get("item_id", "")))
sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 21})
sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
var tree_rows: Array = mp.tree_camp_rows()
var tree_node := ""
if not tree_rows.is_empty():
tree_node = String(tree_rows[0].get("node_id", ""))
sv.set_tree_level(tree_node, 1)
sv.write_to_disk()
# 投影拼装(main._career_data 同构)+ 视图喂入
var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance(), "forge_levels_total": 0,
"workshop": mp.workshop_status(), "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()}
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()}
for i in _db.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
"count": sv.clear_count(li)})
if sv.boss_first_kill_marked(li):
data["boss_first_kills"] += 1
for attr in MetaTable.FORGE_ATTRS:
data["forge_levels_total"] += mp.forge_level(attr)
var co = load("res://presentation/views/career_overlay.gd").new()
co.open_career(data)
check(co.visible, "生涯页开层可见(open_career 直开——零动效路径)")
@@ -4481,20 +4448,6 @@ func _career_overlay_projection() -> void:
"Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text])
check(String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance(),
"钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text)
check(String(co._forge_label.text) == "已购 %d 级(三属性合计)" % data["forge_levels_total"],
"属性锻造投影 = forge_level() 三属性合计(%s)" % co._forge_label.text)
var ws_unlocked := 0
for row in (mp.workshop_status() as Array):
if bool(row.get("unlocked", false)):
ws_unlocked += 1
check(String(co._workshop_label.text) == "已解锁 %d/%d 件" % [ws_unlocked, (mp.workshop_status() as Array).size()],
"道具工坊投影 = workshop_status() 解锁计数(%s——首件解锁夹具在列)" % co._workshop_label.text)
var tree_expect := "尚未购买任何节点"
if tree_node != "":
var trow: Dictionary = tree_rows[0]
tree_expect = "已购 1 项:%s Lv1/%d" % [String(trow.get("display_name", "")), int(trow.get("cap", 1))]
check(String(co._tree_label.text) == tree_expect,
"天赋树概览投影 = tree_camp_rows() 已购聚合(%s)" % co._tree_label.text)
co.visible = false
co.free()
# 旧档载入显示(完成定义 6):v5 现役档形态(计数域两键不存在)升 v6 → 生涯页正常呈现缺省值
@@ -4507,8 +4460,7 @@ func _career_overlay_projection() -> void:
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance(), "forge_levels_total": 0,
"workshop": mp_v5.workshop_status(), "tree": mp_v5.tree_camp_rows(), "fork_groups": mp_v5.tree_fork_groups()}
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance()}
for i in _db.level_count():
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
"count": v5.clear_count(i + 1)})
@@ -4544,43 +4496,30 @@ func _meta_new_save(path: String):
return sv
# ---- 元进度 ②几何定价 ③购买(余额充足持久 / 不足拒绝 / attr 出域拒绝)----
# ---- 元进度 ②树购买面(迭代 21 T2——购买受理/持久/余额不足拒绝/未知节点拒绝;
# 几何定价段随属性锻造柜台退役消亡,行价=表值 flat 单价)----
func _meta_pricing_and_buy() -> void:
print("--- 元进度·几何定价与购买(隔离档 smoke_meta_buy/dry)---")
print("--- 元进度·树购买面(隔离档 smoke_meta_buy/dry)---")
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# ② 几何定价:tier 0/1/2 价格 == 迭代 floor(p × ratio/100) 复算值(tier 0 = base_price)且严格递增
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = _db.meta_forge_row(attr)
var base := int(row.get("base_price", 0))
var ratio := int(row.get("price_ratio_pct", 100))
var all_ok := true
var prev := 0
for tier in 3:
sv.set_forge_level(attr, tier)
var expect := base
for _i in tier:
expect = int(floor(float(expect) * float(ratio) / 100.0))
all_ok = all_ok and mp.forge_price(attr) == expect and (tier == 0 or expect > prev)
prev = expect
check(all_ok, "%s 几何定价 tier 0/1/2 = 迭代 floor 复算且严格递增(base %d × ratio %d%%)" % [attr, base, ratio])
sv.set_forge_level("atk", 0)
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币——2026-10-07 用户裁甲)
sv.add_keystone(1000)
var bal0: int = sv.keystone_balance()
var lvl0: int = sv.forge_level("atk")
var price0: int = mp.forge_price("atk")
check(mp.buy_forge("atk") and sv.forge_level("atk") == lvl0 + 1 and sv.keystone_balance() == bal0 - price0,
"购买成功(Lv %d→%d、余额 %d−%d)" % [lvl0, lvl0 + 1, bal0, price0])
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
check(mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0,
"树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0])
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
check(sv_r.forge_level("atk") == lvl0 + 1 and sv_r.keystone_balance() == bal0 - price0, "购买持久(reload 后级与余额不变)")
# ③ 余额不足 / attr 出域:false 零副作用
check(sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0, "购买持久(reload 后级与余额不变)")
# ③ 余额不足 / 未知节点:false 零副作用
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
check(not mp_dry.buy_forge("atk") and sv_dry.keystone_balance() == 0 and sv_dry.forge_level("atk") == 0,
"余额不足 false 零副作用(0 余额购买拒绝)")
check(not mp_dry.buy_forge("luck") and sv_dry.keystone_balance() == 0, "attr 出域 false 零副作用(luck 不在词表)")
var tn_before: Dictionary = sv_dry.tree_nodes()
var fs_before: Dictionary = sv_dry.fork_sides()
check(not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0
and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before,
"余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)")
check(not mp_dry.tree_buy("no_such_node"), "未知节点 false 零副作用(词表外出域)")
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
@@ -4588,9 +4527,15 @@ func _meta_injection() -> void:
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
sv.add_keystone(mp.forge_price("atk"))
check(mp.buy_forge("atk") and int(mp.hero_override().get("atk", -1)) == int(_db.meta_forge_row("atk").get("flat_value", 0)),
"买 atk 1 级 → hero_override().atk == 行 flat(验收4)")
# 树生命淬炼注入(迭代 21 T4 换代公式:基础值+树淬炼 flat+锻造路线加值位预留 0——2 级 = +10;
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
sv.set_tree_level("sp_hp", 2)
check(int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)),
"生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)")
check(int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0,
"攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)")
check(not mp.hero_override().has("hit"),
"命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
check(adr.start_adventure(920251, {"hero_override": mp.hero_override()}), "注入参数冒险启动(start_adventure params 通道)")
var r = adr.runner()
@@ -4717,8 +4662,11 @@ func _meta_settle_and_workshop() -> void:
"Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)")
mp_a.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
check(Attunement.slots(_db, sv_a) == 1 and sv_a.boss_kill_count() == 1,
"通关 1 关 → m1 达成 Slots=1 且 Boss 击杀总数=1(计数源收束入账)")
check(Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1,
"通关 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕")
sv_a.set_tree_level("att_socket1", 1)
check(Attunement.slots(_db, sv_a) == 1,
"树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)")
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order") == false,
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)")
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order"),
@@ -4728,13 +4676,8 @@ func _meta_settle_and_workshop() -> void:
var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2)
check((mp_a2.attune_camp_status()["equipped"] as Array).has("luck_coin"),
"装配关系读回持久(reload 后投影一致)")
# ⑫ 同调批 T8 断言面:clamp / 充能重置 / 刷新双计数器(white-box——迭代 11 第①批)
var tg := AttunementGrowthTable.new()
tg.slot_cap = 1
tg.milestones = [{"id": "x1", "grant": 1, "requires": [{"key": "clear_count", "threshold": 1}]},
{"id": "x2", "grant": 1, "requires": [{"key": "boss_kill_count", "threshold": 1}]}]
check(tg.slots({"clear_count": 5, "boss_kill_count": 5}) == 1,
"同调位 clamp(Σ达成 grant 2 → slots 1 = slot_cap——谓词表口径)")
# ⑫ 充能重置 / 刷新双计数器(迭代 11 第①批;clamp white-box 断言随 attunement_growth 谓词表
# 退役摘除——迭代 21 T5,V20 摘除清单)
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
check(a12.start_adventure(12, {"attunement": ["dig_charge", "nest_pendulum"]}),
"充能重置场景:装配冒险启动(seed 12)")
@@ -4794,51 +4737,191 @@ func _meta_settle_and_workshop() -> void:
_meta_smoke_cleanup()
# ---- 天赋树冒烟(迭代 12 T8/T9——R1 甲融合案:双币/row-gating/Fork 切换/彩蛋/迁移幂等)----
# ---- 天赋树冒烟(迭代 21 T1/T2——v0.8 12 节点换代:schema/购买面/Fork 一节点双态/彩蛋特化)----
func _smoke_tree() -> void:
print("--- 天赋树(MetaTreeTable · 购买面 · A/B 切换 · 三体系迁移)---")
check(_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 33,
"meta_tree 已加载(33 节点=脊柱 16+锻造 3+钥石 2+socket 3+Fork 8+彩蛋 1——R1 甲融合案)")
check(_db.meta_tree.validate().is_empty(), "MetaTreeTable.validate 零违例(单币枚举/互斥对/彩蛋 ≤3/零 archetype 层级纪律护栏)")
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔天赋树批 V1〕结构换代:12 行 + schema 零违例
check(_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12,
"meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕")
check(_db.meta_tree.validate().is_empty(),
"MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)")
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}——旧表 :402/:405 atk/armor_class 分量换代件
var ley: Dictionary = _db.meta_tree.row_by_id("easter_ley")
var ley_ok: bool = not ley.is_empty()
for k in (ley.get("effects", {}) as Dictionary).keys():
if not (String(k) == "max_hp" or String(k) == "hit"):
ley_ok = false
check(ley_ok, "easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕")
# V1/V4 反例:出域行逐类构造被拒(复制行副本塞词表外值)
var t2 = load("res://data/schemas/meta_tree_table.gd").new()
var bad_vein: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_vein["node_id"] = "bad_vein"
bad_vein["vein"] = "underworld"
t2.rows = [bad_vein]
check(not t2.validate().is_empty(), "schema 反例:vein 出域被拒")
var bad_fork: Dictionary = (_db.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
bad_fork["node_id"] = "bad_fork"
bad_fork["fork_group"] = "fg_mastery"
t2.rows = [bad_fork]
check(not t2.validate().is_empty(), "schema 反例:fork_group 出域(fg_mastery 已退役组)被拒")
var bad_arch: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_arch["node_id"] = "bad_arch"
bad_arch["effects"] = {"archetype": 1}
t2.rows = [bad_arch]
check(not t2.validate().is_empty(), "schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕")
var bad_dangling: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_dangling["requires"] = ["ghost_node"]
t2.rows = [bad_dangling]
check(not t2.validate().is_empty(), "schema 反例:requires 悬空被拒")
# 购买面(V2 主干)
var sv = _meta_new_save(_META_SMOKE_PATHS[8])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# row-gating:行 2 可购需总等级 ≥4——零购时行 2 拒绝
check(not mp.tree_can_buy("sp_hp_r2"), "row-gating:行 2 不可购(总等级 0 < 4——投入点数累计解锁新行)")
check(mp.tree_can_buy("sp_hp_r1") == false or true, "行 1 可购性读面(白板钥石 0=余额面拒)")
sv.add_keystone(1000)
check(mp.tree_buy("sp_hp_r1") and sv.tree_level("sp_hp_r1") == 1, "树购买受理(树全钥石单币——2026-10-07 用户裁甲)")
for i in 3:
mp.tree_buy("sp_atk_r1")
check(not mp.tree_can_buy("sp_atk_r1"), "cap 拦截(atk 行 1 已满 3 级——购买拒绝)")
check(mp.tree_can_buy("sp_hp_r2"), "row-gating 解锁(总等级 4 ≥ 4×1 门槛恰达——行 2 开放)")
# Fork 激活与免费切换(前置链:att_socket1 ← sp_atk_r1 已满级)
check(mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1, "树购买受理(脊柱生命 L1——树全钥石单币)")
check(not mp.tree_can_buy("sp_hp"),
"行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕")
check(mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1"),
"命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)")
check(int(_db.tree_node("forge_workshop").get("price", -1)) == 0, "锻造工坊钥匙零币(free)")
check(mp.tree_can_buy("sp_hp"), "行门槛恰达开放(总等级 4 ≥ 4×1)")
check(mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2, "第 2 级购买受理(门槛恰达当刻开放)")
# Fork 一节点双态(V9)
sv.add_keystone(100)
check(mp.tree_buy("att_socket1"), "Socket 购买受理(同调脉首站——钥石=规则面消费承载)")
check(mp.tree_buy_fork("fork_posture_a"), "Fork 购买=激活侧(posture A——组内互斥承载)")
check(sv.fork_side("fg_posture") == "a", "Fork 激活侧入档(fg_posture=a)")
check(not mp.tree_can_buy("fork_posture_b"), "互斥拦截(同组已激活 A——B 不可购)")
check(mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a",
"Fork 购买=解锁+默认 A 态(一节点双态——单行承载)")
check(not mp.tree_can_buy("fork_posture"), "已解锁 Fork 重复购买拒绝(cap 1)")
check(not mp.tree_toggle_fork("fg_posture", "a"), "同态重复切换零操作(false)")
check(not mp.tree_toggle_fork("fg_sense", "b"), "未解锁 Fork 切换拒绝(解锁唯一入口=购买面)")
var bal_pre_toggle: int = sv.keystone_balance()
check(mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b",
"A/B 免费切换(已激活组换侧零币——营地免费切换裁定)")
check(not mp.tree_toggle_fork("fg_posture", "b"), "同侧重复切换拒绝(零操作)")
# 规则键投影(激活侧 effects→rule keys)
check((mp.tree_rule_keys() as Array).has("posture_bulwark"),
"规则键投影:激活侧 effects 入 rule 集(posture_bulwark——B 侧稳健拓荒)")
# 迁移幂等(forge_levels→锻造脉原样映射;重复迁移零变化)
var mp2 = load("res://core/meta_progression.gd").new(_db, sv)
sv.set_forge_level("atk", 2)
sv.unlock_item("nest_counter")
var m1: Dictionary = mp2.migrate_tree()
check(int(m1.get("forge", 0)) == 1 and int(m1.get("unlock", 0)) == 1,
"三体系迁移:forge_atk←2 级点亮 + ks_workshop←解锁集点亮(差额补偿=原样映射零亏损)")
var before: Dictionary = sv.tree_nodes()
var m2: Dictionary = mp2.migrate_tree()
check(m2.get("forge", 1) == 0 and m2.get("unlock", 1) == 0 and before == sv.tree_nodes(),
"迁移幂等(重复迁移零变化——T10 断言面)")
check(int(sv.tree_level("forge_atk")) == 2 and sv.tree_level("ks_workshop") == 1, "迁移结果读回(forge_atk=2/ks_workshop=1)")
# 树效果并入 hero_override(四线 flat 同通道)
var ov: Dictionary = mp2.hero_override()
check(int(ov.get("atk", -1)) >= 2, "树效果注入:树 atk flat 并入 hero_override(覆盖值通道——锻造成同源叠加)")
"零币切态 b(购买解锁后 A/B 可切)")
check(int(sv.keystone_balance()) == bal_pre_toggle, "切态零消费(钥石余额差分不动)")
check((mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike"),
"规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)")
check(mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike"),
"切回 A 态键集随换(六键词表在册——V11 校验面归单元段)")
# 彩蛋特化觉醒(V8)
check(not mp.tree_can_buy("easter_ley"), "彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)")
check(sv.tree_level("easter_ley") == 0, "觉醒前置未满足不亮(两线未拉满)")
sv.set_tree_level("sp_hp", 4)
sv.set_tree_level("sp_hit", 4)
sv.set_tree_level("att_socket2", 1)
mp.tree_buy("att_socket3") # 经购买口触发 _check_easter_egg
check(sv.tree_level("easter_ley") == 1, "两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)")
var bal_now: int = sv.keystone_balance()
mp.tree_buy("sp_hp")
check(int(sv.keystone_balance()) == bal_now, "觉醒零消费(钥石余额不动)")
_meta_smoke_cleanup()
# ---- 天赋树六键行为冒烟(迭代 21 T3——V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿;
# 引擎直构 + 编排授予段直注(_judge_flag=结算授予段旗判定单口))----
func _smoke_tree_rules() -> void:
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构)
var fx = load("res://core/effect_engine.gd").new(null)
fx.rebuild([], [], ["posture_first_strike", "posture_bulwark"])
check((fx.dispatch("round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1,
"先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕")
check((fx.dispatch("round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty()
and (fx.dispatch("round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
"先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)")
check((fx.dispatch("damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1
and (fx.dispatch("damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty(),
"稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)")
# 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成,编排收集 halve_pct 系既有 damage_mult 路径)
var fz = load("res://core/effect_engine.gd").new(null)
fz.rebuild([], [], ["sense_sharp"])
var got_mult := false
var got_add := false
for h in (fz.dispatch("mine_hit", {}, Callable()) as Array):
if String(h.get("kind", "")) == "damage_mult":
got_mult = true
if String(h.get("kind", "")) == "counter_add":
got_add = true
check(got_mult and got_add, "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)")
fz.counter_add("tree_sense_sharp_used", 2) # 直调置数(V10 注入口径——置关卡域计数后走事件路径)
check((fz.dispatch("mine_hit", {}, Callable()) as Array).is_empty(), "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)")
var exported: Dictionary = fz.counters_export()
var fz2 = load("res://core/effect_engine.gd").new(null)
fz2.rebuild([], [], ["sense_sharp"])
fz2.counters_seed(exported)
check((fz2.dispatch("mine_hit", {}, Callable()) as Array).is_empty(),
"锋:限次跨战持久(adventure 域 counters_export/seed——断点保留同源,不新增存档域不升 v7)")
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _judge_flag 直注
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
check(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)")
var board = orch.minesweeper.current_board()
var safe := Vector2i(-1, -1)
for c in board.all_cells():
if not board.is_mine(c):
safe = c
break
check(safe.x >= 0, "守侧 rig:安全格定位(非雷格=标错负样本)")
check(int(orch.action_economy.pending_ap_delta()) == 0, "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)")
# 场景①:当回合新插旗、结算判标错且旗仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
orch._judge_flag({"cell": safe, "hit": false}, 1)
check(int(orch.action_economy.pending_ap_delta()) == 1,
"守①:当回合标错且旗在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)")
# 场景③④:之前回合标错重判(旗在场状态)与同格重复——去重闸挡回,最多返还一次
orch._judge_flag({"cell": safe, "hit": false}, 2)
orch._judge_flag({"cell": safe, "hit": false}, 3)
check(int(orch.action_economy.pending_ap_delta()) == 1,
"守③④:旧旗重判零触发+同格最多一次(_flag_miss_seen 去重闸=「当回合标错动作」判别器)")
# 场景②负样本:标中旗(hit=true 正向兑付路径)零触发
orch._judge_flag({"cell": safe, "hit": true}, 4)
check(int(orch.action_economy.pending_ap_delta()) == 1, "守②负样本:标中/被挖旗零触发(标错事件只在 miss 分支发射——发射面过滤承载)")
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
var orch2 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777002))
check(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"守⑤ rig:次战编排启动(新战新队列实例)")
check(int(orch2.action_economy.pending_ap_delta()) == 0,
"守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)")
# 按关重置清零点(adventure_runner._start_current_level)
var ar = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777003))
ar.adventure_counters = {"tree_sense_sharp_used": 2, "tree_sense_guard_used": 1, "unrelated_key": 9}
ar._reset_tree_limited_counters()
check(not ar.adventure_counters.has("tree_sense_sharp_used") and not ar.adventure_counters.has("tree_sense_guard_used")
and int(ar.adventure_counters.get("unrelated_key", 0)) == 9,
"限次按关重置:_start_current_level 清零点恰两键(他键零扰动——生命周期四态闭合)")
# 财富之道·散:商品标价折价(level_runner 计价单点)
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004))
lr.set_tree_rules([])
check(int(lr._spend_discounted(100)) == 100, "散未激活:商品标价原价(无 economy_spend 键)")
lr.set_tree_rules(["economy_spend"])
check(int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1,
"散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)")
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
check(orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"聚 rig:基线编排启动(无增益侧)")
orch3.encounter["tree_rules"] = []
var gp_base: int = orch3._roll_gp_drop()
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
check(orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"聚 rig:增益编排启动(tree_rules=economy_hoard)")
orch4.encounter["tree_rules"] = ["economy_hoard"]
var gp_hoard: int = orch4._roll_gp_drop()
check(gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base,
"聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)")
_meta_smoke_cleanup()
@@ -4896,7 +4979,7 @@ func _smoke_save() -> void:
"user://smoke_save_domain.json", "user://smoke_save_cap.json",
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json",
"user://smoke_save_v2compat.json", "user://smoke_save_v5upgrade.json",
"user://smoke_save_v5legacy.json"]
"user://smoke_save_v5legacy.json", "user://smoke_save_v16discard.json"]
var stale: Array = []
for p in paths:
stale.append(p)
@@ -4909,8 +4992,9 @@ func _smoke_save() -> void:
# ① 往返:全访问器写非默认值 → write → 新实例 load → 逐域逐值相等
var w = sm_script.new(paths[0])
w.add_keystone(7)
check(w.set_forge_level("atk", 2) and w.set_forge_level("max_hp", 1) and w.set_forge_level("armor_class", 1),
"属性锻造三档写入")
w.set_tree_level("sp_hp", 2)
w.set_tree_level("sp_hit", 1)
w.set_fork_side("fg_posture", "b") # 树域写入(属性锻造域已退役——forge_levels 域清退换代件)
w.unlock_item("probe_compass")
w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 3, "master_seed": 99})
@@ -4924,8 +5008,8 @@ func _smoke_save() -> void:
var r = sm_script.new(paths[0])
check(r.load_from_disk(), "读回成功")
check(r.keystone_balance() == 10, "钥石余额往返相等(10)")
check(r.forge_level("atk") == 2 and r.forge_level("max_hp") == 1 and r.forge_level("armor_class") == 1,
"锻造三档往返相等")
check(r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b",
"树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)")
check(r.item_unlocked("probe_compass") and not r.item_unlocked("rope"), "道具解锁往返相等")
check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等")
check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3),
@@ -4937,7 +5021,7 @@ func _smoke_save() -> void:
# ② 新档合法:不存在路径 load → true + 全零默认值
var f2 = sm_script.new(paths[1])
check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.forge_level("atk") == 0
check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0
and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty(),
"新档合法态(load true + 全零默认)")
check(f2.adventure_total() == 0 and f2.adventure_cleared() == 0,
@@ -4949,10 +5033,6 @@ func _smoke_save() -> void:
check(not f3.spend_keystone(8) and f3.keystone_balance() == 5, "余额 5 花 8 拒绝且余额不动")
check(f3.spend_keystone(3) and f3.keystone_balance() == 2, "余额内消费成功(5→2)")
# ④ attr 出域拒绝(push_error,值不变)
check(not f3.set_forge_level("luck", 1) and f3.forge_level("luck") == 0 and f3.forge_level("atk") == 0,
"锻造键出域拒绝(值不变)")
# ⑤ 高版本拒读:load false、原文件字节不动、后续写盘拒绝且文件仍字节不动(契约 4)
var hv_text := "{\"version\": 99, \"meta\": {\"keystone_balance\": 1}}"
var fhv := FileAccess.open(paths[3], FileAccess.WRITE)
@@ -5126,6 +5206,21 @@ func _smoke_save() -> void:
check(f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1,
"收束自增恰一·中止局(放弃局:仅总局数+1,通关局数不动)")
# ⑮ 旧档破坏性废弃(迭代 21 T5——契约 4:换代前结构检测=树域废弃重置,禁迁移禁折算)〔天赋树批 V16〕
var f15 = sm_script.new(paths[13])
var fv16 := FileAccess.open(paths[13], FileAccess.WRITE)
fv16.store_string(JSON.stringify({"version": 6, "meta": {"keystone_balance": 33,
"forge_levels": {"max_hp": 3, "atk": 4, "armor_class": 2},
"tree_nodes": {"sp_hp_r1": 3, "forge_atk": 2, "fork_posture_a": 1},
"fork_sides": {"fg_posture": "a", "fg_mastery": "b"}}}))
fv16.close()
check(f15.load_from_disk() and f15.keystone_balance() == 33
and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty(),
"换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动")
var mp16 = load("res://core/meta_progression.gd").new(_db, f15)
f15.add_keystone(50)
check(mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1, "废弃重置后后续购买正常受理(无残留脏节点)")
# ⑭ 收束链入账(迭代 18 卡④ T2——真实收束挂点全链恰一:主流程 adventure_finished →
# MetaProgression.settle_adventure → apply_adventure_summary 单口;两夹具经同链入账)
var mp13 = load("res://core/meta_progression.gd").new(_db, f13b)
+54 -18
View File
@@ -60,28 +60,64 @@ func _ready() -> void:
await get_tree().process_frame
if not main.camp_overlay.visible:
_problems.append("营地层未打开(点击路径)")
elif main.camp_overlay._forge_rows.is_empty():
_problems.append("锻造柜台行缺失(点击路径)")
elif main.camp_overlay._tree_buy_btns.is_empty():
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
else:
var buy_btn: Button = main.camp_overlay._forge_rows[0].btn
if not buy_btn.disabled:
_problems.append("零余额锻造购买按钮未禁用(禁用态失守)")
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
iso_c.add_keystone(9999)
main.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
if buy_btn.disabled:
_problems.append("余额充足后购买按钮仍禁用(禁用态未随刷新)")
var bal_before: int = iso_c.keystone_balance()
var buy_attr: String = String(main.camp_overlay._forge_rows[0].attr)
var price: int = main.meta.forge_price(buy_attr)
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_buy → meta.buy_forge(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.keystone_balance()) != bal_before - price:
_problems.append("营地购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_chk.load_from_disk()
if int(iso_chk.forge_level(buy_attr)) != 1:
_problems.append("营地购买未持久(reload 后级 != 1)")
var buy_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
if String(card.node_id) == "sp_hp":
buy_btn = card.btn
if buy_btn == null:
_problems.append("脊柱生命行缺失(点击路径)")
else:
var bal_before: int = iso_c.keystone_balance()
var price: int = int(main.db.tree_node("sp_hp").get("price", 0))
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.keystone_balance()) != bal_before - price:
_problems.append("营地树购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_chk.load_from_disk()
if int(iso_chk.tree_level("sp_hp")) != 1:
_problems.append("营地树购买未持久(reload 后级 != 1)")
# Fork 双态切换点击路径(V20 换代——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
var socket_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
if String(card.node_id) == "att_socket1":
socket_btn = card.btn
if socket_btn != null:
socket_btn.pressed.emit()
await get_tree().process_frame
main.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
var fork_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
if String(card.node_id) == "fork_posture":
fork_btn = card.btn
if fork_btn == null:
_problems.append("Fork 解锁钮缺失(点击路径)")
else:
fork_btn.pressed.emit()
await get_tree().process_frame
main.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
var side_b: Button = null
for fk in main.camp_overlay._tree_fork_btns:
if String(fk.group) == "fg_posture" and String(fk.side) == "b":
side_b = fk.btn
if side_b == null:
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)")
else:
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
await get_tree().process_frame
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_fk.load_from_disk()
if String(iso_fk.fork_side("fg_posture")) != "b":
_problems.append("Fork 切态未持久(reload 后激活态 != b)")
main._on_camp_closed()
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
+9 -10
View File
@@ -38,17 +38,16 @@ func _ready() -> void:
else:
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
if main.camp_overlay._forge_rows.size() != 3:
problems.append("锻造柜台行数错误(%d ≠ 3)" % main.camp_overlay._forge_rows.size())
if main.camp_overlay._tree_buy_btns.is_empty():
problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)")
else:
var zero_disabled := true
for card in main.camp_overlay._forge_rows:
zero_disabled = zero_disabled and bool(card.btn.disabled) # 零余额全禁用
if not zero_disabled:
problems.append("零余额下锻造购买按钮未全禁用(余额不足禁用态失守)")
if main.camp_overlay._workshop_rows.size() != 6:
problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size())
main._on_camp_closed()
var btn_by_id := {}
for card2 in main.camp_overlay._tree_buy_btns:
btn_by_id[String(card2.node_id)] = card2.btn
if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled):
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)")
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
problems.append("营地关闭后未恢复开始层(开合往返失守)")
+14 -12
View File
@@ -24,10 +24,12 @@
# 绝不碰 user://save.json(与 smoke 存档隔离纪律同源)。
extends SceneTree
# 迭代 21 T5 换代——属性锻造柜台退役后花费策略主语=树脊柱两节点(钥石单币;攻/AC 无树来源
# 〔契约 9——唯一入口=锻造工坊路线,本批未实装〕,原三轴序收敛两节点序)
const SPEND_ORDERS := {
"hp_first": ["max_hp", "atk", "armor_class"],
"atk_first": ["atk", "max_hp", "armor_class"],
"armor_first": ["armor_class", "max_hp", "atk"],
"hp_first": ["sp_hp", "sp_hit"],
"atk_first": ["sp_hit", "sp_hp"],
"armor_first": ["sp_hp", "sp_hit"],
}
const OVERRIDE_KEYS := ["atk", "max_hp", "armor_class"] # 注入契约三键(同 AdventureRunner 白板形)
@@ -175,19 +177,19 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
row["keystone_income"] = int(settle.total)
row["purchases"] = _spend(meta, String(opt.spend))
row["keystone_after"] = meta.keystone_balance()
row["forge"] = {"max_hp": meta.forge_level("max_hp"), "atk": meta.forge_level("atk"),
"armor_class": meta.forge_level("armor_class")}
row["tree"] = {"sp_hp": meta.save.tree_level("sp_hp"), "sp_hit": meta.save.tree_level("sp_hit")}
row["new_unlocks"] = settle.new_unlocks
_emit(row)
counts.campaign += 1
if (r + 1) % 10 == 0:
print("进度 campaign %s %d/%d(锻造 血%d/攻%d/甲%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
meta.forge_level("max_hp"), meta.forge_level("atk"), meta.forge_level("armor_class"),
print("进度 campaign %s %d/%d(树 血%d/命中%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
meta.save.tree_level("sp_hp"), meta.save.tree_level("sp_hit"),
meta.keystone_balance()])
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
# 花费策略(贪心——按优先序反复买当前可负担的最高优先项;几何递增定价下单档连买价格上行自然外溢到次档)
# 花费策略(贪心——按优先序反复买当前可负担的最高优先项;树行门槛随买随涨自然外溢到次节点
# ——迭代 21 T5 换代:购买原语 buy_forge→tree_buy,行门槛=节点等级轴)
func _spend(meta, policy_name: String) -> Array:
var out: Array = []
var order: Array = SPEND_ORDERS.get(policy_name, SPEND_ORDERS["hp_first"])
@@ -195,10 +197,10 @@ func _spend(meta, policy_name: String) -> Array:
while guard > 0:
guard -= 1
var bought := false
for attr in order:
var price: int = meta.forge_price(attr)
if price > 0 and meta.keystone_balance() >= price and meta.buy_forge(attr):
out.append({"attr": attr, "tier": int(meta.forge_level(attr)), "price": price})
for nid in order:
var price: int = int(meta.db.tree_node(nid).get("price", 0))
if price > 0 and meta.keystone_balance() >= price and meta.tree_buy(nid):
out.append({"node_id": nid, "level": meta.save.tree_level(nid), "price": price})
bought = true
break
if not bought:
+2 -1
View File
@@ -44,7 +44,8 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
{"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0}, # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0},
{"path": "res://tests/unit/core/tree_rules_v11_test.gd", "expect": 0}, # 迭代 21 天赋树批 V11——六键合成行闭集校验 # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
]
@@ -0,0 +1,54 @@
# tests/unit/core/tree_rules_v11_test.gd — 天赋树批 V11:六键合成订阅行走效果引擎加载期校验零违例
# (契约 5——效果类型闭集不加新词:六键效果全部走现有 EVENTS/FACTS/OPS/EFFECT_KINDS/DO_KEYS
# 在册词表组合;本测试直构 rebuild 同构合成行(与 effect_engine.gd rebuild 树规则段逐字段同源)
# 调 validate_trigger_row 断零违例。实现侧扩闭集未呈报时,本断言以在册词表为准转红。
# 载体:run_all 单元套件(显式实例化夹具——--script 模式无 autoload)。)
extends SceneTree
func _init() -> void:
var eng = load("res://core/effect_engine.gd")
var tree_tbl = load("res://data/schemas/meta_tree_table.gd")
var errs: Array = []
# 六键合成行(与 rebuild 树规则段同构——逐字段对照,实现漂移在此转红)
var synth := {
"posture_first_strike": [{"on": "round_start",
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}],
"posture_bulwark": [{"on": "damage_taken",
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "shield", "value": 1}}],
"sense_sharp": [{"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "damage_mult", "value": 50}},
{"on": "mine_hit",
"when": [{"fact": "counter", "id": "tree_sense_sharp_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_sharp_used", "value": 1, "domain": "adventure"}}],
"sense_guard": [{"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}},
{"on": "flag_missed",
"when": [{"fact": "counter", "id": "tree_sense_guard_used", "op": "lt", "value": 2}],
"do": {"kind": "counter_add", "counter_id": "tree_sense_guard_used", "value": 1, "domain": "adventure"}}],
}
var six := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"]
for key in six:
if not tree_tbl.RULE_KEYS.has(key):
errs.append("规则键出域 RULE_KEYS 词表: %s" % key)
for tr in (synth.get(key, []) as Array):
eng.validate_trigger_row("tree_synth_" + String(key), {}, [tr], errs)
# 反例:出域事件/出域效果类型在校验面转红(护栏活着——独立数组,命中即护栏有效)
var bad_errs: Array = []
eng.validate_trigger_row("bad_event", {}, [{"on": "no_such_event", "do": {"kind": "grant_ap"}}], bad_errs)
eng.validate_trigger_row("bad_kind", {}, [{"on": "round_start", "do": {"kind": "no_such_kind"}}], bad_errs)
if bad_errs.is_empty():
errs.append("反例护栏失效:出域事件/出域效果类型未被 validate_trigger_row 拒绝")
if errs.is_empty():
print("[tree_rules_v11] all green(六键合成行零违例 + 反例被拒——契约 5 闭集在册词表承载)")
quit(0)
return
for e in errs:
printerr("[tree_rules_v11] %s" % str(e))
quit(1)
@@ -0,0 +1 @@
uid://cuhmdpcunxhrn
@@ -155,13 +155,7 @@ func _test_shop_accessors(db) -> void:
# ---- MetaTable 消费面 ----
func _test_meta_accessors(db) -> void:
var forge_rows: Array = db._meta_rows("forge_products")
if forge_rows.is_empty():
Kit.check("MetaTable forge_products 行族非空", false)
else:
var attr0: String = String(forge_rows[0].attr)
Kit.check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0])
Kit.check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty())
# forge_products 行族断言已摘除(迭代 21 T5——行族/访问器随属性锻造柜台退役,V20 清单)
var key_rows: Array = db._meta_rows("boss_keystone")
if not key_rows.is_empty():
var klv: int = int(key_rows[0].level_index)