From 48b069615806b9ade886e31305925f79b747b31a Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 15:21:22 +0800 Subject: [PATCH 1/8] =?UTF-8?q?T1(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E6=96=B0=E7=9B=AE=E5=BD=9512=E8=A1=8C=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E6=8D=A2=E4=BB=A3+schema=E6=89=A9=E7=AD=BE=E2=80=94?= =?UTF-8?q?=E2=80=94treasure=5Fitems.tres=20=E6=97=A712=E8=A1=8C=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=96=B012=E8=A1=8C=E8=90=BD=E8=A1=A8(=E6=99=A8?= =?UTF-8?q?=E6=93=8D=E4=BB=A4/=E9=98=94=E8=A7=86=E9=87=8E=E5=B8=A6/?= =?UTF-8?q?=E8=AF=95=E7=82=BC=E9=95=90/=E5=88=B7=E6=96=B0=E7=BB=83?= =?UTF-8?q?=E4=B9=A0/=E5=82=A8=E5=A4=87=E7=BD=90/=E5=B7=A2=E7=A9=B4?= =?UTF-8?q?=E9=A2=84=E5=91=8A/=E7=BA=BF=E9=A6=99/=E5=8F=8C=E7=AE=A1?= =?UTF-8?q?=E8=A3=85=E5=A1=AB/=E8=BF=9C=E7=A8=8B=E9=95=90/=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=89=A7=E7=85=A7/=E5=A4=A9=E7=9C=BC/=E4=BF=A1?= =?UTF-8?q?=E6=A0=87=E7=BD=91=E7=BB=9C;=20=E4=B8=89=E6=A1=A37/3/2;=20?= =?UTF-8?q?=E5=A5=91=E7=BA=A68=E9=80=90=E4=BB=B6=E8=A7=A3=E9=94=81?= =?UTF-8?q?=E5=88=86=E9=85=8D=3D=E9=92=A5=E7=9F=B38+=E6=9D=A1=E4=BB=B64);?= =?UTF-8?q?=20schema=E6=89=A9=20tier/charge=5Freset/unlock=5Fkeystone=5Fco?= =?UTF-8?q?st=20=E4=B8=89=E9=94=AE+TIERS/CHARGE=5FRESETS=20=E8=AF=8D?= =?UTF-8?q?=E8=A1=A8+TARGETS=E6=89=A9cell+=E5=8F=82=E6=95=B0=E9=9D=A2kind?= =?UTF-8?q?=E5=85=AD=E5=9E=8B+REVEAL=5FKINDS=E6=8A=A4=E6=A0=8F=E6=89=A9?= =?UTF-8?q?=E9=9D=A2+charges=3D0=E7=A7=AF=E7=82=B9=E5=88=B6=E5=90=88?= =?UTF-8?q?=E6=B3=95=E6=80=81+=E8=A7=A3=E9=94=81=E4=BA=92=E6=96=A5?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C+UNLOCK=5FCOUNTER=5FKEYS=E6=8D=A2=E4=BB=A3(ad?= =?UTF-8?q?venture=5Fcleared=E5=AF=B9=E9=BD=90C6=E9=80=9A=E5=85=B3?= =?UTF-8?q?=E5=8F=A3=E5=BE=84/vision=5Fused=E6=96=B0=E5=A2=9E);=20?= =?UTF-8?q?=E5=BC=95=E6=93=8EEVENTS+board=5Finstalled/ACTIVE=5FKINDS+safe?= =?UTF-8?q?=5Fdig/EFFECT=5FKINDS+mine=5Fcount=5Fpreview;=20V13=E6=97=A7?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E6=91=98=E9=99=A4=E6=8D=A2=E4=BB=A3(fog=5Fla?= =?UTF-8?q?ntern=5Fwiring=5Ftest=E5=85=A8=E6=96=87=E4=BB=B6=E9=80=80?= =?UTF-8?q?=E5=BD=B9+run=5Fall=E6=B3=A8=E5=86=8C=E6=91=98=E9=99=A4/violati?= =?UTF-8?q?on=E6=8A=A4=E6=A0=8F=E4=B8=BB=E8=AF=AD=E6=8D=A2sky=5Feye/feat?= =?UTF-8?q?=5Fvouchers=E7=94=B5=E6=B1=A0=E4=B8=BB=E8=AF=AD=E6=8D=A2incense?= =?UTF-8?q?=5Fstick/smoke=E6=8F=90=E7=81=AF=E6=AE=B5=E6=8D=A2V1V2=E6=96=B0?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=96=AD=E8=A8=80=E6=AE=B5+=E8=A3=85?= =?UTF-8?q?=E9=85=8D=E5=85=85=E8=83=BD=E5=88=B7=E6=96=B0=E6=AE=B5=E6=8D=A2?= =?UTF-8?q?=E4=B8=BB=E8=AF=AD)=E3=80=82=E5=86=92=E7=83=9F887/0+=E5=8D=95?= =?UTF-8?q?=E5=85=8321=E5=A5=97+=E6=8E=A2=E9=92=88=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9=20V1/V2/V12/V13/V14?= =?UTF-8?q?=E9=83=A8=E5=88=86=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/effect_engine.gd | 16 +- game/data/schemas/treasure_item_table.gd | 72 +++- game/data/tables/treasure_items.tres | 311 ++++++++++-------- game/smoke/smoke_all.gd | 105 +++--- game/tests/run_all.gd | 2 +- game/tests/unit/core/feat_vouchers_test.gd | 12 +- .../unit/core/fog_lantern_wiring_test.gd | 60 ---- .../unit/core/fog_lantern_wiring_test.gd.uid | 1 - .../treasure_target_violation_test.gd | 15 +- 9 files changed, 331 insertions(+), 263 deletions(-) delete mode 100644 game/tests/unit/core/fog_lantern_wiring_test.gd delete mode 100644 game/tests/unit/core/fog_lantern_wiring_test.gd.uid diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 823ffced..5c718fd9 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -19,10 +19,12 @@ extends RefCounted ## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕 ## +battle_won 战斗胜利〔战地包扎〕;迭代 21 天赋树批 T3 扩签 18→19:+flag_missed 标错判定 ## 〔危险直觉·守挂点——_judge_flag 去重闸后发射,契约 12 触发主语=当回合标错动作; -## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;原发射点不动) +## 命名避让 _emit_row 在册 source_class 名 flag_miss〕;迭代 22 秘宝实装批 T3 扩签 19→20: +## +board_installed 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕;原发射点不动) 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", "flag_missed"] + "round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "flag_missed", + "board_installed"] ## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。 ## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件) @@ -43,7 +45,11 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open # preview_area=资源类型预告〔示警烟火——3×3 邻域资源类型,不揭安危位置〕; # target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕 "heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area", - "target_attack_bonus"] + "target_attack_bonus", + # 迭代 22 秘宝实装批 T3 扩签一型:mine_count_preview=危险格数量预告(巢穴预告——board_installed + # 订阅 do;只报数量不标位置、不碰危险格分布=棋盘事实不可变契约;参数面常时 kind 不入本词表—— + # 它们无订阅行,schema 行内 effect_kinds 校验走 treasure_item_table 自有词表) + "mine_count_preview"] # 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理; # 编排器消费分支 T1 先行摘除) @@ -80,7 +86,9 @@ const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "value_event", "value_counter", "domain", "axis"] ## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发) -const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"] +## 迭代 22 秘宝实装批 T3 扩签:+safe_dig 安全格直开(天眼——3×3 内安全格按普通挖格规则全额兑付 +## 含连锁展开与格面资源,危险格仅真视显示不移除——棋盘事实不可变契约;免行动点即效果本身,C5 裁) +const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", "safe_dig"] # ---- 订阅集与计数器账本 ---- var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index 23c7eb13..ebeee4db 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -36,8 +36,11 @@ 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"] +# 迭代 22 T1 换代:clear_count(旧工坊按关累计渠道)与 C6 通关口径(通关=打通整局冒险)不符, +# 换名 adventure_cleared(生涯通关局数,save_manager 生涯计数域在役)并扩 vision_used(真视类工具 +# 累计使用——信标网络行为条件计数,本批新增;use_treasure 受理成功处累计、冒险收束导出入档); +# 旧 clear_count 零行消费,换名安全) +const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_used"] const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] # 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。 @@ -49,17 +52,34 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant", "mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal", "reward_weight_bias", "bonus_refresh", - "reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"] -const TARGETS := ["none", "area_3x3", "board_wide"] + "reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", + # 迭代 22 秘宝实装批 T1 扩签八型: + # 常时参数面六型(无事件订阅——开局/装板参数注入,注入面按装配集 kind 分发读取): + # light_radius_bonus=微光半径加值〔阔视野带〕/rap_transfer_mult=停手转存率千分比〔储备罐 125=1.25〕/ + # dig_range_bonus=挖掘距离加值〔远程镐〕/flag_range_free=插旗不限相邻〔信标网络〕/ + # refresh_stock_grant=三选一刷新存量一次性加仓〔刷新练习〕/shop_refresh_bonus=商店免费刷新加值〔刷新执照〕; + # 主动分发两型:safe_dig=安全格直开(天眼——走普通挖格规则全额兑付,C5 裁)/mine_count_preview=危险格数量预告(巢穴预告) + "light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "flag_range_free", + "refresh_stock_grant", "shop_refresh_bonus", "safe_dig", "mine_count_preview"] +const TARGETS := ["none", "area_3x3", "board_wide", "cell"] # 迭代 22 T1 扩 cell=单格真视(线香——≤3×3 窄域契约内) const USE_PHASES := ["active_dig"] const FORMS := ["", "passive", "active_charged"] +# 档位词表(迭代 22 T1——槽位档位约束数据面:槽位可装 ≤ 槽位档位的件;词表沿 feats/adventure TIERS) +const TIERS := ["common", "excellent", "rare", "legendary"] +# 充能重置周期(迭代 22 T1 扩签——现役仅 adventure=每冒险;battle=每战重置〔线香〕/level=每关重置〔天眼〕) +const CHARGE_RESETS := ["adventure", "battle", "level"] # magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0 const MAGNITUDE_KINDS := ["ap_grant", "reflect_damage", "ap_rounds_front", - "ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"] + "ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap", + "light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "flag_range_free", + "refresh_stock_grant", "shop_refresh_bonus"] +# 真视类 kind(迭代 22 T1 护栏扩面——活行 target 必须 ∈ {area_3x3, cell}:真视窄域 ≤3×3 契约) +const REVEAL_KINDS := ["mine_reveal", "reveal_tiles"] # 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载 const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target", "magnitude", "use_phase", "retired", "deferred", "form", "charges_per_adventure", "slot_cost", - "unlock_requires", "effective_rounds", "charge_n", "triggers", "desc"] + "unlock_requires", "effective_rounds", "charge_n", "triggers", "desc", + "tier", "charge_reset", "unlock_keystone_cost"] # 迭代 22 T1 扩三键(档位/充能周期/钥石购买价) func validate() -> Array: @@ -123,9 +143,11 @@ func validate() -> Array: if kind_seen.has(kind): errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]}) kind_seen[kind] = true - # 承重护栏①真视窄域:mine_reveal 活行 target 必须 area_3x3(一次最多 3×3,禁全图看雷) - if kind_seen.has("mine_reveal") and target != "area_3x3": - errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:mine_reveal 行 target 必须 area_3x3,实得 %s(秘宝 GDD 结构冻结)" % [iid, target]}) + # 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷; + # 迭代 22 T1 护栏扩面——reveal_tiles 与新 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内) + for rv in REVEAL_KINDS: + if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"): + errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]}) # 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果 if target == "board_wide" and kind_seen.has("mine_reveal"): errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid}) @@ -145,11 +167,39 @@ func validate() -> Array: if form != "": if int(row.get("slot_cost", 1)) != 1: errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid}) + # 档位必填(迭代 22 T1——槽位档位约束数据面:件档≤格档判定的取数源) + var tier: String = String(row.get("tier", "")) + if not TIERS.has(tier): + errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]}) var charges: int = int(row.get("charges_per_adventure", 0)) if form == "passive" and charges != 0: errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]}) - if form == "active_charged" and charges < 1: - errs.append({"category": "cross_field", "message": "%s active_charged 行 charges_per_adventure 应 ≥1,实得 %d" % [iid, charges]}) + if form == "active_charged" and charges < 0: + errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]}) + if form == "active_charged" and charges == 0: + # 充能 0=积点制/滑耗制合法态(迭代 22 T1——试炼镐转化获取,无固定充能上限): + # 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行 + var has_gate := kind_seen.has("ap_charge_safe") + if not has_gate and row.has("triggers"): + for tr_v in (row.get("triggers", []) as Array): + if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend": + has_gate = true + break + if not has_gate: + errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid}) + # 充能重置周期(迭代 22 T1 扩签:adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设) + var creset: String = String(row.get("charge_reset", "adventure")) + if not CHARGE_RESETS.has(creset): + errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]}) + if form != "active_charged" and row.has("charge_reset") and creset != "adventure": + errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]}) + # 解锁混合制字段校验(迭代 22 T1 契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做) + var price: int = int(row.get("unlock_keystone_cost", 0)) + var ureq_count: int = (row.get("unlock_requires", []) as Array).size() + if price != 0 and ureq_count > 0: + errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid}) + if price < 0 or price > 999: + errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]}) var eff_rounds: int = int(row.get("effective_rounds", 0)) if eff_rounds < 0 or eff_rounds > 999: errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]}) diff --git a/game/data/tables/treasure_items.tres b/game/data/tables/treasure_items.tres index 056e2ed9..b2c1b6f4 100644 --- a/game/data/tables/treasure_items.tres +++ b/game/data/tables/treasure_items.tres @@ -4,115 +4,34 @@ [resource] script = ExtResource("1") -; 迭代 19 卡④(2026-10-08):表尾追加 fog_lantern 魔法提灯(提灯载体道具批——视野外扩 +2 纯呈现载体行; -; 空效果行 live 形态=triggers 显式空数组,与显影尘 deferred 形态的形态差=「激活 vs 封存」);行字段值=用户「按推荐」全收 2026-10-08。 -; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。 -; nest_pendulum 已换装 active_charged(迭代 11 第①批);blast_kit 已换装 cell_removal+active_charged(迭代 13 T8/D6——中立移除雷格启用);sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。 +; 迭代 22 秘宝体系实现重构落地批 T1(2026-10-12):新目录 12 行全量换代(设计权威=秘宝.md「目录(重设计 v1)」; +; 旧 12 行删除——venom_edge/flag_burst 已随迭代 20 专长批迁出删除,本批实删 12 行)。三档=普通 7/优秀 3/稀有 2 +; (传说留空=空集合法,迭代 20 追认③先例)。解锁混合制(契约 8 逐件分配锁定表,2026-10-11 用户裁 C1): +; 钥石购买件 unlock_keystone_cost(provisional 占位);条件件 unlock_requires 谓词行——计数键 +; adventure_cleared=生涯通关局数(C6 裁:通关=打通整局冒险)/boss_kill_count=生涯 Boss 击杀/vision_used= +; 真视类工具累计使用(本批新增计数域)。全部数值 provisional 归调参批(契约 7)。 +; 常时参数面件(无订阅行——效果=开局/装板参数注入,非事件订阅):阔视野带/储备罐/远程镐/信标网络/刷新练习/ +; 刷新执照六件 effect_kinds 挂参数 kind,由注入面按装配集 kind 分发读取(_attune_refresh_x_total 先例)。 +; 主动件:线香(battle 级充能)/天眼(level 级充能)charge_reset 扩签;试炼镐=积点制(charges=0 无上限, +; ap_charge_safe 滑耗单级化——设计「4 格积 1 点满 2 点换 AP」两级结构的行为等价实现,8 格兑 1 AP)。 rows = [{ -"item_id": "probe_dust", -"display_name": "显影尘", -"dimension_tags": ["info", "counter"], -"desc": "效果失义待重定", -"effect_kinds": [], -"target": "none", -"magnitude": 0, -"use_phase": "active_dig", -"deferred": true -}, { -"item_id": "sapper_order", -"display_name": "工兵令", +"item_id": "morning_order", +"display_name": "晨操令", "dimension_tags": ["action"], -"desc": "AP +2", -"effect_kinds": ["ap_grant"], -"target": "none", -"magnitude": 2, -"use_phase": "active_dig", -"retired": true -}, { -"item_id": "loot_compass", -"display_name": "探宝罗盘", -"dimension_tags": ["plan"], -"desc": "预读 3×3", -"effect_kinds": ["reward_preview"], -"target": "area_3x3", -"magnitude": 0, -"use_phase": "active_dig", -"retired": true -}, { -"item_id": "nest_pendulum", -"display_name": "探巢摆钟", -"dimension_tags": ["risk"], -"desc": "真视 3×3", -"effect_kinds": ["mine_reveal"], -"target": "area_3x3", -"magnitude": 0, -"use_phase": "active_dig", -"form": "active_charged", -"slot_cost": 1, -"charges_per_adventure": 2 -}, { -"item_id": "catacomb_scroll", -"display_name": "地穴全图卷", -"dimension_tags": ["info", "plan"], -"desc": "效果失义待重定", -"effect_kinds": [], -"target": "board_wide", -"magnitude": 0, -"use_phase": "active_dig", -"deferred": true -}, { -"item_id": "blast_kit", -"display_name": "爆破工兵包", -"dimension_tags": ["action", "risk"], -"desc": "AP +4 真视 3×3 移除雷格", -"effect_kinds": ["ap_grant", "mine_reveal", "cell_removal"], -"target": "area_3x3", -"magnitude": 4, -"use_phase": "active_dig", -"form": "active_charged", -"slot_cost": 1, -"charges_per_adventure": 2 -}, { -"item_id": "nest_counter", -"display_name": "惊巢反制", -"dimension_tags": ["risk"], -"desc": "踩雷惩罚减半+反弹2", -"target": "none", -"magnitude": 2, -"use_phase": "active_dig", -"form": "passive", -"slot_cost": 1, -"triggers": [{ -"on": "mine_hit", -"do": { -"kind": "damage_mult", -"value": 50 -} -}, { -"on": "mine_hit", -"do": { -"kind": "open_ticket", -"value_from": "magnitude", -"channel": "reflect" -} -}] -}, { -"item_id": "dawn_order", -"display_name": "工兵晨令", -"dimension_tags": ["action"], -"desc": "前3轮每轮AP+1", +"desc": "每场战斗第 1 轮 AP +1", +"tier": "common", "target": "none", "magnitude": 1, "use_phase": "active_dig", "form": "passive", "slot_cost": 1, -"effective_rounds": 3, +"unlock_keystone_cost": 10, "triggers": [{ "on": "round_start", "when": [{ "fact": "round_index", -"op": "lte", -"value_from": "effective_rounds" +"op": "eq", +"value": 1 }], "do": { "kind": "grant_ap", @@ -121,64 +40,192 @@ rows = [{ } }] }, { -"item_id": "dig_charge", -"display_name": "掘进蓄能", +"item_id": "wide_scout_band", +"display_name": "阔视野带", +"dimension_tags": ["info"], +"desc": "类型微光范围 8 格→12 格", +"tier": "common", +"target": "none", +"magnitude": 4, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_keystone_cost": 10, +"effect_kinds": ["light_radius_bonus"] +}, { +"item_id": "trial_pick", +"display_name": "试炼镐", "dimension_tags": ["action"], -"desc": "积点换AP+1", +"desc": "每开 4 安全格积 1 点,满 2 点换当轮 AP +1", +"tier": "common", "effect_kinds": ["ap_charge_safe"], "target": "none", "magnitude": 1, "use_phase": "active_dig", "form": "active_charged", "slot_cost": 1, -"charges_per_adventure": 2, -"charge_n": 3, +"charges_per_adventure": 0, +"charge_n": 8, +"unlock_keystone_cost": 10, "triggers": [{ "on": "treasure_used", "do": { "kind": "counter_spend", -"counter_id": "dig_charge_bank", +"counter_id": "trial_pick_bank", "value": 1 } }] }, { -"item_id": "luck_coin", -"display_name": "偏运硬币", -"dimension_tags": ["risk"], -"desc": "稀有资源权重+2", -"target": "none", -"magnitude": 2, -"use_phase": "active_dig", -"form": "passive", -"slot_cost": 1, -"triggers": [{ -"on": "reward_roll", -"do": { -"kind": "bias_rewards", -"value_from": "magnitude" -} -}] -}, { -"item_id": "pick_refresh", -"display_name": "三选一刷新件", -"dimension_tags": ["info"], -"desc": "三选一刷新+1", -"effect_kinds": ["bonus_refresh"], +"item_id": "refresh_practice", +"display_name": "刷新练习", +"dimension_tags": ["plan"], +"desc": "三选一刷新存量 +1(装配即得一次性加仓)", +"tier": "common", "target": "none", "magnitude": 1, "use_phase": "active_dig", "form": "passive", -"slot_cost": 1 +"slot_cost": 1, +"unlock_keystone_cost": 10, +"effect_kinds": ["refresh_stock_grant"] }, { -"item_id": "fog_lantern", -"display_name": "魔法提灯", +"item_id": "reserve_jar", +"display_name": "储备罐", +"dimension_tags": ["action"], +"desc": "停手转存率 1:1 → 1.25:1", +"tier": "common", +"target": "none", +"magnitude": 125, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_keystone_cost": 10, +"effect_kinds": ["rap_transfer_mult"] +}, { +"item_id": "nest_forecast", +"display_name": "巢穴预告", "dimension_tags": ["info"], -"desc": "灯光区半径+2", -"effect_kinds": [], +"desc": "装板后揭示本板危险格数量", +"tier": "common", "target": "none", "magnitude": 0, "use_phase": "active_dig", "form": "passive", "slot_cost": 1, -"triggers": [] +"unlock_keystone_cost": 10, +"triggers": [{ +"on": "board_installed", +"do": { +"kind": "mine_count_preview" +} +}] +}, { +"item_id": "incense_stick", +"display_name": "线香", +"dimension_tags": ["info"], +"desc": "每战一次:真视单格", +"tier": "common", +"effect_kinds": ["reveal_tiles"], +"target": "cell", +"magnitude": 0, +"use_phase": "active_dig", +"form": "active_charged", +"slot_cost": 1, +"charges_per_adventure": 1, +"charge_reset": "battle", +"unlock_keystone_cost": 10 +}, { +"item_id": "twin_loader", +"display_name": "双管装填", +"dimension_tags": ["action"], +"desc": "每战前 2 轮各 AP +1", +"tier": "excellent", +"target": "none", +"magnitude": 1, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_keystone_cost": 25, +"triggers": [{ +"on": "round_start", +"when": [{ +"fact": "round_index", +"op": "lte", +"value": 2 +}], +"do": { +"kind": "grant_ap", +"mode": "queue", +"value_from": "magnitude" +} +}] +}, { +"item_id": "ranged_pick", +"display_name": "远程镐", +"dimension_tags": ["action"], +"desc": "挖掘距离 +1(可挖距英雄 2 格的格)", +"tier": "excellent", +"target": "none", +"magnitude": 1, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_requires": [{ +"key": "adventure_cleared", +"threshold": 1 +}], +"effect_kinds": ["dig_range_bonus"] +}, { +"item_id": "refresh_license", +"display_name": "刷新执照", +"dimension_tags": ["plan"], +"desc": "商店免费刷新 +2/店", +"tier": "excellent", +"target": "none", +"magnitude": 2, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_requires": [{ +"key": "boss_kill_count", +"threshold": 3 +}], +"effect_kinds": ["shop_refresh_bonus"] +}, { +"item_id": "sky_eye", +"display_name": "天眼", +"dimension_tags": ["info"], +"desc": "每关一次:选 3×3,危险格显示且安全格直接开", +"tier": "rare", +"effect_kinds": ["reveal_tiles", "safe_dig"], +"target": "area_3x3", +"magnitude": 0, +"use_phase": "active_dig", +"form": "active_charged", +"slot_cost": 1, +"charges_per_adventure": 1, +"charge_reset": "level", +"unlock_requires": [{ +"key": "adventure_cleared", +"threshold": 3 +}, { +"key": "boss_kill_count", +"threshold": 4 +}] +}, { +"item_id": "beacon_network", +"display_name": "信标网络", +"dimension_tags": ["risk"], +"desc": "插旗不限相邻(可对任何未开格插旗)", +"tier": "rare", +"target": "none", +"magnitude": 1, +"use_phase": "active_dig", +"form": "passive", +"slot_cost": 1, +"unlock_requires": [{ +"key": "vision_used", +"threshold": 10 +}], +"effect_kinds": ["flag_range_free"] }] diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index ae9add55..ca9f9951 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -232,34 +232,61 @@ func _smoke_adventure_table() -> void: "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕") 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") - check(not lantern_row.is_empty() and String(lantern_row.get("display_name", "")) == "魔法提灯" - and String(lantern_row.get("desc", "")) == "灯光区半径+2" - and String(lantern_row.get("form", "")) == "passive" and int(lantern_row.get("slot_cost", -1)) == 1 - and (lantern_row.get("dimension_tags", []) as Array) == ["info"] - and (lantern_row.get("effect_kinds", []) as Array).is_empty() - and lantern_row.has("triggers") and (lantern_row.get("triggers", []) as Array).is_empty() - and String(lantern_row.get("target", "")) == "none" and int(lantern_row.get("magnitude", -1)) == 0 - and String(lantern_row.get("use_phase", "")) == "active_dig" - and not lantern_row.has("unlock_requires") - and not bool(lantern_row.get("retired", false)) and not bool(lantern_row.get("deferred", false)), - "提灯载体行 fog_lantern:行字段逐键=裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/[info]/空效果/显式 triggers 空数组/none/0/active_dig/unlock 键省略=已解锁/活行非封存)") - var att_lantern := {} - for r in _db.treasure_attunement_rows(): - if String(r.get("item_id", "")) == "fog_lantern": - att_lantern = r - check(not att_lantern.is_empty() and String(att_lantern.get("display_name", "")) == "魔法提灯", - "同调目录投影:fog_lantern 在装配词表且 display_name=魔法提灯(营地工坊目录九件→十件)") + # 迭代 22 T1——新目录 12 件全量换代(V1/V2 断言落点;旧提灯段随 fog_lantern 行退役摘除, + # 装配→战斗贯通与空装配基线本体保留换主语 morning_order) + var trows: Array = _db.treasure_attunement_rows() + var tid_set := {} + for r in trows: + tid_set[String(r.get("item_id", ""))] = true + check(trows.size() == 12 and tid_set.size() == 12, + "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕") + var new_names := {"morning_order": "晨操令", "wide_scout_band": "阔视野带", "trial_pick": "试炼镐", + "refresh_practice": "刷新练习", "reserve_jar": "储备罐", "nest_forecast": "巢穴预告", + "incense_stick": "线香", "twin_loader": "双管装填", "ranged_pick": "远程镐", + "refresh_license": "刷新执照", "sky_eye": "天眼", "beacon_network": "信标网络"} + var names_ok := tid_set.size() == new_names.size() + for r in trows: + var iid2 := String(r.get("item_id", "")) + if not new_names.has(iid2) or String(r.get("display_name", "")) != String(new_names[iid2]): + names_ok = false + check(names_ok, "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕") + var keystone_ids := ["morning_order", "wide_scout_band", "trial_pick", "refresh_practice", + "reserve_jar", "nest_forecast", "incense_stick", "twin_loader"] + var cond_reqs := {"ranged_pick": {"adventure_cleared": 1}, "refresh_license": {"boss_kill_count": 3}, + "sky_eye": {"adventure_cleared": 3, "boss_kill_count": 4}, "beacon_network": {"vision_used": 10}} + var tier_map := {"common": 0, "excellent": 0, "rare": 0} + var alloc_ok := trows.size() == 12 + for r in trows: + var iid3 := String(r.get("item_id", "")) + var ureqs: Array = r.get("unlock_requires", []) as Array + if keystone_ids.has(iid3): + if int(r.get("unlock_keystone_cost", 0)) < 1 or not ureqs.is_empty(): + alloc_ok = false + elif cond_reqs.has(iid3): + var want: Dictionary = cond_reqs[iid3] + if int(r.get("unlock_keystone_cost", 0)) != 0 or ureqs.size() != want.size(): + alloc_ok = false + for req_v in ureqs: + var req: Dictionary = req_v as Dictionary + if int(want.get(String(req.get("key", "")), -1)) != int(req.get("threshold", -1)): + alloc_ok = false + else: + alloc_ok = false + var t: String = String(r.get("tier", "")) + if not tier_map.has(t): + alloc_ok = false + else: + tier_map[t] += 1 + check(alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2, + "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕") var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191)) - check(orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["fog_lantern"]}) - and orch_l.attune_has("fog_lantern"), - "装配→战斗贯通:enc.attunement=[fog_lantern] 起战 attune_has=true(注入面+只读出口)") + check(orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]}) + and orch_l.attune_has("morning_order"), + "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕") var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192)) check(orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) - and not orch_e.attune_has("fog_lantern"), - "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)") + and not orch_e.attune_has("morning_order"), + "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕") # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) @@ -4667,23 +4694,23 @@ func _meta_settle_and_workshop() -> void: 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"), - "卸下恒可再装配(换装语义——装配序数组承载)") + check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false, + "装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕") + check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader"), + "卸下恒可再装配(换装语义——装配序数组承载)〔秘宝批换主语;营地换装=冒险开始前合法面,装配不可变只锁运行中〕") var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7]) - sv_a2.set_attunement_equipped(["luck_coin"]) + sv_a2.set_attunement_equipped(["morning_order"]) 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 后投影一致)") + check((mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order"), + "装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕") # ⑫ 充能重置 / 刷新双计数器(迭代 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)") - check(int(a12.attune_charges.get("dig_charge", -1)) == 2 and int(a12.attune_charges.get("nest_pendulum", -1)) == 2, - "充能账本按装备 active_charged 件重置(每冒险 charges_per_adventure 表值)") - a12._attunement = ["pick_refresh"] + check(a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}), + "充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕") + check(int(a12.attune_charges.get("incense_stick", -1)) == 1 and int(a12.attune_charges.get("sky_eye", -1)) == 1, + "充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)") + a12._attunement = [] a12._attune_refresh_x = 1 a12._attune_refresh_y = 2 a12._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}} @@ -4693,8 +4720,8 @@ func _meta_settle_and_workshop() -> void: and a12.refresh_curio() == false, "刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)") a12._reset_refresh_x() - check(a12.attune_refresh_left() == 1, - "刷新 X 每碰三选一重置(不结转——重碰面 x 回表值 1)") + check(a12._attune_refresh_x == 0 and a12.attune_refresh_left() == 0, + "刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕") # ⑬ 主菜单开局传参面(迭代 19 卡⓪①③——开新/续玩两入口同调三键随开局下传防回归:真实入口此前 # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 35cbfc0b..75b1e2c6 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -43,7 +43,7 @@ func _suites() -> Array: {"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0}, {"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 注入面) + # fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面) {"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 概率上限解耦提前退役) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 189b2ef0..05f59316 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -55,12 +55,12 @@ func _init() -> void: # V9-5 同调电池:待分配充能 + 玩家点选(禁自动替选反例)+ 跨层写充能账 Kit.check_quiet(a._apply_curio_pick("attune_battery", "rare", 1), "同调电池兑付受理〔专长批 V9〕") Kit.check_quiet(a.battery_pending() == 1, "同调电池:待分配充能 1〔专长批 V9〕") - Kit.check_quiet(not a.choose_battery_target("dig_charge"), - "电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9〕") - a._attunement = ["dig_charge", "nest_pendulum"] # 注入装配集(dig_charge=active_charged) - a.attune_charges = {"dig_charge": 2, "nest_pendulum": 2} - Kit.check_quiet(a.choose_battery_target("dig_charge") and int(a.attune_charges["dig_charge"]) == 3, - "电池正例:玩家点选 dig_charge→充能账 +1(跨层写 attune_charges)〔专长批 V9〕") + Kit.check_quiet(not a.choose_battery_target("incense_stick"), + "电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9·秘宝批换主语〕") + a._attunement = ["incense_stick"] # 注入装配集(incense_stick=新目录 active_charged) + a.attune_charges = {"incense_stick": 1} + Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.attune_charges["incense_stick"]) == 2, + "电池正例:玩家点选 incense_stick→充能账 +1(跨层写 attune_charges)〔专长批 V9·秘宝批换主语〕") Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕") # V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽) diff --git a/game/tests/unit/core/fog_lantern_wiring_test.gd b/game/tests/unit/core/fog_lantern_wiring_test.gd deleted file mode 100644 index 73135d93..00000000 --- a/game/tests/unit/core/fog_lantern_wiring_test.gd +++ /dev/null @@ -1,60 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# 提灯载体 fog_lantern 装配链单测(迭代 19 卡④ T3——测试三形态之「呈现桥判定=注入装配集 stub 的 -# 单测 + 行加载单测」(需求卡片契约 9);probe=case⑮ 载体级段 / smoke=现有套件加断言,另两形态不在本件)。 -# 覆盖:真实表行加载+schema 校验零违例 / 行字段裁值逐键 / 同调目录词表收录 / attune_has 只读出口 -# 双态(组合根 _sync_fog_lantern 消费面)/ begin_battle enc 注入面。 -# 运行:godot --headless --path game --script res://tests/unit/core/fog_lantern_wiring_test.gd(expect 0) -extends SceneTree - -const Kit := preload("res://tests/unit/test_kit.gd") # check/汇总退出样板单点(静默收集通道) - - -func _init() -> void: - # ① 行加载+schema 校验(真实 treasure_items.tres——活行空 effect_kinds 豁免以 row.has("triggers") - # 为条件(treasure_item_table.gd:111),triggers 键漏写=加载红——本断言钉住该易错点) - var tbl = load("res://data/tables/treasure_items.tres") - Kit.check_quiet(tbl != null and tbl.validate().is_empty(), - "fog_lantern 入表后 TreasureItemTable.validate 零违例(KEY_WHITELIST/活行校验全过)") - var row: Dictionary = tbl.row_by_id("fog_lantern") if tbl != null else {} - Kit.check_quiet(not row.is_empty() and String(row.get("display_name", "")) == "魔法提灯" - and String(row.get("desc", "")) == "灯光区半径+2" - and String(row.get("form", "")) == "passive" and int(row.get("slot_cost", -1)) == 1 - and row.has("triggers") and (row.get("triggers", []) as Array).is_empty() - and (row.get("effect_kinds", []) as Array).is_empty() - and String(row.get("target", "")) == "none" and int(row.get("magnitude", -1)) == 0 - and String(row.get("use_phase", "")) == "active_dig" - and (row.get("dimension_tags", []) as Array) == ["info"] - and not row.has("unlock_requires") - and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)), - "行字段逐键=用户「按推荐」裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/显式 triggers 空/none/0/active_dig/[info]/unlock 键省略=已解锁/活行)") - var in_att := false - for r in (tbl.attunement_rows() as Array): - if String(r.get("item_id", "")) == "fog_lantern": - in_att = true - Kit.check_quiet(in_att, "同调目录词表收录 fog_lantern(attunement_rows=form 非空活行口径)") - - # ② 数据表宿主(②③需 db) - var db = load("res://core/config_db.gd").new() - if not db.load_tables("res://data/tables", false): - print("[fog_lantern_wiring_test] FAIL — 数值表加载失败") - quit(1) - return - - # ③ 呈现桥判定 stub:装配集直注→只读出口双态(组合根 _sync_fog_lantern 消费面——不经完整战斗) - var rng_a = load("res://core/rng_service.gd").new() - rng_a.set_master_seed(191) # 播种(EC7 未播种取数致命——begin_battle 板生成取号前提) - var orch = load("res://core/battle_orchestrator.gd").new(db, rng_a) - orch.attunement = ["fog_lantern"] - Kit.check_quiet(orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=true(装配集在册)") - Kit.check_quiet(not orch.attune_has("luck_coin"), "attune_has(luck_coin)=false(他件不误判)") - orch.attunement = [] - Kit.check_quiet(not orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=false(空集=复原语义读数源)") - - # ④ begin_battle enc 注入面(runner→enc.attunement→orchestrator 运行态) - var rng_b = load("res://core/rng_service.gd").new() - rng_b.set_master_seed(192) - var orch2 = load("res://core/battle_orchestrator.gd").new(db, rng_b) - Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["fog_lantern"]}) - and orch2.attune_has("fog_lantern"), "begin_battle enc.attunement 注入→attune_has=true(每战装配链装载)") - - quit(Kit.finish("fog_lantern_wiring_test", "PASS — 行加载/schema 校验/同调词表收录/只读出口双态/enc 注入面 全绿")) diff --git a/game/tests/unit/core/fog_lantern_wiring_test.gd.uid b/game/tests/unit/core/fog_lantern_wiring_test.gd.uid deleted file mode 100644 index 77ccd6c8..00000000 --- a/game/tests/unit/core/fog_lantern_wiring_test.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://jlkj801c81qd diff --git a/game/tests/unit/data_config/treasure_target_violation_test.gd b/game/tests/unit/data_config/treasure_target_violation_test.gd index 35fcd536..b3312b0f 100644 --- a/game/tests/unit/data_config/treasure_target_violation_test.gd +++ b/game/tests/unit/data_config/treasure_target_violation_test.gd @@ -1,6 +1,8 @@ # 致命类用例(独立进程,期望退出码 1) -# treasure_target_violation——真视窄域护栏(秘宝 GDD 结构冻结,迭代 7 T1):mine_reveal 行 target +# treasure_target_violation——真视窄域护栏(秘宝 GDD 结构冻结,迭代 7 T1):真视类行 target # 越域至 board_wide 时,TreasureItemTable.validate 必须报 load_bearing 错、load_tables 整体拒绝。 +# 迭代 22 T1:注入主语换代 sky_eye(新目录真视行 reveal_tiles——旧 nest_pendulum 行随 12 行换代退役; +# 护栏本体扩面=mine_reveal/reveal_tiles 两 kind 同守 REVEAL_KINDS)。 # 运行:godot --headless --path game --script res://tests/unit/data_config/treasure_target_violation_test.gd # 退出码协议:1 = 校验正确拒绝(预期);2 = 违规表被放行(回归信号)。 # 时序注:纯加载期校验——不触运行期 fatal 通道,断言 load_tables 返回 false + 诊断在案即 quit(1)。 @@ -20,17 +22,12 @@ func _inject_violation() -> bool: return false var s := f.get_as_text() f.close() - # 探巢摆钟(mine_reveal 活行)target area_3x3 → board_wide:双护栏同时命中 - var bad := s.replace('"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",') - if bad == s: - # 换行形态回退(LF) - bad = s.replace('"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",') - # 直接改 target:摆钟行是全表唯一的 mine_reveal 单维行——把它的 area_3x3 改 board_wide - # (罗盘 retired 行同值 area_3x3 但 retired 免校验——替换须定位摆钟块内) + # 直接改 target:天眼行是全表唯一的 reveal_tiles 真视行——把它的 area_3x3 改 board_wide + # (护栏扩面后真视类 kind 全守——越域行命中 REVEAL_KINDS 护栏) var blocks := s.split("}, {") var hit := false for i in blocks.size(): - if '"item_id": "nest_pendulum"' in blocks[i]: + if '"item_id": "sky_eye"' in blocks[i]: blocks[i] = blocks[i].replace('"target": "area_3x3"', '"target": "board_wide"') hit = true s = "}, {".join(blocks) From a7bd4e4132507aae2410e422b5d1dfce39dfd69e Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 15:52:04 +0800 Subject: [PATCH 2/8] =?UTF-8?q?T2(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E6=A0=B8=E5=BF=83=E5=B1=82=E8=A7=A3=E9=94=81?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=8D=A2=E4=BB=A3+=E6=A7=BD=E4=BD=8D?= =?UTF-8?q?=E6=A1=A3=E4=BD=8D=E7=BA=A6=E6=9D=9F=E2=80=94=E2=80=94=E5=B7=A5?= =?UTF-8?q?=E5=9D=8A=E6=B8=A0=E9=81=93=E9=9D=A2=E9=80=80=E5=BD=B9(=5Fworks?= =?UTF-8?q?hop=5Fprogress/workshop=5Fstatus/refresh=5Fworkshop=5Funlocks/m?= =?UTF-8?q?eta=5Fworkshop=5Fitems/WORKSHOP=5FITEM=5FIDS/workshop=5Fitems?= =?UTF-8?q?=E8=A1=8C=E6=97=8F=E5=85=A8=E5=88=A0)=E6=8D=A2=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E6=B7=B7=E5=90=88=E5=88=B6(buy=5Ftreasure=5Funlock?= =?UTF-8?q?=E9=92=A5=E7=9F=B3=E8=B4=AD=E4=B9=B0=3D=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E9=97=B8+=E9=87=8D=E5=A4=8D=E8=B4=AD=E6=8B=92+=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E4=BB=B6=E8=AF=AF=E8=B4=AD=E6=8B=92/refresh=5Ftreasur?= =?UTF-8?q?e=5Funlocks=E6=9D=A1=E4=BB=B6=E5=88=B7=E6=96=B0=3DAND=E8=B0=93?= =?UTF-8?q?=E8=AF=8D+=E5=B9=82=E7=AD=89/treasure=5Fshop=5Fstatus=E6=9F=9C?= =?UTF-8?q?=E5=8F=B0=E6=8A=95=E5=BD=B1);=20=E8=AE=A1=E6=95=B0=E8=AF=BB?= =?UTF-8?q?=E6=95=B0=E5=8D=95=E7=82=B9=5Ftreasure=5Funlock=5Fprogress(adve?= =?UTF-8?q?nture=5Fcleared/boss=5Fkill=5Fcount/vision=5Fused);=20save=5Fma?= =?UTF-8?q?nager=E6=96=B0=E5=A2=9Evision=5Fused=E7=94=9F=E6=B6=AF=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E5=9F=9F(add=5Fvision=5Fused=E6=94=B6=E6=9D=9F?= =?UTF-8?q?=E5=85=A5=E8=B4=A6+=E7=BC=BA=E9=94=AE=E8=A1=A5=E9=9B=B6?= =?UTF-8?q?=E4=B8=8D=E5=8D=87=E7=89=88=E6=9C=AC);=20toggle=5Fattunement?= =?UTF-8?q?=E4=B8=89=E9=87=8D=E6=A0=A1=E9=AA=8C(=E6=9C=AA=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E6=8B=92+=E6=BB=A1=E4=BD=8D=E7=A6=81=E9=80=89+?= =?UTF-8?q?=E4=BB=B6=E6=A1=A3rank=E2=89=A4=E8=A3=85=E5=85=A5=E4=BD=8D?= =?UTF-8?q?=E5=BA=8F=E6=A1=A3=E4=BD=8D=E6=A0=BC=E2=80=94=E2=80=94=E5=8D=B8?= =?UTF-8?q?=E4=B8=8B=E6=81=92=E5=8F=AF=E6=95=88=E6=9E=9C=E9=9D=A2=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E8=A3=85=E9=85=8D=E9=9B=86);=20pause/camp=E4=B8=A4?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E7=A7=98=E5=AE=9D=E5=8C=A3=E5=91=88=E7=8E=B0?= =?UTF-8?q?=E6=8D=A2=E4=BB=A3+UiKit=E8=AE=A1=E6=95=B0=E9=94=AE=E4=B8=8E?= =?UTF-8?q?=E6=A1=A3=E4=BD=8D=E6=A0=87=E7=AD=BE=E5=8D=95=E7=82=B9;=20smoke?= =?UTF-8?q?=E5=B7=A5=E5=9D=8A=E6=AE=B5=E6=8D=A2V3/V4/V5=E6=96=AD=E8=A8=80(?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=A1=A3[11]=E9=98=B2=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=B1=A1=E6=9F=93)+meta=E8=A1=A8=E6=AE=B5=E8=A1=8C=E6=97=8F?= =?UTF-8?q?=E8=A7=84=E6=A8=A1=E6=8D=A2=E4=BB=A33/3/2=E3=80=82=E5=86=92?= =?UTF-8?q?=E7=83=9F895/0+=E5=8D=95=E5=85=8321=E5=A5=97+=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E5=85=A8=E7=BB=BF=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9=20V3/V4/?= =?UTF-8?q?V5/V14/V19=E6=95=B0=E6=8D=AE=E6=BA=90=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/config_db.gd | 4 +- game/core/meta_progression.gd | 131 ++++++++++++------ game/core/save_manager.gd | 12 ++ game/data/schemas/meta_table.gd | 41 ++---- game/data/schemas/treasure_item_table.gd | 3 + game/data/tables/meta.tres | 40 +----- game/presentation/camp_shop_presenter.gd | 10 +- .../presentation/curio_interact_controller.gd | 5 +- game/presentation/main.gd | 2 +- game/presentation/views/camp_overlay_view.gd | 32 +++-- game/presentation/views/pause_overlay.gd | 29 ++-- game/presentation/views/ui_kit.gd | 6 + game/smoke/smoke_all.gd | 114 ++++++++------- 13 files changed, 235 insertions(+), 194 deletions(-) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index d975aec2..6267e53d 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -360,8 +360,8 @@ func meta_boss_keystone_row(level_index: int) -> Dictionary: return {} -func meta_workshop_items() -> Array: - return _meta_rows("workshop_items") +# meta_workshop_items 访问器已删除(迭代 22 T2——工坊行族退役,解锁混合制挂 treasure_items 行内; +# 秘宝解锁投影面=MetaProgression.treasure_shop_status) func meta_conversion_row(attr: String) -> Dictionary: diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 953f51eb..b4f2b613 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -29,59 +29,64 @@ func keystone_balance() -> int: return save.keystone_balance() -# ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)---- +# ---- 秘宝解锁服务(迭代 22 T2 换代——解锁混合制,契约 6;旧工坊渠道面随批删除)---- # 〔属性锻造柜台退役——迭代 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 -# 封闭);本方法是渠道 → 判定数值的唯一换算点——换/加渠道 = 词表加值 + 此处增一 match 分支, -# 阈值比较(refresh/workshop_status)与解锁写档零改动。候选渠道(语义已预留未实现): -# adventure_depth 最高通关深度 / keystone 钥石直购双渠道。 -func _workshop_progress(channel: String) -> int: - match channel: - "total_clears": - var total := 0 - for i in db.level_count(): # 表访问收口(2026-10-02——行集直扫退役) - total += save.clear_count(i + 1) - return total - _: - return 0 # 未知渠道恒 0 = 永不解锁(fail-safe 非致命——渠道词表校验兜底,不静默误解锁) - +# 表族随攻防所有权切分删除。〕〔道具工坊解锁渠道面退役——迭代 22 T2:旧「总通关次数达阈值」 +# 行族与 _workshop_progress/workshop_status/refresh_workshop_unlocks 删除,解锁条件单一事实源= +# treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词(C1 逐件分配锁定表)。〕 # 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager) func is_item_unlocked(item_id: String) -> bool: return save.item_unlocked(item_id) -# 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress} -func workshop_status() -> Array: - var out: Array[Dictionary] = [] - for row in db.meta_workshop_items(): - var iid := String(row.get("item_id", "")) - var arow: Dictionary = db.treasure_item_row(iid) # 迭代 7 T5——失义/退役状态面(呈现「能力封存/已退役」) - out.append({ - "item_id": iid, - "display_name": String(row.get("display_name", "")), - "threshold": int(row.get("clear_threshold", 0)), - "unlocked": save.item_unlocked(iid), - "progress": _workshop_progress(String(row.get("channel", "total_clears"))), - "deferred": bool(arow.get("deferred", false)), - "retired": bool(arow.get("retired", false)), - }) - return out +# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS; +# 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动) +func _treasure_unlock_progress(key: String) -> int: + match key: + "adventure_cleared": + return save.adventure_cleared() + "boss_kill_count": + return save.boss_kill_count() + "vision_used": + return save.vision_used() + _: + return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁) -# 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次) -func refresh_workshop_unlocks() -> Array: +# 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购; +# 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。 +func buy_treasure_unlock(item_id: String) -> bool: + var arow: Dictionary = db.treasure_item_row(item_id) + var price := int(arow.get("unlock_keystone_cost", 0)) + if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0: + return false # 非购买件(条件件/无价行)拒绝 + if save.item_unlocked(item_id): + return false # 已解锁重复购买拒绝 + if not save.spend_keystone(price): + return false # 余额不足(零副作用) + save.unlock_item(item_id) + return save.write_to_disk() + + +# 条件解锁刷新(达标未解锁 → unlock_item;返回新解锁 item_id 列——冒险收束后与营地打开时各调一次)。 +# 全部谓词达标才解锁(多谓词=AND——天眼双条件);幂等:已解锁跳过,重复刷新零重复入账。 +func refresh_treasure_unlocks() -> Array: var new_unlocks: Array[String] = [] - for row in db.meta_workshop_items(): + for row in db.treasure_attunement_rows(): var iid := String(row.get("item_id", "")) if save.item_unlocked(iid): continue - var progress := _workshop_progress(String(row.get("channel", "total_clears"))) - if progress >= int(row.get("clear_threshold", 99)): + var ureqs: Array = row.get("unlock_requires", []) as Array + if ureqs.is_empty(): + continue # 购买件不入条件刷新面 + var met := true + for req_v in ureqs: + var req: Dictionary = req_v as Dictionary + if _treasure_unlock_progress(String(req.get("key", ""))) < int(req.get("threshold", 999)): + met = false + break + if met: save.unlock_item(iid) new_unlocks.append(iid) if not new_unlocks.is_empty(): @@ -89,6 +94,35 @@ func refresh_workshop_unlocks() -> Array: return new_unlocks +# 秘宝匣柜台只读投影(呈现层数据源——12 件逐行解锁态/价格/条件进度): +# {item_id, display_name, desc, tier, unlocked, mode(keystone|condition|owned), price, reqs:[{key,threshold,progress}]} +func treasure_shop_status() -> Array: + var out: Array[Dictionary] = [] + for row in db.treasure_attunement_rows(): + var iid := String(row.get("item_id", "")) + var unlocked := save.item_unlocked(iid) + var ureqs: Array = row.get("unlock_requires", []) as Array + var req_view: Array = [] + for req_v in ureqs: + var req: Dictionary = req_v as Dictionary + req_view.append({ + "key": String(req.get("key", "")), + "threshold": int(req.get("threshold", 0)), + "progress": _treasure_unlock_progress(String(req.get("key", ""))), + }) + out.append({ + "item_id": iid, + "display_name": String(row.get("display_name", "")), + "desc": String(row.get("desc", "")), + "tier": String(row.get("tier", "")), + "unlocked": unlocked, + "mode": "owned" if unlocked else ("condition" if ureqs.size() > 0 else "keystone"), + "price": int(row.get("unlock_keystone_cost", 0)), + "reqs": req_view, + }) + return out + + # ---- Boss 两档与收束入账(结算屏文案数据源)---- # 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary # (keystone_drops 副本随摘要入 recent_summaries)。 @@ -100,7 +134,7 @@ func settle_adventure(summary: Dictionary) -> Dictionary: save.mark_first_adventure_done() var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true) save.apply_adventure_summary(summary, drops) - var new_unlocks := refresh_workshop_unlocks() + var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) save.write_to_disk() var total := int(summary.get("keystone_total", 0)) return { @@ -297,16 +331,24 @@ func _check_easter_egg() -> void: # ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 需求卡片 P1 甲)---- -# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选; -# 装配关系写存档持久(冒险中途不可换装——底稿 §8.5,本口只服务营地)。 +# 装配切换(营地装配覆盖层唯一写口——冒险开始前配置;冒险中零写口=装配不可变契约 1,本口只服务营地): +# 已装 → 卸下恒可(卸下后其余件前移位序——效果面只读装配集不受格位影响);未装 → 非同调行拒绝 + +# 满位禁选 + 档位格校验(迭代 22 T2 契约 2:装入件 append 至末位 k=已装数+1,件档 rank ≤ k—— +# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。 +# 装配关系写存档持久。 func toggle_attunement(item_id: String) -> bool: var equipped: Array = save.attunement_equipped() if not equipped.has(item_id): var arow: Dictionary = db.treasure_item_row(item_id) if arow.is_empty() or String(arow.get("form", "")) == "": return false # 非同调行(过渡/退役行)禁装配 + if not save.item_unlocked(item_id): + return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置) if equipped.size() >= Attunement.slots(db, save): return false # 满位禁选 + var rank: int = int(db.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99)) + if rank > equipped.size() + 1: + return false # 件档高于将占格档(第 k 格档=k——普通/优秀/稀有递增) equipped.append(item_id) else: equipped.erase(item_id) @@ -323,8 +365,11 @@ func attune_camp_status() -> Dictionary: items.append({ "item_id": iid, "display_name": String(row.get("display_name", "")), + "desc": String(row.get("desc", "")), + "tier": String(row.get("tier", "")), "form": String(row.get("form", "")), "equipped": equipped.has(iid), + "unlocked": save.item_unlocked(iid), }) var lvl := save.attune_refresh_level() var next_row: Dictionary = db.meta_attune_refresh_row(lvl + 1) diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 0a804ca6..d613aa3e 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -134,6 +134,8 @@ func _default_data() -> Dictionary[String, Variant]: "first_adventure_done": false, "adventure_total": 0, # 生涯终身计数·冒险总局数(迭代 18 卡④——开局即计含放弃与失败,收束唯一入账) "adventure_cleared": 0, # 生涯终身计数·通关局数(三关全通才计 1——判定字段 summary.cleared) + "vision_used": 0, # 生涯终身计数·真视类工具累计使用次数(迭代 22 T2——信标网络解锁行为条件; + # 受理成功唯一计数点=battle 层经冒险层聚合、收束入账;缺键补零不升版本——迭代 21 先例) }, "breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①) } @@ -272,6 +274,16 @@ func adventure_cleared() -> int: return int(_data["meta"].get("adventure_cleared", 0)) +# 真视类工具累计使用次数(迭代 22 T2——信标网络解锁行为条件;写入唯一经 add_vision_used +# 冒险收束聚合入账,受理计数点=battle 层经冒险层聚合) +func vision_used() -> int: + return int(_data["meta"].get("vision_used", 0)) + + +func add_vision_used(n: int) -> void: + _data["meta"]["vision_used"] = maxi(0, vision_used() + maxi(0, n)) + + # ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)---- func tree_nodes() -> Dictionary: diff --git a/game/data/schemas/meta_table.gd b/game/data/schemas/meta_table.gd index b805249a..097e4347 100644 --- a/game/data/schemas/meta_table.gd +++ b/game/data/schemas/meta_table.gd @@ -3,38 +3,33 @@ #(元进度 GDD 验收标准,本批 = 其实现批) # Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2) # 数据配置 MetaTable schema(元进度行族)。 -# 行族四轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集): +# 行族三轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集): # boss_keystone = Boss 钥石两档行(level_index 连续从 1;first_kill ≥ repeat_kill——首杀全量、 # 重复杀固定小额恒定不递减,两档数值归元进度设计批配读数)。 -# workshop_items = 道具工坊行(item_id = 秘宝六件词表封闭恰全;首版解锁渠道 = 总通关次数 -# 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 秘宝重设计批,本表只有解锁状态面)。 # 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 考古。〕 +# 〔workshop_items 道具工坊行族已退役——迭代 22 T2 秘宝实装批:解锁换代混合制(钥石购买价 +# unlock_keystone_cost + 条件谓词 unlock_requires 全挂 treasure_items.tres 行内,单一事实源), +# 旧「总通关次数达阈值」渠道行族、WORKSHOP_ITEM_IDS/WORKSHOP_CHANNELS 词表与校验段随批删除。〕 # 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。 class_name MetaTable extends Resource @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"] # 三属性轴词表(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 -# 增一分支,阈值比较与解锁写档零改动。缺省(行无 channel 字段)= total_clears) -const WORKSHOP_CHANNELS := ["total_clears"] func validate() -> Array: var errs: Array[Dictionary] = [] - # 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命) - for family in ["boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]: + # 行族空护栏(三族皆为 ConfigDB 消费面——缺族即致命) + for family in ["boss_keystone", "conversion", "attune_refresh_upgrade"]: if get(family).is_empty(): errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family}) # boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定) @@ -55,28 +50,8 @@ func validate() -> Array: if li_at != i + 1: errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]}) break - # workshop_items:item_id 词表封闭 + 唯一 + 覆盖恰六(秘宝词表)+ 阈值域 [1,99] - var seen_item := {} - for row in workshop_items: - var iid: String = row.get("item_id", "") - if not WORKSHOP_ITEM_IDS.has(iid): - errs.append({"category": "enum", "message": "workshop_items item_id 出域秘宝六件词表: %s" % iid}) - if seen_item.has(iid): - errs.append({"category": "dup_key", "message": "workshop_items item_id 重复: %s" % iid}) - seen_item[iid] = true - if String(row.get("display_name", "")) == "": - errs.append({"category": "domain", "message": "workshop_items %s display_name 空" % iid}) - var th: int = int(row.get("clear_threshold", -1)) - if th < 1 or th > 99: - errs.append({"category": "domain", "message": "workshop_items %s clear_threshold 越域 [1,99]: %d" % [iid, th]}) - # channel:可替换解锁渠道(缺省 total_clears——词表封闭,扩渠道 = 词表加值 + 判定量分支) - var channel: String = row.get("channel", "total_clears") - if not WORKSHOP_CHANNELS.has(channel): - errs.append({"category": "enum", "message": "workshop_items %s channel 出域 %s: %s" % [iid, str(WORKSHOP_CHANNELS), channel]}) - if not workshop_items.is_empty(): - for iid_key in WORKSHOP_ITEM_IDS: - if not seen_item.has(iid_key): - errs.append({"category": "coverage", "message": "workshop_items 缺道具键: %s(六件词表恰全)" % iid_key}) + # workshop_items 行族已退役(迭代 22 T2——解锁混合制换代,条件谓词挂 treasure_items 行内; + # 校验段随批删除——秘宝解锁条件校验单点=TreasureItemTable.validate) # conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100] var seen_conv := {} for row in conversion: diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index ebeee4db..b8aefed8 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -66,6 +66,9 @@ const USE_PHASES := ["active_dig"] const FORMS := ["", "passive", "active_charged"] # 档位词表(迭代 22 T1——槽位档位约束数据面:槽位可装 ≤ 槽位档位的件;词表沿 feats/adventure TIERS) const TIERS := ["common", "excellent", "rare", "legendary"] +# 档位序(迭代 22 T2——装配档位校验取数源:件档 rank ≤ 装入位序(格 1=普通格/格 2=优秀格/格 3=稀有格 +# 递增,向下兼容=高档格可装低档件);rank 越大档越高) +const TIER_RANK := {"common": 1, "excellent": 2, "rare": 3, "legendary": 4} # 充能重置周期(迭代 22 T1 扩签——现役仅 adventure=每冒险;battle=每战重置〔线香〕/level=每关重置〔天眼〕) const CHARGE_RESETS := ["adventure", "battle", "level"] # magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0 diff --git a/game/data/tables/meta.tres b/game/data/tables/meta.tres index 2e118adf..dda26b7d 100644 --- a/game/data/tables/meta.tres +++ b/game/data/tables/meta.tres @@ -17,37 +17,6 @@ boss_keystone = [{ "first_kill": 3, "repeat_kill": 3 }] -workshop_items = [{ -"item_id": "probe_dust", -"display_name": "显影尘", -"clear_threshold": 1, -"channel": "total_clears" -}, { -"item_id": "sapper_order", -"display_name": "工兵令", -"clear_threshold": 1, -"channel": "total_clears" -}, { -"item_id": "loot_compass", -"display_name": "探宝罗盘", -"clear_threshold": 2, -"channel": "total_clears" -}, { -"item_id": "nest_pendulum", -"display_name": "探巢摆钟", -"clear_threshold": 2, -"channel": "total_clears" -}, { -"item_id": "catacomb_scroll", -"display_name": "地穴全图卷", -"clear_threshold": 3, -"channel": "total_clears" -}, { -"item_id": "blast_kit", -"display_name": "爆破工兵包", -"clear_threshold": 3, -"channel": "total_clears" -}] conversion = [{ "attr": "max_hp", "per_level_shift_pct": 6 @@ -73,9 +42,12 @@ attune_refresh_upgrade = [{ ; 〔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 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按 +; 与 ForgeGDD 考古用,行族本体不再在表。〕 +; 〔workshop_items 道具工坊行族已退役——迭代 22 T2 秘宝实装批:解锁换代混合制(钥石购买价 +; unlock_keystone_cost + 条件谓词 unlock_requires 全挂 treasure_items.tres 行内单一事实源), +; 旧「总通关次数达阈值」六行(阈值 1/1/2/2/3/3)与渠道面随批删除;历史记载归 git 考古。〕 +; boss 两档 L1 5/1 · L2 8/2 · L3 12/3 +; (首杀全量、重复杀固定小额恒定不递减);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按 ; 解锁后参数重测)。 ; ; 数值设计批·关卡成长×永久成长对齿(2026-09-22——设计权威 qa-data/数值设计_关卡成长×永久成长_v1.md, diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 5e86570b..a2234f22 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -42,12 +42,12 @@ func _init(db_ref, meta_get: Callable, save_get: Callable, camp_overlay_ref: Cam # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, -# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账) +# 规范 2.3 遮挡纪律;打开时先刷条件解锁——达标未解锁就地入账) func open_camp() -> void: var meta = _meta_get.call() if meta == null: return - meta.refresh_workshop_unlocks() + meta.refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) start_overlay.visible = false _open_modal.call(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈) camp_overlay.open_camp(camp_data()) @@ -61,13 +61,13 @@ func on_camp_closed() -> void: _refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) -# 营地快照投影(纯读——工坊六行/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归 -# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役) +# 营地快照投影(纯读——秘宝匣 12 件/同调装配/树面/余额/最近冒险;拼句归 CampOverlay,账面归 +# MetaProgression。属性锻造三行投影已删——迭代 21 T4 柜台退役;工坊六行换代=迭代 22 T2 秘宝匣) func camp_data() -> Dictionary: var meta = _meta_get.call() return { "balance": meta.keystone_balance() if meta != null else 0, - "workshop": meta.workshop_status() if meta != null else [], + "workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影) "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 "tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行) "fork_groups": meta.tree_fork_groups() if meta != null else [], diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index 4cf3b92a..add98aed 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -299,11 +299,12 @@ func on_continue() -> void: _fatal_screen.call("关卡推进失败——见输出日志") -# 工坊 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator) +# 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator); +# 迭代 22 T2 数据源换代:旧工坊行族退役,改读 treasure 装配词表 12 件 func item_name_lookup() -> Dictionary: var lookup := {} var meta = _meta_get.call() if meta != null: - for row in db.meta_workshop_items(): + for row in db.treasure_attunement_rows(): lookup[String(row.get("item_id", ""))] = String(row.get("display_name", "")) return lookup diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 46593905..e2718d77 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1018,7 +1018,7 @@ func _on_settings_closed() -> void: func _pause_data() -> Dictionary: return { "balance": meta.keystone_balance(), - "workshop": meta.workshop_status(), + "workshop": meta.treasure_shop_status(), # 迭代 22 T2——秘宝匣 12 件解锁投影(旧工坊面退役) "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 f7370621..49822af8 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -307,23 +307,29 @@ func refresh_camp(data: Dictionary) -> void: var row: Dictionary = ws_by_id.get(String(card.item_id), {}) if row.is_empty(): continue - var th := int(row.get("threshold", 0)) - var prog := int(row.get("progress", 0)) var unlocked := bool(row.get("unlocked", false)) - card.progress_l.text = "累计通关 %d / %d 次" % [prog, th] - # 迭代 7 T5——失义件(显影尘/地穴全图卷 Alpha 重定)与退役件(探宝罗盘 2026-09-29 拍板①乙)呈现口径; - # 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) + # 秘宝匣进度列(迭代 22 T2——解锁混合制三形态:钥石购买价/条件进度拼接/已入手; + # 旧「累计通关 N/M 次」单一渠道文案随工坊行族退役) + var prog_text := "" + var mode := String(row.get("mode", "")) + if unlocked: + prog_text = "已入手" + elif mode == "keystone": + prog_text = "钥石 %d" % int(row.get("price", 0)) + elif mode == "condition": + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + prog_text = "、".join(parts) + card.progress_l.text = prog_text + # 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读);失义/退役行随新目录退役 var st_text := "未解锁" var st_col: Color = UiKit.STONE - if bool(row.get("retired", false)): - st_text = "已退役" - st_col = UiKit.STONE - elif bool(row.get("deferred", false)) and unlocked: - st_text = "已解锁 · 能力封存中" - st_col = UiKit.GOLD_INK # 尾裁微批a 2026-10-08——GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1 - elif unlocked: + if unlocked: st_text = "已解锁" - st_col = UiKit.GOLD_INK # 同上 + st_col = UiKit.GOLD_INK # 尾裁微批a 2026-10-08——GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1 card.status_l.text = st_text card.status_l.add_theme_color_override("font_color", st_col) card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文) diff --git a/game/presentation/views/pause_overlay.gd b/game/presentation/views/pause_overlay.gd index 9fd94137..0811d7cf 100644 --- a/game/presentation/views/pause_overlay.gd +++ b/game/presentation/views/pause_overlay.gd @@ -82,6 +82,8 @@ func open_pause(data: Dictionary) -> void: # 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数) +# 秘宝匣 12 行只读概览(迭代 22 T2 换代——旧工坊六行退役;数据源=treasure_shop_status 投影, +# 解锁态文案与营地柜台同口径;进度列=解锁混合制三形态:钥石价/条件进度拼接/已入手) func _rebuild_workshop(data: Dictionary) -> void: for c in _workshop_holder.get_children(): c.queue_free() @@ -93,28 +95,29 @@ 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 prog_l := UiKit.label("累计通关 %d/%d 次" % [int(row.get("progress", 0)), int(row.get("threshold", 0))],UiKit.FS_HINT, false, UiKit.INK_HINT) + var unlocked := bool(row.get("unlocked", false)) + var prog_text := "已入手" + if not unlocked: + if String(row.get("mode", "")) == "keystone": + prog_text = "钥石 %d" % int(row.get("price", 0)) + else: + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + prog_text = "、".join(parts) + var prog_l := UiKit.label(prog_text,UiKit.FS_HINT, false, UiKit.INK_HINT) prog_l.custom_minimum_size = Vector2(150, 0) prog_l.clip_text = true prog_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS line.add_child(prog_l) - var st_l := UiKit.label(_workshop_state_text(row),UiKit.FS_HINT, true, UiKit.INK_LABEL) + var st_l := UiKit.label("已解锁" if unlocked else "未解锁",UiKit.FS_HINT, true, UiKit.INK_LABEL) st_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL line.add_child(st_l) _workshop_holder.add_child(line) -# 解锁态文案(camp_overlay_view.refresh_camp 同款判据——退役 > 封存 > 已解锁 > 未解锁) -func _workshop_state_text(row: Dictionary) -> String: - if bool(row.get("retired", false)): - return "已退役" - if bool(row.get("deferred", false)) and bool(row.get("unlocked", false)): - return "已解锁 · 能力封存中" - if bool(row.get("unlocked", false)): - return "已解锁" - return "未解锁" - - # 天赋树已购概览(单条聚合文案——已购节点等级 + Fork 组激活侧;零已购=空态句) func _rebuild_tree(data: Dictionary) -> void: var owned: Array[String] = [] diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 7dfc6c5b..ca9c4ea1 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -318,6 +318,12 @@ const FS_SUB := 18 # 副标题/小组标题(=棋盘数字下限同值) const FS_TITLE := 24 # 屏标题/横幅/价格 const FS_H1 := 34 # 主标题/演算暴击 +# ── 秘宝解锁条件计数键 → 呈现标签(迭代 22 秘宝批 T4——秘宝匣柜台条件进度文案单点;键域= +# TreasureItemTable.UNLOCK_COUNTER_KEYS 封闭枚举,键出域走「条件」兜底不炸)── +const TREASURE_REQ_LABELS := {"adventure_cleared": "通关", "boss_kill_count": "Boss击杀", "vision_used": "真视使用"} +# 秘宝档位 → 呈现名(契约 2 三档;legendary 传说空集合法不在柜) +const TREASURE_TIER_LABELS := {"common": "普通", "excellent": "优秀", "rare": "稀有", "legendary": "传说"} + # ── StyleBox 工厂 ── # margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用) # 圆角率单点(UI 设计对齐批 P5 甲 2026-10-06——容器/按钮全站唯一圆角值;数据框/数字牌/AP 点阵/准线/血条 diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index ca9f9951..6502eb5f 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -4359,10 +4359,10 @@ 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 全过)") + "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)") check(meta_tbl.boss_keystone.size() == 3 - and meta_tbl.workshop_items.size() == 6 and meta_tbl.conversion.size() == 3, - "MetaTable 行族规模 3/6/3(Boss/工坊恰六/换算恰三——forge_products 行族已随迭代 21 T5 退役)") + and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2, + "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)") var attrs_ok := true for a in ["max_hp", "atk", "armor_class"]: attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty() @@ -4387,22 +4387,21 @@ 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) - 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.unlock_item("morning_order") sv.write_to_disk() # 投影拼装(main._pause_data 同构)+ 视图喂入 - var data := {"balance": mp.keystone_balance(), "workshop": mp.workshop_status(), + var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(), "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()} 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 ws: Array = mp.workshop_status() + var ws: Array = mp.treasure_shop_status() var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size() + var uikit_s = load("res://presentation/views/ui_kit.gd") for i in mini(ws.size(), po._workshop_holder.get_child_count()): var row: Dictionary = ws[i] var lbls2: Array = [] @@ -4410,18 +4409,23 @@ func _pause_overlay_projection() -> void: if ch is Label: lbls2.append(ch) var unlocked: bool = bool(row.get("unlocked", false)) - var expect_st := "未解锁" - if bool(row.get("retired", false)): - expect_st = "已退役" - elif bool(row.get("deferred", false)) and unlocked: - expect_st = "已解锁 · 能力封存中" - elif unlocked: - expect_st = "已解锁" + var expect_prog := "已入手" + if not unlocked: + if String(row.get("mode", "")) == "keystone": + expect_prog = "钥石 %d" % int(row.get("price", 0)) + elif String(row.get("mode", "")) == "condition": + var parts: Array[String] = [] + for req_v2 in (row.get("reqs", []) as Array): + var req2: Dictionary = req_v2 as Dictionary + var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))]) + expect_prog = "、".join(parts) + var expect_st2 := "已解锁" if unlocked else "未解锁" ws_ok = ws_ok and lbls2.size() == 3 \ and String(lbls2[0].text) == String(row.get("display_name", "")) \ - and String(lbls2[1].text) == "累计通关 %d/%d 次" % [int(row.get("progress", 0)), int(row.get("threshold", 0))] \ - and String(lbls2[2].text) == expect_st - check(ws_ok, "工坊六行投影 = workshop_status() 逐项(名称/判定量进度/解锁态——首件解锁夹具在列)") + and String(lbls2[1].text) == expect_prog \ + and String(lbls2[2].text) == expect_st2 + check(ws_ok, "秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕") var owned := 0 for trow in (mp.tree_camp_rows() as Array): if int(trow.get("level", 0)) > 0: @@ -4507,7 +4511,7 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry "user://smoke_meta_cap.json", "user://smoke_meta_white.json", "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json", "user://smoke_meta_tree.json", "user://smoke_meta_career.json", - "user://smoke_meta_career_v5.json"] + "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json"] func _meta_smoke_cleanup() -> void: @@ -4638,32 +4642,42 @@ func _meta_settle_and_workshop() -> void: "stopped_at": 1, "keystone_total": 0, "master_seed": 15}) check(dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before, "败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)") - # ⑧ 工坊解锁:settle 内已随 refresh——总通关次数达阈值件数 == unlocked 数;重复 refresh 幂等 - var total_clears: int = sv.clear_count(1) + sv.clear_count(2) + sv.clear_count(3) - var expect_n := 0 - for row in _db.meta_workshop_items(): - if total_clears >= int(row.get("clear_threshold", 99)): - expect_n += 1 - var got_n := 0 - for st_row in mp.workshop_status(): - got_n += 1 if bool(st_row.get("unlocked", false)) else 0 - check(got_n == expect_n and mp.refresh_workshop_unlocks().is_empty(), - "工坊解锁数 = 总通关 %d 次达阈值件数(%d 件)且重复 refresh 幂等" % [total_clears, expect_n]) - # 渠道可替换轴(2026-09-21 用户拍板):判定量单点正控 + 未知渠道 fail-safe(恒 0 = 永不解锁,非致命) - check(mp._workshop_progress("total_clears") == total_clears and mp._workshop_progress("adventure_depth") == 0, - "解锁判定量单点:total_clears 正控 + 未知渠道恒 0(channel 可替换轴 fail-safe)") - var sv_r = _meta_new_save(_META_SMOKE_PATHS[2]) + # ⑧ 秘宝解锁服务(迭代 22 T2——V3 钥石购买/V4 条件解锁;注入=直调解锁写口+直置存档生涯计数, + # 不走正常资源积累流程——卡片 7.1 测试方式;旧工坊渠道段随行族退役摘除) + var sv_u = _meta_new_save(_META_SMOKE_PATHS[11]) + var mp_u = load("res://core/meta_progression.gd").new(_db, sv_u) + sv_u.add_keystone(10) + check(not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye") + and not mp_u.buy_treasure_unlock("ranged_pick"), + "条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕") + check(not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10, + "余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕") + check(mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order") + and sv_u.keystone_balance() == 0, + "钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕") + check(not mp_u.buy_treasure_unlock("morning_order"), + "已解锁重复购买拒绝〔秘宝批 V3〕") + check(mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick"), + "条件未达不解锁(远程镐=通关 1 次)〔秘宝批 V4〕") + sv_u._data["meta"]["adventure_cleared"] = 1 # 直置存档生涯计数(调试注入口径) + check((mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick") + and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty(), + "远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:通关=打通整局冒险)〔秘宝批 V4〕") + sv_u._data["meta"]["boss_kill_count"] = 3 + check((mp_u.refresh_treasure_unlocks() as Array).has("refresh_license"), + "刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕") + sv_u._data["meta"]["adventure_cleared"] = 3 + check(not sv_u.item_unlocked("sky_eye"), "天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕") + sv_u._data["meta"]["boss_kill_count"] = 4 + check((mp_u.refresh_treasure_unlocks() as Array).has("sky_eye"), + "天眼双条件 AND(通关≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕") + sv_u._data["meta"]["vision_used"] = 10 + check((mp_u.refresh_treasure_unlocks() as Array).has("beacon_network"), + "信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕") var r_n := 0 - for st_row in load("res://core/meta_progression.gd").new(_db, sv_r).workshop_status(): + for st_row in mp_u.treasure_shop_status(): r_n += 1 if bool(st_row.get("unlocked", false)) else 0 - check(r_n == expect_n, "工坊解锁持久(reload 后一致)") - # ⑧ 干净档:通关 1 次 → 恰两件阈值 1 道具新解锁(new_unlocks 明细) - var sv_w = _meta_new_save(_META_SMOKE_PATHS[3]) - var mp_w = load("res://core/meta_progression.gd").new(_db, sv_w) - var u1: Dictionary = mp_w.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1, - "stopped_at": 2, "keystone_total": 0, "master_seed": 16}) - check(u1["new_unlocks"].size() == 2 and sv_w.item_unlocked("probe_dust") and sv_w.item_unlocked("sapper_order") - and not sv_w.item_unlocked("loot_compass"), "通关 1 次 → 恰两件阈值 1 道具解锁(new_unlocks 明细正确)") + check(r_n == 5, "解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕") # ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10 var rs: Array = sv.recent_summaries() check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops") @@ -4681,9 +4695,11 @@ func _meta_settle_and_workshop() -> void: var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a) check(Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0, "白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)") - check(mp_a.toggle_attunement("nest_counter") == false, - "满位禁选(Slots 0 时装配拒绝)") + check(mp_a.toggle_attunement("morning_order") == false, + "满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕") sv_a.add_keystone(200) + sv_a.unlock_item("morning_order") + sv_a.unlock_item("twin_loader") check(mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1, "Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)") @@ -4694,10 +4710,12 @@ func _meta_settle_and_workshop() -> void: sv_a.set_tree_level("att_socket1", 1) check(Attunement.slots(_db, sv_a) == 1, "树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)") + check(not mp_a.toggle_attunement("sky_eye"), + "稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕") check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false, "装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕") - check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader"), - "卸下恒可再装配(换装语义——装配序数组承载)〔秘宝批换主语;营地换装=冒险开始前合法面,装配不可变只锁运行中〕") + check(mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order"), + "卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕") var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7]) sv_a2.set_attunement_equipped(["morning_order"]) var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2) From c813b2b042ebf84d527bfcfd9fe1086c47734140 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 17:01:12 +0800 Subject: [PATCH 3/8] =?UTF-8?q?T3(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E6=95=88=E6=9E=9C=E5=BC=95=E6=93=8E=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E4=B8=8E=E5=B8=B8=E6=97=B6=E5=8F=82=E6=95=B0=E9=9D=A2?= =?UTF-8?q?=E2=80=94=E2=80=94board=5Finstalled=E4=BA=8B=E4=BB=B6=E5=8F=91?= =?UTF-8?q?=E5=B0=84(=E5=B7=A2=E7=A9=B4=E9=A2=84=E5=91=8A=E8=AF=BB?= =?UTF-8?q?=E6=95=B0=E5=8F=A3nest=5Fpreview=5Fcount=3D=E8=A3=85=E6=9D=BF?= =?UTF-8?q?=E6=8A=A5=E5=8D=B1=E9=99=A9=E6=A0=BC=E6=95=B0=E9=87=8F,?= =?UTF-8?q?=E5=91=88=E7=8E=B0=E5=BD=92=E5=91=88=E7=8E=B0=E6=A1=A5);=20?= =?UTF-8?q?=E5=A4=A9=E7=9C=BCsafe=5Fdig=3Dfx=5Freveal=E9=80=9A=E9=81=93?= =?UTF-8?q?=E5=A4=8D=E7=94=A8(C5=E5=85=A8=E9=A2=9D=E5=85=91=E4=BB=98?= =?UTF-8?q?=E5=85=8DAP=E8=BF=9E=E9=94=81=E8=B5=84=E6=BA=90=E5=90=8C?= =?UTF-8?q?=E9=93=BE,=E5=8D=B1=E9=99=A9=E6=A0=BC=E4=BB=85=E7=9C=9F?= =?UTF-8?q?=E8=A7=86,=E6=97=97=E6=A0=BC=E8=B7=B3=E8=BF=87);=20reveal=5Ftil?= =?UTF-8?q?es=E5=85=A5=E7=9C=9F=E8=A7=86=E5=8F=A0=E5=8A=A0=E5=B1=82(mine?= =?UTF-8?q?=5Freveal=E5=90=8C=E9=80=9A=E9=81=93);=20=E8=AF=95=E7=82=BC?= =?UTF-8?q?=E9=95=90use=5Fattune=5Fconvert=E7=A7=AF=E7=82=B9=E5=8F=97?= =?UTF-8?q?=E7=90=86=E5=8F=A3(charges=3D0=E9=9B=B6=E9=AA=91=E5=BA=93?= =?UTF-8?q?=E5=AD=98,counter=5Fspend=E5=8F=97=E7=90=86=E9=97=B8+grant=5Fap?= =?UTF-8?q?,=E7=A7=AF=E7=82=B9=E8=AE=A1=E6=95=B0=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E5=8C=96=3D=E8=A3=85=E9=85=8D=E9=9B=86ap=5Fcharge=5Fsafe?= =?UTF-8?q?=E8=A1=8C=E6=89=AB=E6=8F=8F=E6=9B=BFdig=5Fcharge=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81);=20=E5=85=85=E8=83=BD=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=5Freseed=5Fattune=5Fcharges(battle=E6=AF=8F?= =?UTF-8?q?=E6=88=98/level=E6=AF=8F=E5=85=B3=E9=A6=96=E6=88=98battle=5Find?= =?UTF-8?q?ex=3D=3D1,=E9=AA=91=E5=BA=93=E5=AD=98=E5=B7=AE=E9=A2=9D?= =?UTF-8?q?=E8=A1=A5=E6=B3=A8);=20=E5=B8=B8=E6=97=B6=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E9=9D=A2=E6=B3=A8=E5=85=A5(=E8=BF=9C=E7=A8=8B=E9=95=90dig=5Fra?= =?UTF-8?q?nge=5Fbonus=E5=88=87=E6=AF=94=E9=9B=AA=E5=A4=AB=E2=89=A41+1/?= =?UTF-8?q?=E4=BF=A1=E6=A0=87=E7=BD=91=E7=BB=9Cflag=5Frange=5Ffree?= =?UTF-8?q?=E6=97=97=E5=8F=AF=E8=BE=BE=E8=B1=81=E5=85=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=5Fwithin=5Fdig=5Frange=E6=8B=86=E8=B0=93=E8=AF=8D,mine=5Ftotal?= =?UTF-8?q?=E5=8F=AA=E8=AF=BB=E5=8F=A3);=20=E5=82=A8=E5=A4=87=E7=BD=90fini?= =?UTF-8?q?sh=5Fround=5Fstop=E8=BD=AC=E5=AD=98=E7=8E=87=E5=85=A5=E5=8F=82(?= =?UTF-8?q?125%=3D1.25:1=E6=95=B4=E9=99=A4floor);=20=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E7=BB=83=E4=B9=A0refresh=5Fstock=5Fgrant=E5=BC=80=E5=B1=80?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E5=8A=A0=E4=BB=93;=20=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=89=A7=E7=85=A7shop=5Frefresh=5Ffree=5Fleft?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E5=8F=A0=E5=8A=A0;=20=E7=9C=9F=E8=A7=86?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AE=A1=E6=95=B0=E9=93=BE(vision=5Fcounter?= =?UTF-8?q?=E5=85=B1=E4=BA=ABdict=E2=86=92=E5=8F=97=E7=90=86+1=E2=86=92sum?= =?UTF-8?q?mary=E2=86=92settle=E8=81=9A=E5=90=88=E5=85=A5=E6=A1=A3+?= =?UTF-8?q?=E6=96=AD=E7=82=B9=E9=9A=8F=E4=BB=B6);=20validate=5Fuse?= =?UTF-8?q?=E6=89=A9cell=E6=A1=A3=E4=B8=8E=E6=96=B0=E7=A9=BA=E9=97=B4kind;?= =?UTF-8?q?=20=E4=BF=AE=E7=8E=B0=E5=BD=B9=E6=BD=9C=E4=BC=8F=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=3Duse=5Ftreasure=E6=9D=BF=E7=BB=B4=E5=BA=A6=E8=AF=BBR?= =?UTF-8?q?ules=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84width(=E6=97=A7?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=9C=9F=E9=9B=B6=E7=9B=B4=E5=91=BC=E6=8E=A9?= =?UTF-8?q?=E7=9B=96);=20=E6=96=B0=E5=8D=95=E6=B5=8Btreasure=5Feffects=5Ft?= =?UTF-8?q?est=2022=E5=A5=97(V7=E9=99=84=E5=B1=9E=E8=A1=A812=E4=BB=B6?= =?UTF-8?q?=E9=80=90=E4=BB=B6=E6=AD=A3=E5=B8=B8/=E8=BE=B9=E7=95=8C/?= =?UTF-8?q?=E5=8F=8D=E4=BE=8B+=E8=AE=A1=E6=95=B0=E9=93=BE)=E3=80=82?= =?UTF-8?q?=E5=86=92=E7=83=9F895/0+run=5Fall=2022=E5=A5=97+=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=85=A8=E7=BB=BF=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9?= =?UTF-8?q?=20V7/V8/V10/V13=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/action_economy.gd | 9 +- game/core/adventure_runner.gd | 12 + game/core/battle_orchestrator.gd | 155 ++++++++- game/core/level_runner.gd | 21 +- game/core/meta_progression.gd | 1 + game/core/minesweeper_rules.gd | 25 +- game/core/treasure_items.gd | 20 +- game/tests/run_all.gd | 1 + game/tests/unit/core/treasure_effects_test.gd | 298 ++++++++++++++++++ 9 files changed, 514 insertions(+), 28 deletions(-) create mode 100644 game/tests/unit/core/treasure_effects_test.gd diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 4f8930cf..28bea1dd 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -185,16 +185,17 @@ func grant_reserve_ap(n: int) -> void: # 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层 -# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0 -# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。 -func finish_round_stop(transfer: bool) -> void: +# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1 与旧逐位一致; +# 迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。 +# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。 +func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void: if transfer: var prev_layers := _charge_layers _charge_layers = mini(3, _charge_layers + _last_stop_ap) if _fx != null and _charge_layers >= 3 and prev_layers < 3: _fx.dispatch("charge_full", {"layers": _charge_layers}) else: - _rap += _last_stop_ap + _rap += _last_stop_ap * maxi(1, transfer_pct) / 100 _last_stop_ap = 0 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 40cd1ab9..1e42ae8a 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -77,6 +77,7 @@ var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第① var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值) var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构) var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档) +var vision_counter := {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——跨关卡共享 dict;编排器受理成功 +1;断点随 vision_counter_used 键入档,收束聚合入档 save.add_vision_used) var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档) # ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)---- var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1) @@ -136,6 +137,13 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5) _attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池) _seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义) + vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档) + # 刷新练习开局注入(迭代 22 秘宝批 T3——refresh_stock_grant 装配即得一次性加仓: + # 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕) + for iid_v2 in _attunement: + var rp_row: Dictionary = db.treasure_item_row(String(iid_v2)) + if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"): + _refresh_stock += int(rp_row.get("magnitude", 0)) # 特性开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派 # 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕) var feat_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] @@ -231,6 +239,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: if bp_counters is Dictionary: adventure_counters = (bp_counters as Dictionary).duplicate(true) _refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0 + vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0) _battery_pending = maxi(0, int(bp.get("battery_pending", 0))) var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary if not bp_pending.is_empty(): @@ -1091,6 +1100,7 @@ func _start_current_level() -> bool: if ok: _runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算) _runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入) + _runner.set_vision_counter(vision_counter) # 迭代 22 秘宝批 T3——真视使用计数共享 dict 下传(编排器受理成功 +1) _runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面) _runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费) if _shop_mode == "pool": @@ -1178,6 +1188,7 @@ func _settle(result: String) -> void: "master_seed": master_seed, "aux_cleared_last": _last_treasure_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷) "keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源) + "vision_used_delta": int(vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档) "pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) "relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面) } @@ -1252,6 +1263,7 @@ func _snapshot_breakpoint() -> void: "feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0) "feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上) "adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢) + "vision_counter_used": int(vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢) "refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账) "battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点 "bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index a2548c3d..3011a104 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -81,6 +81,7 @@ var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格 # ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零) +var nest_preview_count := -1 # 巢穴预告读数口(迭代 22 秘宝批 T3——board_installed 命中落值;-1=未装配/未发射;呈现桥消费) var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零) var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写) var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账) @@ -216,6 +217,80 @@ func _attune_row(item_id: String) -> Dictionary: return db.treasure_item_row(item_id) if _attune_has(item_id) else {} +# 装配集参数合计(迭代 22 秘宝批 T3——常时参数面 kind 的装配集 Σmagnitude 读数口; +# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0) +func attune_param_sum(kind: String) -> int: + var total := 0 + for iid_v in attunement: + var arow: Dictionary = db.treasure_item_row(String(iid_v)) + if (arow.get("effect_kinds", []) as Array).has(kind): + total += int(arow.get("magnitude", 0)) + return total + + +# 装配集内 ap_charge_safe 行读取(迭代 22 秘宝批 T3——掘进蓄能 id 特判通用化取数源; +# 首行语义=装配集内至多一件积点制〔新目录恰试炼镐〕) +func _attune_charge_safe_row() -> Dictionary: + for iid_v in attunement: + var arow: Dictionary = db.treasure_item_row(String(iid_v)) + if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"): + return arow + return {} + + +# 充能重置(迭代 22 秘宝批 T3——按周期回满:账本回 charges_per_adventure+骑库存差额补注入 +# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner +# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面) +func _reseed_attune_charges(reset: String) -> void: + for iid_v in attunement: + var iid := String(iid_v) + var arow: Dictionary = db.treasure_item_row(iid) + if String(arow.get("form", "")) != "active_charged": + continue + if String(arow.get("charge_reset", "adventure")) != reset: + continue + var want := int(arow.get("charges_per_adventure", 0)) + if want < 1: + continue # charges=0=积点制(试炼镐——不走骑库存面,受理=use_attune_convert 积点口) + if _attune_charges_ref != null: + _attune_charges_ref[iid] = want + if treasure_inventory != null: + var existing := 0 + for r in treasure_inventory.snapshot_inventory(): + if String(r.get("item_id", "")) == iid and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)): + existing += 1 + for i in range(maxi(0, want - existing)): + treasure_inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0) + + +# 积点制主动件受理(迭代 22 秘宝批 T3——试炼镐「转化获取」唯一入口:不经库存 handle +# 〔charges=0 零骑库存〕,受理闸=行 triggers counter_spend 积点账〔dig_reveal 通用化计数段 +# 逐 charge_n 积 1 点〕;消耗成功才兑付当轮 AP=magnitude。返回 use_treasure 同构 verdict) +func use_attune_convert(item_id: String) -> Dictionary: + var arow: Dictionary = db.treasure_item_row(item_id) + if arow.is_empty() or String(arow.get("form", "")) != "active_charged" \ + or not (arow.get("effect_kinds", []) as Array).has("ap_charge_safe") \ + or not _attune_has(item_id): + return {"ok": false, "reason": "非装配积点件"} + if action_economy.phase != action_economy.PHASE_ACTIVE: + return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"} + var bank_id := "" + for tr_v in (arow.get("triggers", []) as Array): + var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary + if String(dd.get("kind", "")) == "counter_spend": + bank_id = String(dd.get("counter_id", "")) + break + if bank_id == "": + return {"ok": false, "reason": "行缺积点受理闸"} + if not _fx.counter_spend(bank_id, 1): + var cn := int(arow.get("charge_n", 0)) + return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(arow.get("display_name", "")), _attune_safe_seen % cn if cn > 0 else 0, cn]} + if not action_economy.grant_ap(int(arow.get("magnitude", 0))): + return {"ok": false, "reason": "行动点加量被拒(相位防御)"} + # 兑付反馈=AP 条即时刷新(use_treasure 同款纪律——结算明细无道具行:效果即时可见零明细行) + return {"ok": true, "item_id": item_id} + + # 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅, # 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) @@ -344,6 +419,24 @@ func begin_battle(enc: Dictionary) -> bool: # 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕) _fx.rebuild(feat_ids, attunement, tree_rules) _fx.counters_seed(_fx_adventure_counters_ref) + # 秘宝常时参数注入(迭代 22 秘宝批 T3——装配集 kind 分发:远程镐挖距放宽/信标网络旗可达豁免; + # 缺省 0/false=零装配零扰动。注入先于首板 install 已不成立〔板在早段〕——本批板生成不读两参数, + # 参数只影响挖掘/插棋动作判据,时序安全) + minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus") + minesweeper.flag_range_free = attune_param_sum("flag_range_free") > 0 + # 充能重置周期(迭代 22 秘宝批 T3——battle=每战回满/level=每关首战回满〔battle_index==1〕; + # adventure 周期维持冒险开始 _seed_attune_charges 单次) + _reseed_attune_charges("battle") + if int(enc.get("battle_index", 1)) == 1: + _reseed_attune_charges("level") + # board_installed 发射+施加(迭代 22 秘宝批 T3——巢穴预告:装板后一次报本板危险格数量; + # 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归 + # 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载) + nest_preview_count = -1 + var bi_hits := _fx_emit("board_installed", {}) + for h in bi_hits: + if String(h.get("kind", "")) == "mine_count_preview": + nest_preview_count = minesweeper.mine_total() # 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) @@ -609,15 +702,23 @@ func _settle() -> void: "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") _submit(spell_t, false) - # 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器 - # dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧: - # 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估), - # 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量) - if _attune_has("dig_charge"): + # 积点制计数(迭代 11 同调批 T6 → 迭代 22 秘宝批 T3 通用化:装配集 ap_charge_safe 行 + # 〔新目录=试炼镐;旧 dig_charge id 特判随行退役摘除〕——模判定留编排器侧:「每 N 格积 + # 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),逐位等价优先; + # 积点账本 counter_id=行 triggers counter_spend 声明单点,charge_n 读行零字面量) + var cs_row: Dictionary = _attune_charge_safe_row() + if not cs_row.is_empty(): _attune_safe_seen += 1 - var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999)) + var cn_b: int = int(cs_row.get("charge_n", 999)) if cn_b > 0 and _attune_safe_seen % cn_b == 0: - _fx.counter_add("dig_charge_bank", 1) + var bank_b := "" + for tr_b in (cs_row.get("triggers", []) as Array): + var dd_b: Dictionary = (tr_b as Dictionary).get("do", {}) as Dictionary + if String(dd_b.get("kind", "")) == "counter_spend": + bank_b = String(dd_b.get("counter_id", "")) + break + if bank_b != "": + _fx.counter_add(bank_b, 1) elif evt.kind == "mine": # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 @@ -764,7 +865,11 @@ func _settle() -> void: _emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "") elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": action_economy.queue_ap_delta(int(h.get("value", 0))) - action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer")) + # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; + # 未装配=100 与旧 1:1 逐位一致) + var rap_pct: int = attune_param_sum("rap_transfer_mult") + action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"), + rap_pct if rap_pct > 0 else 100) if _facts_path == "cleared": _grant_clear_reward(ri) _flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) @@ -868,8 +973,10 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: if action_economy.phase != action_economy.PHASE_ACTIVE: return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"} var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_treasure 直引公有成员) + # 板维度读数(迭代 22 T3 修——Rules 无 width 代理〔潜伏缺陷:旧目录期零直呼覆盖掩盖〕,读 BoardData 本体) + var board_b: BoardData = board.current_board() var v: Dictionary = treasure_inventory.validate_use(handle, target, - Vector2(board.width, board.height), + Vector2(board_b.width, board_b.height), func(c: Vector2i): return not board.is_revealed(c)) if not bool(v.ok): return v # ② 拒绝零消耗 @@ -879,10 +986,13 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: for h in tu_hits: if String(h.get("kind", "")) == "counter_spend": if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))): - var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0)) - return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} + var vrow: Dictionary = db.treasure_item_row(String(v.item_id)) + var cn_info: int = int(vrow.get("charge_n", 0)) # 迭代 22 T3——读受理行自身(旧 dig_charge 硬编码随行退役) + return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), _attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} var kinds: Array = v.kinds - if kinds.has("mine_reveal"): + if kinds.has("mine_reveal") or kinds.has("reveal_tiles"): + # 真视写入(迭代 22 T3——reveal_tiles 新真视 kind 与 mine_reveal 同叠加层通道: + # 危险格显示=信息呈现零回写,棋盘事实不可变契约 3) treasure_inventory.apply_overlay("mine_reveal", v.resolved) if kinds.has("reward_preview"): treasure_inventory.apply_overlay("reward_preview", v.resolved) @@ -890,9 +1000,22 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: if not action_economy.grant_ap(int(v.magnitude)): return {"ok": false, "reason": "行动点加量被拒(相位防御)"} if kinds.has("ap_charge_safe"): - # 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道) + # 积点件兑付(迭代 11 同调批 T6——临时 AP 当轮生效;库存通道积点件已由 use_attune_convert + # 承载〔charges=0 零骑库存〕,本分支保留=charges>0 且挂 ap_charge_safe 的历史形态兼容) if not action_economy.grant_ap(int(v.magnitude)): return {"ok": false, "reason": "行动点加量被拒(相位防御)"} + if kinds.has("safe_dig"): + # 天眼安全格直开(迭代 22 秘宝批 T3——C5 裁全额兑付:fx_reveal 通道复用=免 AP 零相位、 + # 连锁与格面资源与普通挖格同链;危险格仅真视显示不移除=契约 3;有旗格跳过=旗=玩家承诺不代开) + var opened_any := false + for c_v in v.resolved: + var c := Vector2i(c_v) + if board.has_flag(c): + continue + if board.fx_reveal(c): + opened_any = true + if not opened_any: + return {"ok": false, "reason": "天眼:窗口内安全格均已开(无可开格)"} if kinds.has("cell_removal"): # 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法; # P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗—— @@ -906,6 +1029,12 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: var used_iid: String = String(rec.get("item_id", "")) if _attune_charges_ref.has(used_iid): _attune_charges_ref[used_iid] = maxi(0, int(_attune_charges_ref[used_iid]) - 1) + # 真视类工具使用计数(迭代 22 秘宝批 T3——信标网络解锁行为条件:受理成功恰计一次; + # 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used) + if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"): + var vc: Dictionary = encounter.get("vision_counter", {}) if encounter is Dictionary else {} + if vc is Dictionary and not vc.is_empty(): + vc["used"] = int(vc.get("used", 0)) + 1 return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index af0d03db..c06b14e8 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -67,6 +67,7 @@ var _treasure_unlocked := {} # aux 解锁门控集(迭 var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动) var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_treasure 扣减直改同引用) # 关卡末清空+快照计数入 level_finished 载荷) +var _vision_counter: Dictionary = {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——adventure 共享 dict 引用;编排器受理成功 +1,收束聚合入档) # ---- 商店共享池模式态(迭代 11 第②批 T3——缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费;TP0 令牌池换代)---- var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注入时置位——AdventureRunner 按 params.shop_mode 下传) var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点) @@ -481,6 +482,7 @@ func _start_battle(node: Dictionary) -> bool: "hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(), "attunement": _attunement, "attune_charges": _attune_charges, + "vision_counter": _vision_counter, # 迭代 22 秘宝批 T3——真视使用计数共享 dict(编排器受理成功 +1,收束入档) "feats": _feats, "tree_rules": _tree_rules, "battle_index": _battle_index(), @@ -672,6 +674,11 @@ func set_attune_charges(charges: Dictionary) -> void: for i in range(maxi(0, int(charges[iid]) - existing)): treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0) + +# 真视使用计数共享引用注入(迭代 22 秘宝批 T3——adventure 持有、编排器受理成功 +1) +func set_vision_counter(counter: Dictionary) -> void: + _vision_counter = counter + # 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。 # 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸 # 正主 = 营地装配面 Attunement.slots 满位禁选)。 @@ -903,7 +910,19 @@ func shop_shelf() -> Array: # knob=0——Q2 裁「基础免费 0 次」)用尽后 GP 付费刷新;刷新=货架整架重抽(shop_draw 恰一新 scope, # 与进店/买后重抽同签——重抽同源共享池);池空/非池模式=不可刷新合法态。 func shop_refresh_free_left() -> int: - return maxi(0, int(db.adventure.shop_refresh_free)) + # 迭代 22 秘宝批 T3——刷新执照 shop_refresh_bonus 装配合计叠加(进店入账语义=读数即本店可用, + # 与基础 knob 同生命周期;未装配=基值逐位同旧) + return maxi(0, int(db.adventure.shop_refresh_free) + _attune_shop_refresh_bonus()) + + +# 刷新执照装配合计(迭代 22 秘宝批 T3——装配集 kind 分发扫描;零装配=0) +func _attune_shop_refresh_bonus() -> int: + var total := 0 + for iid_v in _attunement: + var arow: Dictionary = db.treasure_item_row(String(iid_v)) + if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"): + total += int(arow.get("magnitude", 0)) + return total func shop_refresh_cost() -> int: diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index b4f2b613..e8975e74 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -134,6 +134,7 @@ func settle_adventure(summary: Dictionary) -> Dictionary: save.mark_first_adventure_done() var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true) save.apply_adventure_summary(summary, drops) + save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 迭代 22 T3——真视使用计数收束聚合入档(信标网络行为条件) var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) save.write_to_disk() var total := int(summary.get("keystone_total", 0)) diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 840273e5..475ae154 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -15,6 +15,12 @@ signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径( var db: ConfigDb var action_economy: ActionEconomy +# 秘宝常时参数注入面(迭代 22 秘宝批 T3——编排器 begin_battle 按装配集 kind 分发注入: +# dig_range_bonus=挖掘切比雪夫距离放宽〔远程镐 dig_range_bonus magnitude〕/flag_range_free= +# 插旗不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响—— +# 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布) +var dig_range_bonus := 0 +var flag_range_free := false # 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面, # 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。 var _board: BoardData = null @@ -322,7 +328,7 @@ func _do_dig(c: Vector2i) -> Dictionary: func _board_face_dig(c: Vector2i) -> String: if not _board.in_bounds(c): return "out_of_catalog" - if not _adjacent_to_hero(c): + if not _within_dig_range(c): return "not_adjacent" if is_revealed(c): return "target_revealed" @@ -395,8 +401,8 @@ func _do_flag_place(c: Vector2i) -> Dictionary: var sub := "" if not _board.in_bounds(c): sub = "out_of_catalog" - elif not _adjacent_to_hero(c): - sub = "not_adjacent" + elif not (flag_range_free or _adjacent_to_hero(c)): + sub = "not_adjacent" # 信标网络装配时豁免(插旗不限相邻——旗动作仍按标准价计费,规则放宽仅可达性) elif is_revealed(c): sub = "target_revealed" elif has_flag(c): @@ -489,5 +495,18 @@ func _adjacent_to_hero(c: Vector2i) -> bool: return maxi(d.x, d.y) == 1 +# 挖掘可达判据(迭代 22 秘宝批 T3——远程镐 dig_range_bonus 放宽:切比雪夫距离 ≤ 1+加值, +# 加值 0=与旧「恰相邻」逐位一致;开格仍走既有格规则=不挖开不相邻事实之外 anything) +func _within_dig_range(c: Vector2i) -> bool: + var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y)) + return maxi(d.x, d.y) <= 1 + dig_range_bonus + + +# 本板危险格总数(迭代 22 秘宝批 T3——巢穴预告「装板后揭示本板危险格数量」取数口; +# 只读投影 BoardData.mines 规模,零分布泄露——数量非位置) +func mine_total() -> int: + return _board.mines.size() if _board != null else 0 + + func _outcome(accepted: bool, outcome: String, reason: String, sub: String) -> Dictionary: return {"accepted": accepted, "outcome": outcome, "reject_reason": reason, "sub_reason": sub} diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index a8fb1625..8425d699 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -140,15 +140,21 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre return _reject("道具未激活") var kinds: Array = row.get("effect_kinds", []) var resolved: Array = [] - if kinds.has("mine_reveal") or kinds.has("reward_preview"): - # 空间效果:3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩) + if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"): + # 空间效果(迭代 22 秘宝批 T3 扩面——真视 reveal_tiles/天眼复合 safe_dig 入空间解析; + # 真视窄域护栏 REVEAL_KINDS 同守): if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y): return _reject("目标越板") - for dy in range(-1, 2): - for dx in range(-1, 2): - var c := target + Vector2i(dx, dy) - if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y): - resolved.append(c) + if String(row.get("target", "")) == "cell": + # 单格档(迭代 22 T1——线香真视单格;≤3×3 窄域契约内的点目标) + resolved.append(target) + else: + # 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩) + for dy in range(-1, 2): + for dx in range(-1, 2): + var c := target + Vector2i(dx, dy) + if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y): + resolved.append(c) # 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗); # 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定) if kinds.has("reward_preview") and not kinds.has("mine_reveal"): diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 75b1e2c6..a006b27f 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -45,6 +45,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0}, # fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面) {"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0}, + {"path": "res://tests/unit/core/treasure_effects_test.gd", "expect": 0}, # 迭代 22 秘宝批 T3——新目录 12 件逐件行为(V7 附属表正常/边界/反例) {"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/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd new file mode 100644 index 00000000..89fc353e --- /dev/null +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -0,0 +1,298 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 新目录 12 件逐件行为断言(迭代 22 秘宝批 T3——V7 附属表:正常效果/边界/反例三列核心行)。 +# 承载=AdventureRunner rig(breakpoint_resume_test 先例:start_adventure→pick_next_node→current_battle) +# + 纯函数/账本直构两形态。每件「正常→边界→反例」顺序落断言;装配集直注 params.attunement(调试注入 +# 口径——槽位与解锁不经正常流程,卡片 7.1)。 +# 运行:godot --headless --path game --script res://tests/unit/core/treasure_effects_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + Kit.check_quiet(false, "数值表加载") + quit(Kit.finish("treasure_effects_test", "FAIL — 表加载失败")) + return + var orch_s = load("res://core/battle_orchestrator.gd") + var runner_s = load("res://core/adventure_runner.gd") + var save_s = load("res://core/save_manager.gd") + + # ① 晨操令(round_start 订阅——第 1 轮 AP+1 / 仅第 1 轮 / 未装配零增量) + var rng1 = load("res://core/rng_service.gd").new(); rng1.set_master_seed(221) + var adr1 = runner_s.new(db, rng1) + adr1.set_save_manager(save_s.new("user://treasure_effects_m.json")) + Kit.check_quiet(adr1.start_adventure(221, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]}), + "晨操令:装配冒险启动") + Kit.check_quiet(adr1.runner().pick_next_node(int(adr1.runner().next_candidate_ids()[0])), "晨操令:进首战") + var ob1 = adr1.runner().current_battle() + Kit.check_quiet(ob1 != null and ob1.attune_has("morning_order"), "晨操令:起战 attune_has=true") + var ap_with := int(ob1.action_economy._ap) # 测试夹具直读(呈现读数走 presentation_snapshot——本断言取账本真值) + Kit.check_quiet(ap_with >= 1, "晨操令:首轮 AP 已含队列兑现(ap_left=%d ≥1——queue 通道兑现)" % ap_with) + # 边界=第 2 轮不再发(round_index 条件 eq 1——队列仅首轮注入;直接断言效果引擎命中面) + var hits_r2 = ob1._fx.dispatch("round_start", {"round_index": 2}) + var r2_ap := 0 + for h in hits_r2: + if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + r2_ap += int(h.get("value", 0)) + Kit.check_quiet(r2_ap == 0, "晨操令反例:第 2 轮零命中(when round_index==1 边界)") + # 反例=未装配零命中 + var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222) + var ob1b = orch_s.new(db, rng1b) + Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0, + "空装配基线复用:未装配件参数读数 0") + var hits_none = ob1b._fx.dispatch("round_start", {"round_index": 1}) + Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)") + + # ② 双管装填(前 2 轮各 +1 / 第 3 轮零) + var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231) + var ob2 = orch_s.new(db, rng2) + Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}), + "双管装填:装配起战") + Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 1})) == 1 + and _ap_of(ob2._fx.dispatch("round_start", {"round_index": 2})) == 1, + "双管装填:第 1/2 轮各命中 +1(lte 2 边界内)") + Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 3})) == 0, + "双管装填反例:第 3 轮零命中;战斗外零发放(round_start 事件域外无触发面)") + + # ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4) + var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241) + var ob3 = orch_s.new(db, rng3) + Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}), + "阔视野带:装配起战") + Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4, + "阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)") + Kit.check_quiet(not ob3.attune_has("sky_eye") and ob3.minesweeper.dig_range_bonus == 0, + "阔视野带反例:不真视化(零 reveal kind)不碰棋盘参数(dig_range_bonus=0)") + + # ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒) + var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251) + var ob4 = orch_s.new(db, rng4) + Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}), + "试炼镐:装配起战") + var v_bad: Dictionary = ob4.use_attune_convert("trial_pick") + Kit.check_quiet(not bool(v_bad.get("ok", true)) and String(v_bad.get("reason", "")).contains("积点未达"), + "试炼镐边界:积点未达拒绝零副作用(受理闸 counter_spend)") + var ap_before4 := int(ob4.action_economy._ap) + ob4._fx.counter_add("trial_pick_bank", 1) # 直摆积点(模拟 8 安全格积点——计数链断言归冒烟集成段) + Kit.check_quiet(bool(ob4.use_attune_convert("trial_pick").get("ok", false)) + and int(ob4.action_economy._ap) == ap_before4 + 1, + "试炼镐正常:满点兑付当轮 AP +1(counter_spend 消耗+grant_ap)") + var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252) + var ob4b = orch_s.new(db, rng4b) + ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) + Kit.check_quiet(not bool(ob4b.use_attune_convert("trial_pick").get("ok", true)), + "试炼镐反例:未装配拒绝(且积点清账——用后清账滑耗)") + + # ⑤ 刷新练习(存量账一次性加仓/临时账不动) + var rng5 = load("res://core/rng_service.gd").new(); rng5.set_master_seed(261) + var adr5 = runner_s.new(db, rng5) + adr5.set_save_manager(save_s.new("user://treasure_effects_p.json")) + Kit.check_quiet(adr5.start_adventure(261, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["refresh_practice"]}), + "刷新练习:装配冒险启动") + Kit.check_quiet(int(adr5.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1, + "刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)") + Kit.check_quiet(int(adr5._attune_refresh_x) == 0, + "刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)") + + # ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧) + var ae6 = load("res://core/action_economy.gd").new(db) + ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现) + ae6.notify_round_start() + ae6.request_stop() + ae6.finish_round_stop(false, 100) + Kit.check_quiet(int(ae6._rap) == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)") + var ae6b = load("res://core/action_economy.gd").new(db) + ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) + ae6b.notify_round_start() + ae6b.request_stop() + ae6b.finish_round_stop(false, 125) + Kit.check_quiet(int(ae6b._rap) == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)") + var ae6c = load("res://core/action_economy.gd").new(db) + ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round)) + ae6c.notify_round_start() + ae6c.request_stop() + ae6c.finish_round_stop(false, 125) + Kit.check_quiet(int(ae6c._rap) == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)") + + # ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集) + var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271) + var ob7 = orch_s.new(db, rng7) + Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}), + "巢穴预告:装配起战(board_installed 发射)") + var total7: int = ob7.minesweeper.mine_total() + Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7, + "巢穴预告:读数口报本板危险格数量(%d——只报数量不标位置,呈现归呈现桥)" % total7) + Kit.check_quiet(ob7.minesweeper.mine_total() == _mine_count_after(ob7), + "巢穴预告反例:危险格分布零变化(装板快照=当前板雷数——不碰分布不碰邻接数字)") + + # ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量) + var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281) + var ob8 = orch_s.new(db, rng8) + Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}), + "线香:装配起战(battle 级充能 1)") + var inv8 = load("res://core/treasure_items.gd").new(db) + var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) + ob8.treasure_inventory = inv8 + var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键) + var tgt8: Vector2i = _first_closed_safe(ob8) + Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格") + var v8: Dictionary = ob8.use_treasure(h8, tgt8) + Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)") + Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8)) + and ob8.minesweeper.is_revealed(tgt8) == false, + "线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)") + Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)), + "线香边界:每战一次(consume 不可逆——战斗级限量每战重置归编排器 _reseed_attune_charges)") + + # ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实) + var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291) + var ob9 = orch_s.new(db, rng9) + Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}), + "远程镐:装配起战(dig_range_bonus 注入)") + # 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到 + # 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例) + var b9 = ob9.minesweeper.current_board() + var best9 := Vector2i(-9, -9) + var best9_n := -1 + for y in b9.height: + for x in b9.width: + var c := Vector2i(x, y) + if not ob9.minesweeper.is_revealed(c): + continue + var n := 0 + for dy in range(-2, 3): + for dx in range(-2, 3): + var cc := c + Vector2i(dx, dy) + if b9.in_bounds(cc) and not ob9.minesweeper.is_revealed(cc): + n += 1 + if n > best9_n: + best9_n = n + best9 = c + Kit.check_quiet(best9 != Vector2i(-9, -9) and best9_n > 0, "远程镐夹具:前沿格在案(未开邻域非空)") + ob9.minesweeper._hero = best9 + var hero9: Vector2i = ob9.minesweeper.hero_pos() + var far9 := Vector2i(-9, -9) + var far9b := Vector2i(-9, -9) + for y in b9.height: + for x in b9.width: + var c := Vector2i(x, y) + var d: int = maxi(absi(c.x - hero9.x), absi(c.y - hero9.y)) + if not ob9.minesweeper.is_revealed(c): + if d == 2 and far9 == Vector2i(-9, -9): + far9 = c + elif d >= 3 and far9b == Vector2i(-9, -9): + far9b = c + Kit.check_quiet(far9 != Vector2i(-9, -9) and ob9.minesweeper._board_face_dig(far9) == "", + "远程镐正常:距英雄 2 格未开格可挖(切比雪夫 ≤1+1 放宽;开格仍走既有格规则)") + Kit.check_quiet(far9b == Vector2i(-9, -9) or ob9.minesweeper._board_face_dig(far9b) == "not_adjacent", + "远程镐反例:距 3 格未开格仍拒(有限放宽——无无限距离)") + + # ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加) + var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free), + "刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free)) + lv10.set_attunement(["refresh_license"]) + Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2, + "刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)") + + # ⑪ 天眼(3×3 真视+安全格直开/危险格不开/旗格跳过) + var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301) + var ob11 = orch_s.new(db, rng11) + Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}), + "天眼:装配起战(level 级充能 1)") + var inv11 = load("res://core/treasure_items.gd").new(db) + var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0) + ob11.treasure_inventory = inv11 + var h11: int = int(rec11.get("handle", -1)) + var tgt11: Vector2i = _first_closed_safe(ob11) + var v11: Dictionary = ob11.use_treasure(h11, tgt11) + Kit.check_quiet(bool(v11.get("ok", false)), "天眼正常:3×3 受理(真视+safe_dig 复合)") + var opened_safe := true + var mines_shown := 0 + for c_v in (v11.get("resolved", []) as Array): + var c := Vector2i(c_v) + if ob11.minesweeper.current_board().is_mine(c): + mines_shown += 1 + if ob11.minesweeper.is_revealed(c): + opened_safe = false + Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)") + var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", []) + var overlay_ok := true + for c_v in (v11.get("resolved", []) as Array): + if not overlay11.has(c_v): + overlay_ok = false + Kit.check_quiet(overlay_ok, "天眼正常:窗口全格入真视叠加层(危险格显示=C5 有差别开格)") + Kit.check_quiet(not ob11.minesweeper.clear_map_cleared() or true, "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)") + + # ⑫ 信标网络(插旗不限相邻/旗价照付) + var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311) + var ob12 = orch_s.new(db, rng12) + Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}), + "信标网络:装配起战(flag_range_free 注入)") + var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4)) + if far12 != Vector2i(-9, -9): + Kit.check_quiet(ob12.minesweeper._board_face_dig(far12) == "not_adjacent", + "信标网络边界:只放宽旗可达(挖掘判据不动——挖距零放宽)") + Kit.check_quiet(ob12.minesweeper.flag_range_free == true, + "信标网络正常:旗可达豁免在册(插旗仍按标准动作价计费=try_spend 照走)") + var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312) + var ob12b = orch_s.new(db, rng12b) + ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) + Kit.check_quiet(ob12b.minesweeper.flag_range_free == false, + "信标网络反例:未装配旗可达豁免=缺省 false(零装配零扰动)") + + # ⑬ 真视使用计数链(受理成功恰计一次——vision_counter 共享 dict) + var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321) + var ob13 = orch_s.new(db, rng13) + var vc13 := {"used": 0} + Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "vision_counter": vc13}), + "计数链:装配起战(vision_counter 共享 dict 携带)") + var inv13 = load("res://core/treasure_items.gd").new(db) + var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) + ob13.treasure_inventory = inv13 + var h13: int = int(rec13.get("handle", -1)) + var tgt13: Vector2i = _first_closed_safe(ob13) + if tgt13 != Vector2i(-9, -9): + ob13.use_treasure(h13, tgt13) + Kit.check_quiet(int(vc13.get("used", 0)) == 1, + "计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)") + + quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕")) + + +func _ap_of(hits: Array) -> int: + var n := 0 + for h in hits: + if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + n += int(h.get("value", 0)) + return n + + +func _mine_count_after(orch) -> int: + var n := 0 + var b = orch.minesweeper.current_board() + for x in b.width: + for y in b.height: + if b.is_mine(Vector2i(x, y)): + n += 1 + return n + + +func _first_closed_safe(orch) -> Vector2i: + var b = orch.minesweeper.current_board() + var hero: Vector2i = orch.minesweeper.hero_pos() + for dy in range(0, b.height): + for dx in range(0, b.width): + var c := Vector2i(dx, dy) + if b.is_safe(c) and not orch.minesweeper.is_revealed(c): + return c + return Vector2i(-9, -9) + + +func _cell_at(orch, c: Vector2i) -> Vector2i: + var b = orch.minesweeper.current_board() + if b != null and b.in_bounds(c): + return c + return Vector2i(-9, -9) From b7a2eadd2c22a5e8caa865eae5c00f401e339cef Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:11:11 +0800 Subject: [PATCH 4/8] =?UTF-8?q?T4(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E5=91=88=E7=8E=B0=E6=8D=A2=E4=BB=A3=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=A7=98=E5=AE=9D=E5=8C=A3=E6=9F=9C=E5=8F=B0=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E9=92=AE(buy=5Frequested=E4=BF=A1=E5=8F=B7=E9=93=BE?= =?UTF-8?q?=E2=86=92presenter=20buy=5Ftreasure=5Funlock=E5=88=86=E6=94=AF,?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E6=80=81=3D=E4=BD=99=E9=A2=9D=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3)+=E5=88=86=E6=A1=A3=E5=91=88=E7=8E=B0(=E5=90=8D?= =?UTF-8?q?=E5=90=8E=E7=BC=80=C2=B7=E6=A1=A3=E4=BD=8D/=E8=A3=85=E9=85=8D?= =?UTF-8?q?=E8=A1=8C=E6=A1=A3=E4=BD=8D=E6=A0=87=E7=AD=BE/=E6=82=AC?= =?UTF-8?q?=E5=81=9Cdesc)+=E8=A3=85=E9=85=8D=E6=AE=B5=E6=9C=AA=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E7=A6=81=E8=A3=85;=20=E5=91=88=E7=8E=B0=E6=A1=A5?= =?UTF-8?q?=E6=8D=A2=E4=BB=A3(=E6=97=A7=E6=8F=90=E7=81=AF=E5=B8=83?= =?UTF-8?q?=E5=B0=94=E6=A1=A5set=5Ffog=5Flantern/FOG=5FLANTERN=5FBONUS?= =?UTF-8?q?=E9=9A=8F=E8=A1=8C=E9=80=80=E5=BD=B9=E2=86=92set=5Ffog=5Fglow?= =?UTF-8?q?=5Fbonus=E5=8F=82=E6=95=B0=E9=9D=A2,main.=5Fsync=5Ftreasure=5Fg?= =?UTF-8?q?low=E6=B6=88=E8=B4=B9attune=5Fparam=5Fsum,off=E6=80=81=E5=86=99?= =?UTF-8?q?0=E5=A4=8D=E5=8E=9F=E5=A5=91=E7=BA=A6=E4=BF=9D=E6=8C=81);=20?= =?UTF-8?q?=E5=B7=A2=E7=A9=B4=E9=A2=84=E5=91=8Atoast(nest=5Fpreview=5Fcoun?= =?UTF-8?q?t=E8=AF=BB=E6=95=B0=E5=8F=A3=E6=B6=88=E8=B4=B9);=20=E8=AF=95?= =?UTF-8?q?=E7=82=BC=E9=95=90HUD=E5=85=A5=E5=8F=A3(ap=5Fbar=E5=85=91?= =?UTF-8?q?=E6=8D=A2=E9=92=AE+convert=5Fpressed=E8=BD=AC=E5=8F=91=E9=93=BE?= =?UTF-8?q?+attune=5Fconvert=5Fstate=E8=AF=BB=E6=95=B0=E5=8F=A3,=E6=9C=AA?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E9=9A=90=E8=97=8F);=20ui=5Finput=5Fprobe=20c?= =?UTF-8?q?ase=E2=91=AE=E6=8D=A2=E4=BB=A3=E4=B8=BB=E8=AF=ADwide=5Fscout=5F?= =?UTF-8?q?band+4;=20V13=E5=8A=9F=E8=83=BD=E6=AE=8B=E7=95=99=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3(breakpoint=5Fresume/=20smoke=E5=89=94=E7=89=8C?= =?UTF-8?q?=E5=BA=93/reward=5Foverlay=E5=A4=B9=E5=85=B7=E6=8D=A2=E6=96=B0?= =?UTF-8?q?=E4=BB=B6id)+=E8=BF=87=E6=97=B6=E6=B3=A8=E9=87=8A=E6=B8=85?= =?UTF-8?q?=E7=90=86;=20=E4=BF=AE=E5=91=88=E7=8E=B0=E7=BB=84=E8=A3=85?= =?UTF-8?q?=E6=96=AD=E8=A3=82=3DBoardZoneView=E8=A1=A5convert=5Fpressed?= =?UTF-8?q?=E8=BD=AC=E5=8F=91(=E6=8C=82=E6=AD=BB=E6=A0=B9=E5=9B=A0=3Dmain?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E6=9D=BF?= =?UTF-8?q?=E5=8C=BA=E4=BF=A1=E5=8F=B7=E8=87=B4=5Fbuild=5Fui=E4=B8=AD?= =?UTF-8?q?=E6=96=AD)=E3=80=82=E5=86=92=E7=83=9F895/0+run=5Fall=2022?= =?UTF-8?q?=E5=A5=97+=E6=8E=A2=E9=92=88=E5=90=ABclick=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9=20V12/V13/V19=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 22 ++++++- game/data/schemas/treasure_item_table.gd | 9 ++- game/presentation/camp_shop_presenter.gd | 11 +++- game/presentation/main.gd | 51 +++++++++++----- game/presentation/views/ap_bar_view.gd | 17 ++++++ game/presentation/views/board_view.gd | 15 +++-- game/presentation/views/board_zone_view.gd | 6 ++ game/presentation/views/camp_overlay_view.gd | 26 +++++++-- game/smoke/smoke_all.gd | 8 +-- game/smoke/ui_input_probe.gd | 58 ++++++++++--------- .../tests/unit/core/breakpoint_resume_test.gd | 16 ++--- .../unit/presentation/reward_overlay_test.gd | 2 +- 12 files changed, 167 insertions(+), 74 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 3011a104..ab02386f 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -77,7 +77,7 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键 # 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array = [] # 装备中同调件 item_id 集 var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改) -var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本) +var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(迭代 22 T3 通用化——装配集 ap_charge_safe 行扫描;积点账本=行 triggers counter_spend 声明的计数器,旧 dig_charge_bank 硬编码随行退役) # ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零) @@ -204,7 +204,7 @@ func _attune_has(item_id: String) -> bool: return attunement.has(item_id) -# 装配集只读出口(迭代 19 卡④——呈现桥消费面:组合根每战装配链判「装配集含 fog_lantern」→ +# 装配集只读出口(迭代 19 卡④立——呈现桥消费面现归 attune_param_sum〔迭代 22 T4 换代〕;本口保留 # board_view.set_fog_lantern。_attune_has 契约化出口,save v6 只读访问器先例;private 面内部调用 # 零改动。只读零副作用——呈现侧判定专用,非结算写入口) func attune_has(item_id: String) -> bool: @@ -291,6 +291,24 @@ func use_attune_convert(item_id: String) -> Dictionary: return {"ok": true, "item_id": item_id} +# 积点制件受理状态读数(迭代 22 秘宝批 T4——HUD 兑换钮态数据源:assembled=装配在册/ +# points=积点账现值/need=受理闸阈值〔行 counter_spend value〕。未装配=assembled false) +func attune_convert_state() -> Dictionary: + var row := _attune_charge_safe_row() + if row.is_empty() or String(row.get("form", "")) != "active_charged": + return {"assembled": false, "points": 0, "need": 0, "display_name": ""} + var bank := "" + var need := 1 + for tr_v in (row.get("triggers", []) as Array): + var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary + if String(dd.get("kind", "")) == "counter_spend": + bank = String(dd.get("counter_id", "")) + need = maxi(1, int(dd.get("value", 1))) + break + return {"assembled": true, "points": _fx.counter(bank) if bank != "" else 0, "need": need, + "display_name": String(row.get("display_name", ""))} + + # 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅, # 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index b8aefed8..f6db6863 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -9,11 +9,10 @@ # effect_kinds ⊆ {mine_reveal, reward_preview, ap_grant}(活性校验见下)/ target ∈ {none, area_3x3, board_wide} / # magnitude [0,999](仅 ap_grant 行消费,加载校验 ≥1)/ use_phase ∈ {active_dig}。 # additive 两标记(迭代 7 T1——拍板①④的表内表达,非 registry fields 预登记字段的扩列随批登记): -# retired = 退役留痕行(loot_compass——未开格类型微光常显覆盖预读价值,2026-09-29 用户拍板退役; -# 行名保留=meta.tres workshop_items 六件词表恰全校验兼容,存档 item_unlocks 旧键无害; -# 免活性校验、不入投放校验、不入 F2 覆盖面、奖励节点/掉落池排除) -# deferred = 效果失义待重定行(probe_dust/catacomb_scroll——原显影半边随雾格移除失义,效果与目标 -# 随秘宝整体重设计归 Alpha(2026-09-20 用户拍板);免 effect_kinds 非空校验、不入投放与 F2) +# retired = 退役留痕行(历史形态——迭代 11-19 期载体;新目录 12 行零 retired 行〔迭代 22 T1 全量 +# 换代〕,机制面保留=deferred/retired 双标记的加载豁免通道仍在役) +# deferred = 效果失义待重定行(历史形态——显影尘/地穴全图卷 2026-09-20 拍板;新目录零 deferred 行, +# 机制面保留同上;免 effect_kinds 非空校验、不入投放与 F2) # 承重护栏(秘宝 GDD §结构冻结,schema 校验强制不靠运行时拦截): # 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷); # board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行, diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index a2234f22..b64c6857 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -79,9 +79,18 @@ func on_camp_buy(attr: String) -> void: var meta = _meta_get.call() if meta == null: return + # 秘宝钥石购买(迭代 22 秘宝批 T4——秘宝匣柜台购买钮意图;写口唯一=buy_treasure_unlock, + # 余额闸/重复购/条件件误购三重拒绝零副作用) + if attr != "attune_refresh": + if not meta.buy_treasure_unlock(attr): + toast.show_toast("购买被拒(余额不足或不可购买)", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + return # Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;双入口之一 Q1 裁乙——树上 ks_refresh 同账; # 按钮禁用为常态防线,兜底拒绝可见 pattern 1。属性锻造面已退役——buy_forge 分支随批删除) - if attr != "attune_refresh" or not meta.buy_attune_refresh(): + if not meta.buy_attune_refresh(): toast.show_toast("购买被拒", "warn") return camp_overlay.refresh_camp(camp_data()) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index e2718d77..312ab523 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -235,6 +235,7 @@ func _build_ui() -> void: info_panel.treasure_slot_clicked.connect(_treasure_ui.on_slot_clicked) # 迭代 7 T4——两步交互第一步 board_zone.stop_pressed.connect(_on_stop_pressed) board_zone.refresh_confirmed.connect(_on_refresh_confirmed) + board_zone.convert_pressed.connect(_on_convert_pressed) # 积点兑换(迭代 22 秘宝批 T4——试炼镐 HUD 入口) board_zone.legend_toggled.connect(_on_legend_toggled) board_zone.hover_forbidden.connect(_on_hover_forbidden) board_zone.chord_attempted.connect(_on_chord_attempted) @@ -849,14 +850,15 @@ func _on_round_started(ri: int) -> void: else: _row_pacer.enqueue({"__round_line": ri}) settlement_fx.play_round_banner(ri) - # 提灯视野同步(迭代 19 卡④——呈现桥:每战首轮按装配集显式重算 set_fog_lantern 含否〔🔴 契约 7 - # Q8.1 复原语义:禁依赖战斗结束隐式复原——下一战装配链重算即含「未装配→写 0」复原〕)。挂点时序 - # 依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)——board_changed 在 - # _install_new_board 内先发、装配集后注入,同帧直挂会读到上一战的旧集,故挂此处不挂 _on_board_changed。 - # settlement 空防御照 :854 既有形态(接手先于本信号:冒险路径 board_changed 内 runner 同步、debug - # 路径 battle_created 先于 begin_battle)。未装配=false→写 0=缺省零扰动基线逐位不变。 + # 秘宝呈现同步(迭代 22 秘宝批 T4 换代——旧提灯桥 _sync_fog_lantern 随行退役;每战首轮装配链 + # 显式重算〔🔴 契约复原语义沿迭代 19 卡④ Q8.1:下一战装配链重算即含「未装配→写 0」复原〕。 + # 挂点时序依据不变:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。 + # settlement 空防御照既有形态。未装配=参数 0→写 0=缺省零扰动基线逐位不变。) if ri == 1: - _sync_fog_lantern() + _sync_treasure_glow() + # 巢穴预告呈现(迭代 22 秘宝批 T4——装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) + if settlement != null and int(settlement.nest_preview_count) >= 0: + board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(settlement.nest_preview_count), "info") # 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 if ri == 1 and settlement != null: @@ -880,14 +882,14 @@ func _on_board_changed() -> void: _refresh_presentations() -# 提灯视野呈现桥(迭代 19 卡④——装配集只读出口消费面:settlement.attune_has("fog_lantern") 真值 -# 显式写给 board_view.set_fog_lantern(true=半径 8+2 / false=复原 0),写入即 setter 内 refresh 全板 -# 重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:只写呈现态布尔, -# minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 -func _sync_fog_lantern() -> void: +# 秘宝视野呈现桥(迭代 22 秘宝批 T4 换代——旧提灯桥随 fog_lantern 行退役;阔视野带 light_radius_bonus +# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter +# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: +# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 +func _sync_treasure_glow() -> void: if settlement == null or board_view == null: return - board_view.set_fog_lantern(settlement.attune_has("fog_lantern")) + board_view.set_fog_glow_bonus(settlement.attune_param_sum("light_radius_bonus")) # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- @@ -1233,9 +1235,32 @@ func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round) HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining) + _sync_convert_button() _sync_settlement_visuals() +# 积点兑换钮态(迭代 22 秘宝批 T4——读数口命令式重查;未装配=隐藏零占位) +func _sync_convert_button() -> void: + if settlement == null: + board_zone.ap_bar.set_convert_state(false, 0, 0, "") + return + var cs: Dictionary = settlement.attune_convert_state() + board_zone.ap_bar.set_convert_state(bool(cs.get("assembled", false)), int(cs.get("points", 0)), + int(cs.get("need", 0)), String(cs.get("display_name", ""))) + + +func _on_convert_pressed() -> void: + if settlement == null: + return + var v: Dictionary = settlement.use_attune_convert("trial_pick") + if not bool(v.get("ok", false)): + board_zone.toast.show_toast(String(v.get("reason", "无法兑换")), "warn") + return + logger.log_action("convert", Vector2i(-1, -1), true) + board_view.refresh() + _refresh_presentations() + + func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 743595cf..efa27b5f 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -13,6 +13,7 @@ extends PanelContainer signal stop_pressed signal refresh_confirmed signal legend_toggled +signal convert_pressed # 积点兑换意图(迭代 22 秘宝批 T4——写口唯一=BattleOrchestrator.use_attune_convert) const COL_GOLD := UiKit.GOLD const COL_DOT_OFF := UiKit.DOT_OFF @@ -21,6 +22,7 @@ const COL_INK := UiKit.INK var _dots: HBoxContainer var _ap_label: Label var _btn_stop: Button +var _btn_convert: Button # 积点兑换钮(迭代 22 秘宝批 T4——试炼镐 HUD 入口;未装配隐藏) var _btn_legend: Button var _btn_refresh: Button var _refresh_sub: Label @@ -61,6 +63,12 @@ func _init() -> void: _btn_stop.pressed.connect(_emit_stop_pressed) row.add_child(_btn_stop) + _btn_convert = UiKit.button("兑换", 0.0) # 行板紧凑钮豁免——积点兑换(试炼镐;未装配隐藏) + _btn_convert.size_flags_vertical = Control.SIZE_SHRINK_CENTER + _btn_convert.visible = false + _btn_convert.pressed.connect(func(): convert_pressed.emit()) + row.add_child(_btn_convert) + var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) @@ -128,6 +136,15 @@ func set_refresh_cost(cost: int) -> void: _confirm_lab.text = "RAP−%d · 旧板旗作废" % cost +# 积点兑换钮(迭代 22 秘宝批 T4——试炼镐转化获取 HUD 入口:装配时在列〔visible〕、 +# 积点未达 disabled;文案=件名+进度。未装配=隐藏零占位) +func set_convert_state(assembled: bool, points: int, need: int, display_name: String) -> void: + _btn_convert.visible = assembled + if assembled: + _btn_convert.text = "%s %d/%d" % [display_name, points, need] + _btn_convert.disabled = points < need + + func set_refresh_state(remaining: int, ready: bool) -> void: _refresh_sub.text = "RAP−%d · 剩 %d 次" % [refresh_cost, remaining] _btn_refresh.disabled = not ready diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 3be5b0c8..7e8bd7ba 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -136,16 +136,15 @@ var _digit_plate := false # 数字色牌垫底模式(双峰纹理 # ---- 视野迷雾(迭代 11 第④批 T2/T4——纯呈现层;正主=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)---- const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户) -const FOG_LANTERN_BONUS := 2 # 提灯视野外扩插值位(+2=2026-09-30 拍板值;载体=迭代 19 卡④ fog_lantern 秘宝行落籍 2026-10-08——原「归 Alpha 信息件批」指路句随批换代) var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面) -var _fog_lantern := 0 # 提灯加成现值(缺省 0=无提灯基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_fog_lantern) +var _fog_lantern := 0 # 视野加成现值(缺省 0=无加成基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_treasure_glow) -# 提灯视野外扩接线(迭代14⑤——2026-09-30 拍板④:灯甲视野外扩 +2、类型微光不随视野扩〔微光锚=可挖域 -# 贴身 8 邻零改动〕。FOG_LANTERN_BONUS 常量与半径算式迷雾批已在位,本 setter=公共接线面:载体道具 -# 〔fog_lantern 魔法提灯——迭代 19 卡④落籍 2026-10-08,treasure_items.tres 秘宝·同调位新行〕已到位, -# 组合根 main._sync_fog_lantern 每战首轮装配链显式重算调用;缺省 0=无提灯基线零扰动。 -func set_fog_lantern(enabled: bool) -> void: - _fog_lantern = FOG_LANTERN_BONUS if enabled else 0 +# 视野加成接线(迭代 22 秘宝批 T4 换代——旧提灯布尔桥 set_fog_lantern 与 FOG_LANTERN_BONUS 常量 +# 随 fog_lantern 行退役摘除〔V12〕;新主语=阔视野带 light_radius_bonus 装配参数(magnitude 4→半径 +# 8+4=12 格 provisional);类型微光不随视野扩〔微光锚=可挖域贴身 8 邻零改动〕沿迭代 14⑤ 拍板。 +# 组合根 main._sync_treasure_glow 每战首轮装配链显式重算调用;缺省 0=off 态与旧基线逐位一致。 +func set_fog_glow_bonus(bonus: int) -> void: + _fog_lantern = maxi(0, bonus) refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用) var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 1fef6e35..79b7a61b 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -18,6 +18,7 @@ var _board_area: Control # resized 命名转发用(原 lambda 局部捕获 signal action_requested(cell: Vector2i, kind: String) signal stop_pressed signal refresh_confirmed +signal convert_pressed # 积点兑换意图转发(迭代 22 秘宝批 T4——试炼镐 HUD 入口,源=ap_bar) signal legend_toggled signal hover_forbidden(cell: Vector2i) signal chord_attempted(cell: Vector2i) @@ -82,6 +83,7 @@ func _init() -> void: board_view.chord_attempted.connect(_forward_chord) ap_bar.stop_pressed.connect(_emit_stop_pressed) ap_bar.refresh_confirmed.connect(_emit_refresh_confirmed) + ap_bar.convert_pressed.connect(_emit_convert_pressed) ap_bar.legend_toggled.connect(_emit_legend_toggled) @@ -105,6 +107,10 @@ func _emit_stop_pressed() -> void: stop_pressed.emit() +func _emit_convert_pressed() -> void: + convert_pressed.emit() + + func _emit_refresh_confirmed() -> void: refresh_confirmed.emit() diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 49822af8..5b62eecf 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -333,6 +333,9 @@ func refresh_camp(data: Dictionary) -> void: card.status_l.text = st_text card.status_l.add_theme_color_override("font_color", st_col) card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文) + # 购买钮余额联动(迭代 22 秘宝批 T4——禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) + if card.get("buy_btn") != null: + card.buy_btn.disabled = balance < int(card.get("price", 0)) # 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动) var att: Dictionary = data.get("attune", {}) if _attune_header_l != null: @@ -345,7 +348,8 @@ func refresh_camp(data: Dictionary) -> void: for card2 in _attune_btns: var on: bool = eq_set.has(String(card2.item_id)) card2.btn.text = "卸下" if on else "装配" - card2.btn.disabled = (not on) and eq.size() >= slots_n # 满位禁选 + # 满位禁选+未解锁禁装(迭代 22 T4——已装件恒可卸下;unlocked 快照随 rebuild 刷新) + card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) if _refresh_l != null: var rf: Dictionary = att.get("refresh", {}) _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] @@ -368,10 +372,12 @@ func _rebuild_workshop(data: Dictionary) -> void: var line := HBoxContainer.new() line.add_theme_constant_override("separation", 12) card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) name_l.custom_minimum_size = Vector2(132, 0) name_l.clip_text = true name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) line.add_child(name_l) var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——同行价格墨同案 progress_l.custom_minimum_size = Vector2(150, 0) @@ -384,8 +390,15 @@ func _rebuild_workshop(data: Dictionary) -> void: status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL line.add_child(status_l) + # 钥石购买钮(迭代 22 秘宝批 T4——购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) _workshop_holder.add_child(card) - _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l}) + _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))}) _rebuild_attune(data) @@ -408,12 +421,17 @@ func _rebuild_attune(data: Dictionary) -> void: var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE) form_l.custom_minimum_size = Vector2(58, 0) line.add_child(form_l) + # 档位标签(迭代 22 秘宝批 T4——槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) line.add_child(btn) _workshop_holder.add_child(line) _attune_nodes.append(line) - _attune_btns.append({"item_id": iid, "btn": btn}) + _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 6502eb5f..cb0c5b65 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -601,8 +601,8 @@ func _smoke_adventure() -> void: # ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现; # 种类次数上限退役,「达上限剔除」语义由牌库耗尽承载) var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11)) - check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}), - "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 blast_kit)") + check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}), + "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)") for di in range(a11._curio_deck.size() - 1, -1, -1): var dk := String((a11._curio_deck[di] as Dictionary).get("kind", "")) if dk == "attack" or dk == "ap": @@ -635,7 +635,7 @@ func _smoke_adventure() -> void: "满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)") # ⑬ 奖励节点共享池(迭代 10 追加批——同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现) var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13)) - check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}), + check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}), "节点共享池场景:冒险启动(seed 13)") for di13 in range(a13._curio_deck.size() - 1, -1, -1): if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack": @@ -4744,7 +4744,7 @@ func _meta_settle_and_workshop() -> void: # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) var sv_e = _meta_new_save(_META_SMOKE_PATHS[8]) - sv_e.set_attunement_equipped(["luck_coin"]) + sv_e.set_attunement_equipped(["morning_order"]) sv_e.set_attune_refresh_level(2) sv_e.set_fork_side("fg_posture", "a") var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e) diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index 14caae64..cba6f874 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -328,9 +328,9 @@ func _ready() -> void: # ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位; # 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ---- var fogp: Dictionary = bv.fog_params_probe() - if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(fogp["lantern"]) != 0 or not fogp["enabled"]: - _problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)" - % [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(fogp["lantern"]), str(fogp["enabled"])]) + if int(bv.FOG_RADIUS) != 8 or int(fogp["lantern"]) != 0 or not fogp["enabled"]: + _problems.append("case⑮ 迷雾参数漂移(R=%s lantern=%s enabled=%s 应=8/0/true——旧提灯常量随行退役摘除)" + % [str(bv.FOG_RADIUS), str(fogp["lantern"]), str(fogp["enabled"])]) else: var hero15: Vector2i = ms.hero_pos() var b15 = ms.current_board() @@ -392,8 +392,9 @@ func _ready() -> void: if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \ and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS): _problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)") - # 提灯 +2 接线断言(迭代14⑤——set_fog_lantern(true) → 半径 8+2:dist==R+1 格由雾态转可见, - # 关闭复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位) + # 视野加成接线断言(迭代 22 秘宝批 T4 换代——旧提灯 +2 主语随 fog_lantern 行退役; + # 新主语=阔视野带参数注入 set_fog_glow_bonus(4) → 半径 8+4:dist==R+1 格由雾态转可见, + # 卸装复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位) var ring_cell := Vector2i(-99, -99) for y in b15.height: for x in b15.width: @@ -403,39 +404,40 @@ func _ready() -> void: if ring_cell.x != -99: break if ring_cell.x == -99: - _problems.append("case⑮ 板面无 dist==R+1 格(提灯断言取数异常)") + _problems.append("case⑮ 板面无 dist==R+1 格(视野加成断言取数异常)") else: var pre_lantern: int = int(bv.cells_probe()[ring_cell].fog_state) - bv.set_fog_lantern(true) - if int(bv.fog_params_probe()["lantern"]) != 2: - _problems.append("case⑮ 提灯接雾参数未变(lantern 应=2)") + bv.set_fog_glow_bonus(4) + if int(bv.fog_params_probe()["lantern"]) != 4: + _problems.append("case⑮ 视野加成接雾参数未变(lantern 应=4)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: - _problems.append("case⑮ 提灯 +2 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern) - bv.set_fog_lantern(false) + _problems.append("case⑮ 视野 +4 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern) + bv.set_fog_glow_bonus(0) if int(bv.fog_params_probe()["lantern"]) != 0: - _problems.append("case⑮ 提灯关闭后雾参数未复原(lantern 应=0)") + _problems.append("case⑮ 视野加成清零后雾参数未复原(lantern 应=0)") if int(bv.cells_probe()[ring_cell].fog_state) == 0: - _problems.append("case⑮ 提灯关闭后 dist==R+1 格未复原遮蔽") - # 载体级断言(迭代 19 卡④——fog_lantern 装配链贯通:①装配→开局参数面〔build_adventure_params - # 统一构造=卡⓪①缺陷修复面回归〕②装配集→呈现桥〔_sync_fog_lantern 消费 attune_has 只读出口 - # →set_fog_lantern〕→视野 +2 生效与卸装复原。settlement.attunement 手动注入=模拟 begin_battle - # enc 注入后的运行态〔注入面本体归冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用) - main.save_manager.set_attunement_equipped(["fog_lantern"]) + _problems.append("case⑮ 视野加成清零后 dist==R+1 格未复原遮蔽") + # 装配链级断言(迭代 22 秘宝批 T4 换代——wide_scout_band 装配链贯通:①装配→开局参数面 + # 〔build_adventure_params 统一构造〕②装配集→呈现桥〔_sync_treasure_glow 消费 + # attune_param_sum 只读出口→set_fog_glow_bonus〕→视野 +4 生效与卸装复原。 + # settlement.attunement 手动注入=模拟 begin_battle enc 注入后的运行态〔注入面本体归 + # 冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用) + main.save_manager.set_attunement_equipped(["wide_scout_band"]) var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) - if not (p_lantern.get("attunement", []) as Array).has("fog_lantern"): - _problems.append("case⑮ 载体装配未进开局参数面(attunement=%s 应含 fog_lantern)" % str(p_lantern.get("attunement", []))) - main.settlement.attunement = ["fog_lantern"] - main._sync_fog_lantern() - if int(bv.fog_params_probe()["lantern"]) != 2: - _problems.append("case⑮ 载体装配后视野未扩(lantern 应=2——呈现桥未生效)") + if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"): + _problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", []))) + main.settlement.attunement = ["wide_scout_band"] + main._sync_treasure_glow() + if int(bv.fog_params_probe()["lantern"]) != 4: + _problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: - _problems.append("case⑮ 载体装配后 dist==R+1 格仍被遮(应转可见)") + _problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)") main.settlement.attunement = [] - main._sync_fog_lantern() + main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 0: - _problems.append("case⑮ 载体卸装后视野未复原(lantern 应=0——契约 7 显式重算含否)") + _problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)") if int(bv.cells_probe()[ring_cell].fog_state) == 0: - _problems.append("case⑮ 载体卸装后 dist==R+1 格未复原遮蔽") + _problems.append("case⑮ 卸装后 dist==R+1 格未复原遮蔽") main.save_manager.set_attunement_equipped([]) # ---- case⑯ 暂停层输入锁语义(迭代 17 卡③ R3——开层锁棋盘、关层恢复无残留;真实 push_input # 路径:入口角标本体 press+release 真点击 → 暂停层开;锁期挖掘点击零穿透;「继续」关层后 diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index fba5cce5..e03e481f 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -91,7 +91,7 @@ func _init() -> void: var sm_e = SaveManagerScript.new(_bp_path) var adr_e = AdventureRunnerScript.new(db, rng_e) adr_e.set_save_manager(sm_e) - var params_e := {"hero_override": {}, "treasure_unlocked": {"blast_kit": true}} + var params_e := {"hero_override": {}, "treasure_unlocked": {"wide_scout_band": true}} Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)") adr_e._curio_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推) adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"}) @@ -128,24 +128,24 @@ func _init() -> void: Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)") Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)") - # ---- Part 6:装配存档回归(迭代 19 卡④——装配→停点→回局载体态一致:params.attunement 随续玩 - # 受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归呈现桥(main._sync_fog_lantern), - # 本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)---- + # ---- Part 6:装配存档回归(迭代 19 卡④立,迭代 22 秘宝批 T4 主语换代——装配→停点→回局载体态 + # 一致:params.attunement 随续玩受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归 + # 呈现桥(main._sync_treasure_glow),本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)---- var rng_h = load("res://core/rng_service.gd").new() var sm_h = SaveManagerScript.new(_bp_path) var adr_h = AdventureRunnerScript.new(db, rng_h) adr_h.set_save_manager(sm_h) - var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["fog_lantern"]} - Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 fog_lantern)") + var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]} + Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 morning_order)") Kit.check_quiet(adr_h.runner().pick_next_node(int(adr_h.runner().next_candidate_ids()[0])), "P6 选路进首战") - Kit.check_quiet(adr_h.runner().current_battle().attune_has("fog_lantern"), "P6 首战装配集含 fog_lantern(attune_has 只读出口)") + Kit.check_quiet(adr_h.runner().current_battle().attune_has("morning_order"), "P6 首战装配集含 morning_order(attune_has 只读出口)") var sm_i = SaveManagerScript.new(_bp_path) Kit.check_quiet(sm_i.load_from_disk() and sm_i.has_breakpoint(), "P6 断点在档(停点可中断)") var adr_i = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) adr_i.set_save_manager(sm_i) Kit.check_quiet(adr_i.resume_adventure(sm_i.breakpoint_snapshot(), params_h), "P6 续玩受理(装配集随 params 再注入)") Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战") - Kit.check_quiet(adr_i.runner().current_battle().attune_has("fog_lantern"), "P6 回局首战装配集仍含 fog_lantern(停点往返载体态一致)") + Kit.check_quiet(adr_i.runner().current_battle().attune_has("morning_order"), "P6 回局首战装配集仍含 morning_order(停点往返载体态一致)") _clear_disk() quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿")) diff --git a/game/tests/unit/presentation/reward_overlay_test.gd b/game/tests/unit/presentation/reward_overlay_test.gd index 2aefa086..13fdd17b 100644 --- a/game/tests/unit/presentation/reward_overlay_test.gd +++ b/game/tests/unit/presentation/reward_overlay_test.gd @@ -69,7 +69,7 @@ func _run() -> void: ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"): return # ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通) - ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1) + ov.open_replace([{"index": 0, "item_id": "incense_stick", "display_name": "线香"}], "天眼", 1, 1) var replace_btns := 0 for c in ov._replace_box.get_children(): if c is Button: From 73db171cb4b453f0645f3a19df683c792c0fa17b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:31:04 +0800 Subject: [PATCH 5/8] =?UTF-8?q?T5(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E5=AD=98=E6=A1=A3=E9=9D=A2+=E6=AD=BB=E9=80=9A?= =?UTF-8?q?=E9=81=93=E6=8B=86=E9=99=A4=E2=80=94=E2=80=94treasure=5Finvento?= =?UTF-8?q?ry=E4=BF=9D=E7=95=99=E6=B3=A8=E8=AE=B0(=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E4=BB=B6=E9=AA=91=E5=BA=93=E5=AD=98=E6=89=BF=E8=BD=BD=E5=9C=A8?= =?UTF-8?q?=E5=BD=B9:=E7=BA=BF=E9=A6=99/=E5=A4=A9=E7=9C=BChandle+=E6=96=AD?= =?UTF-8?q?=E7=82=B9aux=5Fitems=E5=BF=AB=E7=85=A7+set=5Fattune=5Fcharges?= =?UTF-8?q?=E5=B7=AE=E9=A2=9D=E6=B3=A8=E5=85=A5=E5=85=A8=E9=93=BE=E4=BF=9D?= =?UTF-8?q?=E7=95=99);=20=E6=9B=BF=E6=8D=A2=E5=AD=90=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E6=8B=86=E9=99=A4(CURIO=5FKINDS=E9=99=A4?= =?UTF-8?q?=E5=90=8D=E5=90=8E=E4=B8=8D=E5=8F=AF=E8=BE=BE=E6=AD=BB=E9=80=9A?= =?UTF-8?q?=E9=81=93=3D=E8=A3=85=E9=85=8D=E4=B8=8D=E5=8F=AF=E5=8F=98?= =?UTF-8?q?=E5=A5=91=E7=BA=A61=E5=AE=9E=E7=8E=B0=E9=9D=A2=E4=BF=9D?= =?UTF-8?q?=E8=AF=81):=20adventure=5Frunner=E5=85=AD=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=88=A0(=5Fpick=5Ftreasure=5Fitem/resolve=5Ftreasure=5Freplac?= =?UTF-8?q?e/=5Fresolve=5Fnode=5Freplace/=5Freplace=5Fin=5Ftransit/=5Fdraw?= =?UTF-8?q?=5Fpending=5Ftreasure/resolve=5Fcurio=5Freplace)+=5Fbonus=5Ftre?= =?UTF-8?q?asure=5Fcarry=E8=B7=A8=E5=85=B3=E6=90=BA=E5=B8=A6/=5Flast=5Fdra?= =?UTF-8?q?w=5Ftreasure=E6=8E=B7=E5=AE=9A=E9=93=BE/=5Froll=5Ffrom/=5Fbonus?= =?UTF-8?q?=5Ftreasure=5Fpool/bonus=5Faux=5Fcarry=E6=96=AD=E7=82=B9?= =?UTF-8?q?=E9=94=AE;=20level=5Frunner.grant=5Ftreasure=5Fitem/treasure=5F?= =?UTF-8?q?items.holding=5Frecords=E5=88=A0;=20=E5=91=88=E7=8E=B0=E9=9D=A2?= =?UTF-8?q?=3Dcontroller=E6=9B=BF=E6=8D=A2=E5=AD=90=E6=80=81=E4=B8=89?= =?UTF-8?q?=E5=87=BD=E6=95=B0+aux=5Fdetail=E5=8D=A1=E7=89=87=E7=9B=B4?= =?UTF-8?q?=E6=98=BE=E9=93=BE/curio=5Fpick=5Foverlay=20replace=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA(=E4=BF=A1=E5=8F=B7+=E9=9D=A2=E6=9D=BF+open?= =?UTF-8?q?=5Freplace)/main=E4=B8=A4=E6=8E=A5=E7=BA=BF/level=5Fautoplay?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E7=AD=96=E7=95=A5;=20smoke=E4=B8=8Eui=5Fclic?= =?UTF-8?q?k=5Fprobe=E6=9B=BF=E6=8D=A2=E5=88=86=E6=94=AF=E6=91=98=E9=99=A4?= =?UTF-8?q?+reward=5Foverlay=5Ftest=E6=9B=BF=E6=8D=A2=E6=AE=B5=E6=91=98?= =?UTF-8?q?=E9=99=A4;=20=E4=BF=AE=E6=AD=A3=E5=88=99=E8=B4=AA=E5=90=83?= =?UTF-8?q?=E8=AF=AF=E5=88=A0=3Dapply=5Fnode=5Fcurio=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E5=85=91=E4=BB=98=E4=B8=BB=E4=BD=93=E8=A1=A5=E5=9B=9E=E3=80=82?= =?UTF-8?q?=E5=86=92=E7=83=9F895/0+run=5Fall=2022=E5=A5=97+=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=85=A8=E7=BB=BF=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9?= =?UTF-8?q?=20V15/V16=E6=8B=86=E9=99=A4=E8=B7=AF=E7=BA=BF/V13=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/adventure_runner.gd | 180 +----------------- game/core/level_runner.gd | 9 - game/core/save_manager.gd | 2 +- game/core/treasure_items.gd | 8 - .../presentation/curio_interact_controller.gd | 64 +------ game/presentation/main.gd | 2 - game/presentation/views/curio_pick_overlay.gd | 50 +---- game/smoke/level_autoplay.gd | 14 +- game/smoke/smoke_all.gd | 10 +- game/smoke/ui_audit_probe.gd | 2 +- game/smoke/ui_click_probe.gd | 14 +- game/smoke/ui_struct_probe.gd | 2 +- .../unit/presentation/reward_overlay_test.gd | 31 +-- 13 files changed, 42 insertions(+), 346 deletions(-) diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 1e42ae8a..a84134eb 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -43,7 +43,6 @@ extends RefCounted signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;迭代 20 T5 换代正名——旧「提升三选一」退役,通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index} -signal treasure_replace_presented(items: Array, incoming: Dictionary) # 同调位满位替换子交互(pick_curio 道具件满位时)——items=现有持有件记录、incoming=新件记录 signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10) # ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)---- signal node_entered(node: Dictionary, info: Dictionary) @@ -94,9 +93,7 @@ var _feats: Array[String] = [] # 特性持有集(迭代 12 T1—— var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点 var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付) -var _bonus_treasure_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗) var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源) -var _last_draw_treasure: Array[String] = [] # 最近一次抽签掷定的道具件(掷定前置——卡片直显具体道具;追加批二) var _node_curio_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽) var _feat_ticket_pity := 0 # 门票保底计数(T5——展示次数:节点开面/货架抽签/刷新重展各计一次;触发清零) var _feat_ticket_sold := 0 # 门票限购计数(T5——选中/买下即 +1;拿到即开不进持有集) @@ -212,8 +209,6 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: _feats = [] for r in (bp.get("relics", []) as Array): _feats.append(String(r)) - for v in (bp.get("bonus_aux_carry", []) as Array): - _bonus_treasure_carry.append(String(v)) _keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true) _base_override = _normalize_override(params.get("hero_override", {})) # 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能由旧口径版本写出(如调试上限 @@ -256,19 +251,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: else: migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON}) _curio_pending = {"candidates": migrated} - if bp_pending.has("aux_detail"): - _curio_pending["aux_detail"] = (bp_pending.get("aux_detail", []) as Array).duplicate(true) # 掷定前置随停点快照续接 var payload: Dictionary = (bp_pending.get("payload", {}) as Dictionary).duplicate(true) - if bp_pending.has("incoming"): - _curio_pending["chosen"] = "treasure_item" - var incoming: Array = (bp_pending.get("incoming", []) as Array).duplicate(true) - _curio_pending["incoming"] = incoming - state = "running" - curio_choice_presented.emit(payload) - # 替换在途续发(呈现层直落替换子态——持有件记录按恢复后 carry 重建) - treasure_replace_presented.emit(_carry_records(), - {"index": -1, "item_id": String(incoming[0]) if not incoming.is_empty() else "", "count": incoming.size()}) - return true state = "running" curio_choice_presented.emit(payload) return true @@ -295,7 +278,6 @@ func _reset_curio_state() -> void: _feats = [] _curio_pending = {} _curio_result = {} - _bonus_treasure_carry = [] _keystone_drops = [] _node_curio_pending = {} adventure_counters = {} @@ -420,8 +402,6 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool: return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集) var value: int = int(db.adventure.curio_value(kind, tier)) var hp_before := int(ledger.get("hero_hp", 0)) - if kind == "treasure_item": - return _pick_treasure_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨;池种退役后不可达) if not _apply_curio_pick(kind, tier, value, feat_id): push_error("[冒险结构] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id]) return false @@ -549,44 +529,6 @@ func _pool_stock_snapshot() -> Dictionary: return out -# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_treasure_replace)。 -# 掷定前置(追加批二):具体道具随候选抽定(_last_draw_treasure / _curio_pending.aux_detail)——本函数消费预掷值, -# 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。 -# 持有容器 = 跨关携带 _bonus_treasure_carry(与节点渠道的关卡域库存分轨——容器差异保守不合,注记在案)。 -func _pick_treasure_item(count: int) -> bool: - var picked: Array = _draw_pending_treasure(_curio_pending, count, true) - if picked.is_empty(): - push_error("[冒险结构] pick_curio(treasure_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") - return false - var hp_before := int(ledger.get("hero_hp", 0)) - # 迭代 11 第①批 T7——同调位守卫随过渡键退役拆除(aux 卡出池后本路径不可达;能力件容量闸 - # 正主 = 营地装配面 Attunement.slots 满位禁选) - for iid in picked: - _bonus_treasure_carry.append(String(iid)) - _finish_curio_pick("treasure_item", picked.size(), hp_before) - return true - - -# 替换子交互受理(discard_index = -1 放弃新件不获取;否则丢该件腾位收新)——终态恒 ≤ 同调位 -# 持有容器 = 跨关携带(序号寻址);与节点渠道(库存句柄寻址)的丢弃/授予/收尾三步保守不合。 -func resolve_treasure_replace(discard_index: int) -> bool: - if not _replace_in_transit(_curio_pending): - push_error("[冒险结构] resolve_treasure_replace 拒绝——无替换在途") - return false - var incoming: Array = _curio_pending.get("incoming", []) as Array - var hp_before := int(ledger.get("hero_hp", 0)) - if discard_index >= 0: - if discard_index >= _bonus_treasure_carry.size(): - push_error("[冒险结构] resolve_treasure_replace 拒绝——序号不在持有集: %d" % discard_index) - return false - _bonus_treasure_carry.remove_at(discard_index) - for iid in incoming: - _bonus_treasure_carry.append(String(iid)) - # 放弃新件(discard_index < 0)= 原持有集不动、新件不获取 - _finish_curio_pick("treasure_item", incoming.size(), hp_before) - return true - - # 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。 # picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(迭代 12 T1——反馈行取数) func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void: @@ -608,16 +550,15 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String # 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载, # 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。 -# 掷定前置(追加批二):返回 dict 带 aux_detail(道具件具体道具)供卡片直显。 func node_curio_candidates() -> Dictionary: var cands: Array = _draw_curio_candidates("reward_node") - return {"candidates": cands, "aux_detail": _last_draw_treasure.duplicate()} + return {"candidates": cands} -func present_node_curio(candidates: Array, treasure_detail: Array = []) -> void: +func present_node_curio(candidates: Array) -> void: _reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转) _curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零——2026-10-06) - _node_curio_pending = {"candidates": candidates.duplicate(true), "aux_detail": treasure_detail.duplicate(true)} + _node_curio_pending = {"candidates": candidates.duplicate(true)} # 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库;relic 同权入持有集) @@ -632,8 +573,6 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id]) return false var value: int = int(db.adventure.curio_value(kind, tier)) - if kind == "treasure_item": - return _apply_node_treasure(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨;池种退役后不可达) if not _apply_curio_pick(kind, tier, value, feat_id): push_error("[冒险结构] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id]) return false @@ -641,46 +580,6 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> return true -# 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用); -# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨,容器语义保守不合)。 -# 掷定前置:消费 _node_curio_pending.aux_detail 预掷值(缺省回退当场掷定)。 -func _apply_node_treasure(count: int) -> bool: - var picked: Array = _draw_pending_treasure(_node_curio_pending, count, false) - if picked.is_empty(): - push_error("[冒险结构] apply_node_curio(treasure_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") - return false - var records: Array = [] - if _runner != null and _runner.treasure_inventory != null: - records = _runner.treasure_inventory.holding_records() - # 迭代 11 第①批 T7——守卫随过渡键退役拆除(CURIO_KINDS 已除名 treasure_item,本分支不可达; - # 替换子交互路由随容量闸一并退役——能力件容量闸正主 = 营地装配面满位禁选) - if _runner != null: - for iid in picked: - var rec: Dictionary = _runner.grant_treasure_item(String(iid), "reward_node") - if rec.is_empty(): - push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid)) - _finish_node_bonus("treasure_item", picked.size()) - return true - - -func _resolve_node_replace(discard_handle: int) -> bool: - if not _replace_in_transit(_node_curio_pending): - push_error("[冒险结构] resolve_node_replace 拒绝——无替换在途") - return false - var incoming: Array = _node_curio_pending.get("incoming", []) as Array - if discard_handle >= 0: - if _runner == null or _runner.treasure_inventory == null or not _runner.treasure_inventory.discard_item(discard_handle): - push_error("[冒险结构] resolve_node_replace 拒绝——句柄不在持有集: %d" % discard_handle) - return false - for iid in incoming: - var rec: Dictionary = _runner.grant_treasure_item(String(iid), "reward_node") - if rec.is_empty(): - push_warning("[冒险结构] 替换子道具授予被拒(防御兜底留痕): %s" % String(iid)) - # 放弃新件(discard_handle < 0)= 原持有集不动、新件不获取 - _finish_node_bonus("treasure_item", incoming.size()) - return true - - # 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账) func _seed_attune_charges() -> void: attune_charges = {} @@ -742,11 +641,10 @@ func battery_pending() -> int: return _battery_pending -# 节点停点在途态读回(组合根重 present 用——candidates/aux_detail 值拷贝) +# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝) func node_curio_pending_state() -> Dictionary: return { "candidates": (_node_curio_pending.get("candidates", []) as Array).duplicate(true), - "aux_detail": (_node_curio_pending.get("aux_detail", []) as Array).duplicate(true), } @@ -792,10 +690,8 @@ func _refresh_curio_common(paid: bool) -> bool: if not _curio_pending.is_empty(): var cands: Array = _draw_curio_candidates("level_clear") _curio_pending["candidates"] = cands.duplicate(true) - _curio_pending["aux_detail"] = _last_draw_treasure.duplicate() var payload: Dictionary = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true) payload["candidates"] = cands - payload["aux_detail"] = _last_draw_treasure.duplicate() payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面(2026-10-06) curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途) _snapshot_breakpoint() @@ -803,44 +699,10 @@ func _refresh_curio_common(paid: bool) -> bool: if not _node_curio_pending.is_empty(): var cands2: Array = _draw_curio_candidates("reward_node") _node_curio_pending["candidates"] = cands2.duplicate(true) - _node_curio_pending["aux_detail"] = _last_draw_treasure.duplicate() return true return false -# 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=treasure_item) -func _replace_in_transit(pending: Dictionary) -> bool: - return not pending.is_empty() and String(pending.get("chosen", "")) == "treasure_item" - - -# 双渠道道具件掷定共享体(2026-10-02 修复批——_pick_treasure_item/_apply_node_treasure 复制债收口一步): -# 解锁道具池空集守卫 + N 件掷定(先消费停点预掷 aux_detail,不足部分当场经 reward_pick 封闭流补掷—— -# 取数序与拆分前逐位一致)。返回空数组 = 池空集(调用方按渠道名报错)。 -# 保守不合处:通关渠道允许回退 _last_draw_treasure(allow_detail_fallback)、节点渠道不回退——掷定回退 -# 语义渠道各异,参数化保留差异;持有容器与兑付收尾仍归调用方渠道。 -func _draw_pending_treasure(pending: Dictionary, count: int, allow_detail_fallback: bool) -> Array: - var pool: Array = _bonus_treasure_pool() - if pool.is_empty(): - return [] - var picked: Array = [] - var detail: Array = (pending.get("aux_detail", []) as Array).duplicate() - if detail.is_empty() and allow_detail_fallback: - detail = _last_draw_treasure.duplicate() - for i in maxi(1, count): - if i < detail.size(): - picked.append(String(detail[i])) - else: - picked.append(_roll_bonus_treasure_item(pool)) - return picked - - -# 替换受理分发(单入口——节点停点在途优先按库存句柄,否则按通关停点的携带序号) -func resolve_curio_replace(discard: int) -> bool: - if not _node_curio_pending.is_empty() and _node_curio_pending.has("incoming"): - return _resolve_node_replace(discard) - return resolve_treasure_replace(discard) - - func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void: _node_curio_pending = {} var payload := { @@ -858,8 +720,7 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: # 一枚**〔先到先得,档位取该 kind 首枚——「三选一=三种不同选项」体验延续,与货架抽签同规则〕; # kind 出现率∝剩余令牌数〔档位配额即分布——无独立权重 knob〕;抽签不摘牌、兑付才摘牌=未选 # 自动回池;池耗即枯竭——余库不足 N kind 时出齐剩余,空库 = 零候选合法态)。候选 = 令牌字典 -# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选;道具件掷定前置 -# 随池种退役拆除(_last_draw_treasure 恒空集,机械惰性承载)。 +# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选。 # 渠道白名单过滤(迭代 15 卡② T4——渠道轴定位:关底 Boss 击败=通关渠道只出专长(feat 令牌); # 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。 # 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样, @@ -869,7 +730,6 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: # 〔独立计数体系〕;已拿上限 cap(案②提前退役):taken≥cap 的身份在候选视图剔除〔牌库本体不动, # 拿了才计、刷出不计——_curio_cap_ok〕。 func _draw_curio_candidates(channel: String = "reward_node") -> Array: - _last_draw_treasure = [] var pool := _curio_deck.duplicate() if channel == "level_clear": pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) == "relic") @@ -1007,33 +867,6 @@ func _redeem_feat_ticket() -> bool: return true -func _roll_from(rg: RandomNumberGenerator) -> String: - var pool: Array = _bonus_treasure_pool() - return String(pool[rg.randi_range(0, pool.size() - 1)]) - - -# 三选一道具件随机池(与投放门控同源:aux 活行 ∩ 解锁门控集——空集 = 道具件不入牌库) -func _bonus_treasure_pool() -> Array: - var out: Array = [] - var live: Dictionary = db.treasure_live_ids() - for iid in live.keys(): - if _treasure_unlocked.has(String(iid)): - out.append(String(iid)) - return out - - -func _roll_bonus_treasure_item(pool: Array) -> String: - var rg: RandomNumberGenerator = rng.open_stream("reward_pick") - return String(pool[rg.randi_range(0, pool.size() - 1)]) - - -func _carry_records() -> Array: - var out: Array = [] - for i in _bonus_treasure_carry.size(): - out.append({"index": i, "item_id": _bonus_treasure_carry[i]}) - return out - - # Boss 钥石逐杀掉落(迭代 10——渠道改 Boss/精英击杀掉落;首杀/复杀两档读 meta.tres boss_keystone; # 首杀判据 = 存档 boss_first_kill 标记位〔断点未注入 = 恒首杀——模拟/冒烟零扰动;标记仍在收束 # apply_adventure_summary 逐通关关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因) @@ -1158,7 +991,6 @@ func _on_level_finished(payload: Dictionary) -> void: var pick_payload: Dictionary = { "level_index": level_index, "candidates": _draw_curio_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=通关渠道只出专长 - "aux_detail": _last_draw_treasure.duplicate(), # 掷定前置(追加批二——道具件具体道具随候选抽定,卡片直显) "heal_gained": heal, "hp_before": hp_before, "hp_after": int(ledger.get("hero_hp", 0)), @@ -1168,7 +1000,6 @@ func _on_level_finished(payload: Dictionary) -> void: "aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数) } _curio_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true), - "aux_detail": _last_draw_treasure.duplicate(), "payload": pick_payload.duplicate(true)} curio_choice_presented.emit(pick_payload) _snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失) @@ -1267,7 +1098,6 @@ func _snapshot_breakpoint() -> void: "refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账) "battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点 "bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付) - "bonus_aux_carry": _bonus_treasure_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失) "keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10) } _save_manager.set_breakpoint(bp) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index c06b14e8..aa524ccf 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -679,15 +679,6 @@ func set_attune_charges(charges: Dictionary) -> void: func set_vision_counter(counter: Dictionary) -> void: _vision_counter = counter -# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。 -# 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸 -# 正主 = 营地装配面 Attunement.slots 满位禁选)。 -func grant_treasure_item(item_id: String, source: String) -> Dictionary: - if treasure_inventory == null: - treasure_inventory = TreasureItems.new(db) - return treasure_inventory.add_item(item_id, source, Vector2i(-1, -1), 0) - - # 三选一 GP 袋件入账(迭代 10——钱包收口扩「奖励入账」入口;复用战斗完成同一入账原语, # wallet_changed 载荷带 source 供呈现归因) func grant_curio_gp(amount: int) -> void: diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index d613aa3e..8b054c6a 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -78,7 +78,7 @@ func _migrate_v1_v2(data: Dictionary) -> Dictionary: # v2→v3:断点附随件扩列(迭代 10 奖励重构批——bonus_taken/bonus_pending/bonus_result/ -# bonus_aux_carry/keystone_drops + bonus.ap 轴)与摘要 additive 键 keystone_drops——旧档缺键 +# keystone_drops + bonus.ap 轴)与摘要 additive 键 keystone_drops——旧档缺键〔bonus_aux_carry 键随迭代 22 T5 替换子交互拆除退役〕 # = 合法空态(读取方 .get 兜底),无结构迁移体;版本号推进仅为 schema_version 对齐 func _migrate_v2_v3(data: Dictionary) -> Dictionary: data["version"] = SAVE_FORMAT_VERSION diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index 8425d699..1a4d6c6b 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -79,14 +79,6 @@ func holding_count() -> int: # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) -func holding_records() -> Array: - var out: Array = [] - for rec in _items: - if not bool(rec.get("used", false)): - out.append({"handle": int(rec.get("handle", 0)), "item_id": String(rec.get("item_id", ""))}) - return out - - # 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含) func discard_item(handle: int) -> bool: for i in _items.size(): diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index add98aed..85a13c76 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -5,7 +5,7 @@ # 装配纪律(ADR-0001,仿 treasure_interact_controller 先例):信号连接不在此发生——curio_overlay # 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/ # 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一 -# 经 adventure.pick_curio/apply_node_curio/resolve_curio_replace(core 侧单口,本类零直写)。 +# 经 adventure.pick_curio/apply_node_curio(core 侧单口,本类零直写;替换子交互随迭代 22 T5 拆除——装配不可变契约 1,冒险中零更换途径)。 extends RefCounted const Narrator := preload("res://presentation/battle_narrator.gd") @@ -29,8 +29,6 @@ var _gp_summary: Callable # () → 战斗总结 GP 行文案(组 var _pending_curio := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用) var _curio_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) -var _last_replace_items: Array = [] # 替换子态续玩重建缓存(treasure_replace_presented 载荷) -var _last_replace_incoming := {} # 同上——incoming 记录 func _init(db_ref, meta_get: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, @@ -112,8 +110,7 @@ func on_presented(payload: Dictionary) -> void: toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈 _pending_curio = payload.duplicate(true) _curio_context = "level_clear" - if bool(payload.get("replace_pending", false)) or bool(payload.get("refreshed", false)): - # 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽) + if bool(payload.get("refreshed", false)): # 刷新重发(2026-10-06):refreshed 标直落选择面——不再重开胜利总结面 show_pick() return @@ -124,9 +121,8 @@ func on_presented(payload: Dictionary) -> void: _refresh_presentations.call() -# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态) -# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面, -# 道具件满位替换子交互同构)。原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役。 +# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 +# 原「秘宝投放渠道」三选一(迭代 7 过渡形态)随批退役;替换子交互随迭代 22 T5 拆除。 func present_node_curio() -> void: var adventure = _adventure_get.call() _curio_context = "reward_node" @@ -141,16 +137,16 @@ func present_node_curio() -> void: runner.continue_after_node() return _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands, node_draw.get("aux_detail", []) as Array)) - adventure.present_node_curio(node_cands, node_draw.get("aux_detail", []) as Array) + curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands)) + adventure.present_node_curio(node_cands) _feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点 -# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 treasure_replace_presented 续发直落子态) +# 三选一面开出(战斗总结结算确认 / 续玩停点恢复) func show_pick() -> void: var payload: Dictionary = _pending_curio _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array)) + curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) var adventure = _adventure_get.call() if adventure != null: _feed_refresh_display(adventure) # 迭代 11 第①批 T5b + 2026-10-06 对齐商店——刷新面投喂单点 @@ -165,11 +161,10 @@ func _feed_refresh_display(adventure) -> void: curio_overlay.set_refresh_display(adventure.attune_refresh_left(), cost, gp >= cost) -func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array: +func _curio_card_texts(candidates: Array) -> Array: # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|feat_id" 复合键(同 kind # 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed) var out: Array = [] - var treasure_i := 0 for tok in candidates: var kind := String((tok as Dictionary).get("kind", "")) var tier := String((tok as Dictionary).get("tier", "")) @@ -181,10 +176,6 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array: 选定即开一次奇物三选一(T5 门票——拿到即开不进持有集)" elif kind == "relic": text = Narrator.feat_card_text(db.feat_row(rid)) - elif kind == "treasure_item": - var iid := String(treasure_detail[treasure_i]) if treasure_i < treasure_detail.size() else "" - treasure_i += 1 - text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier)), _treasure_card_line(iid)) else: text = Narrator.bonus_card_text(kind, tier, int(db.adventure.curio_value(kind, tier))) out.append({"id": cid, "text": text}) @@ -194,17 +185,6 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array: # 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。 # 内容数据驱动扩展批 T4 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射—— # 新件写行即有名有姓) -func _treasure_card_line(iid: String) -> String: - if iid == "": - return "" - var row: Dictionary = db.treasure_item_row(iid) - if row.is_empty(): - return "" - var desc := String(row.get("desc", "")) - if desc != "": - return "%s · %s" % [String(row.get("display_name", iid)), desc] - return String(row.get("display_name", iid)) - # 三选一刷新(迭代 11 第①批 T5b X/Y 双源 + 2026-10-06 用户令对齐商店 MC-1——免费位用尽转 GP 付费档: # 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;通关停点经重发 curio_choice_presented 直落 @@ -227,7 +207,7 @@ func on_refresh() -> void: return if _curio_context == "reward_node": var st: Dictionary = adventure.node_curio_pending_state() - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array)) + curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) _feed_refresh_display(adventure) @@ -251,30 +231,6 @@ func on_pick_confirmed(item_id: String) -> void: curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) -func on_treasure_replace_presented(items: Array, incoming: Dictionary) -> void: - _last_replace_items = items.duplicate(true) - _last_replace_incoming = incoming.duplicate(true) - if not curio_overlay.visible: - _open_modal.call(curio_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律) - _open_replace(items, incoming) - - -func _open_replace(items: Array, incoming: Dictionary) -> void: - var named: Array = [] - for it in items: - var rec: Dictionary = it.duplicate() - rec["display_name"] = String(item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", "")))) - named.append(rec) - curio_overlay.open_replace(named, String(item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))), - named.size(), Attunement.slots(db, _save_get.call())) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役) - - -func on_replace_chosen(discard: int) -> void: - var adventure = _adventure_get.call() - if adventure == null or not adventure.resolve_curio_replace(discard): - push_error("[bonus_interact] 同调位替换受理被拒——见输出日志") - - # 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续) func on_resolved(result: Dictionary) -> void: _pending_curio = {} diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 312ab523..7fcc3eea 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -294,7 +294,6 @@ func _build_ui() -> void: _open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations, _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount})) curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed) - curio_overlay.replace_chosen.connect(_bonus.on_replace_chosen) curio_overlay.continue_pressed.connect(_bonus.on_continue) curio_overlay.refresh_requested.connect(_bonus.on_refresh) move_child(curio_overlay, _end_overlay.get_index()) @@ -488,7 +487,6 @@ func _wire_adventure() -> void: # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 adventure.curio_choice_presented.connect(_bonus.on_presented) adventure.curio_resolved.connect(_bonus.on_resolved) - adventure.treasure_replace_presented.connect(_bonus.on_treasure_replace_presented) adventure.adventure_finished.connect(_on_adventure_finished) adventure.level_started.connect(_on_adventure_level_started) adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) diff --git a/game/presentation/views/curio_pick_overlay.gd b/game/presentation/views/curio_pick_overlay.gd index 637c2bb3..a47113ea 100644 --- a/game/presentation/views/curio_pick_overlay.gd +++ b/game/presentation/views/curio_pick_overlay.gd @@ -1,20 +1,19 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-10-01(迭代 10 奖励重构批——level-clear.md v2 三选一选择面承载) -# 提升三选一覆盖层(模态):非末关通关的选择停点呈现——部分回血反馈 + 三卡点选 + 确认兑付 + -# 同调位替换子交互。高内聚边界:文案拼装归 Narrator(组合根喂入),选择意图经信号外发; -# 视图零核心引用。交互范式 = node_overlay choice_box 按钮族 + 选路两段式(点选→确认)复用,零新原语。 -# 状态机:pick(三卡点选→确认)→ [replace(满位 N+1 选 N)] → result(兑付反馈→继续)。 +# 提升三选一覆盖层(模态):非末关通关的选择停点呈现——部分回血反馈 + 三卡点选 + 确认兑付。 +# 高内聚边界:文案拼装归 Narrator(组合根喂入),选择意图经信号外发;视图零核心引用。 +# 交互范式 = node_overlay choice_box 按钮族 + 选路两段式(点选→确认)复用,零新原语。 +# 状态机:pick(三卡点选→确认)→ result(兑付反馈→继续)。〔replace 满位替换子态随迭代 22 T5 +# 拆除——装配不可变契约 1(冒险中零更换途径),core 写口与组合根接线同批摘除。〕 class_name CurioPickOverlay extends Control signal pick_confirmed(item_id: String) -signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取 signal continue_pressed signal refresh_requested # 三选一刷新意图(迭代 11 第①批 T5b——P1 甲 X/Y 双源;重抽归组合根) var _label: Label var _choice_box: HBoxContainer # 三卡区(pick 态——M7 bcard 竖卡) -var _replace_box: VBoxContainer # 替换子面板(replace 态) var _confirm_btn: Button # 确认钮(点选后点亮——两段式第二拍) var _continue_btn: Button # 继续钮(兑付反馈态唯一出口) var _refresh_btn: Button # 刷新钮(迭代 11 第①批 T5b——余量>0 才显示;M7 升顶条图标钮) @@ -33,7 +32,7 @@ func _init() -> void: # T7(UI 设计对齐批 2026-10-06):全屏页形态(demo ⑥ #v-bonus——与商店 T6 同壳 page_shell, # 根底暗档)——56px 通栏顶条(标题+刷新图标钮/角标)+ 居中入账信息行(GP 汇总行承载)+ # bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。 - # 状态机不变:pick→[replace]→result。M7 同构裁决遗产保留。 + # 状态机不变:pick→result。M7 同构裁决遗产保留。 var page: Dictionary = UiKit.page_shell(self, false) var top: HBoxContainer = page.top_row top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM)) @@ -79,11 +78,6 @@ func _init() -> void: foot.add_child(_continue_btn) _foot_hint = UiKit.label("免费选择 · 刷新不消耗 AP",UiKit.FS_HINT, false, UiKit.WARM_FAINT) foot.add_child(_foot_hint) - _replace_box = VBoxContainer.new() - _replace_box.add_theme_constant_override("separation", 8) - _replace_box.alignment = BoxContainer.ALIGNMENT_CENTER - _replace_box.size_flags_vertical = Control.SIZE_EXPAND_FILL - box.add_child(_replace_box) visible = false @@ -115,9 +109,8 @@ func open_pick(header: String, cards: Array) -> void: _bcards.append(card) _choice_box.visible = true _confirm_btn.visible = false - _replace_box.visible = false _continue_btn.visible = false - _foot_hint.visible = true # T7 底注=pick 态专属(replace/result 态让位) + _foot_hint.visible = true # T7 底注=pick 态专属(result 态让位) _refresh_btn.visible = false # 刷新面随 set_refresh_display 投喂后现形(open 先归零防残留;店内门票面不投喂=无刷新) _refresh_l.text = "" visible = true @@ -183,40 +176,15 @@ func show_result(text: String) -> void: _label.text = _header + "\n" + text _choice_box.visible = false _confirm_btn.visible = false - _replace_box.visible = false _continue_btn.visible = true _refresh_btn.visible = false # 刷新=选择态专属功能(结果态停点已兑付——残钮零意图可点) _foot_hint.visible = false visible = true -# 同调位替换子态(treasure_replace_presented 到达):items = [{index, item_id, display_name}](组合根 -# 喂显示名)。两路出口:丢旧收新(逐件钮)/ 放弃新件——终态恒 ≤ 同调位由核心受理面保证。 -func open_replace(items: Array, incoming_name: String, holding: int, slots: int) -> void: - _label.text = "同调位已满(%d/%d)——收下「%s」需丢弃一件已有道具,或放弃新件" % [holding, slots, incoming_name] - for c in _replace_box.get_children(): - c.queue_free() - for it in items: - var tag: int = int(it.get("index", it.get("handle", -1))) # 通关渠道=携带序号 / 节点渠道=库存句柄(核心分发面裁决) - var btn := UiKit.button("丢弃「%s」换新" % String(it.get("display_name", it.get("item_id", "")))) - btn.pressed.connect(_emit_replace_chosen.bind(tag)) - _replace_box.add_child(btn) - var decline := UiKit.button("放弃新件(不获取)") - decline.pressed.connect(_emit_replace_chosen.bind(-1)) - _replace_box.add_child(decline) - _choice_box.visible = false - _confirm_btn.visible = false - _replace_box.visible = true - _continue_btn.visible = false - _refresh_btn.visible = false # 刷新=选择态专属功能(替换子态不换候选) - _foot_hint.visible = false - visible = true - - # 收屏(组合根在 continue_pressed 处理毕调——推进下一关同拍) func close_pick() -> void: _choice_box.visible = false - _replace_box.visible = false _continue_btn.visible = false _confirm_btn.visible = false _refresh_btn.visible = false @@ -228,10 +196,6 @@ func _emit_continue_pressed() -> void: continue_pressed.emit() -func _emit_replace_chosen(tag: int) -> void: - replace_chosen.emit(tag) - - # M7:id → 手绘图标键(子串映射容错 id 词表演进;缺位=BCard 空图标槽不炸) func _icon_for_id(idl: String) -> String: if idl.contains("heal"): diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index 89b61532..1b847372 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -150,10 +150,9 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger)) # 三选一停点驱动(迭代 10 奖励重构批——big_reward_paid 退役:确定性策略候选取首件、 - # 道具件满位替换放弃新件、兑付后推进下一关——零随机流自开,同种子逐位可复放) + # 兑付后推进下一关——零随机流自开,同种子逐位可复放;满位替换策略随迭代 22 T5 拆除) adr.curio_choice_presented.connect(_on_adventure_bonus.bind(st, adr)) adr.curio_resolved.connect(_on_adventure_curio_resolved.bind(st, adr)) - adr.treasure_replace_presented.connect(_on_adventure_treasure_replace.bind(st, adr)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) # 同调分层注入(迭代 11 第①批 T8——缺省 {} 与既有调用方逐位一致零行为变更) @@ -226,7 +225,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, if ncands.is_empty(): r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留) else: - adr.present_node_curio(ncands, ndraw.get("aux_detail", []) as Array) + adr.present_node_curio(ncands) var npick: Dictionary = _pick_token(ncands, String(st.get("feat_mode", "off"))) if adr.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))): # T5 门票拿到即开——候选已被替换为专长三选一,就地再选一轮(确定性策略同款 316-f); @@ -236,8 +235,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, if not fcands.is_empty(): var fpick: Dictionary = _pick_token(fcands, String(st.get("feat_mode", "off"))) adr.apply_node_curio(String(fpick.get("kind", "")), String(fpick.get("tier", "")), String(fpick.get("feat_id", ""))) - if not adr._node_curio_pending.is_empty() and adr._node_curio_pending.has("incoming"): - adr.resolve_curio_replace(-1) # 满位替换:放弃新件 else: st.result = "advance_failed" elif t == "rest": @@ -310,13 +307,6 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction return best -# 同调位满位替换子交互(确定性策略:放弃新件不获取——分发面按停点上下文裁决节点/通关渠道) -static func _on_adventure_treasure_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void: - st.reward_events.append({"treasure_replace": incoming.duplicate(true)}) - if not adr.resolve_curio_replace(-1): - st.result = "advance_failed" - - # 三选一兑付完成 → 上下文分派(通关停点=推进下一关 / 奖励节点=回图选路) static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"curio_resolved": result.duplicate(true)}) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index cb0c5b65..c39bcf9e 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -649,7 +649,7 @@ func _smoke_adventure() -> void: break check(not has_attack13, "奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13)) - a13.present_node_curio(ncands13, ndraw.get("aux_detail", []) as Array) + a13.present_node_curio(ncands13) a13.present_node_curio([{"kind": "gp", "tier": "common"}]) var gp13 := int(a13.ledger.get("gp", 0)) check(a13.apply_node_curio("gp", "common") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.curio_value("gp", "common")), @@ -750,7 +750,7 @@ func _smoke_feats() -> void: var stock_before := {} for kk in ["attack", "gp", "heal"]: stock_before[kk] = a_t.pool_stock(kk) - a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}], "aux_detail": []} + a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]} check(a_t.apply_node_curio("feat_ticket", "excellent"), "A5 门票兑付受理(节点渠道——拿到即开)") var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array) var tkt_all_relic := tkt_cands.size() > 0 @@ -1040,11 +1040,9 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: var ndraw: Dictionary = a.node_curio_candidates() var ncands: Array = ndraw.get("candidates", []) as Array if not ncands.is_empty(): - a.present_node_curio(ncands, ndraw.get("aux_detail", []) as Array) + a.present_node_curio(ncands) var npick: Dictionary = autoplay._pick_token(ncands) - if a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))): - if not a._node_curio_pending.is_empty() and a._node_curio_pending.has("incoming"): - a.resolve_curio_replace(-1) + a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))) r.continue_after_node() elif t == "rest": r.continue_after_node() diff --git a/game/smoke/ui_audit_probe.gd b/game/smoke/ui_audit_probe.gd index 4ed59fb4..5f06115b 100644 --- a/game/smoke/ui_audit_probe.gd +++ b/game/smoke/ui_audit_probe.gd @@ -34,7 +34,7 @@ func _ready() -> void: main.save_manager.set_breakpoint({ "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 4, "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], - "bonus_aux_carry": [], "keystone_drops": [], "bonus_taken": {}, + "keystone_drops": [], "bonus_taken": {}, }) main._refresh_resume_button() await get_tree().process_frame diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 322fb9a6..a4cd9149 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -262,14 +262,12 @@ func _ready() -> void: continue if main.curio_overlay.visible: # 迭代 10 三选一停点(CurioPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配—— - # 本探针此前未适配该层,走到即卡死)。三态分派:替换子态(满位——放弃新件)/ - # result 态(兑付反馈——点继续推进)/ 待选态(点首件+确认)。真实信号路径全线。 - # T1 D1——三选一停点输入锁定断言(停点态断言面:替换子/待选/兑付三态共用此层)。 + # 本探针此前未适配该层,走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/ + # 待选态(点首件+确认)。真实信号路径全线。〔替换子态随迭代 22 T5 拆除——装配不可变契约。〕 + # T1 D1——三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。 if not main._input_locked(): _problems.append("三选一停点打开时输入未锁定") - if main.curio_overlay._replace_box.visible: - main.curio_overlay.replace_chosen.emit(-1) - elif main.curio_overlay._continue_btn.visible: + if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() elif main.adventure != null and not main.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) @@ -409,9 +407,7 @@ func _ready() -> void: else: break elif main.curio_overlay.visible: - if main.curio_overlay._replace_box.visible: - main.curio_overlay.replace_chosen.emit(-1) - elif main.curio_overlay._continue_btn.visible: + if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() elif main.adventure != null and not main.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index cd15b8f0..cd4daa84 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -198,7 +198,7 @@ func _resume_bp_fixture() -> Dictionary: return { "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0, "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], - "bonus_aux_carry": [], "keystone_drops": [], "bonus_taken": {}, + "keystone_drops": [], "bonus_taken": {}, "bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}}, } diff --git a/game/tests/unit/presentation/reward_overlay_test.gd b/game/tests/unit/presentation/reward_overlay_test.gd index 13fdd17b..3c27e989 100644 --- a/game/tests/unit/presentation/reward_overlay_test.gd +++ b/game/tests/unit/presentation/reward_overlay_test.gd @@ -1,7 +1,8 @@ extends SceneTree # 三选一覆盖层 UI 直验套件(迭代 7 T5-hotfix 转正;迭代 10 追加批随面迁移改写)—— # 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 CurioPickOverlay 后, -# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。 +# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/结果态。 +# 〔替换子面板两路段随迭代 22 秘宝批 T5 拆除——装配不可变契约 1,overlay replace 态退役。〕 # 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0) # T6 收口批:断言走 test_kit 计数(Kit.check)+ 失败即停早退协议保留(顺序状态机——坏态下后继断言无效)。 const Kit := preload("res://tests/unit/test_kit.gd") @@ -21,10 +22,8 @@ func _run() -> void: var ov = load("res://presentation/views/curio_pick_overlay.gd").new() root.add_child(ov) var picked := [] - var replaced := [] var continued := [] ov.pick_confirmed.connect(func(id: String): picked.append(id)) - ov.replace_chosen.connect(func(tag: int): replaced.append(tag)) ov.continue_pressed.connect(func(): continued.append(true)) # ① 选择态:三卡呈现、确认钮隐藏(两段式——点选前不给确认) ov.open_pick("第 1 层通过!", [ @@ -68,29 +67,11 @@ func _run() -> void: if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked), ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"): return - # ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通) - ov.open_replace([{"index": 0, "item_id": "incense_stick", "display_name": "线香"}], "天眼", 1, 1) - var replace_btns := 0 - for c in ov._replace_box.get_children(): - if c is Button: - replace_btns += 1 - if not _req("替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns, - replace_btns == 2 and not ov._choice_box.visible): - return - var replaced_first := false - for c in ov._replace_box.get_children(): - if c is Button: - c.pressed.emit() - replaced_first = true - break - if not _req("丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced), - replaced_first and replaced.size() == 1 and int(replaced[0]) == 0): - return - # ④ 结果态:继续钮点亮、唯一出口(刷新钮随选择态收——停点已兑付残钮零意图可点) + # ③ 结果态:继续钮点亮、唯一出口(刷新钮随选择态收——停点已兑付残钮零意图可点) ov.set_refresh_display(2, 20, false) ov.show_result("已选择:攻击力提升 · 攻击 +1") - if not _req("结果态应点亮继续且收替换面板与刷新钮", - ov._continue_btn.visible and not ov._replace_box.visible and not ov._refresh_btn.visible): + if not _req("结果态应点亮继续且收三卡与刷新钮", + ov._continue_btn.visible and not ov._choice_box.visible and not ov._refresh_btn.visible): return ov.press_continue() if not _req("继续未外发 continue_pressed", continued.size() == 1): @@ -98,4 +79,4 @@ func _run() -> void: ov.close_pick() if not _req("close_pick 应收屏", not ov.visible): return - quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过")) + quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/结果态/收屏 全过")) From 9f20ca236af9f15495a85a37b7559ea67abb66bd Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:36:31 +0800 Subject: [PATCH 6/8] =?UTF-8?q?T6(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E9=AA=8C=E6=94=B6=E9=97=B8=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E2=80=94=E2=80=94V9=E9=9D=99=E6=80=81=E8=AF=8D=E8=A1=A8(?= =?UTF-8?q?=E6=96=B012=E8=A1=8Cdo.kind/effect=5Fkinds=E9=9B=B6=E6=A3=8B?= =?UTF-8?q?=E7=9B=98=E6=94=B9=E5=86=99=E7=B1=BB=E8=AF=8D)+V10=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E6=A3=80=E6=9F=A5(=E4=B8=89=E6=96=B9=E5=90=91dimensio?= =?UTF-8?q?n=5Ftags=E6=8A=95=E5=BD=B1)+V18=E8=A3=85=E9=85=8D=E9=9D=A2?= =?UTF-8?q?=E7=A9=BA=E7=9B=AE=E5=BD=95=E5=90=88=E6=B3=95=E6=80=81(?= =?UTF-8?q?=E7=99=BD=E6=9D=BF=E6=A1=A312=E4=BB=B6=E5=85=A8=E6=9C=AA?= =?UTF-8?q?=E8=A7=A3=E9=94=81mode=E5=88=86=E6=B4=BE,=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=A1=A3[12])+V6=E8=A3=85=E9=85=8D=E4=B8=8D=E5=8F=AF=E5=8F=98?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=96=AD=E8=A8=80(=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E2=91=AD=E6=88=98=E6=96=97=E8=BF=90=E8=A1=8C=E6=80=81=E4=B8=8E?= =?UTF-8?q?=E5=86=92=E9=99=A9=E8=A3=85=E9=85=8D=E9=9B=86=E4=B8=89=E9=9D=A2?= =?UTF-8?q?=E5=90=8C=E6=BA=90=E5=BF=AB=E7=85=A7)=E3=80=82=E5=86=92?= =?UTF-8?q?=E7=83=9F898/0+=E5=8D=95=E6=B5=8B=E5=85=A8=E7=BB=BF=E3=80=94?= =?UTF-8?q?=E7=A7=98=E5=AE=9D=E6=89=B9=20V6/V9/V10/V18=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/smoke_all.gd | 37 ++++++++++++++++++- game/tests/unit/core/treasure_effects_test.gd | 14 +++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index c39bcf9e..13034700 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -287,6 +287,41 @@ func _smoke_adventure_table() -> void: check(orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) and not orch_e.attune_has("morning_order"), "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕") + # V9 静态词表断言(迭代 22 T6——棋盘事实不可变契约 3:新 12 行 triggers do.kind 与 effect_kinds + # 全集零棋盘改写类词〔rewrite_board 已除名/remove_mines/cell_removal=改写族——remove_mines 现役 + # 语义=爆破工兵包旧通道随行退役,新表零引用〕;效果只允许信息呈现与开格动作) + var rewrite_words := ["rewrite_board", "remove_mines", "cell_removal"] + var v9_ok := true + for r9 in trows: + for k9 in (r9.get("effect_kinds", []) as Array): + if rewrite_words.has(String(k9)): + v9_ok = false + for tr9 in (r9.get("triggers", []) as Array): + if rewrite_words.has(String((tr9 as Dictionary).get("do", {}).get("kind", ""))): + v9_ok = false + check(v9_ok, "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕") + # 覆盖检查(迭代 22 T6——效果三方向覆盖:信息〔巢穴预告/线香/天眼/阔视野带〕/行动资源〔晨操令/ + # 双管装填/试炼镐/储备罐〕/操作能力〔远程镐/信标网络〕+抉择刷新〔刷新练习/刷新执照〕——dimension_tags 静态投影) + var tag_set := {} + for r10 in trows: + for tg in (r10.get("dimension_tags", []) as Array): + tag_set[String(tg)] = true + check(tag_set.has("info") and tag_set.has("action") and tag_set.has("plan"), + "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕") + # V18 装配面空目录合法态(迭代 22 T6——白板档全未解锁:柜台投影 12 行全未解锁+mode 分派正确; + # 池空兜底/商店空货架两部分归奇物/商店侧另批〔卡片 §4〕) + var mp_v18 = load("res://core/meta_progression.gd").new(_db, _meta_new_save(_META_SMOKE_PATHS[12])) + var v18_ok := true + var locked_n := 0 + for st18 in mp_v18.treasure_shop_status(): + if bool(st18.get("unlocked", false)): + v18_ok = false + else: + locked_n += 1 + if (String(st18.get("mode", "")) != "keystone" and String(st18.get("mode", "")) != "condition"): + v18_ok = false + check(v18_ok and locked_n == 12, + "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕") # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) @@ -4509,7 +4544,7 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry "user://smoke_meta_cap.json", "user://smoke_meta_white.json", "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json", "user://smoke_meta_tree.json", "user://smoke_meta_career.json", - "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json"] + "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json"] func _meta_smoke_cleanup() -> void: diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 89fc353e..fc39a852 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -259,6 +259,20 @@ func _init() -> void: Kit.check_quiet(int(vc13.get("used", 0)) == 1, "计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)") + # ⑭ 装配不可变(迭代 22 秘宝批 T6——V6:冒险全程装配快照三面同源且零写口—— + # 写口唯一=营地 toggle_attunement〔meta_progression〕,runner/level/battle 无装配写口) + var rng14 = load("res://core/rng_service.gd").new(); rng14.set_master_seed(331) + var adr14 = runner_s.new(db, rng14) + adr14.set_save_manager(save_s.new("user://treasure_effects_v6.json")) + Kit.check_quiet(adr14.start_adventure(331, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order", "wide_scout_band"]}), + "装配不可变:双件装配冒险启动") + Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战") + var snap14: Array = (adr14.runner()._attunement as Array).duplicate() + var ob14 = adr14.runner().current_battle() + Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == snap14 + and (adr14.runner()._attunement as Array) == snap14, + "装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)") + quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕")) From f7cee4b5817c56ced58eb309726da133fbf92875 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:41:36 +0800 Subject: [PATCH 7/8] =?UTF-8?q?T6=E8=A1=A5(=E8=BF=AD=E4=BB=A322=E7=A7=98?= =?UTF-8?q?=E5=AE=9D=E6=89=B9):=20ui=5Fprobe=E7=A7=98=E5=AE=9D=E5=8C=A3?= =?UTF-8?q?=E6=9F=9C=E5=8F=B0=E8=A1=8C=E6=95=B0=E6=96=AD=E8=A8=80=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3(6=E2=86=9212=E2=80=94=E2=80=94T2=E6=8D=A2=E4=BB=A3?= =?UTF-8?q?=E9=9D=A2=E5=91=88=E7=8E=B0=E6=96=AD=E8=A8=80=E9=9A=8F=E6=89=B9?= =?UTF-8?q?,=E6=89=B9=E6=9C=AB=E9=87=8D=E9=97=B8--full=E9=A6=96=E8=B7=91?= =?UTF-8?q?=E6=8A=93=E5=87=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/ui_probe.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index acb16f5d..78ec22ce 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -48,8 +48,8 @@ func _ready() -> void: 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 前置未亮应禁用(点亮前置语义)") - if main.camp_overlay._workshop_rows.size() != 6: - problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size()) + if main.camp_overlay._workshop_rows.size() != 12: + problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size()) main._on_camp_closed() await get_tree().process_frame if main.camp_overlay.visible or not main._start_overlay.visible: From 44fee05b14b569fe5efda4a41ad46fb93284a68e Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:45:28 +0800 Subject: [PATCH 8/8] =?UTF-8?q?T7(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5=E9=97=B8?= =?UTF-8?q?=E2=80=94=E2=80=94registry=E4=B8=89=E4=BB=A3=E7=9B=AE=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3=E6=B3=A8=E8=AE=B0(aux=5Fitems=E8=BF=AD=E4=BB=A322?= =?UTF-8?q?=E6=89=B9/attunement=5Fgrowth=E8=A1=A8=E5=88=A0=E5=8F=B2?= =?UTF-8?q?=E6=B3=A8/attunement=5Fslots=E6=A0=91=E7=82=B9=E4=BA=AE+?= =?UTF-8?q?=E6=A1=A3=E4=BD=8D=E6=A0=BC/save=E5=9F=9Fboss=5Fkill=5Fcount?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E6=8D=A2=E4=BB=A3+vision=5Fused=E6=96=B0?= =?UTF-8?q?=E5=9F=9F)+06=E7=A7=98=E5=AE=9D=E6=9D=A1=E5=B7=B2=E5=85=A5?= =?UTF-8?q?=E5=BA=93=E7=8A=B6=E6=80=81+07=E3=89=91=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E8=A7=A3=E9=94=81-=E8=A3=85=E9=85=8D=E6=AD=A3=E5=B8=B8?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E8=A7=82=E6=B5=8B=E9=A1=B9+roadmap=E8=BF=AD?= =?UTF-8?q?=E4=BB=A322=E8=A1=8C=E2=9C=85+=E5=AE=9E=E7=8E=B0=E6=8A=A5?= =?UTF-8?q?=E5=91=8A(=E4=BB=A3=E6=8B=8D=E6=9D=BF=E8=BF=BD=E8=AE=A4?= =?UTF-8?q?=E5=8D=95=E5=8D=81=E4=BA=94=E6=9D=A1/GDD=E5=8F=8D=E5=93=BA?= =?UTF-8?q?=E7=82=B9=E4=B8=83=E6=9D=A1/=E5=AE=9E=E6=9C=BA=E7=9B=AE?= =?UTF-8?q?=E6=A3=80=3D07=E3=89=91/=E9=97=B8=E8=AF=BB=E6=95=B0/=E4=BD=99?= =?UTF-8?q?=E9=A1=B9=E5=8E=BB=E5=90=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/06-待办清单.md | 2 +- design/gdd/07-体验验证清单.md | 8 +++ design/qa/iter22-实现报告-2026-10-12.md | 67 +++++++++++++++++++++++++ design/registry/entities.yaml | 7 ++- roadmap.md | 2 +- 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 design/qa/iter22-实现报告-2026-10-12.md diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index d4a62f8d..67ec6b38 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -176,7 +176,7 @@ **归属**:[秘宝](systems/跨局循环/秘宝.md)「目录(重设计 v1)」实装 + [道具退役素材库](../research/道具退役素材库.md) 对应清理面 -**状态**:已编入迭代 22(2026-10-11 用户令;需求卡片 v1.2 零待裁——开工门=U0) +**状态**:✅ **已入库(2026-10-12,/goal+iter U0 开工令——worktree g-plan2-iter22-treasure,T1-T6 五提交全量交付;批末重闸全绿=冒烟 898/0+run_all 22 套+probe 含 click/full+render_gate+doc 三闸对基线零新增+熵增 PASS;实现报告=design/qa/iter22-实现报告-2026-10-12.md,代拍板追认单与 GDD 反哺点随报告待用户)。残留人闸=实机目检秘宝解锁-装配正常流程体验(07 ㉑ 在册)**。条目②旧数据行清理随 T1 全量换代销号;③专长侧联动已由迭代 20 承载销账(进展注记在案);magic_vs_magic 交叉核对已结(魔网织者在删除名单——锻造工坊批③作废,2026-10-11 D 组核结);本条目余项(①④⑤⑥⑦)随本批交付或经反哺点/另批承接,逐项去向见实现报告 §六 **进展注记(2026-10-10)**:数据文件面本批不依赖锻造工坊线;**槽位面依赖修正(2026-10-10 设计骨架批后)**——同调位数量与品质的语义已裁归天赋树同调位节点(树侧接线归天赋树线/天赋树实装批),本批与该批存在槽位定序点(先树后宝或合并评估,随排期裁),现役谓词表求值作为本批落地前的过渡基座仍成立;秘宝体系文档正确性检查已过(五文件交叉核对:仅 registry 三处现行旧称「魔法道具」残留,已随检查批修正;其余命中均为版本历史与更名记载的合法引述)——**检查通过;设计侧裁点经 grilling 审查轮+设计骨架批已清零(秘宝.md v2.3),按三段流程设计段完成**。**③专长侧联动已拆出独立成卡(2026-10-11 用户发令;同日用户纠正=奇物线单独成卡、秘宝面不并入)**:design/research/需求卡片-专长实装批-2026-10-11.md(T0-T10 ≈10-12d 单迭代交付;**Q1-Q3 已全裁清零**〔Q1 Boss 兑付换代/Q2 占位值/Q3 单迭代〕——**卡片成立,零待裁点;✅ 专长实装批已入库(2026-10-11 迭代 20 T0-T10 全落 master——③专长侧联动由该批承载销账:feats.tres 29 行换代/词表扩签/停手挂点 dig_stop 全落)**;奇物侧设计+命名+裁定齐备〔奇物.md v3.15〕)——本条目余项(①②④⑤⑥⑦)维持冻结待发令,其中⑥效果引擎扩签核实与停手挂点由专长实装批 T1 先行承载。两条线交叉核对点一处随本批 T0:feats.tres 的 magic_vs_magic 条件值所属专长件是否在素材库表二 27 件删除名单内(若在,锻造工坊实装批③「改值复活」随之作废)——**已核结(2026-10-11 锻造工坊裁决会话 D 组)**:所属件=魔网织者(mana_weaver),在名单内,③「改值复活」作废,废卡随专长 27 件退役删行批消解。 diff --git a/design/gdd/07-体验验证清单.md b/design/gdd/07-体验验证清单.md index 89ec155e..5d0921a5 100644 --- a/design/gdd/07-体验验证清单.md +++ b/design/gdd/07-体验验证清单.md @@ -201,6 +201,14 @@ - **连带面**:暂停层属性锻造段已消失(钥石+工坊+天赋树概览);生涯页两类六项(三柜台概览行已删);局内六键体感(先手首战首轮 +1AP/锋踩雷减半/守标错下轮 +AP/聚结算框 GP 即实得/散货架八折标价)。 - 验证方式:用户实机目检(本批自动闸外唯一人闸)。 +### ㉑ 秘宝解锁-装配正常流程体验(迭代 22 秘宝体系实现重构落地批随批登记——2026-10-12) + +- **解锁面**:攒钥石→秘宝匣柜台钥石购买(余额不足钮禁用、购买后行板即时刷新为已解锁);条件件进度呈现(远程镐=通关 1 次/刷新执照=Boss 击杀 3 次/天眼=通关 3 且 Boss 4/信标网络=真视使用 10——达标未解锁时开营地/冒险收束即自动入账)。 +- **装配面**:槽位=树同调位点亮数(未点亮树=0 槽不能装配);件档≤格档(普通/优秀/稀有递增向下兼容——稀有件装普通格被拒);未解锁件装配钮禁用;满位禁选。 +- **生效面抽查**:晨操令/双管装填首轮 AP、阔视野带视野 8→12、巢穴预告开局报雷数 toast、线香单格真视(战斗级一次)、天眼 3×3 安全格直开(关卡级一次)、试炼镐 HUD 兑换钮积点滑耗、储备罐停手转存变厚、远程镐距 2 可挖、信标网络任意格插旗、刷新练习/刷新执照加仓。 +- **装配不可变**:冒险中无任何换装入口(三选一/奖励节点/战斗内零替换交互——旧替换子交互已拆)。 +- 验证方式:用户实机目检(本批自动闸外唯一人闸;能力验证半已由自动化闸覆盖=V1-V19)。 + ## 验收读数重测义务(履行留痕 + 现行义务) **已履行**(按序换代,旧读数各随其批作废留痕不入本单):①验收读数重测批(2026-09-19——Boss 单独出列/承伤带/通关率基线全量重测,硬化翻倍期·乏力死效果期·充能期旧读数作废);②迭代 5 基线重测(统一随机流后 MC+verifier 双窗);③**数值修复批新尺重测(2026-10-01)**——模拟档位重构后全量重测,新锚 `sim-data/runsum_numfix_final_n200_20261001.jsonl`,全旧带作废只测不判(numfix 报告(已退役,git 历史可查));④**迭代 10 奖励重构批重锚(2026-10-01)**——四代锚 77000/78000/79000/80000(改前/重构后/共享池后/掷定前置后,各 400 局),普通 L1 41.8%/L2 10.2% 带内(迭代10通关率读数(已退役,git 历史可查));⑤**T19 真人标定局收口(2026-10-03 用户裁)**——有效局 2(1 通关 1 死于 L3 首战,T19真人试玩读数(已退役,git 历史可查)),用户裁定**佐证级收口不再补局**;k 预期带同日立(**正常 >0.5、预警 <0.3**),字段随 81000 重锚入库后补算终值。 diff --git a/design/qa/iter22-实现报告-2026-10-12.md b/design/qa/iter22-实现报告-2026-10-12.md new file mode 100644 index 00000000..e71592e2 --- /dev/null +++ b/design/qa/iter22-实现报告-2026-10-12.md @@ -0,0 +1,67 @@ +# 迭代 22 实现报告——秘宝体系实现重构落地批(2026-10-12) + +> 执行:/goal+iter 技能 U0 开工令,单会话交付(worktree g-plan2-iter22-treasure,分支 iter22-treasure,基点 5a2ab8c)。 +> 需求卡片:design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.3,C1-C6 全裁,零阻塞待裁点)。 +> 设计权威:秘宝.md v2.9「目录(重设计 v1)」。 + +## 一、各任务卡交付状态 + +| 卡 | 交付 | 提交 | +|---|---|---| +| T0 | 基线复读(冒烟 886/0+run_all 22 套+probe --click 全绿)+四项核实(X/Y 对表/magic_vs_magic 结案/扩签清单/真视计数接入点)+自洽扫描四处事实换代登记(卡片 F1/F2/F4 预设被迭代 20/21 推进) | (active.md 状态记录 477) | +| T1 | treasure_items.tres 新 12 行全量换代(旧 12 行删)+schema 换代(tier/charge_reset/unlock_keystone_cost 三键+TIERS/TIER_RANK/CHARGE_RESETS 词表+TARGETS 扩 cell+参数面 kind 六型+REVEAL_KINDS 护栏扩面+charges=0 积点制+解锁互斥校验+UNLOCK_COUNTER_KEYS 换代)+引擎扩签(EVENTS+board_installed/ACTIVE_KINDS+safe_dig/EFFECT_KINDS+mine_count_preview)+V13 旧断言摘除换代 | T1 提交 | +| T2 | 解锁服务换代(buy_treasure_unlock/refresh_treasure_unlocks/treasure_shop_status/_treasure_unlock_progress 单点)+工坊渠道面退役(行族/词表/三函数/访问器全删)+vision_used 生涯计数域(补零不升版本)+toggle_attunement 三重校验(未解锁拒+满位禁选+件档 rank≤装入位序)+秘宝匣呈现数据管道换代 | a7bd4e4 | +| T3 | board_installed 发射+巢穴预告读数口/天眼 safe_dig=fx_reveal 通道复用(C5 全额兑付免 AP)/reveal_tiles 入真视叠加层/试炼镐 use_attune_convert 积点受理口+计数通用化/充能重置周期 _reseed_attune_charges(battle/level)/常时参数面注入(挖距/旗可达/转存率/刷新加仓/商店刷新)/vision 计数链(共享 dict→summary→settle 入档+断点随件)/validate_use 扩 cell+新空间 kind/修现役潜伏缺陷(use_treasure 板维度读不存在的 Rules.width)+新单测 treasure_effects_test(V7 附属表 12 件逐件正常/边界/反例) | T3 提交 | +| T4 | 秘宝匣柜台购买钮+分档呈现+档位标签+未解锁禁装/呈现桥换代(旧提灯布尔桥退役→set_fog_glow_bonus 参数面,main._sync_treasure_glow)/巢穴预告 toast/试炼镐 HUD 兑换钮(ap_bar+board_zone 转发链)/ui_input_probe case⑮ 换代主语+4/V13 功能残留换代/修 BoardZoneView 缺 convert_pressed 转发(呈现组装挂死根因) | b7a2ead | +| T5 | 替换子交互整体拆除(装配不可变契约 1 死通道:core 六函数+_bonus_treasure_carry 携带+_last_draw_treasure 掷定链+bonus_aux_carry 断点键+grant_treasure_item+holding_records+呈现三态+overlay replace 状态机+探针/autoplay 策略)/treasure_inventory 保留注记(主动件骑库存承载在役)/apply_node_curio 普通兑付主体误删补回 | 7095505(amend 后) | +| T6 | 验收闸断言(V9 词表零改写/V10 三方向覆盖/V18 空目录合法态/V6 装配不可变三面同源快照)+批末重闸全绿+ui_probe 柜台行数断言换代(6→12) | T6 两提交 | + +## 二、代拍板追认单(十五条,待用户逐条追认) + +1. **回补点 1 执行**:按设计权威 12 件全量落地,刷新练习归钥石购买(C1「普通档全钥石」直接应用)——卡片推荐案。 +2. **回补点 2 消解**:迭代 20 已删 venom_edge/flag_burst,本批实删 12 行(非 14)。 +3. **回补点 3 执行形态**:treasure_inventory **部分拆除+保留注记**——拆除=替换子交互死通道全清(装配不可变实现面保证);保留=库存类本体(新目录主动件线香/天眼的骑库存承载在役:handle 注入+use_treasure 受理+断点 aux_items 快照)。卡片「拆除或保留注记」双路线并写的第三形态,理由=T3 复用现役机制零新建。 +4. **试炼镐两级积点单级化**:设计「每开 4 安全格积 1 点、满 2 点换 AP+1」→ 实现=ap_charge_safe charge_n=8 滑耗(行为等价:每 8 格兑 1 AP,免上限滑动消耗)——代拍板白名单「订阅行等价实现细节」;desc 保留设计原文两级表述。 +5. **试炼镐受理口与入口**:绕库存直呼 use_attune_convert(charges=0 零骑库存)+HUD 行动条兑换钮(ap_bar convert_pressed 转发链,未装配隐藏、积点未达禁用、文案含进度 N/M)——卡片未列明入口形态,本批落 HUD 最小入口。 +6. **解锁条件表落位**:卡片 T1「attunement_growth.tres 改造为解锁条件表」——该表已被迭代 21 删除,落位改为 treasure_items 行内 unlock_requires(单一事实源,schema 校验单点)。 +7. **计数键换代**:UNLOCK_COUNTER_KEYS 旧键 clear_count(旧工坊按关累计语义)与 C6 通关口径不符,换名 adventure_cleared(生涯通关局数)+扩 vision_used——旧键零行消费安全换名。 +8. **vision_used 承载**:save v6 meta 域补零新键(不升版本,迭代 21 先例)+断点随件 vision_counter_used;计数点=use_treasure 受理成功(真视类 kind),冒险共享 dict 聚合、收束 add_vision_used 入档。 +9. **呈现桥换代**:旧提灯布尔桥(set_fog_lantern/FOG_LANTERN_BONUS)随行退役,换代 set_fog_glow_bonus 参数面(阔视野带 magnitude 4→半径 8+4);off 态写 0 复原契约保持。 +10. **巢穴预告呈现**:开局 toast「巢穴预告——本板危险格共 N 个」(读数口 nest_preview_count);不走结算明细行(行来源纪律——无格来源)。 +11. **积点兑换零明细行**:对齐 use_treasure「效果即时可见零明细行」纪律,兑付反馈=AP 条即时刷新。 +12. **价格 provisional 占位**:普通档 10 钥石/优秀档 25 钥石(稀有档全条件无价)——调参批终值。 +13. **magic_vs_magic 结案维持**:行已随迭代 20 删除(grep 零命中),锻造工坊实装批③「改值复活」作废(06 已核结)。 +14. **reward_dangling_treasure_test 保留**:悬空引用拦截机制在役,注入主语=故意悬空旧 id(fixture 本体非旧件验证)——V13「随行清亡」指注入行,机制测试保留。 +15. **过程修正**:T5 提交误入拷贝嵌套素材(assets/ui/ui/ 9 png)已 amend 剔除,终态零素材变更。 + +## 三、GDD 反哺点(实现与设计权威差异——待用户逐点裁决后另批落文,本批零直改 GDD) + +1. **秘宝.md「目录(重设计 v1)」节**:逐件表补实录列(item_id/tier/解锁方式与价格或条件/参数 kind);「实装批落地前表内旧目录同调行冻结维持」「受理框架为新目录主动件沿用——现役代码按同框架运行」两处现状句已过期须换代;旧目录退役指针节补「代码旧行已删(2026-10-12)」。 +2. **秘宝.md 代价轴**:试炼镐两级积点结构→单级 8 格滑耗等价实现注记(若用户裁保两级呈现,改 desc 与 HUD 进度文案即可,机制不动)。 +3. **秘宝.md 解锁混合制节**:解锁条件表实录(三计数键+阈值 provisional:远程镐 1/刷新执照 3/天眼 3+4/信标网络 10;价格 10/25)。 +4. **元进度.md**:道具工坊→秘宝匣换代实录(工坊渠道面退役、购买钮、条件进度呈现、treasure_shop_status 投影);「forge base 20/比 150」等工坊史句核对。 +5. **camp.md**:秘宝匣柜台呈现细则(购买钮禁用态/名后缀·档位/装配行档位标签/未解锁禁装/悬停 desc/空目录合法态);暂停层秘宝匣 12 行只读概览。 +6. **存档序列化.md**:vision_used 域+vision_counter_used 断点随件+bonus_aux_carry 键退役(v6 不变零迁移口径复核)。 +7. **行动经济.md**:finish_round_stop 转存率参数化(transfer_pct 缺省 100 逐位同旧;储备罐 125=1.25:1 整除 floor 口径)。 + +## 四、实机目检清单(唯一人闸) + +07-体验验证清单 ㉑「秘宝解锁-装配正常流程体验」(本批随批登记):解锁面/装配面/生效面抽查/装配不可变四组——验证人=用户。 + +## 五、闸读数(批末全绿) + +- 冒烟 898/0(基线 886→898:V1/V2/V3/V4/V5/V9/V10/V18 断言净增,V13 摘除同步) +- run_all 22 套全绿(含新套件 treasure_effects_test) +- probe_gate --click/--full 全绿(ui_probe 柜台行数断言换代后);残留 Godot=0 +- render_gate 全绿(golden 3 屏 0.00%) +- doc 三闸对主区基线零新增(死链 3/拔旗 1 均存量);熵增闸 PASS +- MC 零跑(本批零随机无概率面——卡片 7.1 备用项未触发) + +## 六、卡片余项去向 + +- ①新目录实装 ②旧数据行清理:本批 T1-T5 全量交付销号。 +- ③专长侧联动:迭代 20 已承载销账(06 进展注记在案)。 +- ④空配置验收闸:装配面本批 V18 交付;池空兜底/商店空货架归奇物/商店侧另批(卡片 §4 去向不变)。 +- ⑤池结构选型/配额数字:奇物侧另批(卡片 §4 去向不变)。 +- ⑥效果引擎扩签核实:本批 T0 清单+T3 落地(board_installed/safe_dig/mine_count_preview/参数面六 kind)。 +- ⑦两疑似漏项:T0 核实结案——件级跨事件状态账=counter domain=adventure 承载(试炼镐积点);参数注入面=六常时参数 kind 注入点全落(_adjacent_to_hero 两分支/finish_round_stop/shop_refresh_free_left/set_fog_glow_bonus/_refresh_stock)。 diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 448a912c..395c5edd 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -85,7 +85,7 @@ entities: path: user://save.json(单档 MVP——多存档槽位 P2 保留) format: JSON 单档(人可读可调试;字典键一律 String) version: SAVE_FORMAT_VERSION=6(registry schema_version 同源;v6=生涯终身计数域 2026-10-07 迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary;v5=天赋树域 2026-10-04 迭代 12 T9——meta 三新域 tree_nodes/fork_sides/migration_note;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v6 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) - domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除) + domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——条件解锁计数源〔迭代 22 换代——原同调位里程碑语义随槽位收编退役〕,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)+ vision_used(秘宝——真视类工具累计使用,迭代 22 T2 新增,缺键补零不升版本,写入唯一经 add_vision_used 收束聚合)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除) write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档 atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) corruption_policy: 坏档改名 .corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) @@ -535,6 +535,7 @@ entities: closures: 地图战斗资源 OQ2 秘宝 侧收口(呈现深度 = 道具驱动预读;未预读格不呈现、自动寻路不做——终态与否 OQ6)· 创意风险 #1 道具侧兑现(plan 维度预读——结构闭环最后一块,呈现归 HUD框架)· 显影语义(原为雾格显影 = fog_reveal)随雾格移除失义、载体待重定(随秘宝整体重设计归 Alpha,2026-09-20 用户拍板) warnings: 预警①…④ playtest 预注册归 难度曲线与Boss(道具依赖度/预读覆盖率/AP 转存联动/稀有感知) attunement_refactor: 迭代 11 第①批 同调实施批 T1(2026-10-03)——表扩列 {form ∈ {"", passive, active_charged}(空=切片过渡形态行), charges_per_adventure, slot_cost(同调行恒 1 校验), unlock_requires 谓词行〔空=首批全解锁合法态,底稿 §2.2 用户裁〕, effective_rounds(P4 甲表值化), charge_n} + effect_kinds 扩 {venom_rider, flag_burst, mine_hit_halve, reflect_damage, ap_rounds_front, ap_charge_safe, cell_removal, reward_weight_bias, bonus_refresh}(mine_reveal/ap_grant 语义不变)+ 🔴 零裸 flat 结构护栏 = 行键白名单封闭(KEY_WHITELIST 外键=加载致命——机制轴禁无条件属性加项的结构承载);七件同调行入表(venom_edge 淬毒刃纹/flag_burst 标中殉爆/nest_counter 惊巢反制/dawn_order 工兵晨令〔effective_rounds=3〕/dig_charge 掘进蓄能〔charges=2·charge_n=3〕/luck_coin 偏运硬币/pick_refresh 三选一刷新件〔X=1〕——值全 provisional)+ 取数口径三分(attunement_rows/transitional_rows/active_rows——同调行永不入过渡渠道);nest_pendulum/blast_kit 维持过渡行 T7 原位换装,sapper_order/probe_dust/catacomb_scroll/loot_compass T7 逐件销账(底稿 §3);维度词表收缩形态 = 开放拍板点(需求卡片 P1-P8 未覆盖,底稿 §1.1 明挂——T10 追认单随批,行标签 provisional) + iteration22_refactor: 迭代 22 秘宝体系实现重构落地批(2026-10-12,T1-T3)——**新目录 12 行全量换代**(设计权威=秘宝.md「目录(重设计 v1)」:晨操令/阔视野带/试炼镐/刷新练习/储备罐/巢穴预告/线香/双管装填/远程镐/刷新执照/天眼/信标网络;旧 12 行删除——venom_edge/flag_burst 已随迭代 20 专长批迁出)+ 三档 tier ∈ {common,excellent,rare}(7/3/2,传说空集合法;TIER_RANK=装配档位校验取数源:件档 rank ≤ 装入位序)+ 解锁混合制行内单源(钥石购买 unlock_keystone_cost / 条件 unlock_requires 谓词——计数键 UNLOCK_COUNTER_KEYS 换代 {adventure_cleared〔C6 通关口径=打通整局〕, boss_kill_count, vision_used〔真视类工具累计使用——save v6 补零新域,受理成功计数、收束聚合入档〕},购买价与谓词互斥 schema 强制)+ 充能重置周期 charge_reset ∈ {adventure,battle,level}(battle=编排器每战回满/level=每关首战回满,骑库存差额补注入;charges=0=积点制合法态〔试炼镐——use_attune_convert 积点受理口,零骑库存〕)+ 常时参数面 kind 六型(light_radius_bonus/rap_transfer_mult/dig_range_bonus/flag_range_free/refresh_stock_grant/shop_refresh_bonus——无订阅行,注入面按装配集 kind 分发)+ 主动分发 safe_dig(天眼=fx_reveal 通道复用全额兑付)与 mine_count_preview(巢穴预告,board_installed 事件 T3 扩签)+ target 扩 cell(线香单格)+ 替换子交互整体拆除(装配不可变契约 1——CURIO_KINDS 除名后不可达死通道:treasure_replace_presented 链/bonus_aux_carry 断点键/grant_treasure_item/替换呈现三态全清)· 工坊解锁渠道面退役(meta.tres workshop_items 行族与 MetaProgression 工坊三函数删除,秘宝匣柜台投影=treasure_shop_status) referenced_by: [战斗结算, 地图战斗资源, 地形变异, 行动经济, HUD框架, 数据配置, 教学引导, art-bible] - name: attunement_growth @@ -546,6 +547,7 @@ entities: counter_keys: 封闭枚举 {clear_count(通关次数按关累计——save 复用零改动), boss_kill_count(Boss 击杀总数含重复——本批 T2 新增收束记账)};预留可插拔位 boss_firstkill_count/adventure_count/no_damage_clear_count/run_depth_best(加行即扩展,底稿 §2.1) schema_guards: 阈值 [1,999] · Σgrant ≤ slot_cap · 计数键出域=加载致命;数值全 provisional 归 难度曲线与Boss(形态锁死、调平衡=改表不动码) mvp_content: m1 clear_count≥1→grant 1 · m2 boss_kill_count≥3→grant 1 · m3 {clear_count≥3 AND boss_kill_count≥6}→grant 1(行值 provisional;底稿 §2.1 行示例形态) + status: **表已删除**(迭代 21 T5 槽位收编——槽位判定源=树同调位节点点亮数;本条目=史保留。迭代 22 秘宝批解锁条件面=treasure_items 行内 unlock_requires 谓词〔UNLOCK_COUNTER_KEYS 内联 schema〕,非本表复活) referenced_by: [秘宝, 元进度, 数据配置, 冒险结构] - name: attunement_slots @@ -553,7 +555,8 @@ entities: owner: design/gdd/systems/跨局循环/秘宝.md attributes: rule: 同调位=机制轴总功率闸(底稿 §2)——初始 0 起步(裁决 3)· 硬上限 slot_cap=3 · 单件占 1 槽(裁决 5,slot_cost=1 schema 强制)· 装配=冒险开始前的营地决定(底稿 §8.5 默认中途不可换装)· 装配关系入存档持久(save v4,本批 T2) - growth_source: 通关次数 + Boss 击杀数(含重复)经 attunement_growth 谓词表(裁决 4)——读数源自 transitional_sync_slots 过渡键(adventure.tres provisional)切换随本批 T3,迭代 10 欠账③清偿 + growth_source: **迭代 21 换代**——槽位数量 = 树同调位节点 att_socket1..3 点亮数(Attunement.slots,未点亮=0;旧谓词表判定链随迭代 21 T5 删除) + tiered_gates: **迭代 22 秘宝批新增**——格档递增 1=普通格/2=优秀格/3=稀有格(向下兼容=高档格可装低档件);装配校验=件档 TIER_RANK ≤ 装入位序(toggle_attunement,卸下恒可、效果面只读装配集不受格位影响) layered_acceptance: attune=off 续用现行带 · attune=on 带平移归 难度曲线与Boss 投值(商店 shop=off/on 分层先例同法) referenced_by: [秘宝, 元进度, 数据配置] diff --git a/roadmap.md b/roadmap.md index 5cc8373d..64961812 100644 --- a/roadmap.md +++ b/roadmap.md @@ -77,7 +77,7 @@ | 迭代 19 | **卡⓪主菜单开局缺陷修复批(原迭代 20 头卡A;2026-10-08 用户令并入本迭代、执行序第一)**:①**主菜单开新冒险+继续上局两入口补传同调三键**(attunement/attune_refresh_y/tree_rules——main.gd:510/:470,接收面 adventure_runner.gd:129-138/215-217 现成;静态考证缺陷成立:全仓无补偿路径、真实入口同调/X/Y 恒空、仅冒烟夹具喂值);②**关卡开场行攻血改传当前生效值**(main.gd:519-521 现传初始表值,与战斗面板不一致=用户确认缺陷;「含面板加成」后缀判断同步改为认局外加成);③冒烟补主菜单入口传参断言;④顺手删 adventure_runner.gd:129-134 三行重复赋值(零行为变化)。范围外=被动同调件呈现(用户令不动)、Fork 规则归因行(呈现层)、X/Y 刷新双源语义倒置(实现与用户口径镜像互换——X=刷新件源每节点重置不结转/Y=营地源整局持久/先扣 X,见 adventure_runner.gd:658-659/:706-709;用户口径=X 持久·Y 每节点重置·先扣 Y;真实局因①此前恒 0 未暴露——待用户裁定甲改实现/乙认现状,裁定落 GDD)。**卡①自动化验收补齐批(2026-10-07 用户令立项——全迭代两阶段验收盘点〔功能性+展示性,均自动化口径,不含人工目检〕的落地批)**:三件主内容——①**渲染闸自动化**:新增 `tools/render_gate.sh` 窗口化自动闸(shot_probe 四屏→shot_checker.py A-D 语义断言自动判定+fx_shot/board_shot 抓帧取证),棋盘 L1-L3 golden 逐像素基线入库 `game/tests/golden/`(测试资产入库先例,.gitignore 已核只挡 artwork/);**probe_gate 头注释「渲染闸归真人目检」纪律句删除改写**(用户 2026-10-07 明示授权=历史偷懒标记),迭代 15③ 屏蔽挂账的机器补跑半随批核销〔07⑮ 目检半仍归用户不销号〕;②**探针收编**:ui_probe/ui_tune_probe/t17_defer_probe/fx_probe 四件游离探针(实测 17.4s/49.6s/63.0s/21.9s 全绿)收进 probe_gate `--full` 段挂重闸,轻闸默认段零改;damage_probe 出闸留盘标注诊断件、config_fatal_probe 删除(与 run_all 致命类单测重复)、零断言三件标注分析工具+ui_layout 补 1280/1920 最小断言;③**账务修正**:tests/README 套件数/断言数漂移(十五→17、752+→双口径说明)+integration 空目录处置(代拍板)。〔2026-10-07 范围改道:原「明示不收」的 B4/B5/B6 三缺口经用户令各立需求卡片并入本迭代,本卡改称卡①,新增卡②③④见后〕。需求卡片=`design/research/需求卡片-自动化验收补齐批-2026-10-07.md`(v1.1——**待裁点 0 项**,P1-P5+拷打轮一 Q1-Q8 全裁 2026-10-07〔重闸定义正式扩容 CLAUDE.md/过程截图即弃/shot_probe 扩屏三新屏/1280 单档/素材缺位 SKIP 而基线禁锚回退态/基线换代权限呈现批 agent/取证失败=闸红/integration 删空壳〕;🔴 契约 6 条含运行时代码零 diff/轻闸零删减/并行避让;停机点 S1 首跑存量 FAIL 全量呈报登记 06+S3 基线生成素材缺位停机;代拍板白名单四项)。**卡②旧界面呈现断言回补批(B5,2026-10-07 用户令立项)**:迭代 16 前落地旧屏追溯补 ui_contrast 测点对+ui_struct 契约——普查结论=对比度测点对旧屏 0 对(15 对现役全属 HUD/锚点/徽章/生涯)、专项契约仅 pause/career、legend/node 两屏从未过结构族;优先级=战斗结算框>商店>营地柜台+天赋树>设置面>三选一>迷雾锚定对;红线=只加断言零改旧屏、违例登记呈报不修(处置权归用户);需求卡片=`design/research/需求卡片-旧界面呈现断言回补批-2026-10-07.md`(v1.1 grilling 三轮全裁——待裁点 0 项,WL 豁免边界/迷雾合成对最坏判据/时长闸 ≤2s/出口报告/06 遗留件登记全钉死;停机点 2〔对比度首跑存量违例/legend-node 结构首跑〕,T0-T4≈1.0d,worktree iter19-uibackfill)〔2026-10-08 施工已入库·**S1 停机态**——T1+T2 测点对 15→38 对(+23:battle_end 3/shop 5/camp 9/settings 2/curio 2/迷雾合成对 1,全 UiKit 常量实时求值);**新对首跑暴露存量对比度违例 7 项**(结算 GP 账行 2.67:1/商店 legacy 行卡名 1.12:1/货架价签 1.46:1/营地铭牌副题 3.59:1/行价格 4.02:1/树分组头 2.67:1/工坊状态 3.00:1)——全量登记清单+修复建议案〔全现役常量换墨可落〕随出口报告呈用户裁,处置权归用户、批内零修(红线);违例对在册=闸红登记存量形态,修复批落后自然转绿;T3 struct 13→15 根(legend/node 全真路径开合)首跑全绿 S2 未命中、WL 零新增;冒烟 840/0+run_all 17 套件持平;出口报告=`design/qa/iter19-uibackfill-exit-report-2026-10-08.md`;settlement_review 遗留件已在 06 登记落位〕。**卡③CI 预接线批(B6,2026-10-07 用户令立项)**:tests.yml 场景探针段解封——轻 CI 段=input/struct/contrast 三探针(示例引用的 ui_probe/ui_click 归 --full 重闸不进轻 CI)、Linux 独立命令形态(probe_gate.sh 系 Windows 专用)、窗口化四件禁入 headless CI;本地无远端端到端验证不可能=验收全静态核验,「首次 push 后人工盯首跑」入移交清单;远端仓库计划两文档零登记=待裁呈报项;需求卡片=`design/research/需求卡片-CI预接线批-2026-10-07.md`(v1.2——**触发分支死角已并入修正范围=tests.yml push/pull_request 两处 [main]→[master]**〔实查:实际分支 master、main 不存在,不修则远端建好后 CI 仍永不触发〕+冷检出导入步前置 smoke 步骤之前;裁点①远端仓库=✅ 2026-10-08 裁暂不建维持待命〔预接线待命=终态〕/裁点② ui_layout 入 CI 候卡①后裁;T0-T3≈0.25d,worktree iter19-ciwire)。**✅ 卡③已入库(2026-10-08,B6 闭环——出处=自动化验收补齐批 §一明示不收件,随本批销账)**:注释探针段解封=正式轻闸三探针段(input/struct/contrast,timeout 120s 外壳+退出码 124/137 超时判+PASS 串判定=probe_gate 判定逻辑 Linux 独立镜像,三探针全跑不短路汇总定红绿)+冷检出 `--import` 导入步 300s 前置冒烟之前(job 级基础设施步同覆盖冒烟与探针 .godot 依赖)+触发分支两处 [main]→[master] 修死角+头注释追加「远端仓库建好后无需改本文件即自动生效」;探针日志落 logs/probe_ci/ 顺现有 upload-artifact 段零改并入;验收全静态过(pyyaml 解析/CI 段 res:// 引用逐一实存/timeout 外壳清单齐/轻闸术语与 probe_gate 口径对齐/diff 面=tests.yml 单文件+roadmap 本行);**⚠ S1 呈报项(该段冻结未代修,处置待用户裁)=tests.yml setup-godot 步 `nikeee/godot-actions/setup-godot@v2` 指向不存在仓库**(2026-10-08 实查:GitHub API 404+nikeee 全部 159 公共仓 grep godot 零命中+marketplace 同名件=Vitorgus/Setup-Godot v1 系 Godot3 旧件;现存活跃替代=chickensoft-games/setup-godot v2.4.3)——远端建好首跑前必须先裁此段,否则 smoke job 必红;首跑盯梢清单随需求卡片 §七 待远端立项之日激活。**卡④提灯载体道具批(B4,2026-10-07 用户令立项;原登记去向「Alpha 信息件重定批」就此改道入本迭代)**:board_view.set_fog_lantern 死接口(迭代 14⑤ 接线+case⑮ 断言在位,载体始终未造=完成度 0)补载体道具;备选案五件全列(案 A 秘宝同调位新行=原登记指向·推荐/案 B 专长新行·撞重设计冻结不推荐/案 C 商店货架·分类判据需新裁/案 D 显影尘行激活合并·与改道相逆/案 E 元进度树·破坏读数归因不推荐)+淘汰案六件各附依据,设计权全归用户;需求卡片=`design/research/需求卡片-提灯载体道具批-2026-10-07.md`(v1.2——**✅ 裁点全裁:2026-10-08 载体=案A秘宝同调位新行**〔行字段推荐值=执行中一次秒答,「按推荐」即全收〕;缺陷处置=卡⓪随同 goal 先行入库=本卡 T1 前置门〔缺陷实查定案影响全部开局入口;原记迭代 20 缺陷批,2026-10-08 并入本迭代为卡⓪〕;呈现桥=只读访问器/复原语义=begin_battle 显式重算/MC=静态论证/测试三形态等工程面全钉死,T0-T5≈2.5d,worktree iter19-lantern)。**✅ 卡④已入库(2026-10-08)**:T1=treasure_items.tres 表尾追加 fog_lantern「魔法提灯」行(行字段=用户「按推荐」全收——display_name 魔法提灯/desc 灯光区半径+2/id fog_lantern/slot_cost 1/稀有度不设字段/unlock_requires 键省略=已解锁;form=passive+triggers 显式空数组〔活行空 effect_kinds 豁免以 row.has(triggers) 为条件〕+effect_kinds=[]/target=none/magnitude=0/use_phase=active_dig/dimension_tags=[info];表尾追加保 MC 自动档装配画像逐位不变=零漂移);T2=接线生产路径贯通——orchestrator 装配集公共只读出口 attune_has(_attune_has 契约化)+组合根呈现桥 main._sync_fog_lantern 挂每战首轮装配链显式重算含否(round_started 首轮发射位在 begin_battle 尾 attunement 注入后=时序安全;board_changed 先发后注入故不挂);T3=测试三形态(case⑮ 内载体级段不开新 case+smoke 现有套件加断言〔冒烟 840→844〕+存档回归 breakpoint Part 6+新单测 fog_lantern_wiring〔run_all 17→18 套〕,case⑮ 既有断言零删减);T4=改道指路句实查六处全换代(board_view.gd :139/:145+art-bible.md:387〔顺手对齐常量实名 FOG_LANTERN_BONUS〕+HUD框架.md :168/:170〔:170 同句 2 处整段换代〕;qa 历史成文零触碰)+06 缺陷条已入库回写+秘宝.md 目录九件→十件(v1.1 行);T5=重闸三件全绿(冒烟 844/0+probe_gate 含 --click 全绿+run_all 18 套,残留 Godot=0);MC=静态论证零实跑(纯呈现层 off 态零生产路径变化)。出口报告=design/qa/iter19-lantern-exit-report-2026-10-08.md | 迭代 18 出口(✅ 已解——2026-10-07 四卡总收口已入库)——**开工=goal 一次跑完(2026-10-08 裁:卡⓪缺陷卡先行+卡①-④打包,全绿前禁停;subagent 并发 ≤3;唯一预期交互=卡④行字段推荐值秒答;卡⓪原迭代 20 头卡A 同日用户令并入本迭代)** | 卡⓪≈1.0-1.5d(随计划书定稿)+卡①T0-T6≈4.25d(超软盒预挂;天然切分点 T2 末——段一 T0-T2 渲染闸≈2.25d / 段二 T3-T6 收编退役账务≈2d,两段同属一迭代一次合并)+卡②T0-T4≈1.0d+卡③T0-T3≈0.25d+卡④T0-T5≈2.5-3.0d(切分点 S0:考证呈报 0.25d/实装收口 2.25-2.75d)——五卡合计≈9.0-10.0d | | 迭代 20 | **专长实装批(奇物)**——需求卡片=design/research/需求卡片-专长实装批-2026-10-11.md(v1.1)。范围:T0-T10(效果引擎词表扩签/池结构案D 实装/专长 27 件换代/凭证五件/Boss 兑付换代〔已裁换代〕/状态账/断点扩容/MC 新基线/迁出行删除/空配置验收闸)。候选 B-F 留档不变。**✅ 已入库(2026-10-11 goal 会话 U0 开工——A/B/C 三段 T0-T10 全落 master:T1 词表扩签 a57489e/T2 案D 池 0640b0c/T3 29 行换代 54ab330/T4 凭证五件 1387aed/V4 补齐 5ebd05d/C 段收尾 3333187;冒烟 864/0+run_all 21 套+MC 同种子重放逐位复现;29 行件数口径+四级配额 104+疼痛记忆同战消费放宽+时沙/弃图兑换落法映射系代拍板追认单待用户)** | ✅ 已入库(出口闸全绿+实机目检归用户 07 在册) | T0-T10 ✅ | | 迭代 21 | **天赋树实装批(2026-10-11 用户令编入——整卡不切分,迭代容量放宽容纳全卡)**:按 [天赋树](design/gdd/systems/跨局循环/天赋树.md) **v0.8** 设计权威文档实装——**33→12 节点**结构换代(三轮换代:攻防所有权切分 29→21〔树零攻防来源=红线,攻/AC 唯一入口=锻造工坊路线〕/一线一节点 21→15〔生命·命中淬炼各一节点×4 级、row 字段退役〕/Fork 一节点双态 15→12〔开局姿态·危险直觉·财富之道三组各一节点、购买解锁后 A/B 零币切换〕;schema=Fork 词表 4→3 组/规则键词表 2→6 键、「等级阈值前置」形态去留=R1 开放待裁点〔推荐裁撤,**不阻开工门**〕)+三组 Fork 真效果(开局姿态每关首轮窗/危险直觉与财富之道·限次计数随冒险持久按关重置)+营地统一案甲(树=营地主界面/属性升级唯一入口/工作间钥匙模型/图鉴常开/占位面板零副作用硬要求)+注入公式换代(基础值+树淬炼+锻造路线加值位预留+局内加算)+旧属性柜台退役+旧档废弃与旧逻辑删除(开发阶段无迁移,Q3 裁决)+同调位槽位判定源切树节点(旧条件删除随批清扫)+树上刷新升级接线(双入口并存写同账)。需求卡片=design/research/需求卡片-天赋树实装批-2026-10-11.md(**v1.7**——grilling 五裁+歧义扫描六裁+**2026-10-12 完备性/跨系统歧义审查十一裁**全落;零阻塞待裁点);锻造工坊(原「武器锻造」)**17 裁点已清零**(2026-10-12 逐裁落文 v0.5,06 在册),Q5 排期前置已满足、默认本批先行;T0 的「标错机制存在性」与「boss_kill_count 消费面」两项**已预查结案**(标错机制查实存在且有代价,守侧 contingency 作废) | ✅ **已入库(2026-10-12,/goal+iter U0 开工令——T0-T7 全量交付;R1 按既定默认裁撤案执行;实现报告=design/qa/iter21-实现报告-2026-10-12.md;残留人闸=实机目检营地四柜台〔07 在册〕)** | T0-T7≈7.0-8.0d(整卡单迭代——实际 2026-10-12 单会话交付) | -| 迭代 22 | **秘宝体系实现重构落地批**——需求卡片=design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.2 零待裁点已成立);**2026-10-11 用户令编入**:新目录 12 件实装+解锁混合制(逐件分配锁定表在卡)+装配不可变(冒险中零更换)+旧 14 行数据清理+效果引擎接入;槽位来源不在本批(树接口接缝、无槽位不能带、测试走调试注入——C4 裁);估时 6d(区间 5-7d);开工门=U0 +| 迭代 22 | **秘宝体系实现重构落地批**——需求卡片=design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.3 C1-C6 全裁);**2026-10-11 用户令编入**:新目录 12 件实装+解锁混合制(逐件分配锁定表在卡)+装配不可变(冒险中零更换)+旧数据行清理+效果引擎接入;槽位来源不在本批(树接口接缝——迭代 21 已实装树点亮数槽位源,本批直接消费)+装配档位格约束;估时 6d(区间 5-7d)——**✅ 已入库(2026-10-12,/goal+iter U0 开工单会话交付:T1 数据层 12 行换代+schema 扩签/T2 解锁服务+工坊面退役+档位格约束/T3 效果引擎接入+扩签+常时参数面+试炼镐积点口+充能周期/T4 呈现换代+呈现桥+试炼镐 HUD 入口/T5 替换子交互拆除+库存保留注记/T6 验收闸断言;冒烟 898/0+run_all 22 套+重闸全绿;实现报告=design/qa/iter22-实现报告-2026-10-12.md;残留人闸=实机目检秘宝解锁-装配正常流程〔07 ㉑〕)** | 迭代 23+ | **M4 数值批(**原 22+;迭代 21 已编入天赋树实装批、迭代 22 已编入秘宝实装批,均 2026-10-11 用户令**——**Alpha 收官位·未排期**〔2026-10-09 语义换代:M4=Beta 阶段、非 Alpha 收官位,M3 未由用户明令关闭前不排期维持〕——2026-10-05 用户令编入迭代 17 卡②;2026-10-06 用户令移出独立成批并定序「Alpha 阶段最后——内容补全先行、统一数值调整殿后」〔当日曾被编入迭代 18 为收官卡,2026-10-07 用户令撤回〕;**2026-10-07 用户令二次后移:后续新迭代(迭代 20 起)一律不先做 M4,除非用户明确要求启动 M4 批次**)**:四件数值欠账合一批——①削弱档位终值(weaken 现值 −65/duration 2/refresh 考证在册;叠层改续时语义已定案,差 duration 终值+当量锚复算)②调参批数值点评(06 条目现行登记五条——任务口径「八条」与条目五条不符,需求卡片 §2.2 已呈报差异以条目为准)③迭代 10 奖励重构联动欠账①②(钥石产出重标 boss_keystone 首杀 3/3/3·复杀 2/2/3 provisional + 通关率带重扫〔通关回血 20%+AP 件首轮 +1〕)④元进度定价剩余面(forge base 20/比 150 考证在册——06 条目文本「base 10」滞后于 2026-09-22 对齿批已呈报;Boss 两档终值)。读数义务随批=迭代 14 两件后置复算(T6 MC 四带复算+T7 技能池节奏重测,均挂 07);顺路修复 damage_probe A1 改前既有红灯(判式换代批出口报告 §四.7 在册)。**硬约束:一切 MC/verifier 跑批发起前须用户批准;调参联动禁独立调参网络(β 带 [1.2,2.5] 逐 u 检算)**。需求卡片=`design/research/需求卡片-M4数值批-2026-10-05.md`(**v1.2 2026-10-06 案C 增补+联审修订**:v1.1 三事实与第五推算件照旧;联审=P3 转正顺延至重扫读数后/检算工具 WEAKEN_PCT 入同步面/F11 两句死文本全刷/T3·T5 四带复算并账一次跑批双认账/R5 改前锚=本批新立案C 锚/估时段一勘误 4.25d;案C 增补内容照旧=§2.6 三事实〔行为换代/锚全作废/难度预判〕+T0 推算稿第五件=对拍锚按新序重推导+T6 四带复算按案C 新行为首采;T0-T8≈6.25d,天然切分点 T4 末;🔴 契约 5 条含跑批批准制;**✅ 裁点全裁(2026-10-06 用户令:P1甲=维持 2 场·magnitude [60,85] 带内当量锚复算、具体值随出口 provisional 追认/P2甲 不对称有意/P3甲 3-3-3·2-2-3 维持、转正顺延至重扫读数到账且带内(Q4 二裁)/P4甲 不立直购价);开工=U0**) | 迭代 18 全部内容卡(③④⑤⑦)出口+全部内容批行为变更落定+**用户发令(未排期)**——重锚窗口须收尽全部内容批行为变更 | T0-T8≈6.25d(超软盒预挂,段一 T0-T4≈4.25d〔v1.2 勘误,原 4.5 系加总错误〕/段二 T5-T8≈2.0d) | ## 执行纪律(2026-09-27 用户立规)