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)