From 96fc61c701c518bc156fbf5af4db550bacc610c0 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 07:54:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(tree):=20=E8=BF=AD=E4=BB=A321=E5=A4=A9?= =?UTF-8?q?=E8=B5=8B=E6=A0=91=E5=AE=9E=E8=A3=85=E6=89=B9=20T1-T6=20?= =?UTF-8?q?=E4=B8=BB=E4=BD=93=E2=80=94=E2=80=9412=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=8D=A2=E4=BB=A3+=E5=85=AD=E9=94=AE=E6=8E=A5=E7=BA=BF+?= =?UTF-8?q?=E8=90=A5=E5=9C=B0=E5=9B=9B=E6=9F=9C=E5=8F=B0=EF=BC=882026-10-1?= =?UTF-8?q?2=20U0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 全绿 --- game/core/adventure_runner.gd | 9 + game/core/attunement.gd | 34 +- game/core/battle_orchestrator.gd | 30 +- game/core/config_db.gd | 21 +- game/core/effect_engine.gd | 42 +- game/core/level_runner.gd | 31 +- game/core/meta_progression.gd | 183 +++----- game/core/save_manager.gd | 65 ++- game/data/schemas/attunement_growth_table.gd | 81 ---- .../schemas/attunement_growth_table.gd.uid | 1 - game/data/schemas/meta_table.gd | 39 +- game/data/schemas/meta_tree_table.gd | 91 ++-- game/data/schemas/treasure_item_table.gd | 11 +- game/data/tables/attunement_growth.tres | 34 -- game/data/tables/meta.tres | 25 +- game/data/tables/meta_tree.tres | 391 ++++------------- game/presentation/camp_shop_presenter.gd | 32 +- game/presentation/main.gd | 24 +- game/presentation/views/camp_overlay_view.gd | 287 +++++++------ game/presentation/views/career_overlay.gd | 50 +-- game/presentation/views/pause_overlay.gd | 34 +- game/smoke/adventure_monte_carlo.gd | 10 +- game/smoke/smoke_all.gd | 397 +++++++++++------- game/smoke/ui_click_probe.gd | 72 +++- game/smoke/ui_probe.gd | 19 +- game/smoke/verifier_batch.gd | 26 +- game/tests/run_all.gd | 3 +- game/tests/unit/core/tree_rules_v11_test.gd | 54 +++ .../unit/core/tree_rules_v11_test.gd.uid | 1 + game/tests/unit/data_config/config_db_test.gd | 8 +- 30 files changed, 907 insertions(+), 1198 deletions(-) delete mode 100644 game/data/schemas/attunement_growth_table.gd delete mode 100644 game/data/schemas/attunement_growth_table.gd.uid delete mode 100644 game/data/tables/attunement_growth.tres create mode 100644 game/tests/unit/core/tree_rules_v11_test.gd create mode 100644 game/tests/unit/core/tree_rules_v11_test.gd.uid diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 977c47b0..40cd1ab9 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -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) diff --git a/game/core/attunement.gd b/game/core/attunement.gd index a864ac8a..937f1e52 100644 --- a/game/core/attunement.gd +++ b/game/core/attunement.gd @@ -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 diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index fae9e11c..a2548c3d 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -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 自动玩家的唯一读取出口—— diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 27062768..d975aec2 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -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: diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index ca8f8f63..823ffced 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -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 = {} diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 1f80217e..af0d03db 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -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 零复算)。 diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 2f86c344..953f51eb 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -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 甲)---- diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index c1ea07d7..0a804ca6 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -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 diff --git a/game/data/schemas/attunement_growth_table.gd b/game/data/schemas/attunement_growth_table.gd deleted file mode 100644 index e345bb00..00000000 --- a/game/data/schemas/attunement_growth_table.gd +++ /dev/null @@ -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) diff --git a/game/data/schemas/attunement_growth_table.gd.uid b/game/data/schemas/attunement_growth_table.gd.uid deleted file mode 100644 index 73fbae35..00000000 --- a/game/data/schemas/attunement_growth_table.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://tfgsbs87xakg diff --git a/game/data/schemas/meta_table.gd b/game/data/schemas/meta_table.gd index fd57bf3d..b805249a 100644 --- a/game/data/schemas/meta_table.gd +++ b/game/data/schemas/meta_table.gd @@ -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: diff --git a/game/data/schemas/meta_tree_table.gd b/game/data/schemas/meta_tree_table.gd index 95f3e349..3d7b4e70 100644 --- a/game/data/schemas/meta_tree_table.gd +++ b/game/data/schemas/meta_tree_table.gd @@ -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) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index 36da1597..23c7eb13 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -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 diff --git a/game/data/tables/attunement_growth.tres b/game/data/tables/attunement_growth.tres deleted file mode 100644 index 7e53f08d..00000000 --- a/game/data/tables/attunement_growth.tres +++ /dev/null @@ -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 -}] -}] diff --git a/game/data/tables/meta.tres b/game/data/tables/meta.tres index 376b9f36..2e118adf 100644 --- a/game/data/tables/meta.tres +++ b/game/data/tables/meta.tres @@ -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 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按 ; 解锁后参数重测)。 diff --git a/game/data/tables/meta_tree.tres b/game/data/tables/meta_tree.tres index 527d04b3..e91f16f5 100644 --- a/game/data/tables/meta_tree.tres +++ b/game/data/tables/meta_tree.tres @@ -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。 diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index e4f3ced4..5e86570b 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -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() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index ba62edc7..46593905 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -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(), } diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 5061adf6..f7370621 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -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) diff --git a/game/presentation/views/career_overlay.gd b/game/presentation/views/career_overlay.gd index 1d9f6a9a..ba0890d7 100644 --- a/game/presentation/views/career_overlay.gd +++ b/game/presentation/views/career_overlay.gd @@ -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() diff --git a/game/presentation/views/pause_overlay.gd b/game/presentation/views/pause_overlay.gd index 994abcb0..9fd94137 100644 --- a/game/presentation/views/pause_overlay.gd +++ b/game/presentation/views/pause_overlay.gd @@ -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)] diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 202c19ce..64011bea 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -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(): diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 6fd1cba9..43b96210 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -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) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index cab4cc11..322fb9a6 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -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: diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 43f43e61..95e6b585 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -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("营地关闭后未恢复开始层(开合往返失守)") diff --git a/game/smoke/verifier_batch.gd b/game/smoke/verifier_batch.gd index 720a4555..6dbb72ce 100644 --- a/game/smoke/verifier_batch.gd +++ b/game/smoke/verifier_batch.gd @@ -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: diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index dc86407f..35cbfc0b 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -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 属性轴) ] diff --git a/game/tests/unit/core/tree_rules_v11_test.gd b/game/tests/unit/core/tree_rules_v11_test.gd new file mode 100644 index 00000000..a5064fc6 --- /dev/null +++ b/game/tests/unit/core/tree_rules_v11_test.gd @@ -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) diff --git a/game/tests/unit/core/tree_rules_v11_test.gd.uid b/game/tests/unit/core/tree_rules_v11_test.gd.uid new file mode 100644 index 00000000..c02c8731 --- /dev/null +++ b/game/tests/unit/core/tree_rules_v11_test.gd.uid @@ -0,0 +1 @@ +uid://cuhmdpcunxhrn diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index 1a8786a0..d30dd945 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -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)