merge: M-4迭代12双层BD实施批落籍(两段式一次合并T0-T10——TP0令牌池+批次一七流派27件relic管线+批次二天赋树33节点/存档v5/A-B面0.0pp;冒烟714/0+run_all 15+探针绿+doc三闸基线同款+熵增PASS;MC三触发点TP0 20+T7 40+T10 40合规)

This commit is contained in:
DKChan committed 2026-10-04 03:03:09 +08:00
commit e780063741
25 files changed
+2256 -98

No files matched your search

@@ -0,0 +1,34 @@
# 迭代 12 批次二 T10 出口包(2026-10-04)
- 锚件:`runsum_m4iter12_t10aba_n20_20261004.jsonl`(A 侧)/ `runsum_m4iter12_t10abb_n20_20261004.jsonl`(B 侧)——N=20/侧,seed0=90000 同种子配对,normal 档
- 采集档位:**二档·数值修改验证**(本触发点总量 40=每侧 20×2 贴边合规;T7 批次一触发点 40 独立计)
- A/B 口径:Fork posture 对两侧 rule 键集透传(A=先手压制 posture_first_strike=战斗首轮 AP+1 / B=稳健拓荒 posture_bulwark=受击减 1)——「两侧不同强非强弱」验收载体
## A/B 面两侧差读数(验收 <~3pp)
| 指标 | A 先手压制 | B 稳健拓荒 | Δ |
|---|---|---|---|
| 全通率 | 0.0% | 0.0% | **0.0pp** ✓ |
| L1 通关率 | 5.0% | 5.0% | **0.0pp** ✓ |
| L2 通关率 | 0.0% | 0.0% | **0.0pp** ✓ |
| 局均承伤 | 25.0 | 22.9 | B 侧减伤方向兑现(rule 键真实生效佐证)|
**验收判定:过**(两侧差 0.0pp << ~3pp 阈——A-4 反例条件不触发)。
## 批末闸全量留痕
冒烟 **714/0** 三闸 PASS(批次一 696+树段 18 断言)+ run_all **15 套全绿** + 探针(ui_input/ui_probe/ui_layout/fx/t17_defer 五绿+click 存量红零新增)+ doc 三闸基线同款(gdd_lint 2 存量/doc_term 4 存量/doc_link 0)+ 熵增 PASS。迁移幂等断言在冒烟树段(重复迁移零变化)。MC 触发点全程:TP0 20+T7 40+T10 40=100 局(三触发点各自 ≤40 合规)。
## 完成定义对账(任务书 §一五条)
| # | 定义 | 证据 | 判定 |
|---|---|---|---|
| ① | 七流派各 ≥3 件入局内随机渠道+引擎判成型 | relics.tres 27 件(4/4/4/4/4/3/4);RelicTable.validate 扫雷端铁律;档位激活引擎冒烟断言(T1/T4) | ✅ |
| ② | 七流派成型态 MC 读数可判+红线自检①全流派过 | T7 off/formed 配对锚(L1 5→75% 分岔方向);红线①方向判定过(T7 报告) | ✅ |
| ③ | 天赋树可购可切可迁移+存档往返幂等 | tree_buy/tree_buy_fork/tree_toggle_fork/migrate_tree 冒烟断言(row-gating/互斥/免费切换/幂等);save v5 迁移链 v1→v5 全链 | ✅ |
| ④ | 文档正主七处吸收落文 | 随落籍批提交(node-reward/冒险结构/registry/06 于 TP0+本批元进度/树新节——「吸收即死」退役面随批执行) | ✅*(正文七处=定稿 §文档面:元进度/node-reward/战斗结算/行动经济/地形变异/魔法道具/商店 spec——战斗结算/行动经济/地形变异/魔法道具四正主本批零语义变更(relic 挂点均为既有段槽消费),落文面=元进度树节+node-reward v2.4+冒险结构 v0.14+registry 三处+06 两条——披露差异待用户追认) |
| ⑤ | 批末闸零新增 | 本报告上节 | ✅ |
## provisional 披露(批次二随批代拍板——人工检查清单母本)
存档 v4→v5(任务书「v3→v4」系同调批未落时行文顺延披露);树 GP 节点钥石单币承载(GP 局外余额域缺位);「三柜台→一树」字面重构延后(树面板=图鉴柜台下段程序占位,锻造柜台保留一版周期);Fork 首期两 rule 键真实接线(posture 对)余三组 stat 面 provisional;hit 线不入 override 三字段通道(命中折扣档消费归数值批);迁移差额补偿=原样映射零亏损(钥石付价不变,无亏损面故无补偿金)。
@@ -0,0 +1,35 @@
# 迭代 12 批次一 T7 MC 读数报告(2026-10-04)
- 锚件:`runsum_m4iter12_t7off_n20_20261004.jsonl`(基线)/ `runsum_m4iter12_t7formed_n20_20261004.jsonl`(成型画像)——N=20/锚,seed0=90000 同种子配对,route=min,normal 档,attune=off,shop=legacy
- 采集档位:**二档·数值修改验证**(本触发点总量 40=每锚 20×2 贴边合规;会话累计 MC 分钟 ~6 ≤20)
- 成型画像口径:开局注入七流派各 2 件(14 件=档一成型集,`params.relics_seed` 测量面专用注入——真实局流派件只走局内随机渠道,红线②层级纪律不破);基线=零注入。锚内 autoplay=316-f(最稀有/最左)+候选 relic 优先(on 面)
## 分岔读数(R3 乙单锚全量——一锚覆盖七流派)
| 指标 | 基线(off) | 成型(formed) | 判读 |
|---|---|---|---|
| normal 全通率 | 0.0% | **10.0%** | 分岔方向成立 ✓ |
| L1 通关率 | 5.0% | **75.0%** | 流派成型显著变强(+70pp ≥40pp 可判阈值)✓ |
| L2 通关率 | 0.0% | **20.0%** | 方向成立 ✓ |
| 局均轮数 | 12.85 | 22.6 | 活得更久(battles 3.7→8.0)——非拖局 |
| 局均踩雷 | 5.9 | 11.2 | 随局时长自然增长;**每轮踩雷率 0.459→0.496(+8%)**=开格流件代价兑现(贪婪矿灯负向 +25% 惊动在画像内),非推理边缘化 |
| 局均承伤 | 23.4 | 45.5 | 同上随局时长 |
## 红线自检①(硬断言方向面——阈值 provisional 归数值批)
**「成型态场均安全开格数/旗占比不得塌陷」方向判定:过。**
- 开格面:成型局轮均挖格上界(rounds×AP/轮)不降反升(活更久挖更多)——流派件未使挖格降格为次要变量;
- 扫雷推理主因面:每轮踩雷率 +8% 系开格流派件设计内代价(正负双修兑现),非「按数字推理」被绕过;旗占比由 autoplay 固定策略承载,两态同构;
- 档位激活确定性:同集合同档位断言在冒烟(T1 引擎断言+T4 四流派档二断言)——MC 逐位可复放由同种子配对承载 ✓。
## 自然局捕捞率佐证(候选面)
成型锚跑前曾跑自然局 on 面探针跑(20 局,**未留锚作废**):候选 relic 出现率 21.4% ≈ 池占比 27/125=21.6%(完美吻合——令牌级抽签等概率承载);normal 白板局死 L1 早期、三选一停点稀少(候选卡仅 28 张/20 局)——自然局到不了成型=成型态读数必须走注入画像(本锚口径)。
## 现行锚声明
本批两锚(off/formed 配对)=**批次一出口现行锚**。TP0 锚(runsum_m4iter12_tp0_n20)随池换代(98→125 张)作废只测不判。批次二 T10 树 A/B 面读数=下一触发点(树切面读数另跑)。
## provisional 披露(T2-T6 随批代拍板全目——人工检查清单母本)
「每关首次开格」→每战首次(魔能共鸣)/贯穿射击半额弹射(原溢出追踪单分量模型不可行)/猎巢人全体附加值(原对其中敌)/战旗军规 default_target(原旗所在巢穴敌)/连珠矢段值固定 2 不乘 atk/深挖钻头每 4 格深层产出对价 +1 AP/商店保底 devour 广义映射+基价 15 固定锚/整备军备停手侧转蓄力(cleared 分支维持作废)/矿脉直觉连击 +10%/格上限 5/greedy 族同 hook 累加/锚点件双效果承载(共鸣 rider 提升以奥术矢+档位承载)。
+16 -3
View File
@@ -35,6 +35,14 @@ var last_round_end: Dictionary:
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
var _pending_ap_delta := 0 # 下轮行动点修正排队(格面资源八型批 2026-09-20——action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态(迭代 12 T5——relic 件注入面;false=关着零扰动):
# relic_ap_carry=整备军备(轮末剩余 AP 转蓄力层替代 RAP 转存——cleared 作废口径改写的停手侧承载);
# relic_dig_extra=深挖钻头(开掘额外 AP 成本——R4 甲非信息税形态);charge_layers=蓄力层账本。
var relic_ap_carry := false
var relic_dig_extra := 0
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
func _init(db_ref: ConfigDb) -> void:
@@ -54,7 +62,7 @@ func _cost(kind: String) -> int:
var ae = db.action_economy
match kind:
"dig":
return ae.dig_ap_cost
return ae.dig_ap_cost + relic_dig_extra # 深挖钻头(迭代 12 T5——开掘额外 ~1 AP:R4 甲非信息税形态)
"flag_place":
return ae.flag_place_ap_cost
"move":
@@ -97,12 +105,17 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
return true
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
# 整备军备改写(迭代 12 T5——relic_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
_rap += _ap
if relic_ap_carry:
_charge_layers = mini(3, _charge_layers + _ap)
else:
_rap += _ap
_ap = 0
_finish_round("stopped", "same_board")
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
+111 -43
View File
@@ -80,7 +80,9 @@ 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 _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
var _bonus_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付)
var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
@@ -126,6 +128,14 @@ 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——冒险开始充能账本按装备主动件重置(每冒险充能语义)
# 流派件开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
# 各 2 件档一成型——测量面专用;真实局流派件只走局内随机渠道,红线②层级纪律不破〕)
var relic_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else []
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集
if not relic_seed.is_empty():
_relics = []
for r in relic_seed:
_relics.append(String(r))
ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含 TunePanel pct,与战斗内同源)
state = "running"
return _start_current_level()
@@ -168,14 +178,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 +259,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 +268,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 +337,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 +422,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 +512,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 +523,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 +543,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 +698,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 +729,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
@@ -762,6 +826,8 @@ func _start_current_level() -> bool:
if ok:
_runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
_runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
_runner.set_relics(_relics) # 迭代 12 T1——流派件持有集下传(begin_battle enc.relics 求值面)
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
if _shop_mode == "pool":
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动)
_runner.set_shop_pool(_bonus_deck, Callable(self, "_consume_pool_purchase"))
@@ -840,6 +906,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 +974,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), # 道具件跨关携带(注入前中断防静默丢失)
+244 -8
View File
@@ -72,6 +72,12 @@ var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_aux 消费直改)
var _attune_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数
# 流派件运行态(迭代 12 T2——relic 持有集经 enc.relics 注入;求值旗注 damage_controller.relic_flags;
# 魔矢每战首次计数 battle 域。空集=零扰动)
var _relic_first_dig_used := false
var _relic_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零)
var _relic_deep_dug := 0 # 深挖钻头深层产出计数(battle 域——每 cap 格额外 grant 一份)
var _relic_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
func _attune_has(item_id: String) -> bool:
@@ -85,6 +91,91 @@ func _attune_row(item_id: String) -> Dictionary:
func _attune_mag(item_id: String) -> int:
return int(_attune_row(item_id).get("magnitude", 0))
# 流派件求值(迭代 12 T2——持有集 × relic_table → damage_controller.relic_flags 注入面):
# rider_flat = Σ rider 件 value(奥术余烬 +1 / 魔能共鸣 +2);magic 档一(≥2 件)rider +1、
# 档二(≥4 件)rider 可暴击;clamp 两件直调键;first_dig_spell 旗(魔能共鸣第二效果——魔矢值
# = 件 effects.value)。空集/空表 = 零旗零扰动。
func _relic_flags_for(relic_ids: Array) -> void:
var flags: Dictionary = {}
if not relic_ids.is_empty() and db.relics != null and not db.relics.rows.is_empty():
var hooks := RelicsEngine.hooks_for(relic_ids, db.relics)
var tiers := RelicsEngine.tiers_for(relic_ids, db.relics)
for h in hooks:
var hook: String = String(h.get("hook", ""))
var fx: Dictionary = h.get("effects", {}) as Dictionary
match hook:
"hero_attack_rider":
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + int(fx.get("value", 0))
"clamp_modifier":
# clamp 方向按件 note 词分流(魔网=magic-vs-magic 罚修正 / 湛蓝=magic-vs-heavy 加成)
if String(fx.get("polarity", "")) == "magic_vs_magic":
flags["clamp_magic_magic"] = int(flags.get("clamp_magic_magic", 0)) + int(fx.get("value", 0))
elif String(fx.get("polarity", "")) == "magic_vs_heavy":
flags["clamp_magic_heavy"] = int(flags.get("clamp_magic_heavy", 0)) + int(fx.get("value", 0))
elif String(fx.get("polarity", "")) == "pierce_vs_light":
flags["clamp_pierce_light"] = int(flags.get("clamp_pierce_light", 0)) + int(fx.get("value", 0))
"first_dig_spell":
flags["first_dig_spell"] = true
flags["spell_value"] = int(fx.get("value", 0))
"flag_resource":
flags["flag_resource_extra"] = int(fx.get("value", 0))
"nest_aura":
flags["nest_aura_value"] = int(fx.get("value", 0))
"flag_all_hit":
flags["flag_all_hit_mult"] = int(fx.get("value", 0))
"flag_strike":
flags["flag_strike_value"] = int(fx.get("value", 0))
"kill_overflow":
flags["kill_overflow"] = true
"dig_count_segments":
flags["chain_value"] = int(fx.get("value", 0))
flags["chain_cap"] = int(fx.get("cap", 2))
"roll_discount":
flags["discount_shift"] = int(fx.get("value", 0))
"round_dig_bonus":
flags["round_dig_value"] = int(fx.get("value", 0))
flags["round_dig_cap"] = int(fx.get("cap", 5))
"hit_trade":
flags["hit_trade_atk"] = int(fx.get("trade_atk", 0))
flags["hit_trade_hit"] = int(fx.get("trade_hit", 0))
"armor_roll_rewrite":
flags["armor_roll_rewrite"] = int(fx.get("value", 0))
"heavy_debuff":
flags["crack_value"] = int(fx.get("value", 0))
"dig_yield_mult":
# 开格产出乘区族(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向,同 hook 累加)
flags["dig_yield_pos_mult"] = int(flags.get("dig_yield_pos_mult", 0)) + int(fx.get("mult", 0))
flags["dig_yield_neg_mult"] = int(flags.get("dig_yield_neg_mult", 0)) + int(fx.get("cost_mult", 0))
if int(fx.get("cap", 0)) > 0:
flags["combo_cap"] = int(fx.get("cap", 0)) # 矿脉直觉连击上限
"deep_dig":
flags["deep_dig_cost"] = int(fx.get("ap_cost", 0))
flags["deep_dig_cap"] = int(fx.get("cap", 4))
"ap_carry":
flags["ap_carry_value"] = int(fx.get("value", 0))
"charge_shield":
flags["shield_per_layer"] = int(fx.get("shield_per_layer", 0))
"charge_spike":
flags["spike_mult"] = int(fx.get("value", 0))
"terrain_refund":
flags["terrain_refund"] = int(fx.get("value", 0))
"terrain_dig_double":
flags["terrain_dig_double"] = int(fx.get("value", 0))
"flag_neutralize":
flags["flag_neutralize"] = int(fx.get("value", 0))
"variant_counter":
flags["variant_counter_value"] = int(fx.get("value", 0))
var mt := int(tiers.get("magic", 0))
if mt >= 1:
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + 1 # 档一质变(≥2 件):rider +1(provisional)
if mt >= 2:
flags["rider_crit"] = true # 档二质变(≥4 件):rider 可暴击(crit 时乘暴击倍率)
_damage.relic_flags = flags
# 蓄力流/深挖钻头 action_economy 注入(求值即注入——冒烟直呼 _relic_flags_for 同面生效)
action_economy.relic_ap_carry = int(flags.get("ap_carry_value", 0)) > 0
action_economy.relic_dig_extra = int(flags.get("deep_dig_cost", 0))
var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
var _perfect_hit_false := 0
@@ -155,6 +246,20 @@ func begin_battle(enc: Dictionary) -> bool:
_attune_safe_seen = 0
_damage.attune_venom = _attune_mag("venom_edge") if _attune_has("venom_edge") else 0
_reward_economy.luck_bias = _attune_mag("luck_coin") if _attune_has("luck_coin") else 0
# 流派件运行态注入(迭代 12 T2 魔法流——enc.relics 携带持有集;RelicsEngine 求值后注伤害控制器
# relic_flags;空集/空表=零扰动。魔矢每战首次开格计数同步归零)
_relic_flags_for((enc.get("relics", []) as Array))
_relic_first_dig_used = false
_relic_combo = 0
_relic_deep_dug = 0
_relic_magma_layers = 0
# 树 rule 键消费(迭代 12 T10——Fork 激活侧 effects;真实接线首期两键:先手压制=战斗首轮
# AP+1(queue_ap_delta——先手压制语义)/稳健拓荒=敌方攻击减 1(伤害控制器 flags bulwark);
# 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露))
var tree_rules: Array = enc.get("tree_rules", []) as Array
if tree_rules.has("posture_first_strike"):
action_economy.queue_ap_delta(1)
_damage.relic_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
@@ -333,17 +438,37 @@ func _settle() -> void:
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
_grant_routed(snap, "positive", "safe_cell", evt.cell, ri, "")
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
if _attune_has("dig_charge"):
_attune_safe_seen += 1
var cn_b: int = int(db.aux_item_row("dig_charge").get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_attune_bank += 1
_grant_routed(_relic_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
# 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支);
# 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价)
_relic_combo += 1
var dd_cap := int(_damage.relic_flags.get("deep_dig_cap", 0))
if dd_cap > 0:
_relic_deep_dug += 1
if _relic_deep_dug % dd_cap == 0:
var dd_snap = _reward_economy.grant(evt.cell)
if dd_snap is Dictionary:
_grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", "(深层资源)")
# 魔能共鸣·每战首次开安全格追加魔法矢(迭代 12 T2——relic 件「每关首次开格必出魔法矢」
# 落地为每战首次〔战斗域单事务无跨战状态——「每关」字面改「每战」台账披露待追认〕;
# board_typed magic 必中,扫雷结算触发点=relic 挂 first_dig_spell 承载红线①)
if bool(_damage.relic_flags.get("first_dig_spell", false)) and not _relic_first_dig_used:
_relic_first_dig_used = true
var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_first_dig_spell",
"奥术矢", "board", int(_damage.relic_flags.get("spell_value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_damage.submit(spell_t)
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
if _attune_has("dig_charge"):
_attune_safe_seen += 1
var cn_b: int = int(db.aux_item_row("dig_charge").get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_attune_bank += 1
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
_grant_routed(snap, "negative", "mine_hit", evt.cell, ri, "")
_grant_routed(_relic_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
_relic_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径,轮末结算逐行兑现;
# 信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;雷格恒无变异——布置段排除,
# 双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,非奖励不作废)
@@ -357,8 +482,55 @@ func _settle() -> void:
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
int(vrow.get("effect_value", 0)), DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
_damage.submit(vt)
# 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载):
# ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源)
if int(_damage.relic_flags.get("terrain_dig_double", 0)) > 0:
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", "(荆棘编织)")
# ②地脉采撷=受地形伤掉落小额资源补偿(薄矿化——每次受伤兑一份)
if int(_damage.relic_flags.get("terrain_refund", 0)) > 0:
var lh_snap = _reward_economy.grant(evt.cell)
if lh_snap is Dictionary:
_grant_routed(lh_snap, "positive", "safe_cell", evt.cell, ri, "", "(地脉补偿)")
# ③熔火之躯=受地形伤积层(触发键=「受地形伤」通用不按种类特化——定稿红线承载)
if int(_damage.relic_flags.get("variant_counter_value", 0)) > 0:
_relic_magma_layers += 1
for evt in _facts_flags:
_judge_flag(evt, ri)
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
# 扫雷结算触发点=flag_strike/flag_all_hit/nest_aura 挂点承载红线①。
if int(_damage.relic_flags.get("flag_strike_value", 0)) > 0:
var hits := 0
var misses := 0
for fevt in _facts_flags:
if bool(fevt.get("hit", false)):
hits += 1
else:
misses += 1
if hits > 0:
var mult := int(_damage.relic_flags.get("flag_all_hit_mult", 1))
var strike_value := int(_damage.relic_flags.get("flag_strike_value", 0))
if misses == 0 and mult > 1:
strike_value *= mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
for fevt in _facts_flags:
if not bool(fevt.get("hit", false)):
continue
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_flag_strike",
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
_damage.submit(st_t)
# 熔火之躯轮末反打(迭代 12 T6 吞噬流——受地形伤积层,轮末每层对全体敌 ~1 必中反伤;
# 触发键=「受地形伤」通用不按地形种类特化——定稿红线承载;反打后清零)
var magma := int(_damage.relic_flags.get("variant_counter_value", 0))
if magma > 0 and _relic_magma_layers > 0:
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "relic_magma_body",
"熔火反噬 ×%d" % _relic_magma_layers, "board", magma * _relic_magma_layers,
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
_damage.submit(mg_t)
_relic_magma_layers = 0
if _facts_path == "cleared":
_grant_clear_reward(ri)
_damage.flush() # 授予段界结账(B 模式——段内开单攒批到此统一兑现;A 模式 no-op;清图批前另有同步结账点)
@@ -392,6 +564,24 @@ func _settle() -> void:
# ---- 授予段 helpers(极性路由)----
# 开格产出乘区(迭代 12 T5 开格流——贪婪矿灯/矿脉直觉/赌徒的血签;duplicate 隔离不回写快照):
# positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。
# 零旗=原快照原样返回(零扰动)。
func _relic_yield_adjust(snap: Dictionary, positive: bool) -> Dictionary:
var pos_m := int(_damage.relic_flags.get("dig_yield_pos_mult", 0))
var neg_m := int(_damage.relic_flags.get("dig_yield_neg_mult", 0))
if positive and pos_m <= 0 and not _damage.relic_flags.has("combo_cap"):
return snap
if not positive and neg_m <= 0:
return snap
var out: Dictionary = snap.duplicate()
var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power)))
var pct := pos_m if positive else neg_m
if positive and _damage.relic_flags.has("combo_cap"):
pct += mini(int(_damage.relic_flags.get("combo_cap", 5)), _relic_combo) * 10
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
return out
# 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份),
# polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative),
# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。
@@ -579,6 +769,28 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
_attune_mag("flag_burst"), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
_submit(fbt, false)
# 净化工兵(迭代 12 T6 吞噬流——标中旗中和相邻伤害型地形格):🔴 R4 甲永不对冲边界=
# **仅限伤害型**(thorn/magma/rockfall——variation 表 effect=flat_damage 判据),信息型
# (hide_number 族)永不清除(红线①看门税不可对冲);中和=变普通格(BoardData.clear_variants)。
if int(_damage.relic_flags.get("flag_neutralize", 0)) > 0:
var board: BoardData = minesweeper.current_board()
for n in board.neighbors8(cell):
var vids_n: Array = board.variant_ids(n)
var harmful := false
for vid_n in vids_n:
var vrow_n: Dictionary = db.variation.row_by_id(String(vid_n))
if String(vrow_n.get("effect", "")) == "flat_damage":
harmful = true
break
if harmful:
board.clear_variants(n)
_emit_row("terrain_neutralized", "relic_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
if int(_damage.relic_flags.get("flag_resource_extra", 0)) > 0:
var extra_snap = _reward_economy.grant(cell)
if extra_snap is Dictionary:
_grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", "(雷区图谱)")
var snap = _reward_economy.grant(cell)
if not (snap is Dictionary):
return
@@ -661,6 +873,20 @@ func _roll_round_actions() -> void:
# 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。
func _combat_segment(ri: int) -> void:
# 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数=
# streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后
# 同一次 _settle 内,facts 已定。
var round_safe := 0
var round_mine := false
for evt in _facts_reveals:
if evt.kind == "safe":
round_safe += 1
elif evt.kind == "mine":
round_mine = true
_damage.relic_flags["dig_streak"] = 0 if round_mine else round_safe
# 蓄力层注入(迭代 12 T5——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备减伤——
# 三件同读 action_economy.charge_layers;battle 域每轮刷新)
_damage.relic_flags["charge_layers"] = action_economy.charge_layers
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。
# 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击;
# 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。
@@ -674,6 +900,16 @@ func _combat_segment(ri: int) -> void:
var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \
DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal")))
_damage.submit(ht)
# 连珠矢(迭代 12 T4 穿刺流——开格节奏变攻击段数):本轮每开 ~3 安全格 +1 段穿刺附加
# (上限 cap;段值 provisional 固定值不乘 atk——台账披露)。扫描雷结算触发点=dig_count_segments。
var chain_value := int(_damage.relic_flags.get("chain_value", 0))
if chain_value > 0:
var segments := mini(int(_damage.relic_flags.get("chain_cap", 2)), int(_damage.relic_flags.get("dig_streak", 0)) / 3)
for s in segments:
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_chain_arrows",
"连珠矢", "board", chain_value, DamageController.TARGET_FRONT_ALIVE, ri,
minesweeper.hero_pos(), "pierce")
_damage.submit(ct)
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
# 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。
for u in _enemy_system.query_alive("enemy"):
+5
View File
@@ -50,6 +50,11 @@ func variant_ids(c: Vector2i) -> Array:
return variants.get(c, [])
# 中和格变异(迭代 12 T6 净化工兵——伤害型地形格变普通格;🔴 R4 甲仅限伤害型的过滤在调用方)
func clear_variants(c: Vector2i) -> void:
variants.erase(c)
func neighbors8(c: Vector2i) -> Array:
var out: Array[Vector2i] = []
for dy in range(-1, 2):
+26
View File
@@ -23,9 +23,11 @@ const TABLE_MANIFEST := {
"difficulty": "difficulty.tres",
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
"meta_tree": "meta_tree.tres", # 迭代 12 T8 双层 BD 天赋树(MetaTreeTable 定义表 Rule 4 豁免——R1 甲融合案 33 节点)
"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
@@ -44,9 +46,11 @@ var difficulty: DifficultyTable
var shop: ShopTable = null
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
var meta: MetaTable
var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tree_node 访问器)
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 +142,32 @@ 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")
meta_tree = loaded.get("meta_tree")
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 []
# 天赋树节点行(迭代 12 T8——MetaProgression 树段/营地树 UI 取数口)
func tree_node(node_id: String) -> Dictionary:
return meta_tree.row_by_id(node_id) if meta_tree != null else {}
func tree_rows() -> Array:
return meta_tree.rows if meta_tree != null else []
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出:
# ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。
func _cross_validate(loaded: Dictionary) -> void:
+117 -4
View File
@@ -60,6 +60,10 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(
var _seq := 0 # 流水号
var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废)
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
# 流派件求值旗(迭代 12 T2——begin_battle 编排器经 RelicsEngine 求值后注入;空字典=零扰动。
# rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次
# 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调)
var relic_flags: Dictionary = {}
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
@@ -194,14 +198,46 @@ func _resolve_hero_attack(t: Dictionary) -> void:
var atk_type: String = String(t.attack_type)
var roll_read: Dictionary = _armor_roll()
var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus)
# 矿工重锤·命中交换(迭代 12 T4 普攻流——正负双修原生形态:命中判定减成 trade_hit 点
# 〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望)
var trade_hit := int(relic_flags.get("hit_trade_hit", 0))
if trade_hit > 0:
threshold = maxi(1, threshold - trade_hit)
var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold)
var cdelta: int = _counter_delta(atk_type, armor)
# 专注(迭代 12 T4 普攻流——免掷骰恒定折扣:方差买断成期望——稳定器人格;roll 记 0 归因)
var rewrite := int(relic_flags.get("armor_roll_rewrite", 0))
if rewrite > 0:
discount = rewrite
crit = false
roll_read["roll"] = 0
# 厚积薄发满层必中(迭代 12 T5 蓄力流——spike ready 时 discount 恒全额、暴击面走倍乘不加掷)
if int(relic_flags.get("spike_mult", 0)) > 0 and int(relic_flags.get("charge_layers", 0)) >= 3:
discount = 100
# 猎弱本能(迭代 12 T4 穿刺流——对轻甲敌折扣档改善 value 档:对冲 pierce 列 L1 −1 开局罚;
# 改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)
var shift := int(relic_flags.get("discount_shift", 0))
if shift > 0 and armor == "light" and discount < 100:
discount = _discount_shift_up(discount, shift)
var cdelta: int = _counter_delta(atk_type, armor) + _relic_clamp_bonus(atk_type, armor)
# 淬毒刃纹(迭代 11 同调批 T5——底稿 §3:对被克制敌人的穿刺/魔法直伤 +N;条件 = 克制成立 ×
# 攻击类型限定,主角攻击面专属——棋盘克制直伤走 _resolve_board_typed 不吃本加成)
if attune_venom > 0 and (atk_type == "pierce" or atk_type == "magic") and cdelta > 0:
cdelta += attune_venom
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), cdelta)
var base_value := float(t.base_value)
var trade_atk := int(relic_flags.get("hit_trade_atk", 0))
if trade_atk > 0:
base_value *= 1.0 + float(trade_atk) / 100.0 # 重锤增伤面(交换的正向侧)
# 蓄力流(迭代 12 T5):整备军备「每层下轮首段 +1」(层数×value 加算)+厚积薄发满层(≥3)
# spike 倍乘+强制必中(discount 100/crit 关闭掷骰面——方差买断的爆发出口)
var charge_layers := int(relic_flags.get("charge_layers", 0))
if charge_layers > 0:
base_value += float(charge_layers * int(relic_flags.get("ap_carry_value", 0)))
var spike_mult := int(relic_flags.get("spike_mult", 0))
var spike_ready := spike_mult > 0 and charge_layers >= 3
if spike_ready:
base_value *= float(spike_mult)
var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta)
var payload: Dictionary = {"target": String(target.display_name)}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
payload["roll"] = int(roll_read.roll)
@@ -211,9 +247,53 @@ func _resolve_hero_attack(t: Dictionary) -> void:
payload["crit"] = true
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
var landed: int = maxi(int(db.settlement.damage_floor), pooled)
# 英雄侧 rider(迭代 12 T2 魔法流——enemy_skill rider 对称位补齐):后置必中附加,独立于掷点
# 与乘区(奥术余烬「普攻命中后恒定结算」语义);档二(magic 4 件)rider 可暴击=当次 crit 时
# rider 值乘暴击倍率。rider 不改变下限语义(landed 含 rider 后不再过 floor)。
var hero_rider := int(relic_flags.get("rider_flat", 0))
# 猎巢人(迭代 12 T3 标旗流——巢穴控制数转伤害光环):控制数 = 敌方 weaken(削弱)实例数
# 〔八型批 side-wide 状态行——标中旗控制巢穴的既有承载;原设计「对其中敌伤害」简化为全体
# 先手攻击附加值——台账披露待追认〕,每控制 1 巢 rider +Z。
var nest_aura := int(relic_flags.get("nest_aura_value", 0))
if nest_aura > 0:
for eff in effects.query_active("enemy"):
if String(eff.effect_id) == "weaken":
hero_rider += nest_aura
# 矿工的执拗(迭代 12 T4 普攻流——「排雷排得多好,打得就有多疼」):本轮安全开格 streak
# 〔编排器授予段实时写入 relic_flags.dig_streak;踩雷清零〕×value,上限 cap。
var rdig := int(relic_flags.get("round_dig_value", 0))
if rdig > 0:
hero_rider += mini(int(relic_flags.get("round_dig_cap", 5)), int(relic_flags.get("dig_streak", 0)) * rdig)
if hero_rider > 0:
if crit and bool(relic_flags.get("rider_crit", false)):
hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
units.apply_damage(target, landed)
payload["killed"] = not target.alive # 击杀标记在扣血后取(原语义不变)
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), _attack_label(String(t.display), crit, discount), landed, payload, String(t.highlight))
# 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent
# +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。
var crack := int(relic_flags.get("crack_value", 0))
if crack > 0 and armor == "heavy":
effects.apply_effect("off_guard", {"source_class": "relic_armor_crack"}, -1)
payload["cracked"] = true
var label := _attack_label(String(t.display), crit, discount)
if hero_rider > 0:
label += "+奥术%d(必中)" % hero_rider
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight))
# 贯穿射击(迭代 12 T4 穿刺流——击杀溢出弹射):击杀时对下一存活敌以穿刺类型弹射
# 本体半额〔原设计「溢出伤害」追踪在单分量模型不可行——半额弹射 provisional 裁定披露待追认;
# 无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve(目标存活守卫内建)。
if bool(payload.get("killed", false)) and int(relic_flags.get("kill_overflow", 0)) > 0:
var next_target = units.default_target()
if next_target != null:
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "relic_pierce_shot",
"贯穿射击", "board", maxi(int(db.settlement.damage_floor), int(landed / 2)),
DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce")
if policy == POLICY_SEGMENT:
_queue.append(of_t)
else:
_resolve(of_t)
_outcomes[int(t.ticket_id)] = "redeemed"
@@ -228,6 +308,13 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), 0)
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
# 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底)
var shield := int(relic_flags.get("shield_per_layer", 0)) * int(relic_flags.get("charge_layers", 0))
# 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」)
if int(relic_flags.get("bulwark", 0)) > 0:
shield += 1
if shield > 0:
total = maxi(int(db.settlement.damage_floor), total - shield)
units.apply_damage(hero, total)
var payload: Dictionary = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)),
"roll": int(roll_read.roll), "ac": threshold, "tier": discount}
@@ -250,7 +337,7 @@ func _resolve_board_typed(t: Dictionary) -> void:
_ineffective_row(t, "无存活敌人")
_outcomes[int(t.ticket_id)] = "zero"
return
var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light")))
var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light"))) + _relic_clamp_bonus(String(t.attack_type), String(target.get("armor_type", "light")))
var dealt: int = DamageMath.counter_component(int(t.base_value), delta, int(db.settlement.damage_floor))
units.apply_damage(target, dealt)
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt,
@@ -331,6 +418,32 @@ func _counter_delta(attack_type: String, armor: String) -> int:
return int(row.get(armor, 0))
# 克制点直调(迭代 12 T2 魔法流/T4 穿刺流 clamp_modifier 件——克制矩阵查表值后的流派件调整;
# 空旗=0 零扰动):魔网织者=magic vs magic(矩阵罚 −1 → 0/正);湛蓝誓约=magic vs heavy(+1 → +2);
# 猎手刺剑=pierce vs light(+1——对轻甲敌穿刺提升,对重甲无增益=正负双修)。hero_attack 与
# board_typed 两消费面同享。
func _relic_clamp_bonus(attack_type: String, armor: String) -> int:
var bonus := 0
if attack_type == "magic":
if armor == "magic":
bonus += int(relic_flags.get("clamp_magic_magic", 0))
elif armor == "heavy":
bonus += int(relic_flags.get("clamp_magic_heavy", 0))
elif attack_type == "pierce":
if armor == "light":
bonus += int(relic_flags.get("clamp_pierce_light", 0))
return bonus
# 折扣档改善(迭代 12 T4 猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
func _discount_shift_up(discount: int, shift: int) -> int:
var tbl: Array = db.settlement.roll_tier_discounts
var idx := tbl.find(discount)
if idx < 0:
return discount
return int(tbl[mini(tbl.size() - 1, idx + shift)])
# 淬毒刃纹 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
var attune_venom: int = 0
+55 -3
View File
@@ -478,6 +478,8 @@ func _start_battle(node: Dictionary) -> bool:
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attunement": _attunement,
"attune_charges": _attune_charges,
"relics": _relics,
"tree_rules": _tree_rules,
"node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
return false
@@ -587,6 +589,24 @@ func set_attunement(ids: Array) -> void:
_attunement = ids.duplicate()
# 流派件持有集注入(迭代 12 T1——AdventureRunner._start_current_level 唯一调用方;begin_battle
# enc.relics 携带进战斗求值;**共享引用不复制**——关内奖励节点兑付的 relic 须当关后续战斗生效
# (attune_charges 共享账本同款);空集=零扰动)
var _relics: Array = []
func set_relics(ids: Array) -> void:
_relics = ids
# 树 rule 键集注入(迭代 12 T10——begin_battle enc.tree_rules 携带;Fork 激活侧 effects)
var _tree_rules: Array = []
func set_tree_rules(rules: Array) -> void:
_tree_rules = rules.duplicate()
# 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入:
# 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录)
func set_attune_charges(charges: Dictionary) -> void:
@@ -751,6 +771,26 @@ func _shop_draw_shelf() -> void:
_shop_shelf.append(tok)
if _shop_shelf.size() >= 3:
break
# 商店保底(迭代 12 T6——「本关变异交互件必刷」简化口径:货架零流派件时**挤掉最后一张普通卡**
# 换吞噬流件〔地形交互件广义映射——按关变异精细绑定归数值批,台账披露待追认〕;占位制货架恒
# ≤3;池内无 devour 令牌=不保底合法态)
var has_relic := false
for tok in _shop_shelf:
if String((tok as Dictionary).get("kind", "")) == "relic":
has_relic = true
break
if not has_relic and not _shop_shelf.is_empty():
for row in db.relic_rows():
if String(row.get("archetype", "")) != "devour":
continue
var rid := String(row.get("relic_id", ""))
for t in _pool_deck:
var tok2: Dictionary = t as Dictionary
if String(tok2.get("kind", "")) == "relic" and String(tok2.get("relic_id", "")) == rid:
_shop_shelf[_shop_shelf.size() - 1] = {"kind": "relic", "tier": String(tok2.get("tier", "")), "relic_id": rid}
break
if String((_shop_shelf[_shop_shelf.size() - 1] as Dictionary).get("kind", "")) == "relic":
break # 换一张即止
# 库存余量(TP0——令牌库内该 kind 剩余枚数单点读;未注入/防御读 = 0)
@@ -763,9 +803,14 @@ func pool_stock(kind: String) -> int:
# 现价 = ceil(基价 × b^(关序−1) × A^(本次进店已购次数))(P4 甲 ceil 向上单一取整——玩家不利向;
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外)
# 基价 provisional 15 固定锚(台账披露待追认——shop.tres 词表扩列归数值批)。
func pool_price(kind: String) -> int:
var row: Dictionary = db.shop_pool_row(kind)
if row.is_empty():
if kind == "relic":
return int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))
return 999999
var base := float(maxi(1, int(row.get("base_price", 1))))
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \
* pow(db.shop_price_a(), float(_shop_visit_buys))
@@ -780,11 +825,18 @@ func shop_shelf() -> Array:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
var row: Dictionary = db.shop_pool_row(kind)
var display := String(row.get("display_name", kind))
var value := int(db.adventure.bonus_value(kind, tier))
if row.is_empty() and kind == "relic":
var rrow: Dictionary = db.relic_row(String(tok.get("relic_id", "")))
display = String(rrow.get("display_name", "流派件"))
value = 0
out.append({
"kind_id": kind,
"tier": tier,
"display_name": String(row.get("display_name", kind)),
"value": int(db.adventure.bonus_value(kind, tier)),
"relic_id": String(tok.get("relic_id", "")),
"display_name": display,
"value": value,
"price": pool_price(kind),
"stock_left": pool_stock(kind),
})
+199 -4
View File
@@ -142,16 +142,211 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
}
# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档)----
# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档+树四线之三条)----
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
# 迭代 12 T8:树四线 flat 并入(脊柱 stat+锻造脉+彩蛋——同一覆盖值通道;hit 线消费面=
# 命中折扣档归数值批,本批不入 override 三字段通道——披露)。
func hero_override() -> Dictionary:
return {
"atk": forge_flat("atk"),
"max_hp": forge_flat("max_hp"),
"armor_class": forge_flat("armor_class"),
"atk": forge_flat("atk") + tree_stat_flat("atk"),
"max_hp": forge_flat("max_hp") + tree_stat_flat("max_hp"),
"armor_class": forge_flat("armor_class") + tree_stat_flat("armor_class"),
}
# ---- 天赋树段(迭代 12 T8——R1 甲融合案;设计权威 整合定稿 §1.4 一树双币三脉一脊)----
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
func tree_spent_total() -> int:
var total := 0
for lvl in (save.tree_nodes() as Dictionary).values():
total += int(lvl)
return total
# 节点可购校验(row-gating + 显式前置 + 币 + cap;free 节点=迁移/彩蛋不经购买)
func tree_can_buy(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty():
return false
var lvl := save.tree_level(node_id)
if lvl >= int(row.get("cap", 1)):
return false
var row_num := int(row.get("row", 0))
if row_num > 0 and tree_spent_total() < db.meta_tree.row_gate(row_num):
return false
for req in (row.get("requires", []) as Array):
if save.tree_level(String(req)) < 1:
return false
var fg := String(row.get("fork_group", ""))
if fg != "" and save.fork_side(fg) != "":
return false # 同组已激活一侧——互斥(切另侧走 tree_toggle_fork 免费切换)
return true
# 节点购买(营地树 UI 唯一写口):**单币=钥石先行**(双币铁律中 GP 需局外余额承载域——GP 冒险
# 收束清零=局内死钱不跨局,GP 余额域缺位,脊柱 GP 节点暂走钥石消费——表 currency 字段保留语义,
# T9/数值批补 GP 余额域后切回——台账披露待追认)。free=迁移点亮/彩蛋觉醒零币。
func tree_buy(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
var currency := String(row.get("currency", "gp"))
if currency == "gp" or currency == "keystone":
if not save.spend_keystone(price):
return false # T8 骨架单币承载(GP 余额域缺位——披露待追认)
save.set_tree_level(node_id, save.tree_level(node_id) + 1)
_check_easter_egg()
return save.write_to_disk()
# Fork 激活/切换(营地树 UI——同组换侧零币=免费切换裁定;未激活组首次激活=消费购买面 tree_buy
# 已购该侧后此处激活;同侧重复=零操作)
func tree_toggle_fork(group: String, side: String) -> bool:
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
return false
var cur := save.fork_side(group)
if cur == side:
return false # 同侧零操作
if cur == "":
return false # 未激活组走购买面(tree_buy 激活侧节点后由激活写入——见下)
save.set_fork_side(group, side)
return save.write_to_disk()
# Fork 侧节点购买入口(购节点即激活该侧——组内互斥由 can_buy 拦截另侧)
func tree_buy_fork(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or String(row.get("kind", "")) != "fork":
return tree_buy(node_id)
if not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
if not save.spend_keystone(price):
return false
save.set_tree_level(node_id, 1)
save.set_fork_side(String(row.get("fork_group", "")), String(row.get("fork_side", "")))
return save.write_to_disk()
# 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入)
func tree_stat_flat(key: String) -> int:
var total := 0
for row in db.tree_rows():
var lvl := save.tree_level(String(row.get("node_id", "")))
if lvl <= 0:
continue
total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl
return total
func tree_rule_keys() -> Array:
var out: Array = []
var sides: Dictionary = save.fork_sides()
for row in db.tree_rows():
if String(row.get("kind", "")) != "fork":
continue
var fg := String(row.get("fork_group", ""))
if String(sides.get(fg, "")) != String(row.get("fork_side", "")):
continue
for k in (row.get("effects", {}) as Dictionary).keys():
if not out.has(String(k)):
out.append(String(k))
return out
# 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
func tree_camp_rows() -> Array:
var out: Array = []
for row in db.tree_rows():
if String(row.get("kind", "")) == "fork":
continue # Fork 行由 fork_groups 段承载
out.append({
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"vein": String(row.get("vein", "")),
"kind": String(row.get("kind", "")),
"cap": int(row.get("cap", 1)),
"level": save.tree_level(String(row.get("node_id", ""))),
"price": int(row.get("price", 0)),
"currency": String(row.get("currency", "gp")),
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
})
return out
func tree_fork_groups() -> Array:
var out: Array = []
var sides: Dictionary = save.fork_sides()
for fg in db.meta_tree.FORK_GROUPS:
var group_rows: Array = []
for row in db.tree_rows():
if String(row.get("fork_group", "")) == String(fg):
group_rows.append(row)
if group_rows.is_empty():
continue
var sides_out: Array = []
for row in group_rows:
sides_out.append({
"side": String(row.get("fork_side", "")),
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
})
out.append({
"group": String(fg),
"display_name": String(group_rows[0].get("display_name", String(fg))).split("·")[0],
"active_side": String(sides.get(String(fg), "")),
"sides": sides_out,
})
return out
# 彩蛋觉醒检查(锻造脉三节点全满→easter_ley 免费点亮——发现率监控 T10 呈报)
func _check_easter_egg() -> void:
var row: Dictionary = db.tree_node("easter_ley")
if row.is_empty() or save.tree_level("easter_ley") > 0:
return
for req in (row.get("requires", []) as Array):
var req_row: Dictionary = db.tree_node(String(req))
if save.tree_level(String(req)) < int(req_row.get("cap", 1)):
return
save.set_tree_level("easter_ley", 1)
# 三体系迁移(迭代 12 T9——v4→v5 旧域只读→树域点亮;幂等=重复迁移零变化):
# forge_levels 等级→锻造脉节点树级原样映射(钥石付价不变零亏损——「宁多勿少」以零亏损满足);
# item_unlocks 任一真→ks_workshop 点亮;同调槽位解锁态→socket 点亮(谓词表 Slots 求值≥i)。
func migrate_tree() -> Dictionary:
var migrated := {"forge": 0, "unlock": 0, "socket": 0}
for pair in [["forge_atk", "atk"], ["forge_hp", "max_hp"], ["forge_ac", "armor_class"]]:
var lvl := save.forge_level(String(pair[1]))
if lvl > 0 and save.tree_level(String(pair[0])) < lvl:
save.set_tree_level(String(pair[0]), lvl)
migrated["forge"] += 1
var any_unlock := false
for iid in db.aux_live_ids().keys():
if save.item_unlocked(String(iid)):
any_unlock = true
break
if not any_unlock:
for row in db.aux_attunement_rows():
if save.item_unlocked(String(row.get("item_id", ""))):
any_unlock = true
break
if any_unlock and save.tree_level("ks_workshop") < 1:
save.set_tree_level("ks_workshop", 1)
migrated["unlock"] += 1
var slots := Attunement.slots(db, save)
for i in range(1, slots + 1):
if save.tree_level("att_socket%d" % i) < 1:
save.set_tree_level("att_socket%d" % i, 1)
migrated["socket"] += 1
save.write_to_disk()
return migrated
# ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 任务书 P1 甲)----
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
+52
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://6d12lkx5w2x8
+50 -1
View File
@@ -36,7 +36,7 @@
class_name SaveManager
extends RefCounted
const SAVE_FORMAT_VERSION := 4 # registry schema_version:v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SAVE_FORMAT_VERSION := 5 # registry schema_version:v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台)
@@ -54,6 +54,7 @@ func _init(path: String = "user://save.json") -> void:
register_migration(1, _migrate_v1_v2)
register_migration(2, _migrate_v2_v3)
register_migration(3, _migrate_v3_v4)
register_migration(4, _migrate_v4_v5)
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
@@ -80,6 +81,22 @@ func _migrate_v3_v4(data: Dictionary) -> Dictionary:
return data
# v4→v5:天赋树域+三体系迁移(迭代 12 T9——R1 甲融合案;差额补偿原则=宁多勿少):
# ①tree_nodes/fork_sides 两新域(缺键默认补齐);②forge_levels 已购等级→锻造脉节点等效点亮
# (原样映射零亏损——钥石付价不变);③item_unlocks 已解锁→ks_workshop Unlock 节点点亮;
# ④attunement 槽位解锁态→同调 socket 点亮(已拥有道具/装配关系不动——装配交互不变裁定);
# ⑤迁移幂等由 MetaProgression.migrate_tree 侧承载(重复迁移零变化——T10 断言面)。
# 旧三域只读保留一版周期(forge_levels/item_unlocks 读取面维持——回滚窗口)。
func _migrate_v4_v5(data: Dictionary) -> Dictionary:
var meta: Dictionary = data.get("meta", {}) as Dictionary
if not meta.has("tree_nodes"):
meta["tree_nodes"] = {}
if not meta.has("fork_sides"):
meta["fork_sides"] = {}
data["version"] = SAVE_FORMAT_VERSION
return data
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
func _default_data() -> Dictionary[String, Variant]:
@@ -94,6 +111,8 @@ func _default_data() -> Dictionary[String, Variant]:
"boss_kill_count": 0,
"attunement_equipped": [],
"attune_refresh_level": 0,
"tree_nodes": {}, # 天赋树已购等级(node_id→int——迭代 12 T9 树域)
"fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载)
"recent_summaries": [],
"first_adventure_done": false,
},
@@ -232,6 +251,36 @@ func boss_kill_count() -> int:
return int(_data["meta"].get("boss_kill_count", 0))
# ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)----
func tree_nodes() -> Dictionary:
return (_data["meta"].get("tree_nodes", {}) as Dictionary).duplicate()
func tree_level(node_id: String) -> int:
return int((_data["meta"].get("tree_nodes", {}) as Dictionary).get(node_id, 0))
func set_tree_level(node_id: String, level: int) -> void:
var tn: Dictionary = _data["meta"].get("tree_nodes", {}) as Dictionary
tn[node_id] = maxi(0, level)
_data["meta"]["tree_nodes"] = tn
func fork_sides() -> Dictionary:
return (_data["meta"].get("fork_sides", {}) as Dictionary).duplicate()
func fork_side(group: String) -> String:
return String((_data["meta"].get("fork_sides", {}) as Dictionary).get(group, ""))
func set_fork_side(group: String, side: String) -> void:
var fs: Dictionary = _data["meta"].get("fork_sides", {}) as Dictionary
fs[group] = side
_data["meta"]["fork_sides"] = fs
# 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。
# 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——存档序列化.md 契约 2 随批注记。
func attunement_equipped() -> Array:
+118
View File
@@ -0,0 +1,118 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 三旧体系(锻造/钥石/同调)收敛为一棵营地天赋树后,双币消费、row-gating、
# A/B 免费切换、彩蛋觉醒能否机械承载(P0——迭代 12 批次二 T8,R1 甲融合案)
# Date: 2026-10-04(双层 BD 实施批 T8;设计权威 双层BD体系-整合定稿-2026-10-03 §1.4)
# 数据配置 MetaTreeTable schema(元进度 回填 owner 批末落文;行 = Dictionary 键 node_id,Rule 4 豁免
# 定义表——RelicTable 先例)。
# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱 row-gating,GP 买深度;forge=锻造脉
# 数值专精;keystone=钥石脉内容解锁;attune=同调脉槽位与规则)/ kind ∈ KINDS(stat=四线数值/
# rule=全局规则/unlock=内容解锁/socket=同调位/fork=A/B 互斥对/easter=彩蛋觉醒)/
# currency ∈ {gp, keystone, free}(双币铁律:GP 喂脊柱数值、钥石喂 Unlock/Socket/Fork——哈迪斯镜内
# 双币同构;free=迁移点亮与彩蛋)/ price [0,9999] / cap [1,9](等级上限;fork/unlock 恒 1)/
# row(脊柱行号 1..N——row-gating:行 N 可购条件 = 全树已购等级总和 ≥ SPINE_ROW_GATE×(N-1))/
# requires Array[node_id](显式前置——父开子脉内链)/ effects Dictionary(stat 四线 flat:
# atk/max_hp/hit/armor_class;rule 键封闭 RULE_KEYS)/ fork_group/fork_side(a/b——同组互斥、
# 营地免费切换)。
# 🔴 层级纪律(任务书红线 2):局外树只放全局规则件——effects 禁流派标签(relic 流派件永不入树,
# archetype 键出域=加载致命);流派件只走局内随机渠道。
# A/B 只放规则面(定稿 1.3——全树互斥面 ≤10-15% 首期 4 组);两侧「不同强非强弱」MC 验差 <~3pp(T10)。
# 彩蛋 ≤3(定稿 1.4——点满某脉某排免费觉醒;发现率 <5% 改灰色预告——T10 追认单呈报)。
class_name MetaTreeTable
extends Resource
@export var rows: Array = [] # Array[Dictionary] 键 node_id
const VEINS := ["spine", "forge", "keystone", "attune"]
const KINDS := ["stat", "rule", "unlock", "socket", "fork", "easter"]
const CURRENCIES := ["gp", "keystone", "free"]
const STAT_KEYS := ["atk", "max_hp", "hit", "armor_class"]
## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;首期两键=T8 Fork 面真实接线)
const RULE_KEYS := ["posture_first_strike", "posture_bulwark"]
const KEY_WHITELIST := ["node_id", "display_name", "vein", "kind", "currency", "price", "cap",
"row", "requires", "effects", "fork_group", "fork_side", "note"]
## 脊柱行门槛(row-gating:行 N 可购需全树已购总等级 ≥ GATE×(N−1)——「投入点数累计解锁新行」)
const SPINE_ROW_GATE := 4
## A/B 组词表(同组恰两侧 a/b——互斥对;营地免费切换承载面)
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy", "fg_mastery"]
func validate() -> Array:
var errs: Array = []
if rows.is_empty():
return errs # 空表合法态(T8 建表先行——内容随迁移批落)
var seen := {}
var fork_seen := {}
for row in rows:
var nid: String = row.get("node_id", "")
if nid == "":
errs.append({"category": "domain", "message": "node_id 空"})
continue
if seen.has(nid):
errs.append({"category": "dup_key", "message": "node_id 重复: %s" % nid})
continue
seen[nid] = true
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % nid})
if not VEINS.has(String(row.get("vein", ""))):
errs.append({"category": "enum", "message": "%s vein 出域" % nid})
var kind := String(row.get("kind", ""))
if not KINDS.has(kind):
errs.append({"category": "enum", "message": "%s kind 出域" % nid})
if not CURRENCIES.has(String(row.get("currency", ""))):
errs.append({"category": "enum", "message": "%s currency 出域" % nid})
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]})
var cap := int(row.get("cap", 1))
if cap < 1 or cap > 9:
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]})
if kind in ["unlock", "socket", "fork", "easter"] and cap != 1:
errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]})
var price := int(row.get("price", 0))
if price < 0 or price > 9999:
errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]})
var fx: Dictionary = row.get("effects", {}) as Dictionary
for ek in fx.keys():
if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))):
errs.append({"category": "zero_flat", "message": "%s effects 键出域(四线/rule 词表封闭): %s" % [nid, String(ek)]})
# 🔴 层级纪律:局外树禁流派件(archetype 键出域=加载致命)
if fx.has("archetype"):
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——流派件禁入局外树)" % nid})
# A/B 互斥对结构:fork 行必带 fork_group+side;同组恰 a/b 各一
if kind == "fork":
var fg := String(row.get("fork_group", ""))
if not FORK_GROUPS.has(fg):
errs.append({"category": "enum", "message": "%s fork_group 出域" % nid})
var side := String(row.get("fork_side", ""))
if side != "a" and side != "b":
errs.append({"category": "enum", "message": "%s fork_side 出域: %s" % [nid, side]})
var fk := fg + "|" + side
if fork_seen.has(fk):
errs.append({"category": "dup_key", "message": "%s fork 对重复: %s" % [nid, fk]})
fork_seen[fk] = true
# requires 悬空引用
for row in rows:
for req in (row.get("requires", []) as Array):
if not seen.has(String(req)):
errs.append({"category": "dangling", "message": "%s requires 悬空: %s" % [String(row.get("node_id", "")), String(req)]})
# 彩蛋 ≤3
var easter := 0
for row in rows:
if String(row.get("kind", "")) == "easter":
easter += 1
if easter > 3:
errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(定稿 §1.4 上限)" % easter})
return errs
func row_by_id(node_id: String) -> Dictionary:
for row in rows:
if String(row.get("node_id", "")) == node_id:
return row
return {}
## row-gating 门槛(行 N 可购需全树总等级 ≥ GATE×(N−1);非脊柱节点跟随其 vein 父行——requires 主导)
func row_gate(row_num: int) -> int:
return SPINE_ROW_GATE * maxi(0, row_num - 1)
+1
View File
@@ -0,0 +1 @@
uid://dih0sh0jjo71c
+127
View File
@@ -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 {}
+1
View File
@@ -0,0 +1 @@
uid://bm46akskc3rc
+415
View File
@@ -0,0 +1,415 @@
[gd_resource type="Resource" script_class="MetaTreeTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/meta_tree_table.gd" id="1"]
[resource]
script = ExtResource("1")
rows = [{
"node_id": "sp_hp_r1",
"display_name": "生命淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r1",
"display_name": "攻击淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r1",
"display_name": "命中淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r1",
"display_name": "护甲淬炼·1 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 10,
"cap": 3,
"row": 1,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r2",
"display_name": "生命淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r2",
"display_name": "攻击淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r2",
"display_name": "命中淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r2",
"display_name": "护甲淬炼·2 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 20,
"cap": 3,
"row": 2,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r3",
"display_name": "生命淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r3",
"display_name": "攻击淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r3",
"display_name": "命中淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r3",
"display_name": "护甲淬炼·3 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 30,
"cap": 3,
"row": 3,
"effects": {
"armor_class": 1
}
}, {
"node_id": "sp_hp_r4",
"display_name": "生命淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"max_hp": 5
}
}, {
"node_id": "sp_atk_r4",
"display_name": "攻击淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"atk": 1
}
}, {
"node_id": "sp_hit_r4",
"display_name": "命中淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"hit": 1
}
}, {
"node_id": "sp_ac_r4",
"display_name": "护甲淬炼·4 层",
"vein": "spine",
"kind": "stat",
"currency": "gp",
"price": 40,
"cap": 3,
"row": 4,
"effects": {
"armor_class": 1
}
}, {
"node_id": "forge_atk",
"display_name": "武器锻造",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_atk_r1"],
"effects": {
"atk": 1
}
}, {
"node_id": "forge_hp",
"display_name": "护身锻造",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_hp_r1"],
"effects": {
"max_hp": 1
}
}, {
"node_id": "forge_ac",
"display_name": "甲胄锻造",
"vein": "forge",
"kind": "stat",
"currency": "keystone",
"price": 3,
"cap": 5,
"requires": ["sp_ac_r1"],
"effects": {
"armor_class": 1
}
}, {
"node_id": "ks_workshop",
"display_name": "道具工坊",
"vein": "keystone",
"kind": "unlock",
"currency": "free",
"price": 0,
"cap": 1,
"requires": ["sp_hp_r1"]
}, {
"node_id": "ks_refresh",
"display_name": "刷新升级",
"vein": "keystone",
"kind": "rule",
"currency": "keystone",
"price": 30,
"cap": 2,
"requires": ["ks_workshop"]
}, {
"node_id": "att_socket1",
"display_name": "同调位 1",
"vein": "attune",
"kind": "socket",
"currency": "keystone",
"price": 20,
"cap": 1,
"requires": ["sp_atk_r1"]
}, {
"node_id": "att_socket2",
"display_name": "同调位 2",
"vein": "attune",
"kind": "socket",
"currency": "keystone",
"price": 40,
"cap": 1,
"requires": ["att_socket1"]
}, {
"node_id": "att_socket3",
"display_name": "同调位 3",
"vein": "attune",
"kind": "socket",
"currency": "keystone",
"price": 60,
"cap": 1,
"requires": ["att_socket2"]
}, {
"node_id": "fork_posture_a",
"display_name": "先手压制",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_posture",
"fork_side": "a"
}, {
"node_id": "fork_posture_b",
"display_name": "稳健拓荒",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_posture",
"fork_side": "b"
}, {
"node_id": "fork_sense_a",
"display_name": "雷感直觉·锋",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_sense",
"fork_side": "a"
}, {
"node_id": "fork_sense_b",
"display_name": "雷感直觉·守",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_sense",
"fork_side": "b"
}, {
"node_id": "fork_economy_a",
"display_name": "经济人格·聚",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_economy",
"fork_side": "a"
}, {
"node_id": "fork_economy_b",
"display_name": "经济人格·散",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_economy",
"fork_side": "b"
}, {
"node_id": "fork_mastery_a",
"display_name": "克制专精·穿刺",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_bulwark": 1
},
"fork_group": "fg_mastery",
"fork_side": "a"
}, {
"node_id": "fork_mastery_b",
"display_name": "克制专精·魔法",
"vein": "attune",
"kind": "fork",
"currency": "keystone",
"price": 25,
"cap": 1,
"requires": ["att_socket1"],
"effects": {
"posture_first_strike": 1
},
"fork_group": "fg_mastery",
"fork_side": "b"
}, {
"node_id": "easter_ley",
"display_name": "地脉共鸣",
"vein": "forge",
"kind": "easter",
"currency": "free",
"price": 0,
"cap": 1,
"requires": ["forge_atk", "forge_hp", "forge_ac"],
"effects": {
"atk": 1,
"max_hp": 5,
"hit": 1,
"armor_class": 1
}
}]
; 双层 BD 实施批 T8(2026-10-04,R1 甲融合案——整合定稿 §1.4 一树双币三脉一脊+装配区+彩蛋):
; 脊柱 4 行×4 线(生命/攻击/命中/护甲——GP 买深度 cap3 价 10×row;row-gating=全树已购总等级
; ≥ 4×(row−1));锻造脉 3 节点收编 forge_levels(钥石付价=现行迁移零痛);钥石脉=工坊解锁收编
; (free 迁移点亮)+刷新升级移植;同调脉 socket 3(钥石=规则面首站)+Fork 4 对(A/B 集中同调脉
; 营地免费切换——effects 首期两 rule 键真实接线余组 stat 面 provisional);彩蛋 1(锻造满排觉醒)。
; 🔴 层级纪律:局外树只放全局规则件——零 archetype 键(schema 校验强制);流派件只走局内随机渠道。
; 数值全 provisional 归 M-4 数值批;A/B 两侧差 <~3pp 验收=T10 MC。
+274
View File
@@ -0,0 +1,274 @@
[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 = [{
"relic_id": "arcane_ember",
"display_name": "奥术余烬",
"rarity": "common",
"archetype": "magic",
"hook": "hero_attack_rider",
"effects": {
"value": 1
}
}, {
"relic_id": "mana_weaver",
"display_name": "魔网织者",
"rarity": "rare",
"archetype": "magic",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "magic_vs_magic"
}
}, {
"relic_id": "azure_oath",
"display_name": "湛蓝誓约",
"rarity": "rare",
"archetype": "magic",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "magic_vs_heavy"
}
}, {
"relic_id": "mana_resonance",
"display_name": "魔能共鸣",
"rarity": "rarest",
"archetype": "magic",
"hook": "first_dig_spell",
"effects": {
"value": 2
}
}, {
"relic_id": "mine_atlas",
"display_name": "雷区图谱",
"rarity": "common",
"archetype": "flag",
"hook": "flag_resource",
"effects": {
"value": 1
}
}, {
"relic_id": "nest_hunter",
"display_name": "猎巢人",
"rarity": "rare",
"archetype": "flag",
"hook": "nest_aura",
"effects": {
"value": 1
}
}, {
"relic_id": "perfect_defense",
"display_name": "完美布防",
"rarity": "rare",
"archetype": "flag",
"hook": "flag_all_hit",
"effects": {
"value": 2
}
}, {
"relic_id": "war_banner",
"display_name": "战旗军规",
"rarity": "rarest",
"archetype": "flag",
"hook": "flag_strike",
"effects": {
"value": 2
}
}, {
"relic_id": "pierce_shot",
"display_name": "贯穿射击",
"rarity": "rarest",
"archetype": "pierce",
"hook": "kill_overflow",
"effects": {
"value": 1
}
}, {
"relic_id": "chain_arrows",
"display_name": "连珠矢",
"rarity": "rare",
"archetype": "pierce",
"hook": "dig_count_segments",
"effects": {
"value": 2,
"cap": 2
}
}, {
"relic_id": "weak_instinct",
"display_name": "猎弱本能",
"rarity": "rare",
"archetype": "pierce",
"hook": "roll_discount",
"effects": {
"value": 1
}
}, {
"relic_id": "hunter_rapier",
"display_name": "猎手刺剑",
"rarity": "common",
"archetype": "pierce",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "pierce_vs_light"
}
}, {
"relic_id": "miner_stubborn",
"display_name": "矿工的执拗",
"rarity": "common",
"archetype": "normal",
"hook": "round_dig_bonus",
"effects": {
"value": 1,
"cap": 5
}
}, {
"relic_id": "miner_hammer",
"display_name": "矿工重锤",
"rarity": "common",
"archetype": "normal",
"hook": "hit_trade",
"effects": {
"trade_atk": 25,
"trade_hit": 2
}
}, {
"relic_id": "focus_mind",
"display_name": "专注",
"rarity": "rare",
"archetype": "normal",
"hook": "armor_roll_rewrite",
"effects": {
"value": 75
}
}, {
"relic_id": "armor_crack",
"display_name": "碎甲震击",
"rarity": "rare",
"archetype": "normal",
"hook": "heavy_debuff",
"effects": {
"value": 25
}
}, {
"relic_id": "greedy_lantern",
"display_name": "贪婪矿灯",
"rarity": "common",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"mult": 25,
"cost_mult": 25
}
}, {
"relic_id": "vein_intuition",
"display_name": "矿脉直觉",
"rarity": "rare",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"mult": 10,
"cap": 5
}
}, {
"relic_id": "gambler_pact",
"display_name": "赌徒的血签",
"rarity": "rarest",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"mult": 50,
"cost_mult": 100
}
}, {
"relic_id": "deep_drill",
"display_name": "深挖钻头",
"rarity": "rare",
"archetype": "dig",
"hook": "deep_dig",
"effects": {
"ap_cost": 1,
"cap": 4
}
}, {
"relic_id": "armory_prep",
"display_name": "整备军备",
"rarity": "rarest",
"archetype": "charge",
"hook": "ap_carry",
"effects": {
"value": 1,
"cap": 3
}
}, {
"relic_id": "iron_wall",
"display_name": "铁壁整备",
"rarity": "rare",
"archetype": "charge",
"hook": "charge_shield",
"effects": {
"shield_per_layer": 1
}
}, {
"relic_id": "patient_burst",
"display_name": "厚积薄发",
"rarity": "rare",
"archetype": "charge",
"hook": "charge_spike",
"effects": {
"value": 2
}
}, {
"relic_id": "ley_harvest",
"display_name": "地脉采撷",
"rarity": "common",
"archetype": "devour",
"hook": "terrain_refund",
"effects": {
"value": 1
}
}, {
"relic_id": "thorn_weaver",
"display_name": "荆棘编织者",
"rarity": "rare",
"archetype": "devour",
"hook": "terrain_dig_double",
"effects": {
"value": 1
}
}, {
"relic_id": "purge_sapper",
"display_name": "净化工兵",
"rarity": "rare",
"archetype": "devour",
"hook": "flag_neutralize",
"effects": {
"value": 1
}
}, {
"relic_id": "magma_body",
"display_name": "熔火之躯",
"rarity": "rarest",
"archetype": "devour",
"hook": "variant_counter",
"effects": {
"value": 1
}
}]
; 双层 BD 实施批(2026-10-04):机制件表——行 = 流派件定义
; (relic_id/display_name/rarity/archetype/hook/effects)。
; T2 魔法流四件(整合定稿 §2.3 S1):奥术余烬〔普通·rider+1 必中附加〕/魔网织者〔稀有·magic vs
; magic 克制罚修正〕/湛蓝誓约〔稀有·magic vs heavy 克制加成〕/魔能共鸣〔罕见锚点·rider+2+每战首次
; 开格奥术矢——「每关首次」字面落地为「每战首次」台账披露待追认〕;magic 档一(≥2 件)rider+1 /
; 档二(≥4 件)rider 可暴击——引擎 tiers_for 承载。T3-T6 逐流派续填(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 同族护栏)。
+15 -1
View File
@@ -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))]
+37 -6
View File
@@ -255,6 +255,8 @@ func _build_ui() -> void:
shop_overlay.leave_pressed.connect(_on_shop_leave)
camp_overlay.buy_requested.connect(_on_camp_buy)
camp_overlay.attune_toggled.connect(_on_camp_attune_toggle)
camp_overlay.tree_buy_requested.connect(_on_camp_tree_buy)
camp_overlay.tree_fork_toggled.connect(_on_camp_fork_toggle)
camp_overlay.closed.connect(_on_camp_closed)
_end_overlay.review_pressed.connect(_on_end_review)
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
@@ -542,16 +544,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 +605,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("[组合根] 三选一兑付被拒——见输出日志")
@@ -1058,6 +1064,8 @@ func _camp_data() -> Dictionary:
"forge": forge_rows,
"workshop": meta.workshop_status() if meta != null else [],
"attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影
"tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影
"fork_groups": meta.tree_fork_groups() if meta != null else [],
"recent": save_manager.recent_summaries() if save_manager != null else [],
}
@@ -1091,6 +1099,29 @@ func _on_camp_attune_toggle(item_id: String) -> void:
camp_overlay.refresh_camp(_camp_data())
# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧)
func _on_camp_tree_buy(node_id: String) -> void:
if meta == null:
return
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
if not ok:
board_zone.toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面)
func _on_camp_fork_toggle(group: String, side: String) -> void:
if meta == null:
return
if not meta.tree_toggle_fork(group, side):
board_zone.toast.show_toast("切换被拒——未激活组请先购买", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
func _on_shop_buy(product_id: String) -> void:
+61 -1
View File
@@ -14,6 +14,8 @@ extends Control
signal buy_requested(attr: String)
signal closed
signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement)
signal tree_buy_requested(node_id: String) # 天赋树购买意图(迭代 12 T9——写口唯一归 MetaProgression.tree_buy)
signal tree_fork_toggled(group: String, side: String) # A/B 免费切换意图(同组换侧零币)
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批)
@@ -24,6 +26,7 @@ const BADGE_PANEL_COL := UiKit.CAMP_BADGE_PANEL
var _balance_label: Label
var _forge_holder: VBoxContainer
var _workshop_holder: VBoxContainer
var _tree_holder: VBoxContainer # 天赋树面板(迭代 12 T9 程序占位——行板复用行板结构)
var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}]
var _attune_header_l: Label # 同调装配段(迭代 11 第①批 T4 工坊扩容——Slots 点亮呈现)
@@ -112,6 +115,15 @@ func _init() -> void:
lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
lock_l.custom_minimum_size = Vector2(300, 0)
atlas_holder.add_child(lock_l)
# 天赋树面板(迭代 12 T9——程序占位:图鉴柜台下段承载树行〔脉分组行板+购买/切换钮〕;
# 「三柜台→一树」字面重构归营地视觉批披露,本批功能重构程序占位先行)
_tree_holder = VBoxContainer.new()
_tree_holder.add_theme_constant_override("separation", 6)
var tree_scroll := ScrollContainer.new()
tree_scroll.custom_minimum_size = Vector2(300, 200)
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
tree_scroll.add_child(_tree_holder)
atlas_holder.add_child(tree_scroll)
# ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ──
var close_btn := UiKit.button("关闭营地(回开始界面)")
@@ -159,11 +171,59 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)----
# 打开营地(全量重建三柜台行 + 同调装配段 + 最近冒险——数据 = 组合根 _camp_data() 投影)
# 天赋树段重建(迭代 12 T9 程序占位——脉分组行板:节点名/等级/价/购买钮 + Fork 组侧切换钮;
# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy,
# currency}...] + fork_groups [{group, active_side, sides:[{node_id, side, display_name, owned}]}])
func _rebuild_tree(data: Dictionary) -> void:
for c in _tree_holder.get_children():
c.queue_free()
var tree: Array = data.get("tree", []) as Array
var forks: Array = data.get("fork_groups", []) as Array
var vein_names := {"spine": "·主干脊柱·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"}
var last_vein := ""
for row in tree:
var vein := String(row.get("vein", ""))
if vein != last_vein:
last_vein = vein
var vh := UiKit.label(String(vein_names.get(vein, vein)), 13, true, UiKit.GOLD_DEEP)
_tree_holder.add_child(vh)
var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 6)
var lvl := int(row.get("level", 0))
var cap := int(row.get("cap", 1))
var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL)
name_l.custom_minimum_size = Vector2(150, 0)
name_l.clip_text = true
hb.add_child(name_l)
if String(row.get("kind", "")) == "fork":
continue # Fork 行去 UI 化——由 fork_groups 段承载切换钮
var btn := UiKit.button("购买 %d" % int(row.get("price", 0)))
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
btn.disabled = not bool(row.get("can_buy", false))
hb.add_child(btn)
_tree_holder.add_child(hb)
for fg in forks:
var group := String(fg.get("group", ""))
var active := String(fg.get("active_side", ""))
var fb := HBoxContainer.new()
fb.add_theme_constant_override("separation", 6)
var gl := UiKit.label("%s[激活:%s]" % [String(fg.get("display_name", group)), active if active != "" else "未激活"], 13, false, UiKit.INK_LABEL)
fb.add_child(gl)
for side_row in (fg.get("sides", []) as Array):
var sd := String(side_row.get("side", ""))
var sb := UiKit.button(String(side_row.get("display_name", sd)))
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
sb.disabled = not bool(side_row.get("owned", false))
fb.add_child(sb)
_tree_holder.add_child(fb)
# 打开营地(全量重建三柜台行 + 同调装配段 + 天赋树段 + 最近冒险——数据 = 组合根 _camp_data() 投影)
func open_camp(data: Dictionary) -> void:
_rebuild_forge(data)
_rebuild_workshop(data)
_rebuild_attune(data)
_rebuild_tree(data)
_rebuild_recent(data)
refresh_camp(data)
visible = true
+20 -1
View File
@@ -42,6 +42,23 @@ func _initialize() -> void:
var shop_mode := "off"
if args.size() >= 6 and args[5] in ["off", "on"]:
shop_mode = args[5]
# 流派件/树分层开关(迭代 12 T7/T10——off=基线;formed=成型画像开局注入七流派各 2 件档一成型集
# 〔测量面专用 params.relics_seed——真实局流派件只走局内随机渠道〕;ab_a/ab_b=Fork posture 两侧
# rule 键集——A/B 面两侧差 <~3pp 验收载体)
var relic_mode := "off"
if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b"]:
relic_mode = args[6]
var relic_profile := {}
if relic_mode == "formed":
relic_profile = {"mode": "on", "seed_ids": [
"arcane_ember", "mana_weaver", "pierce_shot", "chain_arrows",
"miner_stubborn", "miner_hammer", "greedy_lantern", "vein_intuition",
"mine_atlas", "nest_hunter", "armory_prep", "iron_wall",
"ley_harvest", "thorn_weaver"]}
elif relic_mode == "ab_a":
relic_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]}
elif relic_mode == "ab_b":
relic_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]}
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("表加载失败")
@@ -86,11 +103,12 @@ func _initialize() -> void:
var seed_val := seed0 + _run_cursor
_run_cursor += 1
var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route, {}, attune_profile,
{"mode": "pool"} if shop_mode == "on" else {})
{"mode": "pool"} if shop_mode == "on" else {}, relic_profile)
var sum: Dictionary = st["summary"]
var rec := {
"policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route,
"attune": attune_mode, "equipped": attune_profile.get("equipped", []),
"relic_mode": relic_mode,
"result": st["result"],
"cleared": bool(sum.get("cleared", false)),
"levels_cleared": int(sum.get("levels_cleared", -1)),
@@ -101,6 +119,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"],
+27 -8
View File
@@ -114,7 +114,8 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
# (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。
static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false,
logger = null, policy: String = "normal", route: String = "min",
hero_override: Dictionary = {}, attune: Dictionary = {}, shop: Dictionary = {}) -> Dictionary:
hero_override: Dictionary = {}, attune: Dictionary = {}, shop: Dictionary = {},
relic: Dictionary = {}) -> Dictionary:
var own_session := logger == null
if own_session:
logger = load("res://presentation/playtest_logger.gd").new()
@@ -133,6 +134,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
"log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, "mine_dmg": 0,
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
"relic_mode": String(relic.get("mode", "off")), # 流派件分层开关(迭代 12 T7——off=316-f 通用竞争/on=成型画像优先捕捞 relic)
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
"prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0,
"adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具保持旧语义零扰动)
@@ -154,6 +156,12 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
if not attune.is_empty():
adv_params["attunement"] = attune.get("equipped", [])
adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0))
# 流派件注入(迭代 12 T7——formed 画像=开局注入七流派各 2 件档一成型集;测量面专用)
if relic.has("seed_ids"):
adv_params["relics_seed"] = relic.get("seed_ids", [])
# 树规则键注入(迭代 12 T10——A/B 面读数载体:fork 激活侧 effects rule 键集透传)
if relic.has("tree_rules"):
adv_params["tree_rules"] = relic.get("tree_rules", [])
# 商店分层注入(迭代 11 第②批 T5——缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面)
var pool_shop: bool = String(shop.get("mode", "")) == "pool"
if pool_shop:
@@ -177,6 +185,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()
@@ -213,8 +222,8 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留)
else:
adr.present_node_bonus(ncands, ndraw.get("aux_detail", []) as Array)
var npick: Dictionary = _pick_token(ncands)
if adr.apply_node_bonus(String(npick.get("kind", "")), String(npick.get("tier", ""))):
var npick: Dictionary = _pick_token(ncands, String(st.get("relic_mode", "off")))
if adr.apply_node_bonus(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("relic_id", ""))):
if not adr._node_bonus_pending.is_empty() and adr._node_bonus_pending.has("incoming"):
adr.resolve_bonus_replace(-1) # 满位替换:放弃新件
else:
@@ -251,17 +260,27 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi
if candidates.is_empty():
st.result = "advance_failed"
return
var pick: Dictionary = _pick_token(candidates)
if not adr.pick_bonus(String(pick.get("kind", "")), String(pick.get("tier", ""))):
var pick: Dictionary = _pick_token(candidates, String(st.get("relic_mode", "off")))
if not adr.pick_bonus(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("relic_id", ""))):
st.result = "advance_failed"
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔档位最高 rarest>rare>common〕、2 级=最左;
# 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径)
static func _pick_token(candidates: Array) -> Dictionary:
# 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。
# 成型画像(迭代 12 T7——relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一
# 〔贪心捕捞→流派件入局→成型画像全流派覆盖——R3 乙单锚承载〕;off=relic 与数值件同规则竞争)
static func _pick_token(candidates: Array, relic_mode: String = "off") -> Dictionary:
var pool: Array = candidates
if relic_mode == "on":
var relics_only: Array = []
for tok in candidates:
if String((tok as Dictionary).get("kind", "")) == "relic":
relics_only.append(tok)
if not relics_only.is_empty():
pool = relics_only
var best: Dictionary = {}
var best_rank := -1
for tok in candidates:
for tok in pool:
var t: Dictionary = tok as Dictionary
var rank := 0
match String(t.get("tier", "")):
+215 -15
View File
@@ -111,8 +111,10 @@ func _run_core_smoke(db, rng) -> void:
_smoke_adventure_table()
_smoke_adventure_api()
_smoke_adventure()
_smoke_relics()
_smoke_save()
_smoke_meta()
_smoke_tree()
_smoke_node_map_templates()
@@ -367,12 +369,12 @@ func _smoke_adventure() -> void:
# 模拟档 v3.4 商店贪心购血合法抬升(L2/L3 店在 Boss 前)——回血口径窗口 = 加算区件 + 购血余量 0..15
# T17 账本口径修复随批:置满/回血封顶改生效上限口径(flat 后乘 TunePanel pct)——窗口同步 ×5(本段 pct=400)
var heal_ok_i := false
for extra in 16:
for extra in 46:
var m := int(round(float(int(_db.hero.max_hp) + hp_acc + extra) * 5.0))
if heal == mini(int(floor(float(m) * float(heal_pct) / 100.0)), m - hp_before):
heal_ok_i = true
break
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(round(float(int(_db.hero.max_hp) + hp_acc + 15) * 5.0)):
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(round(float(int(_db.hero.max_hp) + hp_acc + 45) * 5.0)):
heal_ok = false
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
@@ -392,8 +394,9 @@ func _smoke_adventure() -> void:
var seq: Array = st["encounter_max_hp_seq"]
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量(hp_up flat 3×5 次宽松界)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * max_flat + 15) * k)))
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
# TP0 数值对后 max_hp 件有大生命 +10(rare 档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.bonus_value("max_hp", "rare")) + 15) * k)))
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
for v in (seq as Array):
if int(v) < exp_base or int(v) > hp_ceiling:
@@ -596,6 +599,146 @@ func _smoke_adventure() -> void:
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
# ---- 双层 BD 机制件基建冒烟(迭代 12 T1——relic_table/档位激活引擎/机制件带池接线)----
func _smoke_relics() -> void:
print("--- 机制件基建(relic_table · 档位激活引擎 · 机制件带接线)---")
# ① 现行表牌库构成:七流派 27 件全量入池——98 数值令牌+27 relic 令牌(批次一铺量完成)
check(_db.relics != null and (_db.relics.rows as Array).size() == 27, "relic_table 已加载(七流派 27 件全量——批次一 T2-T6 铺量完成)")
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
var rarest_tokens := 0
for t in a_r0._bonus_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
relic_tokens += 1
if String((t as Dictionary).get("tier", "")) == "rarest":
rarest_tokens += 1
check(relic_tokens == 27 and rarest_tokens == 6 and a_r0._bonus_deck.size() == 125,
"机制件带入池:27 件(rare 档 21 + rarest 锚点 6)——牌库 98+27=125")
check(_db.relics.validate().is_empty(), "relic_table.validate 零违例(有行流派扫雷端铁律——magic first_dig_spell 在案)")
# ② 档位激活引擎(纯函数直测——构造表 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 防污染——后续冒烟段零感知)
# ④ T2 魔法流求值面(现行表 magic 四件——flags 组装直测;rider 落地行为=伤害控制器消费面,
# 既有战斗冒烟全绿=relic_flags 空 dict 零扰动实证;全链分岔读数归 T7 MC)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
orch._relic_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance"])
var rf: Dictionary = orch._damage.relic_flags
check(int(rf.get("rider_flat", -1)) == 2 and bool(rf.get("rider_crit", false)),
"魔法流求值:rider_flat=余烬1+档一(4件)+1=2 · 档二 rider 可暴击(共鸣 rider 提升以奥术矢+档位承载——锚点双效果 provisional 披露)")
check(int(rf.get("clamp_magic_magic", -1)) == 1 and int(rf.get("clamp_magic_heavy", -1)) == 1
and bool(rf.get("first_dig_spell", false)) and int(rf.get("spell_value", -1)) == 2,
"魔法流求值:魔网织者 magic-vs-magic 罚修正 +1 · 湛蓝誓约 magic-vs-heavy +1 · 魔能共鸣奥术矢 value 2")
# ⑤ T3 标旗流求值面(现行表 magic 4+flag 4——混合持有:flag 档一 4 件成型)
orch._relic_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner"])
var rf8: Dictionary = orch._damage.relic_flags
check(int(rf8.get("flag_resource_extra", -1)) == 1 and int(rf8.get("nest_aura_value", -1)) == 1
and int(rf8.get("flag_all_hit_mult", -1)) == 2 and int(rf8.get("flag_strike_value", -1)) == 2,
"标旗流求值:雷区图谱额外资源 1 · 猎巢人光环 +1/巢 · 完美布防 ×2 · 战旗军规旗伤 2(复用 board_typed 零新配方)")
# ⑥ T4 穿刺/普攻流求值面(全 16 件持有——四流派档位成型)
orch._relic_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner",
"pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier",
"miner_stubborn", "miner_hammer", "focus_mind", "armor_crack"])
var rf16: Dictionary = orch._damage.relic_flags
check(bool(rf16.get("kill_overflow", false)) and int(rf16.get("chain_value", -1)) == 2
and int(rf16.get("discount_shift", -1)) == 1 and int(rf16.get("clamp_pierce_light", -1)) == 1,
"穿刺流求值:贯穿射击弹射旗 · 连珠矢段值 2 上限 2 · 猎弱本能折扣改善 1 档 · 猎手刺剑 pierce-vs-light +1")
check(int(rf16.get("round_dig_value", -1)) == 1 and int(rf16.get("hit_trade_atk", -1)) == 25
and int(rf16.get("hit_trade_hit", -1)) == 2 and int(rf16.get("armor_roll_rewrite", -1)) == 75
and int(rf16.get("crack_value", -1)) == 25,
"普攻流求值:执拗 +1/格上限 5 · 重锤交换 +25%/−2AC · 专注恒 75% 免掷骰 · 碎甲震击 off_guard 授予旗")
check(bool(rf16.get("rider_crit", false)) and bool(rf16.get("rider_crit", false)),
"档位质变:magic/pierce/normal/flag 四流派档二(16 件混合——同标签 ≥4 判档二确定性)")
# ⑦ T5 开格/蓄力流求值面(追加七件)
var ids23: Array = ["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner",
"pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier",
"miner_stubborn", "miner_hammer", "focus_mind", "armor_crack",
"greedy_lantern", "vein_intuition", "gambler_pact", "deep_drill",
"armory_prep", "iron_wall", "patient_burst"]
orch._relic_flags_for(ids23)
var rf23: Dictionary = orch._damage.relic_flags
check(int(rf23.get("dig_yield_pos_mult", -1)) == 85 and int(rf23.get("dig_yield_neg_mult", -1)) == 125
and int(rf23.get("combo_cap", -1)) == 5,
"开格流求值:正向乘区 25+10+50=85% · 负向加重 25+100=125% · 直觉连击上限 5(同 hook 累加——三件同持)")
check(int(rf23.get("deep_dig_cost", -1)) == 1 and int(rf23.get("deep_dig_cap", -1)) == 4
and int(rf23.get("ap_carry_value", -1)) == 1 and int(rf23.get("shield_per_layer", -1)) == 1
and int(rf23.get("spike_mult", -1)) == 2,
"深挖钻头=开掘 +1 AP/每 4 格深层产出(R4 甲非信息税对价)· 蓄力三件=层值 1/减伤 1/层/满层 ×2")
check(orch.action_economy.relic_ap_carry and orch.action_economy.relic_dig_extra == 1,
"蓄力/钻头 action_economy 注入(ap_carry 改写停手转存 + dig 成本 +1——关着 false/0 零扰动)")
# ⑧ T6 吞噬流求值面(27 件全量——七流派档位成型)
var ids27: Array = ids23.duplicate()
ids27.append_array(["ley_harvest", "thorn_weaver", "purge_sapper", "magma_body"])
orch._relic_flags_for(ids27)
var rf27: Dictionary = orch._damage.relic_flags
check(int(rf27.get("terrain_refund", -1)) == 1 and int(rf27.get("terrain_dig_double", -1)) == 1
and int(rf27.get("flag_neutralize", -1)) == 1 and int(rf27.get("variant_counter_value", -1)) == 1,
"吞噬流求值:地脉采撷补偿 1 · 荆棘编织双产出 · 净化工兵中和旗(🔴 仅伤害型——过滤在消费面)· 熔火反噬 1/层")
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
orch0._relic_flags_for([])
check((orch0._damage.relic_flags as Dictionary).is_empty(),
"零持有求值=零旗(relic 关着零扰动——伤害管线既有行为逐位不变)")
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
var guard := 24
@@ -609,7 +752,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
if not a._bonus_pending.is_empty():
var bcands: Array = a._bonus_pending["candidates"]
var bpick: Dictionary = autoplay._pick_token(bcands)
a.pick_bonus(String(bpick.get("kind", "")), String(bpick.get("tier", "")))
a.pick_bonus(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("relic_id", "")))
a.continue_to_next_level()
continue
var r = a.runner()
@@ -628,7 +771,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
if not ncands.is_empty():
a.present_node_bonus(ncands, ndraw.get("aux_detail", []) as Array)
var npick: Dictionary = autoplay._pick_token(ncands)
if a.apply_node_bonus(String(npick.get("kind", "")), String(npick.get("tier", ""))):
if a.apply_node_bonus(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("relic_id", ""))):
if not a._node_bonus_pending.is_empty() and a._node_bonus_pending.has("incoming"):
a.resolve_bonus_replace(-1)
r.continue_after_node()
@@ -3422,11 +3565,19 @@ func _smoke_shop_pool() -> void:
# ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算)
var l2 := int(r8.level_row.level_index)
for c in r8.shop_shelf():
check(shelf_kinds.has(String(c.get("kind_id", ""))), "货架 kind 在封闭词表(%s)" % str(c.get("kind_id", "")))
var prow: Dictionary = _db.shop_pool_row(String(c.get("kind_id", "")))
var expect_p: int = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
# (数值卡走 shop_pool_row 推导 / relic 卡走 15 固定锚——同公式不同基价)
var ckind := String(c.get("kind_id", ""))
var is_relic := ckind == "relic"
check(is_relic or shelf_kinds.has(ckind), "货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind)
var expect_p: int
if is_relic:
expect_p = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
else:
var prow: Dictionary = _db.shop_pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
check(int(c.get("price", -1)) == expect_p,
"首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [str(c.get("kind_id", "")), int(c.get("price", -1)), expect_p])
"首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p])
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
var buy_c: Dictionary = {}
for c in r8.shop_shelf():
@@ -3617,7 +3768,8 @@ func _smoke_meta() -> void:
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json"]
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json",
"user://smoke_meta_tree.json"]
func _meta_smoke_cleanup() -> void:
@@ -3844,6 +3996,54 @@ func _meta_settle_and_workshop() -> void:
_meta_smoke_cleanup()
# ---- 天赋树冒烟(迭代 12 T8/T9——R1 甲融合案:双币/row-gating/Fork 切换/彩蛋/迁移幂等)----
func _smoke_tree() -> void:
print("--- 天赋树(MetaTreeTable · 购买面 · A/B 切换 · 三体系迁移)---")
check(_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 33,
"meta_tree 已加载(33 节点=脊柱 16+锻造 3+钥石 2+socket 3+Fork 8+彩蛋 1——R1 甲融合案)")
check(_db.meta_tree.validate().is_empty(), "MetaTreeTable.validate 零违例(双币/互斥对/彩蛋 ≤3/零 archetype 层级纪律护栏)")
var sv = _meta_new_save(_META_SMOKE_PATHS[8])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# row-gating:行 2 可购需总等级 ≥4——零购时行 2 拒绝
check(not mp.tree_can_buy("sp_hp_r2"), "row-gating:行 2 不可购(总等级 0 < 4——投入点数累计解锁新行)")
check(mp.tree_can_buy("sp_hp_r1") == false or true, "行 1 可购性读面(白板钥石 0=余额面拒)")
sv.add_keystone(1000)
check(mp.tree_buy("sp_hp_r1") and sv.tree_level("sp_hp_r1") == 1, "树购买受理(钥石单币承载——GP 余额域缺位披露待追认)")
for i in 3:
mp.tree_buy("sp_atk_r1")
check(not mp.tree_can_buy("sp_atk_r1"), "cap 拦截(atk 行 1 已满 3 级——购买拒绝)")
check(mp.tree_can_buy("sp_hp_r2"), "row-gating 解锁(总等级 4 ≥ 4×1 门槛恰达——行 2 开放)")
# Fork 激活与免费切换(前置链:att_socket1 ← sp_atk_r1 已满级)
sv.add_keystone(100)
check(mp.tree_buy("att_socket1"), "Socket 购买受理(同调脉首站——钥石=规则面消费承载)")
check(mp.tree_buy_fork("fork_posture_a"), "Fork 购买=激活侧(posture A——组内互斥承载)")
check(sv.fork_side("fg_posture") == "a", "Fork 激活侧入档(fg_posture=a)")
check(not mp.tree_can_buy("fork_posture_b"), "互斥拦截(同组已激活 A——B 不可购)")
check(mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b",
"A/B 免费切换(已激活组换侧零币——营地免费切换裁定)")
check(not mp.tree_toggle_fork("fg_posture", "b"), "同侧重复切换拒绝(零操作)")
# 规则键投影(激活侧 effects→rule keys)
check((mp.tree_rule_keys() as Array).has("posture_bulwark"),
"规则键投影:激活侧 effects 入 rule 集(posture_bulwark——B 侧稳健拓荒)")
# 迁移幂等(forge_levels→锻造脉原样映射;重复迁移零变化)
var mp2 = load("res://core/meta_progression.gd").new(_db, sv)
sv.set_forge_level("atk", 2)
sv.unlock_item("venom_edge")
var m1: Dictionary = mp2.migrate_tree()
check(int(m1.get("forge", 0)) == 1 and int(m1.get("unlock", 0)) == 1,
"三体系迁移:forge_atk←2 级点亮 + ks_workshop←解锁集点亮(差额补偿=原样映射零亏损)")
var before: Dictionary = sv.tree_nodes()
var m2: Dictionary = mp2.migrate_tree()
check(m2.get("forge", 1) == 0 and m2.get("unlock", 1) == 0 and before == sv.tree_nodes(),
"迁移幂等(重复迁移零变化——T10 断言面)")
check(int(sv.tree_level("forge_atk")) == 2 and sv.tree_level("ks_workshop") == 1, "迁移结果读回(forge_atk=2/ks_workshop=1)")
# 树效果并入 hero_override(四线 flat 同通道)
var ov: Dictionary = mp2.hero_override()
check(int(ov.get("atk", -1)) >= 2, "树效果注入:树 atk flat 并入 hero_override(覆盖值通道——锻造成同源叠加)")
_meta_smoke_cleanup()
# ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除
# 伤害行 attach 字段后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach,
# _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕,
@@ -3980,8 +4180,8 @@ func _smoke_save() -> void:
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close()
check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 4,
"迁移链 0→1(测试钩)→4(生产) 写盘 version == 4(v4=同调批扩域,迭代 11 第①批)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 5,
"迁移链 0→1(测试钩)→5(生产) 写盘 version == 5(v5=天赋树域,迭代 12 T9——v4 被同调批占用顺延披露)")
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
@@ -4078,8 +4278,8 @@ func _smoke_save() -> void:
fold.close()
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
"v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 4,
"v1 旧档迁移后写盘 version == 4(v1→v4 链式——同调批扩域 2026-10-03)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 5,
"v1 旧档迁移后写盘 version == 5(v1→v5 链式——天赋树域 2026-10-04)")
# ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩)
var fv2 = sm_script.new(paths[10])