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:
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 提升以奥术矢+档位承载)。
|
||||
@@ -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
@@ -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), # 道具件跨关携带(注入前中断防静默丢失)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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 甲)----
|
||||
|
||||
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://6d12lkx5w2x8
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dih0sh0jjo71c
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bm46akskc3rc
|
||||
@@ -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。
|
||||
@@ -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 同族护栏)。
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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])
|
||||
|
||||
Reference in new issue
Block a user