T1机制件基建(迭代12双层BD——relic_table+档位激活引擎+机制件带池接线):RelicTable schema新建(relic_id/rarity三档对齐令牌池/archetype七流派标签/hook挂点枚举23点位/effects键白名单零裸flat同族护栏+扫雷触发点铁律=有行流派各≥1扫雷端〔部分填充合法·七流派全量归T7出口〕);RelicsEngine纯函数引擎(集2档一/4档二/散件保底·成型=tier≥1·挂点投影表序稳定);relics.tres空表合法态(池98张零扰动硬断言);池接线=relic活行每件1张入稀有/罕见档(common/rare→rare·rarest→rarest·永不入普通档)+令牌身份键relic_id(同档多件可同屏)+兑付入持有集先校验后摘牌+_relics冒险级跨关携带+断点扩键relics持有集/余库relic_id随还原+摘要additive键relics(清账前快照——pool_stock同坑预防)+RUNSUM行relics字段;呈现=relic卡「流派件·流派名」+卡片id三段复合键;autoplay读数面;冒烟14新断言(空表零扰动/引擎四断言/接线含断点往返)→684/0+run_all 15套全绿

This commit is contained in:
DKChan committed 2026-10-04 00:39:26 +08:00
1 parent 2352eab988
commit c25e8bab3e
12 files changed
+413 -50

No files matched your search

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