feat(pool): 迭代20 T2 池结构案D 换代(专长实装批 A段二)

四级档位:TIERS common/excellent/rare/legendary(跨层框架档名映射承袭)——TIER_QUOTAS
70/25/3/0=98 直映射零偏离已裁结构锚(传说=头奖位留空);配额/件值字段物理改名
(旧 rare→excellent、旧 rarest→rare)+凭证四 kind 占位(rap/attune_battery/time_sand/
perfect_insurance,配额暂 0=T4 落路由同批投放)。

概率与上限解耦(案②令牌池+提前退役):curio_cap_* 每 kind 独立字段(99=无上限占位,
实数归调参批)+专长 cap 行字段面;taken 身份维计数(relic:<feat_id>)拿了才计刷出不计;
cap 满在候选视图剔除(牌库本体不动=概率位独立存活);断点 bonus_taken 身份维键随档还原。

曝光槽:三选一=2 格全池+1 格优秀档以上子池(子池独立洗牌同 stream 续消费;子池空回退
全池;池空直通同族兜底);凭证(feat_ticket)不进抽签池只经保底注入;商店货架同 cap 过滤。

潜伏缺陷修正:效果引擎 has 算子 null 安全(敌技伤害载荷无 terrain_kind 事实回落 null 时
判「不在场」而非 String(null) 抛错——候选流变化后 devour 件首次被持有而暴露)。

测试:pool_case_d_test 新套(V6 令牌三态/V7 曝光槽回退/V8 解耦提前退役)入 run_all 20 套;
smoke 三档锚/数值对/池构成/上限带四处断言换代(V14),冒烟 848/0+probe_gate --click 全绿。
This commit is contained in:
DKChan committed 2026-10-11 23:18:35 +08:00
1 parent a57489e970
commit 0640b0c06d
10 files changed
+384 -103

No files matched your search

