奇物批T3+T4:relic_*→feat_*词族全换(feat_table.gd/feats.tres/feats_engine.gd三文件改名+.uid+feat_flags/feat_id/feat_row等全token+config_db注册键feats两端+entities.yaml换代注记——kind值relic/身份前缀relic:/relics存档键/摘要键按数据契约保留);T4渠道白名单过滤(_draw_curio_candidates(channel):通关渠道只出专长/节点渠道零专长——过滤式不拆池,TIER_QUOTAS锚零改动,devour货架保底随渠道定位退役,迁移保feat_id,pick_curio测试feat_id透传;冒烟773/0+run_all 16全绿+断点往返PASS)

This commit is contained in:
DKChan committed 2026-10-05 19:28:20 +08:00
1 parent 8e2694de49
commit 6812ebbb73
24 files changed
+385 -384

No files matched your search

+8 -8
View File
@@ -36,12 +36,12 @@ var _settlement_consumed := false # AC-14 门:notify_settlement_don
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=深挖钻头付费开掘对价(R4 甲非信息税形态;2026-10-04 试玩缺陷修复——对价挂付费
# feat_ap_carry=整备军备(轮末剩余 AP 转蓄力层替代 RAP 转存——cleared 作废口径改写的停手侧承载);
# feat_dig_extra=深挖钻头付费开掘对价(R4 甲非信息税形态;2026-10-04 试玩缺陷修复——对价挂付费
# 开掘侧:AP 富余时挖掘付全价 1+extra 积付费计数、AP 不足时降级只付基础 1 照常开格不积计数——
# 原「持有期所有挖掘无条件 +1」实现把对价错挂成普通挖掘税,AP=1 锁死开格);charge_layers=蓄力层账本。
var relic_ap_carry := false
var relic_dig_extra := 0
var feat_ap_carry := false
var feat_dig_extra := 0
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
@@ -95,8 +95,8 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
_reject("phase")
return false
var cost := _cost("dig")
if relic_dig_extra > 0 and _ap >= cost + relic_dig_extra:
cost += relic_dig_extra
if feat_dig_extra > 0 and _ap >= cost + feat_dig_extra:
cost += feat_dig_extra
_deep_paid_count += 1
if _ap < cost:
_reject("resource_face")
@@ -136,13 +136,13 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
# 整备军备改写(迭代 12 T5——relic_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
# 整备军备改写(迭代 12 T5——feat_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if relic_ap_carry:
if feat_ap_carry:
_charge_layers = mini(3, _charge_layers + _ap)
else:
_rap += _ap
+75 -63
View File
@@ -81,7 +81,7 @@ var _resume_bp := {} # 续玩快照暂存(resume_adventur
# ---- 三选一停点态(迭代 10 奖励重构批)----
var _curio_taken := {} # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载;池库存正主=_curio_deck)
var _curio_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起规则改写型特性带=relic 令牌随表追加)
var _relics: Array[String] = [] # 特性持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树)
var _feats: Array[String] = [] # 特性持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树)
var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
@@ -130,12 +130,12 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_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 []
var feat_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))
if not feat_seed.is_empty():
_feats = []
for r in feat_seed:
_feats.append(String(r))
ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含调试浮窗 pct,与战斗内同源)
state = "running"
return _start_current_level()
@@ -179,7 +179,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
for id in _curio_taken.keys():
_curio_taken[id] = maxi(0, int(bp_taken.get(id, 0)))
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
# 迭代 12 T1——relic 令牌 relic_id 随还原)
# 迭代 12 T1——relic 令牌 feat_id 随还原)
var bp_deck = bp.get("bonus_deck", null)
if bp_deck is Array:
var restored: Array = []
@@ -187,13 +187,13 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
var tok: Dictionary = t as Dictionary
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", ""))
rtok["feat_id"] = String(tok.get("feat_id", ""))
restored.append(rtok)
_curio_deck = restored
# 特性持有集还原(迭代 12 T1——缺键 = 旧版断点防御空集)
_relics = []
_feats = []
for r in (bp.get("relics", []) as Array):
_relics.append(String(r))
_feats.append(String(r))
for v in (bp.get("bonus_aux_carry", []) as Array):
_bonus_treasure_carry.append(String(v))
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
@@ -220,7 +220,11 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
var migrated: Array = []
for c in (bp_pending.get("candidates", []) as Array):
if c is Dictionary:
migrated.append({"kind": String((c as Dictionary).get("kind", "")), "tier": String((c as Dictionary).get("tier", ""))})
# T4——feat_id 随迁移(通关渠道候选=纯专长令牌,身份键 relic|tier|feat_id 三段全要)
var mig := {"kind": String((c as Dictionary).get("kind", "")), "tier": String((c as Dictionary).get("tier", ""))}
if String(mig["kind"]) == "relic":
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
migrated.append(mig)
else:
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
_curio_pending = {"candidates": migrated}
@@ -259,7 +263,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
func _reset_curio_state() -> void:
_curio_taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
_curio_deck = _build_curio_deck()
_relics = []
_feats = []
_curio_pending = {}
_curio_result = {}
_bonus_treasure_carry = []
@@ -276,16 +280,16 @@ func _build_curio_deck() -> Array:
for tier in db.adventure.TIERS:
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
deck.append({"kind": String(kind), "tier": String(tier)})
for row in db.relic_rows():
for row in db.feat_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", ""))})
deck.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
return deck
# 令牌唯一身份(抽签去重/兑付匹配共用——属性加成型特性=kind,规则改写型特性=relic_id;同档多张不同 relic 可同屏)
# 令牌唯一身份(抽签去重/兑付匹配共用——属性加成型特性=kind,规则改写型特性=feat_id;同档多张不同 relic 可同屏)
func _token_identity(tok: Dictionary) -> String:
if String(tok.get("kind", "")) == "relic":
return "relic:" + String(tok.get("relic_id", ""))
return "relic:" + String(tok.get("feat_id", ""))
return String(tok.get("kind", ""))
@@ -337,64 +341,64 @@ func continue_to_next_level() -> bool:
# ---- 提升三选一(迭代 10 奖励重构批——选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
# 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 relic_id;迭代 12 T1 规则改写型特性带)
func _find_token(candidates: Array, kind: String, tier: String, relic_id: String = "") -> Dictionary:
# 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 feat_id;迭代 12 T1 规则改写型特性带)
func _find_token(candidates: Array, kind: String, tier: String, feat_id: String = "") -> Dictionary:
for t in candidates:
var tok: Dictionary = t as Dictionary
if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier:
continue
if kind == "relic" and String(tok.get("relic_id", "")) != relic_id:
if kind == "relic" and String(tok.get("feat_id", "")) != feat_id:
continue
return tok
return {}
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
# 另须 relic_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束(随池种退役不可达)。
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
func pick_curio(kind: String, tier: String = "", relic_id: String = "") -> bool:
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
if state != "running" or _curio_pending.is_empty():
push_error("[冒险结构] pick_curio 拒绝——无待选停点(state=%s)" % state)
return false
var candidates: Array = _curio_pending.get("candidates", []) as Array
if _find_token(candidates, kind, tier, relic_id).is_empty():
push_error("[冒险结构] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, relic_id])
if _find_token(candidates, kind, tier, feat_id).is_empty():
push_error("[冒险结构] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
var value: int = int(db.adventure.curio_value(kind, tier))
var hp_before := int(ledger.get("hero_hp", 0))
if kind == "treasure_item":
return _pick_treasure_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨;池种退役后不可达)
if not _apply_curio_pick(kind, tier, value, relic_id):
push_error("[冒险结构] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, relic_id])
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[冒险结构] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
_finish_curio_pick(kind, value, hp_before, tier, relic_id)
_finish_curio_pick(kind, value, hp_before, tier, feat_id)
return true
# 兑付载荷 relic 伴随键(迭代 12 T1——反馈行取数面;非 relic 件零新增键)
func _relic_payload_keys(relic_id: String) -> Dictionary:
if relic_id == "":
func _feat_payload_keys(feat_id: String) -> Dictionary:
if feat_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", ""))}
var row: Dictionary = db.feat_row(feat_id)
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
# 三选一件兑付共享体(2026-10-02 修复批双胞胎收口;TP0 改令牌消费——摘牌单点;迭代 12 T1 relic 分支):
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限——
# 与账本置满/通关回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面
# 按挂点消费,档位质变由 RelicsEngine 求值)。兑付即从 _curio_deck 摘除该令牌(选中踢池;
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从 _curio_deck 摘除该令牌(选中踢池;
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
func _apply_curio_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)
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
if kind == "relic" and db.feat_row(feat_id).is_empty():
push_error("[冒险结构] relic 兑付失败——表无此件: %s" % feat_id)
return false # 先校验后摘牌(令牌零丢失)
if not _consume_deck_token(kind, tier, relic_id):
push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, relic_id])
if not _consume_deck_token(kind, tier, feat_id):
push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id])
return false
if kind == "relic":
if not _relics.has(relic_id):
_relics.append(relic_id)
if not _feats.has(feat_id):
_feats.append(feat_id)
_curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1
return true
match kind:
@@ -412,7 +416,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, relic_id: String
_curio_taken["ap"] = int(_curio_taken.get("ap", 0)) + 1
"gp":
if _runner != null:
_runner.grant_bonus_gp(value)
_runner.grant_curio_gp(value)
_curio_taken["gp"] = int(_curio_taken.get("gp", 0)) + 1
"heal":
ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)) + value, effective_max_hp())
@@ -422,13 +426,13 @@ func _apply_curio_pick(kind: String, tier: String, value: int, relic_id: String
return true
# 从牌库摘除一枚令牌(TP0——选中踢池单点;迭代 12 T1 relic 按 relic_id 精确摘;找不到 = false)
func _consume_deck_token(kind: String, tier: String, relic_id: String = "") -> bool:
# 从牌库摘除一枚令牌(TP0——选中踢池单点;迭代 12 T1 relic 按 feat_id 精确摘;找不到 = false)
func _consume_deck_token(kind: String, tier: String, feat_id: String = "") -> bool:
for i in _curio_deck.size():
var tok: Dictionary = _curio_deck[i] as Dictionary
if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier:
continue
if kind == "relic" and String(tok.get("relic_id", "")) != relic_id:
if kind == "relic" and String(tok.get("feat_id", "")) != feat_id:
continue
_curio_deck.remove_at(i)
return true
@@ -436,8 +440,8 @@ func _consume_deck_token(kind: String, tier: String, relic_id: String = "") -> b
# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写)
func relics_held() -> Array:
return _relics.duplicate()
func feats_held() -> Array:
return _feats.duplicate()
# ---- 商店共享池消费面(迭代 11 第②批 T3——P1 共享池单点账本;TP0 令牌池换代——库存=牌库剩余令牌)----
@@ -513,7 +517,7 @@ func resolve_treasure_replace(discard_index: int) -> bool:
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(迭代 12 T1——反馈行取数)
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", relic_id: String = "") -> void:
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
_curio_pending = {}
_curio_result = {
"picked": picked,
@@ -523,7 +527,7 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String
"hp_after": int(ledger.get("hero_hp", 0)),
"level_index": level_index,
}
_curio_result.merge(_relic_payload_keys(relic_id))
_curio_result.merge(_feat_payload_keys(feat_id))
curio_resolved.emit(_curio_result.duplicate(true))
_snapshot_breakpoint()
@@ -534,7 +538,7 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
# 掷定前置(追加批二):返回 dict 带 aux_detail(道具件具体道具)供卡片直显。
func node_curio_candidates() -> Dictionary:
var cands: Array = _draw_curio_candidates()
var cands: Array = _draw_curio_candidates("reward_node")
return {"candidates": cands, "aux_detail": _last_draw_treasure.duplicate()}
@@ -544,21 +548,21 @@ func present_node_curio(candidates: Array, treasure_detail: Array = []) -> void:
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库;relic 同权入持有集)
func apply_node_curio(kind: String, tier: String = "", relic_id: String = "") -> bool:
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
if _node_curio_pending.is_empty():
push_error("[冒险结构] apply_node_curio 拒绝——无待选停点")
return false
var candidates: Array = _node_curio_pending.get("candidates", []) as Array
if _find_token(candidates, kind, tier, relic_id).is_empty():
push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, relic_id])
if _find_token(candidates, kind, tier, feat_id).is_empty():
push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
return false
var value: int = int(db.adventure.curio_value(kind, tier))
if kind == "treasure_item":
return _apply_node_treasure(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨;池种退役后不可达)
if not _apply_curio_pick(kind, tier, value, relic_id):
push_error("[冒险结构] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, relic_id])
if not _apply_curio_pick(kind, tier, value, feat_id):
push_error("[冒险结构] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
return false
_finish_node_bonus(kind, value, tier, relic_id)
_finish_node_bonus(kind, value, tier, feat_id)
return true
@@ -646,7 +650,7 @@ func refresh_curio() -> bool:
_attune_refresh_x -= 1
else:
_attune_refresh_y -= 1
var cands: Array = _draw_curio_candidates()
var cands: Array = _draw_curio_candidates("level_clear")
_curio_pending["candidates"] = cands.duplicate(true)
_curio_pending["aux_detail"] = _last_draw_treasure.duplicate()
var payload: Dictionary = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true)
@@ -660,7 +664,7 @@ func refresh_curio() -> bool:
_attune_refresh_x -= 1
else:
_attune_refresh_y -= 1
var cands2: Array = _draw_curio_candidates()
var cands2: Array = _draw_curio_candidates("reward_node")
_node_curio_pending["candidates"] = cands2.duplicate(true)
_node_curio_pending["aux_detail"] = _last_draw_treasure.duplicate()
return true
@@ -700,7 +704,7 @@ func resolve_curio_replace(discard: int) -> bool:
return resolve_treasure_replace(discard)
func _finish_node_bonus(picked: String, value: int, tier: String = "", relic_id: String = "") -> void:
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
_node_curio_pending = {}
var payload := {
"context": "reward_node",
@@ -709,7 +713,7 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", relic_id:
"value": value,
"level_index": level_index,
}
payload.merge(_relic_payload_keys(relic_id))
payload.merge(_feat_payload_keys(feat_id))
curio_resolved.emit(payload)
@@ -719,9 +723,17 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", relic_id:
# 自动回池;池耗即枯竭——余库不足 N kind 时出齐剩余,空库 = 零候选合法态)。候选 = 令牌字典
# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选;道具件掷定前置
# 随池种退役拆除(_last_draw_treasure 恒空集,机械惰性承载)。
func _draw_curio_candidates() -> Array:
# 渠道白名单过滤(迭代 15 卡② T4——渠道轴定位:关底 Boss 击败=通关渠道只出专长(feat 令牌);
# 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。
# 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样,
# 白名单在候选视图层生效——配额结构锚 TIER_QUOTAS 70/25/3=98 零改动(专长令牌本系活行追加面,在锚外)。
func _draw_curio_candidates(channel: String = "reward_node") -> Array:
_last_draw_treasure = []
var pool := _curio_deck.duplicate()
if channel == "level_clear":
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) == "relic")
else:
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) != "relic")
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
for i in pool.size():
var j := rg.randi_range(i, pool.size() - 1)
@@ -737,7 +749,7 @@ func _draw_curio_candidates() -> Array:
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", ""))
cand["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
out.append(cand)
if out.size() >= int(db.adventure.curio_candidate_count):
break
@@ -828,7 +840,7 @@ 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_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面)
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
if _shop_mode == "pool":
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动)
@@ -876,7 +888,7 @@ func _on_level_finished(payload: Dictionary) -> void:
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
var pick_payload: Dictionary = {
"level_index": level_index,
"candidates": _draw_curio_candidates(),
"candidates": _draw_curio_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=通关渠道只出专长
"aux_detail": _last_draw_treasure.duplicate(), # 掷定前置(追加批二——道具件具体道具随候选抽定,卡片直显)
"heal_gained": heal,
"hp_before": hp_before,
@@ -908,7 +920,7 @@ func _settle(result: String) -> void:
"aux_cleared_last": _last_treasure_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——特性终局持有集:清账前快照,成型判读取数面)
"relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面)
}
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
@@ -976,7 +988,7 @@ func _snapshot_breakpoint() -> void:
"aux_items": treasure_snapshot,
"bonus_taken": _curio_taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
"bonus_deck": _curio_deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
"relics": _relics.duplicate(), # 特性持有集(迭代 12 T1——局内随机渠道获得态续玩防丢失)
"relics": _feats.duplicate(), # 特性持有集(迭代 12 T1——局内随机渠道获得态续玩防丢失)
"bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"bonus_aux_carry": _bonus_treasure_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失)
+64 -64
View File
@@ -72,13 +72,13 @@ var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改)
var _attune_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数
# 特性运行态(迭代 12 T2——relic 持有集经 enc.relics 注入;求值旗注 damage_controller.relic_flags;
# 特性运行态(迭代 12 T2——relic 持有集经 enc.feats 注入;求值旗注 damage_controller.feat_flags;
# 魔矢每战首次计数 battle 域。空集=零扰动)
var _relic_first_dig_used := false
var _relic_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零)
var _relic_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限)
var _relic_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照)
var _relic_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
var _feat_first_dig_used := false
var _feat_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零)
var _feat_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限)
var _feat_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照)
var _feat_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
func _attune_has(item_id: String) -> bool:
@@ -94,15 +94,15 @@ func _attune_mag(item_id: String) -> int:
return int(_attune_row(item_id).get("magnitude", 0))
# 特性求值(迭代 12 T2——持有集 × relic_table → damage_controller.relic_flags 注入面):
# 特性求值(迭代 12 T2——持有集 × feat_table → damage_controller.feat_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:
func _feat_flags_for(feat_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)
if not feat_ids.is_empty() and db.feats != null and not db.feats.rows.is_empty():
var hooks := FeatsEngine.hooks_for(feat_ids, db.feats)
var tiers := FeatsEngine.tiers_for(feat_ids, db.feats)
for h in hooks:
var hook: String = String(h.get("hook", ""))
var fx: Dictionary = h.get("effects", {}) as Dictionary
@@ -173,10 +173,10 @@ func _relic_flags_for(relic_ids: Array) -> void:
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))
_damage.feat_flags = flags
# 蓄力流/深挖钻头 action_economy 注入(求值即注入——冒烟直呼 _feat_flags_for 同面生效)
action_economy.feat_ap_carry = int(flags.get("ap_carry_value", 0)) > 0
action_economy.feat_dig_extra = int(flags.get("deep_dig_cost", 0))
var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
var _perfect_hit_false := 0
@@ -247,21 +247,21 @@ 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_given = 0
_relic_paid_at_round_start = 0
_relic_magma_layers = 0
# 特性运行态注入(迭代 12 T2 魔法流——enc.feats 携带持有集;FeatsEngine 求值后注伤害控制器
# feat_flags;空集/空表=零扰动。魔矢每战首次开格计数同步归零)
_feat_flags_for((enc.get("feats", []) as Array))
_feat_first_dig_used = false
_feat_combo = 0
_feat_deep_given = 0
_feat_paid_at_round_start = 0
_feat_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
_damage.feat_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
@@ -465,29 +465,29 @@ func _settle() -> void:
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
_grant_routed(_relic_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
_grant_routed(_feat_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
# 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支);
# 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价)
_relic_combo += 1
_feat_combo += 1
# 深挖钻头 v2(2026-10-04 二次修正——用户实测「付 2AP 无可见提升纯挨打」:v1 配额制
# 4 次付费 8AP 换 1 份随机资源=净负期望+收益埋明细带不可感知):改**本格双份**——
# 付费挖掘的本格资源直接双份(多花 1AP 买多一份同格产出,账目直白期望微正)。
# 本轮付费次数 N=deep_paid_count−轮初快照;safe 事件按 N 上限逐格追加第二份(「深挖」)。
var dd_cost := int(_damage.relic_flags.get("deep_dig_cost", 0))
var dd_cost := int(_damage.feat_flags.get("deep_dig_cost", 0))
if dd_cost > 0:
var paid_this_round := int(action_economy.deep_paid_count) - _relic_paid_at_round_start
if paid_this_round > 0 and _relic_deep_given < paid_this_round:
_relic_deep_given += 1
var paid_this_round := int(action_economy.deep_paid_count) - _feat_paid_at_round_start
if paid_this_round > 0 and _feat_deep_given < paid_this_round:
_feat_deep_given += 1
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)),
if bool(_damage.feat_flags.get("first_dig_spell", false)) and not _feat_first_dig_used:
_feat_first_dig_used = true
var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell",
"奥术矢", "board", int(_damage.feat_flags.get("spell_value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_damage.submit(spell_t)
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
@@ -499,8 +499,8 @@ func _settle() -> void:
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
_grant_routed(_relic_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
_relic_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
_grant_routed(_feat_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
_feat_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径,轮末结算逐行兑现;
# 信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;雷格恒无变异——布置段排除,
# 双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,非奖励不作废)
@@ -516,25 +516,25 @@ func _settle() -> void:
_damage.submit(vt)
# 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载):
# ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源)
if int(_damage.relic_flags.get("terrain_dig_double", 0)) > 0:
if int(_damage.feat_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:
if int(_damage.feat_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
if int(_damage.feat_flags.get("variant_counter_value", 0)) > 0:
_feat_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:
if int(_damage.feat_flags.get("flag_strike_value", 0)) > 0:
var hits := 0
var misses := 0
for fevt in _facts_flags:
@@ -543,26 +543,26 @@ func _settle() -> void:
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))
var mult := int(_damage.feat_flags.get("flag_all_hit_mult", 1))
var strike_value := int(_damage.feat_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",
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_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,
var magma := int(_damage.feat_flags.get("variant_counter_value", 0))
if magma > 0 and _feat_magma_layers > 0:
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_magma_body",
"熔火反噬 ×%d" % _feat_magma_layers, "board", magma * _feat_magma_layers,
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
_damage.submit(mg_t)
_relic_magma_layers = 0
_feat_magma_layers = 0
if _facts_path == "cleared":
_grant_clear_reward(ri)
_damage.flush() # 授予段界结账(B 模式——段内开单攒批到此统一兑现;A 模式 no-op;清图批前另有同步结账点)
@@ -592,7 +592,7 @@ func _settle() -> void:
_emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "")
_exit_distribution()
# 深挖钻头 v2 轮初快照滚动(本轮付费次数已消费——下一轮 N=当前值−新快照)
_relic_paid_at_round_start = int(action_economy.deep_paid_count)
_feat_paid_at_round_start = int(action_economy.deep_paid_count)
_settling = false
@@ -601,18 +601,18 @@ func _settle() -> void:
# 开格产出乘区(迭代 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"):
func _feat_yield_adjust(snap: Dictionary, positive: bool) -> Dictionary:
var pos_m := int(_damage.feat_flags.get("dig_yield_pos_mult", 0))
var neg_m := int(_damage.feat_flags.get("dig_yield_neg_mult", 0))
if positive and pos_m <= 0 and not _damage.feat_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
if positive and _damage.feat_flags.has("combo_cap"):
pct += mini(int(_damage.feat_flags.get("combo_cap", 5)), _feat_combo) * 10
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
return out
@@ -832,7 +832,7 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
# 净化工兵(迭代 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:
if int(_damage.feat_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)
@@ -844,10 +844,10 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
break
if harmful:
board.clear_variants(n)
_emit_row("terrain_neutralized", "relic_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
_emit_row("terrain_neutralized", "feat_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
if int(_damage.relic_flags.get("flag_resource_extra", 0)) > 0:
if int(_damage.feat_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, "", "(雷区图谱)")
@@ -952,10 +952,10 @@ func _combat_segment(ri: int) -> void:
round_safe += 1
elif evt.kind == "mine":
round_mine = true
_damage.relic_flags["dig_streak"] = 0 if round_mine else round_safe
_damage.feat_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
_damage.feat_flags["charge_layers"] = action_economy.charge_layers
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。
# 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击;
# 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。
@@ -971,11 +971,11 @@ func _combat_segment(ri: int) -> void:
_damage.submit(ht)
# 连珠矢(迭代 12 T4 穿刺流——开格节奏变攻击段数):本轮每开 ~3 安全格 +1 段穿刺附加
# (上限 cap;段值 provisional 固定值不乘 atk——台账披露)。扫描雷结算触发点=dig_count_segments。
var chain_value := int(_damage.relic_flags.get("chain_value", 0))
var chain_value := int(_damage.feat_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)
var segments := mini(int(_damage.feat_flags.get("chain_cap", 2)), int(_damage.feat_flags.get("dig_streak", 0)) / 3)
for s in segments:
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "relic_chain_arrows",
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_chain_arrows",
"连珠矢", "board", chain_value, DamageController.TARGET_FRONT_ALIVE, ri,
minesweeper.hero_pos(), "pierce")
_damage.submit(ct)
+7 -7
View File
@@ -27,7 +27,7 @@ const TABLE_MANIFEST := {
"treasure_items": "treasure_items.tres", # 迭代 7 T1 秘宝内容批(TreasureItemTable 六行——定义表 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 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
"feats": "feats.tres", # 迭代 12 T1 双层 BD 规则改写型特性基建(FeatTable 定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
}
var action_economy: ActionEconomyTable
@@ -50,7 +50,7 @@ var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tre
var treasure_items: TreasureItemTable # 迭代 7 T1 类型化持用(消费面走 treasure_* 访问器)
var adventure: AdventureTable
var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器)
var relics: RelicTable # 迭代 12 T1 类型化持用(消费面走 relic_* 访问器)
var feats: FeatTable # 迭代 12 T1 类型化持用(消费面走 feat_* 访问器)
var is_ready := false
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
@@ -142,7 +142,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
treasure_items = loaded.get("treasure_items")
adventure = loaded.get("adventure")
attunement_growth = loaded.get("attunement_growth")
relics = loaded.get("relics")
feats = loaded.get("feats")
meta_tree = loaded.get("meta_tree")
is_ready = not has_errors
return not has_errors
@@ -150,13 +150,13 @@ func load_tables(root: String, collect_only: bool) -> bool:
# ---- relic 访问器(迭代 12 T1——消费面一律走本组,禁直读行集)----
func relic_row(relic_id: String) -> Dictionary:
return relics.row_by_id(relic_id) if relics != null else {}
func feat_row(feat_id: String) -> Dictionary:
return feats.row_by_id(feat_id) if feats != null else {}
# 规则改写型特性带令牌构建源(池接线——adventure_runner._build_curio_deck 消费;行 rarity→投放档映射)
func relic_rows() -> Array:
return relics.rows if relics != null else []
func feat_rows() -> Array:
return feats.rows if feats != null else []
# 天赋树节点行(迭代 12 T8——MetaProgression 树段/营地树 UI 取数口)
+28 -28
View File
@@ -60,10 +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 求值后注入;空字典=零扰动。
# 特性求值旗(迭代 12 T2——begin_battle 编排器经 FeatsEngine 求值后注入;空字典=零扰动。
# rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次
# 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调)
var relic_flags: Dictionary = {}
var feat_flags: Dictionary = {}
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
@@ -200,40 +200,40 @@ func _resolve_hero_attack(t: Dictionary) -> void:
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))
var trade_hit := int(feat_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)
# 专注(迭代 12 T4 普攻流——免掷骰恒定折扣:方差买断成期望——稳定器人格;roll 记 0 归因)
var rewrite := int(relic_flags.get("armor_roll_rewrite", 0))
var rewrite := int(feat_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:
if int(feat_flags.get("spike_mult", 0)) > 0 and int(feat_flags.get("charge_layers", 0)) >= 3:
discount = 100
# 猎弱本能(迭代 12 T4 穿刺流——对轻甲敌折扣档改善 value 档:对冲 pierce 列 L1 −1 开局罚;
# 改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)
var shift := int(relic_flags.get("discount_shift", 0))
var shift := int(feat_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)
var cdelta: int = _counter_delta(atk_type, armor) + _feat_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 base_value := float(t.base_value)
var trade_atk := int(relic_flags.get("hit_trade_atk", 0))
var trade_atk := int(feat_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))
var charge_layers := int(feat_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))
base_value += float(charge_layers * int(feat_flags.get("ap_carry_value", 0)))
var spike_mult := int(feat_flags.get("spike_mult", 0))
var spike_ready := spike_mult > 0 and charge_layers >= 3
if spike_ready:
base_value *= float(spike_mult)
@@ -250,22 +250,22 @@ func _resolve_hero_attack(t: Dictionary) -> void:
# 英雄侧 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))
var hero_rider := int(feat_flags.get("rider_flat", 0))
# 猎巢人(迭代 12 T3 标旗流——巢穴控制数转伤害光环):控制数 = 敌方 weaken(削弱)实例数
# 〔八型批 side-wide 状态行——标中旗控制巢穴的既有承载;原设计「对其中敌伤害」简化为全体
# 先手攻击附加值——台账披露待追认〕,每控制 1 巢 rider +Z。
var nest_aura := int(relic_flags.get("nest_aura_value", 0))
var nest_aura := int(feat_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))
# 〔编排器授予段实时写入 feat_flags.dig_streak;踩雷清零〕×value,上限 cap。
var rdig := int(feat_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)
hero_rider += mini(int(feat_flags.get("round_dig_cap", 5)), int(feat_flags.get("dig_streak", 0)) * rdig)
if hero_rider > 0:
if crit and bool(relic_flags.get("rider_crit", false)):
if crit and bool(feat_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
@@ -273,9 +273,9 @@ func _resolve_hero_attack(t: Dictionary) -> void:
payload["killed"] = not target.alive # 击杀标记在扣血后取(原语义不变)
# 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent
# +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。
var crack := int(relic_flags.get("crack_value", 0))
var crack := int(feat_flags.get("crack_value", 0))
if crack > 0 and armor == "heavy":
effects.apply_effect("off_guard", {"source_class": "relic_armor_crack"}, -1)
effects.apply_effect("off_guard", {"source_class": "feat_armor_crack"}, -1)
payload["cracked"] = true
var label := _attack_label(String(t.display), crit, discount)
if hero_rider > 0:
@@ -284,10 +284,10 @@ func _resolve_hero_attack(t: Dictionary) -> void:
# 贯穿射击(迭代 12 T4 穿刺流——击杀溢出弹射):击杀时对下一存活敌以穿刺类型弹射
# 本体半额〔原设计「溢出伤害」追踪在单分量模型不可行——半额弹射 provisional 裁定披露待追认;
# 无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve(目标存活守卫内建)。
if bool(payload.get("killed", false)) and int(relic_flags.get("kill_overflow", 0)) > 0:
if bool(payload.get("killed", false)) and int(feat_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",
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "feat_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:
@@ -309,9 +309,9 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
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))
var shield := int(feat_flags.get("shield_per_layer", 0)) * int(feat_flags.get("charge_layers", 0))
# 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」)
if int(relic_flags.get("bulwark", 0)) > 0:
if int(feat_flags.get("bulwark", 0)) > 0:
shield += 1
if shield > 0:
total = maxi(int(db.settlement.damage_floor), total - shield)
@@ -337,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"))) + _relic_clamp_bonus(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"))) + _feat_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,
@@ -422,16 +422,16 @@ func _counter_delta(attack_type: String, armor: String) -> int:
# 空旗=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:
func _feat_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))
bonus += int(feat_flags.get("clamp_magic_magic", 0))
elif armor == "heavy":
bonus += int(relic_flags.get("clamp_magic_heavy", 0))
bonus += int(feat_flags.get("clamp_magic_heavy", 0))
elif attack_type == "pierce":
if armor == "light":
bonus += int(relic_flags.get("clamp_pierce_light", 0))
bonus += int(feat_flags.get("clamp_pierce_light", 0))
return bonus
@@ -4,15 +4,15 @@
# 同流派标签 relic 计数 ≥2 → 档一(流派核心规则);≥4 → 档二(完全体);<2 = 散件(单体保底——
# 每件效果独立生效,档位只做质变不做启动,反例 B-2)。
# 纯函数零 await 零 RNG 零副作用(Attunement 求值服务同款纪律);核心层零特判延伸——引擎只读
# relic_table 行集与持有集,接线点消费 tiers/hooks 投影。
class_name RelicsEngine
# feat_table 行集与持有集,接线点消费 tiers/hooks 投影。
class_name FeatsEngine
extends RefCounted
## 档位求值:relic_ids 持有集 × 行集 → {archetype: tier}(tier ∈ 0/1/2;无行流派不出现)
static func tiers_for(relic_ids: Array, table: RelicTable) -> Dictionary:
## 档位求值:feat_ids 持有集 × 行集 → {archetype: tier}(tier ∈ 0/1/2;无行流派不出现)
static func tiers_for(feat_ids: Array, table: FeatTable) -> Dictionary:
var counts := {}
for rid in relic_ids:
for rid in feat_ids:
var row: Dictionary = table.row_by_id(String(rid)) if table != null else {}
if row.is_empty():
continue
@@ -30,16 +30,16 @@ static func tiers_for(relic_ids: Array, table: RelicTable) -> Dictionary:
return out
## 挂点投影:持有集 → [{relic_id, hook, archetype, tier, effects}](表序稳定——同集合同投影,
## 挂点投影:持有集 → [{feat_id, hook, archetype, tier, effects}](表序稳定——同集合同投影,
## MC 逐位可复放承载)
static func hooks_for(relic_ids: Array, table: RelicTable) -> Array:
static func hooks_for(feat_ids: Array, table: FeatTable) -> Array:
var out: Array = []
for rid in relic_ids:
for rid in feat_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", "")),
"feat_id": String(row.get("feat_id", "")),
"hook": String(row.get("hook", "")),
"archetype": String(row.get("archetype", "")),
"effects": (row.get("effects", {}) as Dictionary).duplicate(true),
File renamed without changes.
+13 -30
View File
@@ -479,7 +479,7 @@ 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,
"feats": _feats,
"tree_rules": _tree_rules,
"node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
@@ -591,18 +591,18 @@ func set_attunement(ids: Array) -> void:
# 特性持有集注入(迭代 12 T1——AdventureRunner._start_current_level 唯一调用方;begin_battle
# enc.relics 携带进战斗求值;**共享引用不复制**——关内奖励节点兑付的 relic 须当关后续战斗生效
# enc.feats 携带进战斗求值;**共享引用不复制**——关内奖励节点兑付的 relic 须当关后续战斗生效
# (attune_charges 共享账本同款);空集=零扰动)
var _relics: Array = []
var _feats: Array = []
func set_relics(ids: Array) -> void:
_relics = ids
func set_feats(ids: Array) -> void:
_feats = ids
# 特性持有集读面(迭代 12 呈现补齐——HUD C 区特性段数据源)
func relics_held() -> Array:
return _relics.duplicate()
func feats_held() -> Array:
return _feats.duplicate()
# 树 rule 键集注入(迭代 12 T10——begin_battle enc.tree_rules 携带;Fork 激活侧 effects)
@@ -638,7 +638,7 @@ func grant_treasure_item(item_id: String, source: String) -> Dictionary:
# 三选一 GP 袋件入账(迭代 10——钱包收口扩「奖励入账」入口;复用战斗完成同一入账原语,
# wallet_changed 载荷带 source 供呈现归因)
func grant_bonus_gp(amount: int) -> void:
func grant_curio_gp(amount: int) -> void:
gp = maxi(0, gp + amount)
wallet_changed.emit("gain", gp, {"amount": amount, "source": "curio_pick"})
@@ -779,26 +779,9 @@ 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 # 换一张即止
# 商店保底退役(迭代 15 卡② T4——渠道轴定位裁决:商店渠道只出属性值提升+资源,专长在非 Boss
# 渠道只以门票形态出现(奇物.md v2.7/A4 断言)。迭代 12 T6「货架零特性挤卡换吞噬流件」简化口径
# 与渠道白名单直接冲突(devour=规则改写型=专长系),随渠道分离退役;专长商店入口=T5 门票。
# 库存余量(TP0——令牌库内该 kind 剩余枚数单点读;未注入/防御读 = 0)
@@ -836,13 +819,13 @@ func shop_shelf() -> Array:
var display := String(row.get("display_name", kind))
var value := int(db.adventure.curio_value(kind, tier))
if row.is_empty() and kind == "relic":
var rrow: Dictionary = db.relic_row(String(tok.get("relic_id", "")))
var rrow: Dictionary = db.feat_row(String(tok.get("feat_id", "")))
display = String(rrow.get("display_name", "特性"))
value = 0
out.append({
"kind_id": kind,
"tier": tier,
"relic_id": String(tok.get("relic_id", "")),
"feat_id": String(tok.get("feat_id", "")),
"display_name": display,
"value": value,
"price": pool_price(kind),