From c25e8bab3e03c5a624f6fe4c40e123c30b7cbfed Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 4 Oct 2026 00:39:26 +0800 Subject: [PATCH] =?UTF-8?q?T1=E6=9C=BA=E5=88=B6=E4=BB=B6=E5=9F=BA=E5=BB=BA?= =?UTF-8?q?=EF=BC=88=E8=BF=AD=E4=BB=A312=E5=8F=8C=E5=B1=82BD=E2=80=94?= =?UTF-8?q?=E2=80=94relic=5Ftable+=E6=A1=A3=E4=BD=8D=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E5=BC=95=E6=93=8E+=E6=9C=BA=E5=88=B6=E4=BB=B6=E5=B8=A6?= =?UTF-8?q?=E6=B1=A0=E6=8E=A5=E7=BA=BF=EF=BC=89=EF=BC=9ARelicTable=20schem?= =?UTF-8?q?a=E6=96=B0=E5=BB=BA=EF=BC=88relic=5Fid/rarity=E4=B8=89=E6=A1=A3?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E4=BB=A4=E7=89=8C=E6=B1=A0/archetype?= =?UTF-8?q?=E4=B8=83=E6=B5=81=E6=B4=BE=E6=A0=87=E7=AD=BE/hook=E6=8C=82?= =?UTF-8?q?=E7=82=B9=E6=9E=9A=E4=B8=BE23=E7=82=B9=E4=BD=8D/effects?= =?UTF-8?q?=E9=94=AE=E7=99=BD=E5=90=8D=E5=8D=95=E9=9B=B6=E8=A3=B8flat?= =?UTF-8?q?=E5=90=8C=E6=97=8F=E6=8A=A4=E6=A0=8F+=E6=89=AB=E9=9B=B7?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E7=82=B9=E9=93=81=E5=BE=8B=3D=E6=9C=89?= =?UTF-8?q?=E8=A1=8C=E6=B5=81=E6=B4=BE=E5=90=84=E2=89=A51=E6=89=AB?= =?UTF-8?q?=E9=9B=B7=E7=AB=AF=E3=80=94=E9=83=A8=E5=88=86=E5=A1=AB=E5=85=85?= =?UTF-8?q?=E5=90=88=E6=B3=95=C2=B7=E4=B8=83=E6=B5=81=E6=B4=BE=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=BD=92T7=E5=87=BA=E5=8F=A3=E3=80=95=EF=BC=89?= =?UTF-8?q?=EF=BC=9BRelicsEngine=E7=BA=AF=E5=87=BD=E6=95=B0=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=88=E9=9B=862=E6=A1=A3=E4=B8=80/4=E6=A1=A3?= =?UTF-8?q?=E4=BA=8C/=E6=95=A3=E4=BB=B6=E4=BF=9D=E5=BA=95=C2=B7=E6=88=90?= =?UTF-8?q?=E5=9E=8B=3Dtier=E2=89=A51=C2=B7=E6=8C=82=E7=82=B9=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=E8=A1=A8=E5=BA=8F=E7=A8=B3=E5=AE=9A=EF=BC=89=EF=BC=9B?= =?UTF-8?q?relics.tres=E7=A9=BA=E8=A1=A8=E5=90=88=E6=B3=95=E6=80=81?= =?UTF-8?q?=EF=BC=88=E6=B1=A098=E5=BC=A0=E9=9B=B6=E6=89=B0=E5=8A=A8?= =?UTF-8?q?=E7=A1=AC=E6=96=AD=E8=A8=80=EF=BC=89=EF=BC=9B=E6=B1=A0=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=3Drelic=E6=B4=BB=E8=A1=8C=E6=AF=8F=E4=BB=B61=E5=BC=A0?= =?UTF-8?q?=E5=85=A5=E7=A8=80=E6=9C=89/=E7=BD=95=E8=A7=81=E6=A1=A3?= =?UTF-8?q?=EF=BC=88common/rare=E2=86=92rare=C2=B7rarest=E2=86=92rarest?= =?UTF-8?q?=C2=B7=E6=B0=B8=E4=B8=8D=E5=85=A5=E6=99=AE=E9=80=9A=E6=A1=A3?= =?UTF-8?q?=EF=BC=89+=E4=BB=A4=E7=89=8C=E8=BA=AB=E4=BB=BD=E9=94=AErelic=5F?= =?UTF-8?q?id=EF=BC=88=E5=90=8C=E6=A1=A3=E5=A4=9A=E4=BB=B6=E5=8F=AF?= =?UTF-8?q?=E5=90=8C=E5=B1=8F=EF=BC=89+=E5=85=91=E4=BB=98=E5=85=A5?= =?UTF-8?q?=E6=8C=81=E6=9C=89=E9=9B=86=E5=85=88=E6=A0=A1=E9=AA=8C=E5=90=8E?= =?UTF-8?q?=E6=91=98=E7=89=8C+=5Frelics=E5=86=92=E9=99=A9=E7=BA=A7?= =?UTF-8?q?=E8=B7=A8=E5=85=B3=E6=90=BA=E5=B8=A6+=E6=96=AD=E7=82=B9?= =?UTF-8?q?=E6=89=A9=E9=94=AErelics=E6=8C=81=E6=9C=89=E9=9B=86/=E4=BD=99?= =?UTF-8?q?=E5=BA=93relic=5Fid=E9=9A=8F=E8=BF=98=E5=8E=9F+=E6=91=98?= =?UTF-8?q?=E8=A6=81additive=E9=94=AErelics=EF=BC=88=E6=B8=85=E8=B4=A6?= =?UTF-8?q?=E5=89=8D=E5=BF=AB=E7=85=A7=E2=80=94=E2=80=94pool=5Fstock?= =?UTF-8?q?=E5=90=8C=E5=9D=91=E9=A2=84=E9=98=B2=EF=BC=89+RUNSUM=E8=A1=8Cre?= =?UTF-8?q?lics=E5=AD=97=E6=AE=B5=EF=BC=9B=E5=91=88=E7=8E=B0=3Drelic?= =?UTF-8?q?=E5=8D=A1=E3=80=8C=E6=B5=81=E6=B4=BE=E4=BB=B6=C2=B7=E6=B5=81?= =?UTF-8?q?=E6=B4=BE=E5=90=8D=E3=80=8D+=E5=8D=A1=E7=89=87id=E4=B8=89?= =?UTF-8?q?=E6=AE=B5=E5=A4=8D=E5=90=88=E9=94=AE=EF=BC=9Bautoplay=E8=AF=BB?= =?UTF-8?q?=E6=95=B0=E9=9D=A2=EF=BC=9B=E5=86=92=E7=83=9F14=E6=96=B0?= =?UTF-8?q?=E6=96=AD=E8=A8=80=EF=BC=88=E7=A9=BA=E8=A1=A8=E9=9B=B6=E6=89=B0?= =?UTF-8?q?=E5=8A=A8/=E5=BC=95=E6=93=8E=E5=9B=9B=E6=96=AD=E8=A8=80/?= =?UTF-8?q?=E6=8E=A5=E7=BA=BF=E5=90=AB=E6=96=AD=E7=82=B9=E5=BE=80=E8=BF=94?= =?UTF-8?q?=EF=BC=89=E2=86=92684/0+run=5Fall=2015=E5=A5=97=E5=85=A8?= =?UTF-8?q?=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/adventure_runner.gd | 143 +++++++++++++++++++-------- game/core/config_db.gd | 14 +++ game/core/relics_engine.gd | 52 ++++++++++ game/core/relics_engine.gd.uid | 1 + game/data/schemas/relic_table.gd | 127 ++++++++++++++++++++++++ game/data/schemas/relic_table.gd.uid | 1 + game/data/tables/relics.tres | 16 +++ game/presentation/battle_narrator.gd | 16 ++- game/presentation/main.gd | 16 +-- game/smoke/adventure_monte_carlo.gd | 1 + game/smoke/level_autoplay.gd | 1 + game/smoke/smoke_all.gd | 75 ++++++++++++++ 12 files changed, 413 insertions(+), 50 deletions(-) create mode 100644 game/core/relics_engine.gd create mode 100644 game/core/relics_engine.gd.uid create mode 100644 game/data/schemas/relic_table.gd create mode 100644 game/data/schemas/relic_table.gd.uid create mode 100644 game/data/tables/relics.tres diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 5164adfc..d7e44af8 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -80,7 +80,8 @@ var _save_manager = null # 断点承载注入(迭代 8 卡① var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递) # ---- 三选一停点态(迭代 10 奖励重构批)---- var _bonus_taken := {} # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载;池库存正主=_bonus_deck) -var _bonus_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账) +var _bonus_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起机制件带=relic 令牌随表追加) +var _relics: Array[String] = [] # 流派件持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树) var _bonus_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点 var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付) var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗) @@ -168,14 +169,22 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary for id in _bonus_taken.keys(): _bonus_taken[id] = maxi(0, int(bp_taken.get(id, 0))) - # 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径) + # 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径; + # 迭代 12 T1——relic 令牌 relic_id 随还原) var bp_deck = bp.get("bonus_deck", null) if bp_deck is Array: var restored: Array = [] for t in (bp_deck as Array): var tok: Dictionary = t as Dictionary - restored.append({"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}) + var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))} + if String(rtok["kind"]) == "relic": + rtok["relic_id"] = String(tok.get("relic_id", "")) + restored.append(rtok) _bonus_deck = restored + # 流派件持有集还原(迭代 12 T1——缺键 = 旧版断点防御空集) + _relics = [] + for r in (bp.get("relics", []) as Array): + _relics.append(String(r)) for v in (bp.get("bonus_aux_carry", []) as Array): _bonus_aux_carry.append(String(v)) _keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true) @@ -241,6 +250,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: func _reset_bonus_state() -> void: _bonus_taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0} _bonus_deck = _build_bonus_deck() + _relics = [] _bonus_pending = {} _bonus_result = {} _bonus_aux_carry = [] @@ -249,16 +259,27 @@ func _reset_bonus_state() -> void: # 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = BONUS_IDS×TIERS 逐层, -# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻) +# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:机制件带=relic 活行每件 1 张追加 +# (rarity common/rare→稀有令牌档、rarest→罕见令牌档——relic 永不入普通档;空表=零追加零扰动)) func _build_bonus_deck() -> Array: var deck: Array = [] for kind in db.adventure.BONUS_IDS: for tier in db.adventure.TIERS: for i in int(db.adventure.bonus_token_count(String(kind), String(tier))): deck.append({"kind": String(kind), "tier": String(tier)}) + for row in db.relic_rows(): + var rtier := "rarest" if String(row.get("rarity", "")) == "rarest" else "rare" + deck.append({"kind": "relic", "tier": rtier, "relic_id": String(row.get("relic_id", ""))}) return deck +# 令牌唯一身份(抽签去重/兑付匹配共用——数值件=kind,机制件=relic_id;同档多张不同 relic 可同屏) +func _token_identity(tok: Dictionary) -> String: + if String(tok.get("kind", "")) == "relic": + return "relic:" + String(tok.get("relic_id", "")) + return String(tok.get("kind", "")) + + # TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度) func set_hero_override(atk_pct: int, hp_pct: int) -> void: _override = [atk_pct, hp_pct] @@ -307,46 +328,66 @@ func continue_to_next_level() -> bool: # ---- 提升三选一(迭代 10 奖励重构批——选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)---- -# 候选令牌匹配(kind+tier 恒等——同 kind 大小两档可同屏,kind 单键不构成唯一身份) -func _find_token(candidates: Array, kind: String, tier: String) -> Dictionary: +# 候选令牌匹配(身份键恒等——数值件 kind+tier、机制件 relic_id;迭代 12 T1 机制件带) +func _find_token(candidates: Array, kind: String, tier: String, relic_id: String = "") -> Dictionary: for t in candidates: var tok: Dictionary = t as Dictionary - if String(tok.get("kind", "")) == kind and String(tok.get("tier", "")) == tier: - return tok + if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier: + continue + if kind == "relic" and String(tok.get("relic_id", "")) != relic_id: + continue + return tok return {} -# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(kind+tier 精确匹配)。 -# 属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、 -# 道具件受同调位约束(满位转替换子交互——随池种退役不可达,机械惰性保留)。 +# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件 +# 另须 relic_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、 +# relic 件入流派件持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束(随池种退役不可达)。 # 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。 -func pick_bonus(kind: String, tier: String = "") -> bool: +func pick_bonus(kind: String, tier: String = "", relic_id: String = "") -> bool: if state != "running" or _bonus_pending.is_empty(): push_error("[冒险结构] pick_bonus 拒绝——无待选停点(state=%s)" % state) return false var candidates: Array = _bonus_pending.get("candidates", []) as Array - if _find_token(candidates, kind, tier).is_empty(): - push_error("[冒险结构] pick_bonus 拒绝——令牌不在候选: %s|%s" % [kind, tier]) + if _find_token(candidates, kind, tier, relic_id).is_empty(): + push_error("[冒险结构] pick_bonus 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, relic_id]) return false var value: int = int(db.adventure.bonus_value(kind, tier)) var hp_before := int(ledger.get("hero_hp", 0)) if kind == "aux_item": return _pick_aux_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨;池种退役后不可达) - if not _apply_bonus_pick(kind, tier, value): - push_error("[冒险结构] pick_bonus 拒绝——未知件: %s|%s" % [kind, tier]) + if not _apply_bonus_pick(kind, tier, value, relic_id): + push_error("[冒险结构] pick_bonus 拒绝——未知件: %s|%s|%s" % [kind, tier, relic_id]) return false - _finish_bonus_pick(kind, value, hp_before, tier) + _finish_bonus_pick(kind, value, hp_before, tier, relic_id) return true -# 三选一件兑付共享体(2026-10-02 修复批双胞胎收口;TP0 改令牌消费——摘牌单点): +# 兑付载荷 relic 伴随键(迭代 12 T1——反馈行取数面;非 relic 件零新增键) +func _relic_payload_keys(relic_id: String) -> Dictionary: + if relic_id == "": + return {} + var row: Dictionary = db.relic_row(relic_id) + return {"relic_id": relic_id, "relic_name": String(row.get("display_name", "")), "relic_archetype": String(row.get("archetype", ""))} + + +# 三选一件兑付共享体(2026-10-02 修复批双胞胎收口;TP0 改令牌消费——摘牌单点;迭代 12 T1 relic 分支): # 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限—— -# 与账本置满/通关回血同式 effective_max_hp)。兑付即从 _bonus_deck 摘除该令牌(选中踢池; +# 与账本置满/通关回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面 +# 按挂点消费,档位质变由 RelicsEngine 求值)。兑付即从 _bonus_deck 摘除该令牌(选中踢池; # 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。 -func _apply_bonus_pick(kind: String, tier: String, value: int) -> bool: - if not _consume_deck_token(kind, tier): - push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s" % [kind, tier]) +func _apply_bonus_pick(kind: String, tier: String, value: int, relic_id: String = "") -> bool: + if kind == "relic" and db.relic_row(relic_id).is_empty(): + push_error("[冒险结构] relic 兑付失败——表无此件: %s" % relic_id) + return false # 先校验后摘牌(令牌零丢失) + if not _consume_deck_token(kind, tier, relic_id): + push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, relic_id]) return false + if kind == "relic": + if not _relics.has(relic_id): + _relics.append(relic_id) + _bonus_taken["relic"] = int(_bonus_taken.get("relic", 0)) + 1 + return true match kind: "attack": bonus["atk"] = int(bonus.get("atk", 0)) + value @@ -372,16 +413,24 @@ func _apply_bonus_pick(kind: String, tier: String, value: int) -> bool: return true -# 从牌库摘除一枚令牌(TP0——选中踢池单点;找不到 = false 由调用方拒绝) -func _consume_deck_token(kind: String, tier: String) -> bool: +# 从牌库摘除一枚令牌(TP0——选中踢池单点;迭代 12 T1 relic 按 relic_id 精确摘;找不到 = false) +func _consume_deck_token(kind: String, tier: String, relic_id: String = "") -> bool: for i in _bonus_deck.size(): var tok: Dictionary = _bonus_deck[i] as Dictionary - if String(tok.get("kind", "")) == kind and String(tok.get("tier", "")) == tier: - _bonus_deck.remove_at(i) - return true + if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier: + continue + if kind == "relic" and String(tok.get("relic_id", "")) != relic_id: + continue + _bonus_deck.remove_at(i) + return true return false +# 流派件持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) +func relics_held() -> Array: + return _relics.duplicate() + + # ---- 商店共享池消费面(迭代 11 第②批 T3——P1 共享池单点账本;TP0 令牌池换代——库存=牌库剩余令牌)---- # 库存余量(TP0:stock = 牌库内该 kind 剩余令牌数——「池消耗即上限」,种类次数上限退役; @@ -454,8 +503,8 @@ func resolve_aux_replace(discard_index: int) -> bool: # 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。 -# picked = kind;tier 随载荷(TP0——大小档位读数面) -func _finish_bonus_pick(picked: String, value: int, hp_before: int, tier: String = "") -> void: +# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(迭代 12 T1——反馈行取数) +func _finish_bonus_pick(picked: String, value: int, hp_before: int, tier: String = "", relic_id: String = "") -> void: _bonus_pending = {} _bonus_result = { "picked": picked, @@ -465,6 +514,7 @@ func _finish_bonus_pick(picked: String, value: int, hp_before: int, tier: String "hp_after": int(ledger.get("hero_hp", 0)), "level_index": level_index, } + _bonus_result.merge(_relic_payload_keys(relic_id)) bonus_resolved.emit(_bonus_result.duplicate(true)) _snapshot_breakpoint() @@ -484,22 +534,22 @@ func present_node_bonus(candidates: Array, aux_detail: Array = []) -> void: _node_bonus_pending = {"candidates": candidates.duplicate(true), "aux_detail": aux_detail.duplicate(true)} -# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库) -func apply_node_bonus(kind: String, tier: String = "") -> bool: +# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库;relic 同权入持有集) +func apply_node_bonus(kind: String, tier: String = "", relic_id: String = "") -> bool: if _node_bonus_pending.is_empty(): push_error("[冒险结构] apply_node_bonus 拒绝——无待选停点") return false var candidates: Array = _node_bonus_pending.get("candidates", []) as Array - if _find_token(candidates, kind, tier).is_empty(): - push_error("[冒险结构] apply_node_bonus 拒绝——令牌不在候选: %s|%s" % [kind, tier]) + if _find_token(candidates, kind, tier, relic_id).is_empty(): + push_error("[冒险结构] apply_node_bonus 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, relic_id]) return false var value: int = int(db.adventure.bonus_value(kind, tier)) if kind == "aux_item": return _apply_node_aux(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨;池种退役后不可达) - if not _apply_bonus_pick(kind, tier, value): - push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s|%s" % [kind, tier]) + if not _apply_bonus_pick(kind, tier, value, relic_id): + push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s|%s|%s" % [kind, tier, relic_id]) return false - _finish_node_bonus(kind, value, tier) + _finish_node_bonus(kind, value, tier, relic_id) return true @@ -639,15 +689,17 @@ func resolve_bonus_replace(discard: int) -> bool: return resolve_aux_replace(discard) -func _finish_node_bonus(picked: String, value: int, tier: String = "") -> void: +func _finish_node_bonus(picked: String, value: int, tier: String = "", relic_id: String = "") -> void: _node_bonus_pending = {} - bonus_resolved.emit({ + var payload := { "context": "reward_node", "picked": picked, "tier": tier, "value": value, "level_index": level_index, - }) + } + payload.merge(_relic_payload_keys(relic_id)) + bonus_resolved.emit(payload) # 令牌池洗牌不放回抽 3(TP0 v2.2——Fisher-Yates 全库洗牌后按序收集;**同 kind 去重每 kind 至多 @@ -668,11 +720,14 @@ func _draw_bonus_candidates() -> Array: var out: Array = [] var seen: Array = [] for tok in pool: - var k := String((tok as Dictionary).get("kind", "")) - if seen.has(k): + var ident := _token_identity(tok) + if seen.has(ident): continue - seen.append(k) - out.append({"kind": k, "tier": String((tok as Dictionary).get("tier", ""))}) + seen.append(ident) + var cand := {"kind": String((tok as Dictionary).get("kind", "")), "tier": String((tok as Dictionary).get("tier", ""))} + if ident.begins_with("relic:"): + cand["relic_id"] = String((tok as Dictionary).get("relic_id", "")) + out.append(cand) if out.size() >= int(db.adventure.bonus_candidate_count): break return out @@ -840,6 +895,7 @@ func _settle(result: String) -> void: "aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷) "keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源) "pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) + "relics": _relics.duplicate(), # additive 扩签(迭代 12 T1——流派件终局持有集:清账前快照,成型判读取数面) } # 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源; # 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知) @@ -907,6 +963,7 @@ func _snapshot_breakpoint() -> void: "aux_items": aux_items, "bonus_taken": _bonus_taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载) "bonus_deck": _bonus_deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原) + "relics": _relics.duplicate(), # 流派件持有集(迭代 12 T1——局内随机渠道获得态续玩防丢失) "bonus_pending": _bonus_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约) "bonus_result": _bonus_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付) "bonus_aux_carry": _bonus_aux_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 9e252a10..e07da475 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -26,6 +26,7 @@ const TABLE_MANIFEST := { "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) "attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免) + "relics": "relics.tres", # 迭代 12 T1 双层 BD 机制件基建(RelicTable 定义表 Rule 4 豁免——AuxItemTable 先例;空表合法态=主时间线零扰动) } var action_economy: ActionEconomyTable @@ -47,6 +48,7 @@ var meta: MetaTable var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var adventure: AdventureTable var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器) +var relics: RelicTable # 迭代 12 T1 类型化持用(消费面走 relic_* 访问器) var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} @@ -138,10 +140,22 @@ func load_tables(root: String, collect_only: bool) -> bool: aux_items = loaded.get("aux_items") adventure = loaded.get("adventure") attunement_growth = loaded.get("attunement_growth") + relics = loaded.get("relics") is_ready = not has_errors return not has_errors +# ---- relic 访问器(迭代 12 T1——消费面一律走本组,禁直读行集)---- + +func relic_row(relic_id: String) -> Dictionary: + return relics.row_by_id(relic_id) if relics != null else {} + + +# 机制件带令牌构建源(池接线——adventure_runner._build_bonus_deck 消费;行 rarity→投放档映射) +func relic_rows() -> Array: + return relics.rows if relics != null else [] + + # 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出: # ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。 func _cross_validate(loaded: Dictionary) -> void: diff --git a/game/core/relics_engine.gd b/game/core/relics_engine.gd new file mode 100644 index 00000000..a37ec3bf --- /dev/null +++ b/game/core/relics_engine.gd @@ -0,0 +1,52 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Date: 2026-10-04(双层 BD 实施批 T1——标签档位激活引擎) +# 流派成型判据机制化(整合定稿 设计工具 1——以撒套装/小骨铭文先例): +# 同流派标签 relic 计数 ≥2 → 档一(流派核心规则);≥4 → 档二(完全体);<2 = 散件(单体保底—— +# 每件效果独立生效,档位只做质变不做启动,反例 B-2)。 +# 纯函数零 await 零 RNG 零副作用(Attunement 求值服务同款纪律);核心层零特判延伸——引擎只读 +# relic_table 行集与持有集,接线点消费 tiers/hooks 投影。 +class_name RelicsEngine +extends RefCounted + + +## 档位求值:relic_ids 持有集 × 行集 → {archetype: tier}(tier ∈ 0/1/2;无行流派不出现) +static func tiers_for(relic_ids: Array, table: RelicTable) -> Dictionary: + var counts := {} + for rid in relic_ids: + var row: Dictionary = table.row_by_id(String(rid)) if table != null else {} + if row.is_empty(): + continue + var arch := String(row.get("archetype", "")) + counts[arch] = int(counts.get(arch, 0)) + 1 + var out := {} + for arch in counts.keys(): + var n := int(counts[arch]) + if n >= 4: + out[arch] = 2 + elif n >= 2: + out[arch] = 1 + else: + out[arch] = 0 + return out + + +## 挂点投影:持有集 → [{relic_id, hook, archetype, tier, effects}](表序稳定——同集合同投影, +## MC 逐位可复放承载) +static func hooks_for(relic_ids: Array, table: RelicTable) -> Array: + var out: Array = [] + for rid in relic_ids: + var row: Dictionary = table.row_by_id(String(rid)) if table != null else {} + if row.is_empty(): + continue + out.append({ + "relic_id": String(row.get("relic_id", "")), + "hook": String(row.get("hook", "")), + "archetype": String(row.get("archetype", "")), + "effects": (row.get("effects", {}) as Dictionary).duplicate(true), + }) + return out + + +## 成型判读(读数面口径):流派 tier ≥1 = 成型(档一即判——任务书完成定义「标签+档位激活引擎可判成型」) +static func is_formed(tiers: Dictionary, archetype: String) -> bool: + return int(tiers.get(archetype, 0)) >= 1 diff --git a/game/core/relics_engine.gd.uid b/game/core/relics_engine.gd.uid new file mode 100644 index 00000000..1d722a33 --- /dev/null +++ b/game/core/relics_engine.gd.uid @@ -0,0 +1 @@ +uid://6d12lkx5w2x8 diff --git a/game/data/schemas/relic_table.gd b/game/data/schemas/relic_table.gd new file mode 100644 index 00000000..f6487ef7 --- /dev/null +++ b/game/data/schemas/relic_table.gd @@ -0,0 +1,127 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Validation Question: 七核心流派各 ≥3 件入局内随机渠道后,标签+档位激活引擎能否机械判「成型」、 +# 机制件带能否挂进令牌池稀有/罕见档随机投放(P0——迭代 12 双层 BD 实施批 T1) +# Date: 2026-10-04(双层 BD 实施批 T1——任务书五裁定稿;设计权威 双层BD体系-整合定稿-2026-10-03 v3) +# 数据配置 RelicTable schema(双层BD/机制件 回填 owner 批末落文;行 = Dictionary 键 relic_id,Rule 4 豁免 +# 定义表——AuxItemTable 先例)。 +# 行字段:relic_id 主键 / display_name / rarity ∈ {common, rare, rarest}(对齐令牌池三档命名—— +# 投放档映射:common/rare→稀有令牌档、rarest→罕见令牌档;relic 令牌永不入普通档=「挂稀有/罕见档」 +# 追认 b 接线口径)/ archetype ∈ ARCHETYPES 七流派标签 / hook ∈ HOOKS 挂点枚举(结算段槽——整合定稿 +# §2.1 哈迪斯恩赐槽同构)/ effects Dictionary(hook 消费参数,provisional 投值)。 +# 正负双修(整合定稿 B2 判据):effects 允许负向键(cost_* 族)——纯增益件须专项论证(反例 B-3),加载期 +# 仅校验键域不禁纯增益(专项论证件以 positive_only 批注登记,人工清单呈报)。 +# 档位激活(整合定稿 设计工具 1——以撒套装/小骨铭文):同标签计数 ≥2→档一(流派核心规则)/≥4→档二 +# (完全体);散件单体保底=每件效果独立生效,档位只做质变(反例 B-2)——引擎纯函数 RelicsEngine。 +# 🔴 层级纪律(任务书红线 2):流派件只走局内随机渠道(三选一/商店/格面)——本表行永不入同调位 +# (aux_items 表)/局外树(T8 节点表);扫雷触发点铁律:每流派 ≥1 行 hook ∈ MINE_HOOKS(加载校验, +# 违反=红线①触碰面)。 +class_name RelicTable +extends Resource + +@export var rows: Array = [] # Array[Dictionary] 键 relic_id + +## 流派标签词表(七核心——喂雷/淘金不在本批,R5 甲/缓议) +const ARCHETYPES := ["magic", "pierce", "normal", "dig", "flag", "charge", "devour"] + +## 稀有度词表(对齐令牌池三档;投放档映射 relic→rare / rarest→rarest) +const RARITIES := ["common", "rare", "rarest"] + +## 挂点枚举(结算段槽——首批 23 点位;每 hook 恰一流派域消费,T2-T6 接线) +const HOOKS := [ + # 魔法流 + "hero_attack_rider", # 普攻命中后追加必中魔法伤(奥术余烬/魔能共鸣) + "clamp_modifier", # 克制点直调(魔网织者/湛蓝誓约/猎手刺剑) + "first_dig_spell", # 每关首次开格必出魔法矢(魔能共鸣第二效果) + # 穿刺流 + "kill_overflow", # 击杀溢出弹射(贯穿射击) + "dig_count_segments", # 开格计数转攻击段数(连珠矢) + "roll_discount", # 折扣档改写(猎弱本能前移一档) + # 普攻流 + "round_dig_bonus", # 轮内安全开格数转攻击加成(矿工的执拗;踩雷清零=mine_hit 监听同口) + "hit_trade", # 命中交换(矿工重锤——正负双修原生形态) + "armor_roll_rewrite", # 免掷骰恒折扣(专注) + "heavy_debuff", # 命中重甲附加护甲裂解(碎甲震击) + # 开格流 + "dig_yield_mult", # 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向) + "deep_dig", # 深层资源掘取(深挖钻头——R4 甲非信息税形态:开掘额外 AP 代价) + # 标旗流 + "flag_resource", # 标中雷获格面资源(雷区图谱) + "nest_aura", # 控制巢穴数转伤害光环(猎巢人) + "flag_all_hit", # 同轮全中旗伤倍增(完美布防) + "flag_strike", # 轮末旗伤开单(战旗军规——复用 board_typed) + # 蓄力流 + "ap_carry", # 轮末 AP 结转蓄力(整备军备——改写 cleared 作废口径) + "charge_shield", # 蓄力层转减伤(铁壁整备) + "charge_spike", # 满层 spike(厚积薄发) + # 吞噬流 + "terrain_refund", # 受地形伤掉落资源补偿(地脉采撷——伤害仍发生) + "terrain_dig_double", # 挖开伤害型地形格双产出(荆棘编织者) + "flag_neutralize", # 标中旗中和相邻伤害型地形格(净化工兵——🔴 R4 甲仅限伤害型) + "variant_counter", # 受地形伤积层轮末反打(熔火之躯——触发键不按地形种类特化) +] + +## 扫雷结算触发点 hook 集(🔴 红线①承载:每流派 ≥1 行 hook 命中本集——加载校验) +const MINE_HOOKS := ["first_dig_spell", "dig_count_segments", "round_dig_bonus", "dig_yield_mult", + "deep_dig", "flag_resource", "nest_aura", "flag_all_hit", "flag_strike", "ap_carry", + "terrain_refund", "terrain_dig_double", "flag_neutralize", "variant_counter"] + +## effects 键白名单(🔴 零裸 flat 同族护栏——机制轴禁无条件属性加项;键出域=加载致命) +## 全部条件触发/机制修正参数;cost_* 族=正负双修负向面 +const EFFECT_KEYS := ["value", "value2", "tier_value", "per_stack", "cap", "ap_cost", "chance", + "mult", "cost_mult", "cost_flat", "polarity", "spell_kind", "segment_cap", "discount_shift", + "trade_atk", "trade_hit", "shield_per_layer", "spike_mult", "layers_to_shield", + "neutralize_range", "counter_damage", "resource_kind", "rider_kind", "note"] + + +func validate() -> Array: + var errs: Array = [] + if rows.is_empty(): + return errs # 空表合法态(T1 基建先行——T2 起逐流派填行) + var seen := {} + for row in rows: + var rid: String = row.get("relic_id", "") + if rid == "": + errs.append({"category": "domain", "message": "relic_id 空"}) + continue + if seen.has(rid): + errs.append({"category": "dup_key", "message": "relic_id 重复: %s" % rid}) + continue + seen[rid] = true + if String(row.get("display_name", "")) == "": + errs.append({"category": "domain", "message": "%s display_name 空" % rid}) + if not RARITIES.has(String(row.get("rarity", ""))): + errs.append({"category": "enum", "message": "%s rarity 出域 %s" % [rid, str(RARITIES)]}) + var arch := String(row.get("archetype", "")) + if not ARCHETYPES.has(arch): + errs.append({"category": "enum", "message": "%s archetype 出域 %s" % [rid, str(ARCHETYPES)]}) + var hook := String(row.get("hook", "")) + if not HOOKS.has(hook): + errs.append({"category": "enum", "message": "%s hook 出域(挂点枚举封闭): %s" % [rid, hook]}) + for k in row.keys(): + if not ["relic_id", "display_name", "rarity", "archetype", "hook", "effects", "note"].has(String(k)): + errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [rid, String(k)]}) + var fx: Dictionary = row.get("effects", {}) as Dictionary + for ek in fx.keys(): + if not EFFECT_KEYS.has(String(ek)): + errs.append({"category": "zero_flat", "message": "%s effects 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(ek)]}) + # 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥1 行 hook ∈ MINE_HOOKS(部分填充合法——T2-T6 + # 逐流派渐进;七流派全量覆盖完成度由批次一出口断言承载,加载闸不卡渐进填行) + var archs_with_rows := {} + for row in rows: + archs_with_rows[String(row.get("archetype", ""))] = true + for arch in archs_with_rows.keys(): + var covered := false + for row in rows: + if String(row.get("archetype", "")) == String(arch) and MINE_HOOKS.has(String(row.get("hook", ""))): + covered = true + break + if not covered: + errs.append({"category": "load_bearing", "message": "流派 %s 无扫雷结算触发点行(🔴 红线①铁律——每流派 ≥1 件挂扫雷端)" % String(arch)}) + return errs + + +func row_by_id(relic_id: String) -> Dictionary: + for row in rows: + if String(row.get("relic_id", "")) == relic_id: + return row + return {} diff --git a/game/data/schemas/relic_table.gd.uid b/game/data/schemas/relic_table.gd.uid new file mode 100644 index 00000000..45b83903 --- /dev/null +++ b/game/data/schemas/relic_table.gd.uid @@ -0,0 +1 @@ +uid://bm46akskc3rc diff --git a/game/data/tables/relics.tres b/game/data/tables/relics.tres new file mode 100644 index 00000000..336f5678 --- /dev/null +++ b/game/data/tables/relics.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="RelicTable" load_steps=2 format=3] + +[ext_resource type="Script" path="res://data/schemas/relic_table.gd" id="1"] + +[resource] +script = ExtResource("1") +rows = [] + +; 双层 BD 实施批 T1(2026-10-04):机制件基建——relic_table 建表,行 = 流派件定义 +; (relic_id/display_name/rarity/archetype/hook/effects)。空表合法态 = 主时间线零扰动 +; (池零 relic 令牌零变化);T2-T6 逐流派填行 27 件(magic 4/flag 4/pierce 4/normal 4/ +; dig 4/charge 3/devour 4——整合定稿 §2.3 七流派件面)。 +; 稀有度→投放档映射:common/rare → 稀有令牌档、rarest → 罕见令牌档(relic 永不入普通令牌档 +; ——「机制件带挂稀有/罕见档」追认 b 接线口径,普通档 70 张纯数值维持)。 +; 🔴 层级纪律:本表行永不入同调位(aux_items)/局外树(T8 节点表)——流派件只走局内随机渠道。 +; 数值全 provisional 归 M-4 数值批;effects 键白名单封闭(零裸 flat 同族护栏)。 diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index 550dab0c..e3179231 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -123,10 +123,24 @@ static func bonus_card_text(id: String, tier: String, value: int, aux_line: Stri # 三选一兑付反馈行(选中件效果 + 丢弃提示;tier 缺省普通档——旧载荷防御口径) static func bonus_result_text(result: Dictionary) -> String: - var name := bonus_card_text(String(result.get("picked", "")), String(result.get("tier", "")), int(result.get("value", 0))).replace("\n", " · ") + var name := "" + if String(result.get("picked", "")) == "relic" and String(result.get("relic_id", "")) != "": + name = relic_card_text({"relic_id": String(result.get("relic_id", "")), "display_name": String(result.get("relic_name", "")), "archetype": String(result.get("relic_archetype", ""))}).replace("\n", " · ") + else: + name = bonus_card_text(String(result.get("picked", "")), String(result.get("tier", "")), int(result.get("value", 0))).replace("\n", " · ") return "已选择:%s\n其余选项已丢弃(不可反悔)" % name +# 流派件卡片文案(迭代 12 T1——「流派件 · 流派名」形制+件名;空行 = 防御通用文案) +static func relic_card_text(row: Dictionary) -> String: + if row.is_empty() or String(row.get("relic_id", "")) == "": + return "流派件\n???" + var arch_names := {"magic": "魔法", "pierce": "穿刺", "normal": "普攻", "dig": "开格", + "flag": "标旗", "charge": "蓄力", "devour": "吞噬"} + var arch := String(arch_names.get(String(row.get("archetype", "")), "流派")) + return "流派件 · %s\n%s" % [arch, String(row.get("display_name", row.get("relic_id", "")))] + + # Boss 胜利战斗总结结算行(settlement-summary v1.1——钥石增量行;组合根拼 GP 行后追加) static func bonus_keystone_line(payload: Dictionary) -> String: return "钥石 +%d · 累计 %d" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))] diff --git a/game/presentation/main.gd b/game/presentation/main.gd index bf5d3978..2772d1b1 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -542,16 +542,19 @@ var _last_replace_incoming := {} # 同上——incoming 记录 func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array: - # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier" 复合键(同 kind 大小两档 - # 可同屏,kind 单键不构成唯一身份),确认回程拆键(_on_bonus_pick_confirmed) + # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind + # 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(_on_bonus_pick_confirmed) var out: Array = [] var aux_i := 0 for tok in candidates: var kind := String((tok as Dictionary).get("kind", "")) var tier := String((tok as Dictionary).get("tier", "")) - var cid := "%s|%s" % [kind, tier] + var rid := String((tok as Dictionary).get("relic_id", "")) + var cid := "%s|%s|%s" % [kind, tier, rid] var text := "" - if kind == "aux_item": + if kind == "relic": + text = Narrator.relic_card_text(db.relic_row(rid)) + elif kind == "aux_item": var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else "" aux_i += 1 text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)), _aux_card_line(iid)) @@ -600,11 +603,12 @@ func _on_bonus_refresh() -> void: func _on_bonus_pick_confirmed(item_id: String) -> void: if adventure == null: return - # TP0 令牌池——卡片 id "kind|tier" 拆键(缺 | = 旧载荷防御归普通档) + # TP0 令牌池——卡片 id "kind|tier|relic_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段) var parts := item_id.split("|") var kind := parts[0] var tier := parts[1] if parts.size() > 1 else "" - var ok: bool = adventure.apply_node_bonus(kind, tier) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier) + var rid := parts[2] if parts.size() > 2 else "" + var ok: bool = adventure.apply_node_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid) if not ok: push_error("[组合根] 三选一兑付被拒——见输出日志") diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 833466c6..c6bdc09b 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -101,6 +101,7 @@ func _initialize() -> void: "mine_dmg": int(st.get("mine_dmg", 0)), "taken": int(st["hero_dmg"]) + int(st.get("mine_dmg", 0)), "refreshes": int(st["refreshes"]), "choices": int(st["choices"]), + "relics": st.get("relics", []), "visited_count": (st["visited_ids"] as Array).size(), "encounter_max_hp_seq": st["encounter_max_hp_seq"], "reward_events": st["reward_events"], diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index 26c86ea8..741a1dd0 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -177,6 +177,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ # 终局池读数(迭代 11 第②批 T5——摘要 additive 键 pool_stock:_settle 清账前快照, # 商店买走 vs 奖励领走 vs 枯竭的终局面;事后读 adr.pool_stock 会被收束清账重置=满库假象) st["pool_stock"] = (st.get("summary", {}).get("pool_stock", {}) as Dictionary).duplicate(true) + st["relics"] = (st.get("summary", {}).get("relics", []) as Array).duplicate() # 流派件终局持有集(迭代 12 T1——摘要 additive 键清账前快照,同 pool_stock 模式) st.log_path = logger.log_path() if own_session: logger.close_session() diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 88cf6fd0..4959f447 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -111,6 +111,7 @@ func _run_core_smoke(db, rng) -> void: _smoke_adventure_table() _smoke_adventure_api() _smoke_adventure() + _smoke_relics() _smoke_save() _smoke_meta() _smoke_node_map_templates() @@ -596,6 +597,80 @@ func _smoke_adventure() -> void: "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)") +# ---- 双层 BD 机制件基建冒烟(迭代 12 T1——relic_table/档位激活引擎/机制件带池接线)---- + +func _smoke_relics() -> void: + print("--- 机制件基建(relic_table · 档位激活引擎 · 机制件带接线)---") + # ① 空表零扰动:现行 relics.tres 空——牌库纯数值令牌零 relic(主时间线零扰动硬断言) + check(_db.relics != null and _db.relics.rows.is_empty(), "relic_table 已加载且空表合法态(T1 基建先行——T2 起逐流派填行)") + var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71)) + check(a_r0.start_adventure(71, {"first_script": true}), "空表场景:冒险启动(seed 71)") + var relic_tokens := 0 + for t in a_r0._bonus_deck: + if String((t as Dictionary).get("kind", "")) == "relic": + relic_tokens += 1 + check(relic_tokens == 0 and a_r0._bonus_deck.size() == 98, + "空表牌库 = 纯数值令牌 98 张零 relic(机制件带零投放——主时间线零扰动)") + # ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag + 1 件 rarest 锚点) + var rt := RelicTable.new() + rt.rows = [ + {"relic_id": "m1", "display_name": "魔一", "rarity": "common", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"value": 1}}, + {"relic_id": "m2", "display_name": "魔二", "rarity": "common", "archetype": "magic", "hook": "first_dig_spell", "effects": {"value": 1}}, + {"relic_id": "m3", "display_name": "魔三", "rarity": "rare", "archetype": "magic", "hook": "clamp_modifier", "effects": {"value": 1}}, + {"relic_id": "m4", "display_name": "魔四", "rarity": "rare", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"tier_value": 2}}, + {"relic_id": "f1", "display_name": "旗一", "rarity": "common", "archetype": "flag", "hook": "flag_resource", "effects": {"value": 1}}, + ] + check(rt.validate().is_empty(), "构造 relic 表 validate 零违例(magic 4 件含 first_dig_spell 扫雷端——红线①铁律过)") + var tiers := RelicsEngine.tiers_for(["m1", "m2", "m3", "m4", "f1"], rt) + check(int(tiers.get("magic", -1)) == 2 and int(tiers.get("flag", -1)) == 0, + "档位激活:magic 4 件→档二 · flag 1 件→散件档 0(集 2 档一/4 档二——以撒套装/小骨铭文先例)") + check(RelicsEngine.is_formed(tiers, "magic") and not RelicsEngine.is_formed(tiers, "flag"), + "成型判读:magic 成型(tier≥1)· flag 未成型(散件单体保底——档位只质变不启动)") + var t2 := RelicsEngine.tiers_for(["m1", "m2"], rt) + check(int(t2.get("magic", -1)) == 1, "档位激活:2 件→档一(同集合同档位确定性——MC 逐位可复放承载)") + var hooks := RelicsEngine.hooks_for(["m1", "f1"], rt) + check(hooks.size() == 2 and String(hooks[0].get("hook", "")) == "hero_attack_rider", + "挂点投影:表序稳定(同集合→同投影)") + # ③ 机制件带接线(临时表注入——relic 令牌入牌库/兑付入持有集/踢池/断点往返;测试尾还原现行表) + var live_rows: RelicTable = _db.relics + _db.relics = rt + var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) + check(a_r.start_adventure(72, {"first_script": true}), "机制件带场景:冒险启动(seed 72——注入 5 行临时表)") + var deck_relic := 0 + for t in a_r._bonus_deck: + if String((t as Dictionary).get("kind", "")) == "relic": + deck_relic += 1 + check(deck_relic == 5 and a_r._bonus_deck.size() == 103, + "机制件带入池:5 行 relic 每件 1 张(common/rare→稀有档 rarest→罕见档)——牌库 98+5=103") + var relic_seen: Dictionary = {} + for i in 60: + for t in a_r._draw_bonus_candidates(): + if String((t as Dictionary).get("kind", "")) == "relic": + relic_seen[String(t.get("relic_id", ""))] = true + check(relic_seen.size() >= 2, "抽签出 relic 候选(60 次重抽 ≥2 种不同 relic——身份键 relic_id 同档可多件)") + a_r._bonus_pending = {"candidates": [{"kind": "relic", "tier": "rare", "relic_id": "m3"}, {"kind": "gp", "tier": "common"}], "payload": {}} + var deck_m3 := 0 + for t in a_r._bonus_deck: + if String((t as Dictionary).get("relic_id", "")) == "m3": + deck_m3 += 1 + check(a_r.pick_bonus("relic", "rare", "m3") and (a_r.relics_held() as Array).has("m3") and a_r.pool_stock("relic") == deck_relic - 1, + "relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1]) + # 断点往返(持有集+余库 relic_id 随行) + var sm_r = load("res://core/save_manager.gd").new("user://_smoke_relics_bp.json") + sm_r.load_from_disk() + a_r.set_save_manager(sm_r) + a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"}) + var bp_r: Dictionary = sm_r.breakpoint_snapshot() + check((bp_r.get("relics", []) as Array).has("m3"), "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)") + var bp_deck_m3 := 0 + for t in (bp_r.get("bonus_deck", []) as Array): + if String((t as Dictionary).get("relic_id", "")) == "m3": + bp_deck_m3 += 1 + check(bp_deck_m3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(m3 零残留——续玩不重复获得)") + sm_r.clear_breakpoint() + _db.relics = live_rows # 还原现行空表(共享 _db 防污染——后续冒烟段零感知) + + # 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排) func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: var guard := 24