+68 -17
View File
@@ -180,6 +180,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary
for id in _curio_taken.keys():
_curio_taken[id] = maxi(0, int(bp_taken.get(id, 0)))
for id in bp_taken.keys():
_curio_taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 迭代 20 T2——身份维键(relic:<feat_id>)随快照还原
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
# 迭代 12 T1——relic 令牌 feat_id 随还原)
var bp_deck = bp.get("bonus_deck", null)
@@ -283,8 +285,10 @@ func _reset_curio_state() -> void:
# 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = CURIO_KINDS×TIERS 逐层,
# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:规则改写型特性带=relic 活行每件 1 张追加
# (rarity common/rare→稀有令牌档、rarest→罕见令牌档——relic 永不入普通档;空表=零追加零扰动))
# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:规则改写型特性带=relic 活行每件 1 张追加。
# 迭代 20 T2/T3 四级换代——relic 令牌档 = 行 rarity 直映射(四级词表内直用;旧三档值 transitional
# 回退映射 rarest→rare/其余→excellent,T3 行换代后回退分支自然休眠);relic 入普通档随新档位框架
# 解禁〔旧「永不入普通档」系 TP0 三档时代约束,四级框架未沿用——奇物.md 池结构节无此限制〕)
func _build_curio_deck() -> Array:
var deck: Array = []
for kind in db.adventure.CURIO_KINDS:
@@ -292,7 +296,8 @@ func _build_curio_deck() -> Array:
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
deck.append({"kind": String(kind), "tier": String(tier)})
for row in db.feat_rows():
var rtier := "rarest" if String(row.get("rarity", "")) == "rarest" else "rare"
var rraw := String(row.get("rarity", ""))
var rtier := rraw if db.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
deck.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
return deck
@@ -429,6 +434,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String =
if not _feats.has(feat_id):
_feats.append(feat_id)
_curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1
_curio_taken["relic:" + feat_id] = int(_curio_taken.get("relic:" + feat_id, 0)) + 1 # 迭代 20 T2 案②——身份维已拿计数(cap 消费面)
return true
# 加算形态路由(内容数据驱动扩展批 T5——match 收敛进效果类型:属性值提升四类走
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
@@ -787,38 +793,82 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
# 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。
# 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样,
# 白名单在候选视图层生效——配额结构锚 TIER_QUOTAS 70/25/3=98 零改动(专长令牌本系活行追加面,在锚外)。
# 迭代 20 T2 案D 换代——曝光槽:非 Boss 渠道三选一 = **2 格全池 + 1 格优秀档以上子池**
# (rank≥excellent;子池空该格回退全池;池空直通同族兜底);凭证(feat_ticket)不进子池
# 〔独立计数体系〕;已拿上限 cap(案②提前退役):taken≥cap 的身份在候选视图剔除〔牌库本体不动,
# 拿了才计、刷出不计——_curio_cap_ok〕。
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")
pool = pool.filter(func(tok):
var k := String((tok as Dictionary).get("kind", ""))
return k != "relic" and k != "feat_ticket")
pool = pool.filter(func(tok):
var t: Dictionary = tok as Dictionary
return _curio_cap_ok(String(t.get("kind", "")), String(t.get("feat_id", ""))))
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
for i in pool.size():
var j := rg.randi_range(i, pool.size() - 1)
var tmp = pool[i]
pool[i] = pool[j]
pool[j] = tmp
# 子池(优秀档以上)独立洗牌——同 stream 续消费,确定性保持
var sub := pool.filter(func(tok):
return db.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT))
for i in sub.size():
var j2 := rg.randi_range(i, sub.size() - 1)
var tmp2 = sub[i]
sub[i] = sub[j2]
sub[j2] = tmp2
var out: Array = []
var seen: Array = []
for tok in pool:
var ident := _token_identity(tok)
if seen.has(ident):
continue
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["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
out.append(cand)
if out.size() >= int(db.adventure.curio_candidate_count):
# 收集器:全池序取 n_full 格(普通档打底)→ 子池序取 1 格(曝光槽)→ 子池空回退全池续取
var want := int(db.adventure.curio_candidate_count)
var n_full := maxi(1, want - 1) if want >= 2 else want
while out.size() < want:
var src: Array = pool
if out.size() >= n_full and not sub.is_empty():
src = sub # 曝光槽位——优先子池
var placed := false
for tok in src:
var ident := _token_identity(tok)
if seen.has(ident):
continue
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["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
out.append(cand)
placed = true
break
if not placed:
if out.size() >= n_full and not sub.is_empty() and not pool.is_empty():
sub = [] # 子池枯竭——曝光槽回退全池续取
continue
break # 全池枯竭——出齐剩余(池空直通同族兜底)
# T5 门票注入(非 Boss 渠道唯一专长入口——替换末位候选占一席,候选数不变;判定含 pity 清零记账)
if channel == "reward_node" and not out.is_empty() and _feat_ticket_consider(rg):
out[out.size() - 1] = {"kind": "feat_ticket", "tier": "rare"}
out[out.size() - 1] = {"kind": "feat_ticket", "tier": "excellent"}
return out
# 已拿上限判定(迭代 20 T2 案②——身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
func _curio_cap_ok(kind: String, feat_id: String = "") -> bool:
var cap := 99
if kind == "relic":
var row: Dictionary = db.feat_row(feat_id)
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
return _curio_taken.get("relic:" + feat_id, 0) < cap
if not db.adventure.CURIO_KINDS.has(kind):
return true
cap = int(db.adventure.curio_cap(kind))
return int(_curio_taken.get(kind, 0)) < cap
# —— 专长获取门票(T5 迭代 15 卡②——非 Boss 渠道的专长入口;四口径契约④:拿到即开不可囤积/
# 罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出第 6 必出——计数=展示次数、刷新
# 重展计入、节点与商店共享、触发清零、限购满不再触发。调用即一次展示判定与记账)——
@@ -944,8 +994,9 @@ func _start_current_level() -> bool:
_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 模式零注入零扰动)
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"))
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。
# 迭代 20 T2——cap 检查回调四参注入)
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"), Callable(self, "_curio_cap_ok"))
_runner.set_adventure_counters(adventure_counters) # 迭代 20 T6——adventure 域计数器共享引用下传
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
+5 -2
View File
@@ -111,8 +111,11 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
if not adv.CURIO_KINDS.has(kind):
push_diag.call("error", "shelf_dangling_kind", "ShopTable.pool_shelf kind %s 不在 AdventureTable.CURIO_KINDS(共享池牌库外种)" % kind)
continue
# TP0 令牌池——货架件须为有限令牌种(配额 >0;种类次数上限退役后库存=剩余令牌数)
var quota := int(adv.curio_quota(kind, adv.TIER_COMMON)) + int(adv.curio_quota(kind, adv.TIER_RARE)) + int(adv.curio_quota(kind, adv.TIER_RAREST))
# TP0 令牌池——货架件须为有限令牌种(配额 >0;种类次数上限退役后库存=剩余令牌数。
# 迭代 20 T2 四级换代——逐档累计遍历 TIERS 词表,不再点名档位常量)
var quota := 0
for tier_v in adv.TIERS:
quota += int(adv.curio_quota(kind, String(tier_v)))
if quota < 1:
push_diag.call("error", "shelf_zero_quota", "ShopTable.pool_shelf kind %s 令牌配额为 0(牌库空种入货架 = 库存无锚)" % kind)
+1 -1
View File
@@ -272,7 +272,7 @@ func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
return false
if actual is Array:
return not (actual as Array).is_empty()
return String(actual) != ""
return actual != null and String(actual) != "" # null 安全(迭代 20 T2 修正——敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错;候选流变化后 devour 件首次被持有而暴露的潜伏缺陷)
"mod_zero":
var m := int(val)
return m > 0 and int(actual) % m == 0
+7 -2
View File
@@ -72,6 +72,7 @@ var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注
var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点)
var _pool_consume := Callable() # 池消费 handler(AdventureRunner._consume_pool_purchase——_apply_curio_pick 同口写加算区+账本+摘牌)
var _pool_ticket := Callable() # 门票展示判定 handler(T5——AdventureRunner._feat_ticket_consider;节点与商店共享计数)
var _pool_cap_check := Callable() # cap 检查回调(迭代 20 T2——AdventureRunner._curio_cap_ok;案②提前退役)
var _shop_shelf: Array = [] # 当前货架(进店抽 3/买后全重抽——令牌字典 {"kind","tier"} 序,同 kind 去重每 kind 至多一卡)
var _shop_visit_buys := 0 # 本次进店已购次数(A 链定价输入——离开清零)
var _shop_paid_refreshes := 0 # 本次进店已付费刷新次数(MC-1——GP 刷新价曲线输入,离开清零)
@@ -748,10 +749,12 @@ func player_request_refresh() -> bool:
# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 +
# 池消费 handler。legacy 模式不注入(_shop_pool_mode 恒 false——off 态零 RNG 零池消费零扰动)。
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable()) -> void:
# 迭代 20 T2——cap 检查回调(案②提前退役:cap 满 kind 剔出货架候选视图)。
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void:
_pool_deck = deck_ref
_pool_consume = consume_handler
_pool_ticket = ticket_consider # T5——门票展示判定(AdventureRunner._feat_ticket_consider;同流取数复用 shop_draw scope)
_pool_cap_check = cap_check
_shop_pool_mode = true
@@ -772,6 +775,8 @@ func _shop_draw_shelf() -> void:
var sellable: Array = []
for row in db.shop_pool_rows():
var shelf_kind := String(row.get("kind_id", ""))
if _pool_cap_check.is_valid() and not bool(_pool_cap_check.call(shelf_kind, "")):
continue # 迭代 20 T2 案②——cap 满提前退役(货架候选视图剔除;牌库本体不动)
for t in _pool_deck:
var tok: Dictionary = t as Dictionary
if String(tok.get("kind", "")) == shelf_kind:
@@ -796,7 +801,7 @@ func _shop_draw_shelf() -> void:
# T5 门票注入(非 Boss 渠道唯一专长入口——货架与节点共享保底计数;判定含 pity 记账,替换末位占一席。
# 取数复用本函数 shop_draw scope 的 stream——零新 scope,确定性序列不漂移)
if _pool_ticket.is_valid() and not _shop_shelf.is_empty() and bool(_pool_ticket.call(stream)):
_shop_shelf[_shop_shelf.size() - 1] = {"kind": "feat_ticket", "tier": "rare"}
_shop_shelf[_shop_shelf.size() - 1] = {"kind": "feat_ticket", "tier": "excellent"} # 迭代 20 T2——档名四级换代(旧 rare 语义=优秀)
# 商店保底退役(迭代 15 卡② T4——渠道轴定位裁决:商店渠道只出属性值提升+资源,专长在非 Boss
# 渠道只以门票形态出现(奇物.md v2.7/A4 断言)。迭代 12 T6「货架零特性挤卡换吞噬流件」简化口径
# 与渠道白名单直接冲突(devour=规则改写型=专长系),随渠道分离退役;专长商店入口=T5 门票。