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:
1 parent
a57489e970
commit
0640b0c06d
10 files changed
+384
-103
No files matched your search
@@ -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 恒空集,
|
||||
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 门票。
|
||||
|
||||
@@ -24,16 +24,21 @@ extends Resource
|
||||
## 类内异质性:金币与回复是即时兑付,不进持有集、不持续生效。)
|
||||
## 三选一提升池 kind 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)。
|
||||
## treasure_item 池种退役(迭代 11 第①批 T7——同调批出池);heal 恢复件新入池(v2.2 令牌池追认 b)。
|
||||
const CURIO_KINDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap", "heal", "refresh"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
|
||||
## 令牌档位词表(v2.2 三档位——稀有度命名对齐 MC 策略判读序 316-f:1 级最稀有=rarest 优先)
|
||||
const TIERS: Array[String] = ["common", "rare", "rarest"]
|
||||
## 迭代 20 T4——资源凭证五件入池:rap 储备金袋/attune_battery 同调电池/time_sand 时沙/
|
||||
## perfect_insurance 完美保险(配额暂 0——T4 落路由同批给投放数,防无路由 kind 出候选)。
|
||||
const CURIO_KINDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap", "heal", "refresh",
|
||||
"rap", "attune_battery", "time_sand", "perfect_insurance"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
|
||||
## 令牌档位词表(迭代 20 T2 换代——跨层四级稀有度框架〔01-核心循环 v1.5 档名映射:普通→普通、
|
||||
## 稀有→优秀、罕见→稀有、传说=新增头奖位〕;1 级最稀有=legendary 优先)
|
||||
const TIERS: Array[String] = ["common", "excellent", "rare", "legendary"]
|
||||
const TIER_COMMON := "common"
|
||||
const TIER_EXCELLENT := "excellent"
|
||||
const TIER_RARE := "rare"
|
||||
const TIER_RAREST := "rarest"
|
||||
## 档位配额结构锚(v2.2 追认 b 定稿——validate() 硬断言;改总量=重裁非调参)。
|
||||
## 罕见档 5→3:原「道具件 2」席位随 treasure_item 池种退役(迭代 11 第①批 T7,落地晚于追认 b)收窄,
|
||||
## 池总量 100→98——TP0 台账自决+人工清单披露项。
|
||||
const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3}
|
||||
const TIER_LEGENDARY := "legendary"
|
||||
## 档位配额结构锚(迭代 20 T2 四级换代——70/25/3 直映射改名承袭〔普通 70/优秀 25/稀有 3〕+
|
||||
## 传说 0=头奖位留空,池总量 98 不变=零偏离已裁结构锚数字〔配额纪律:改总量须用户重裁〕;
|
||||
## validate() 硬断言;改总量=重裁非调参)。
|
||||
const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 3, "legendary": 0}
|
||||
|
||||
# —— 通关部分回血(固定型,非末关通关兑付)——
|
||||
@export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满
|
||||
@@ -44,31 +49,55 @@ const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3}
|
||||
@export var shop_refresh_cost_base: int = 20 # [1,999] knob(MC-1 provisional——GP 刷新基价)
|
||||
@export var shop_refresh_cost_curve: float = 1.8 # [1.0,4.0] knob(MC-1 provisional——按已付费刷新数幂曲线,A 链同族)
|
||||
|
||||
# —— 令牌配额(kind × tier 计数;未列组合 = 0——heal 只入稀有/ap 只入罕见,v2.2 档位构成)——
|
||||
# —— 令牌配额(kind × tier 计数;未列组合 = 0。迭代 20 T2 四级换代:旧 rare→excellent、
|
||||
# 旧 rarest→rare 物理承袭〔70/25/3 零偏离〕+凭证四 kind 占位 rare 档配额 0〔T4 落路由同批投放〕)——
|
||||
@export var curio_quota_attack_common: int = 18 # [0,99]
|
||||
@export var curio_quota_attack_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_attack_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_common: int = 17 # [0,99]
|
||||
@export var curio_quota_max_hp_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_common: int = 17 # [0,99]
|
||||
@export var curio_quota_armor_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_gp_common: int = 18 # [0,99]
|
||||
@export var curio_quota_gp_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_rare: int = 3 # [0,99]——恢复件(原 5;T6 挤占 2 席位予 refresh——用户 2026-10-05 裁方案B,总量 98 锚不变)
|
||||
@export var curio_quota_refresh_rare: int = 2 # [0,99]——刷新次数件(T6 新入池资源类成员;provisional 归曲线调参批)
|
||||
@export var curio_quota_ap_rarest: int = 3 # [0,99]——AP 件(罕见档;道具件 2 席位随池种退役收窄)
|
||||
@export var curio_quota_gp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(原 5;T6 挤占 2 席位予 refresh——用户 2026-10-05 裁方案B,总量 98 锚不变)
|
||||
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(T6 新入池资源类成员;provisional 归曲线调参批)
|
||||
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档;旧罕见档物理承袭)
|
||||
@export var curio_quota_rap_rare: int = 0 # [0,99]——储备金袋(T4 路由同批投放;provisional 归调参批)
|
||||
@export var curio_quota_attune_battery_rare: int = 0 # [0,99]——同调电池(同上)
|
||||
@export var curio_quota_time_sand_rare: int = 0 # [0,99]——时沙(同上)
|
||||
@export var curio_quota_perfect_insurance_rare: int = 0 # [0,99]——完美保险(同上)
|
||||
|
||||
# —— 令牌件值(小/大数值对——v2.2 追认 b 定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)——
|
||||
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
|
||||
@export var curio_value_attack_rare: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
|
||||
@export var curio_value_max_hp_rare: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
|
||||
@export var curio_value_armor_rare: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_rare: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_rare: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_rare: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rarest: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
|
||||
@export var curio_value_attune_battery_rare: int = 1 # [0,9] knob(provisional)——同调电池充能 +N
|
||||
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
|
||||
@export var curio_value_perfect_insurance_rare: int = 1 # [0,9] knob(provisional)——完美保险 资格保护次数
|
||||
|
||||
# —— 已拿上限 cap(迭代 20 T2 概率与上限解耦〔奇物.md 池结构案D〕——案②令牌池+提前退役:
|
||||
# 概率位=档+令牌数〔牌库物理张数〕、上限=本字段〔拿了才计、刷出不计〕;cap 满后该 kind 从候选
|
||||
# 视图剔除〔提前退役〕。99=实质无上限占位,实数归调参批;专长 cap 在 feats.tres 行字段〕)——
|
||||
@export var curio_cap_attack: int = 99 # [1,99]
|
||||
@export var curio_cap_max_hp: int = 99 # [1,99]
|
||||
@export var curio_cap_armor: int = 99 # [1,99]
|
||||
@export var curio_cap_gp: int = 99 # [1,99]
|
||||
@export var curio_cap_ap: int = 99 # [1,99]
|
||||
@export var curio_cap_heal: int = 99 # [1,99]
|
||||
@export var curio_cap_refresh: int = 99 # [1,99]
|
||||
@export var curio_cap_rap: int = 99 # [1,99]
|
||||
@export var curio_cap_attune_battery: int = 99 # [1,99]
|
||||
@export var curio_cap_time_sand: int = 99 # [1,99]
|
||||
@export var curio_cap_perfect_insurance: int = 99 # [1,99]
|
||||
|
||||
# —— 专长获取门票(迭代 15 卡② T5——非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
|
||||
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
|
||||
@@ -97,45 +126,72 @@ func curio_apply_route(kind: String) -> String:
|
||||
return String(CURIO_APPLY_ROUTES.get(kind, ""))
|
||||
|
||||
|
||||
## 令牌配额读数(未列 kind×tier 组合 = 0——heal 无普通档/ap 无普通稀有档按 v2.2 构成)
|
||||
## 令牌配额读数(未列 kind×tier 组合 = 0——迭代 20 T2 四级构成:属性四类 common/excellent、
|
||||
## heal/refresh excellent、ap rare、凭证四 kind rare 占位 0)
|
||||
func curio_quota(kind: String, tier: String) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_quota_attack_common
|
||||
["attack", TIER_RARE]: return curio_quota_attack_rare
|
||||
["attack", TIER_EXCELLENT]: return curio_quota_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_quota_max_hp_common
|
||||
["max_hp", TIER_RARE]: return curio_quota_max_hp_rare
|
||||
["max_hp", TIER_EXCELLENT]: return curio_quota_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_quota_armor_common
|
||||
["armor", TIER_RARE]: return curio_quota_armor_rare
|
||||
["armor", TIER_EXCELLENT]: return curio_quota_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_quota_gp_common
|
||||
["gp", TIER_RARE]: return curio_quota_gp_rare
|
||||
["heal", TIER_RARE]: return curio_quota_heal_rare
|
||||
["refresh", TIER_RARE]: return curio_quota_refresh_rare
|
||||
["ap", TIER_RAREST]: return curio_quota_ap_rarest
|
||||
["gp", TIER_EXCELLENT]: return curio_quota_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_quota_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_quota_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_quota_ap_rare
|
||||
["rap", TIER_RARE]: return curio_quota_rap_rare
|
||||
["attune_battery", TIER_RARE]: return curio_quota_attune_battery_rare
|
||||
["time_sand", TIER_RARE]: return curio_quota_time_sand_rare
|
||||
["perfect_insurance", TIER_RARE]: return curio_quota_perfect_insurance_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 令牌件值(kind × tier;v2.2 数值对——小/大两档)
|
||||
## 令牌件值(kind × tier;四级构成随配额表)
|
||||
func curio_value(kind: String, tier: String = TIER_COMMON) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_value_attack_common
|
||||
["attack", TIER_RARE]: return curio_value_attack_rare
|
||||
["attack", TIER_EXCELLENT]: return curio_value_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_value_max_hp_common
|
||||
["max_hp", TIER_RARE]: return curio_value_max_hp_rare
|
||||
["max_hp", TIER_EXCELLENT]: return curio_value_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_value_armor_common
|
||||
["armor", TIER_RARE]: return curio_value_armor_rare
|
||||
["armor", TIER_EXCELLENT]: return curio_value_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_value_gp_common
|
||||
["gp", TIER_RARE]: return curio_value_gp_rare
|
||||
["heal", TIER_RARE]: return curio_value_heal_rare
|
||||
["refresh", TIER_RARE]: return curio_value_refresh_rare
|
||||
["ap", TIER_RAREST]: return curio_value_ap_rarest
|
||||
["gp", TIER_EXCELLENT]: return curio_value_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_value_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_value_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_value_ap_rare
|
||||
["rap", TIER_RARE]: return curio_value_rap_rare
|
||||
["attune_battery", TIER_RARE]: return curio_value_attune_battery_rare
|
||||
["time_sand", TIER_RARE]: return curio_value_time_sand_rare
|
||||
["perfect_insurance", TIER_RARE]: return curio_value_perfect_insurance_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 档位判读序(MC 策略 316-f——1 级最稀有:rarest > rare > common;并列同档取最左由调用方稳定序保证)
|
||||
## 已拿上限读数(迭代 20 T2 案②——kind 维度;专长 cap 走 feats.tres 行字段不走本面)
|
||||
func curio_cap(kind: String) -> int:
|
||||
match kind:
|
||||
"attack": return curio_cap_attack
|
||||
"max_hp": return curio_cap_max_hp
|
||||
"armor": return curio_cap_armor
|
||||
"gp": return curio_cap_gp
|
||||
"ap": return curio_cap_ap
|
||||
"heal": return curio_cap_heal
|
||||
"refresh": return curio_cap_refresh
|
||||
"rap": return curio_cap_rap
|
||||
"attune_battery": return curio_cap_attune_battery
|
||||
"time_sand": return curio_cap_time_sand
|
||||
"perfect_insurance": return curio_cap_perfect_insurance
|
||||
return 99
|
||||
|
||||
|
||||
## 档位判读序(MC 策略 316-f——1 级最稀有:legendary > rare > excellent > common;并列同档取最左由调用方稳定序保证)
|
||||
func tier_rank(tier: String) -> int:
|
||||
match tier:
|
||||
TIER_RAREST: return 3
|
||||
TIER_RARE: return 2
|
||||
TIER_LEGENDARY: return 4
|
||||
TIER_RARE: return 3
|
||||
TIER_EXCELLENT: return 2
|
||||
TIER_COMMON: return 1
|
||||
return 0
|
||||
|
||||
@@ -148,9 +204,12 @@ func tier_total(tier: String) -> int:
|
||||
return total
|
||||
|
||||
|
||||
## 牌库总量(结构锚——罕见档随道具件退役收窄后 98 张)
|
||||
## 牌库总量(结构锚——四级换代后 98 张不变:70+25+3+0)
|
||||
func deck_total() -> int:
|
||||
return tier_total(TIER_COMMON) + tier_total(TIER_RARE) + tier_total(TIER_RAREST)
|
||||
var total := 0
|
||||
for tier in TIERS:
|
||||
total += tier_total(tier)
|
||||
return total
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
@@ -168,10 +227,12 @@ func validate() -> Array:
|
||||
var v := curio_value(kind, tier)
|
||||
if v < 0 or v > 999:
|
||||
errs.append({"category": "domain", "message": "curio_value(%s,%s) 越域 [0,999]: %d" % [kind, tier, v]})
|
||||
if curio_cap(kind) < 1 or curio_cap(kind) > 99:
|
||||
errs.append({"category": "domain", "message": "curio_cap(%s) 越域 [1,99]: %d" % [kind, curio_cap(kind)]})
|
||||
for tier in TIERS:
|
||||
var total := tier_total(tier)
|
||||
if total != int(TIER_QUOTAS.get(tier, -1)):
|
||||
errs.append({"category": "domain", "message": "档位配额结构失锚(v2.2 追认 b——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
|
||||
errs.append({"category": "domain", "message": "档位配额结构失锚(四级锚 70/25/3/0——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
|
||||
if curio_candidate_count < 1 or curio_candidate_count > 6:
|
||||
errs.append({"category": "domain", "message": "curio_candidate_count 越域 [1,6]: %d" % curio_candidate_count})
|
||||
return errs
|
||||
@@ -7,33 +7,37 @@ script = ExtResource("1")
|
||||
clear_heal_pct = 20
|
||||
curio_candidate_count = 3
|
||||
curio_quota_attack_common = 18
|
||||
curio_quota_attack_rare = 5
|
||||
curio_quota_attack_excellent = 5
|
||||
curio_quota_max_hp_common = 17
|
||||
curio_quota_max_hp_rare = 5
|
||||
curio_quota_max_hp_excellent = 5
|
||||
curio_quota_armor_common = 17
|
||||
curio_quota_armor_rare = 5
|
||||
curio_quota_armor_excellent = 5
|
||||
curio_quota_gp_common = 18
|
||||
curio_quota_gp_rare = 5
|
||||
curio_quota_heal_rare = 3
|
||||
curio_quota_refresh_rare = 2
|
||||
curio_quota_ap_rarest = 3
|
||||
curio_quota_gp_excellent = 5
|
||||
curio_quota_heal_excellent = 3
|
||||
curio_quota_refresh_excellent = 2
|
||||
curio_quota_ap_rare = 3
|
||||
curio_value_attack_common = 1
|
||||
curio_value_attack_rare = 2
|
||||
curio_value_attack_excellent = 2
|
||||
curio_value_max_hp_common = 5
|
||||
curio_value_max_hp_rare = 10
|
||||
curio_value_max_hp_excellent = 10
|
||||
curio_value_armor_common = 1
|
||||
curio_value_armor_rare = 2
|
||||
curio_value_armor_excellent = 2
|
||||
curio_value_gp_common = 20
|
||||
curio_value_gp_rare = 50
|
||||
curio_value_heal_rare = 6
|
||||
curio_value_refresh_rare = 1
|
||||
curio_value_ap_rarest = 1
|
||||
curio_value_gp_excellent = 50
|
||||
curio_value_heal_excellent = 6
|
||||
curio_value_refresh_excellent = 1
|
||||
curio_value_ap_rare = 1
|
||||
feat_ticket_chance = 20
|
||||
feat_ticket_quota = 2
|
||||
feat_ticket_limit = 1
|
||||
feat_ticket_pity_step = 5
|
||||
|
||||
; TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
|
||||
; 迭代 20 T2 池结构案D 换代(2026-10-11,奇物.md 池结构与抽取 v1):四级档位 70/25/3/0 = 98 张
|
||||
; (普通 70/优秀 25/稀有 3/传说 0=头奖位留空——旧 70/25/3 直映射改名承袭零偏离已裁结构锚);
|
||||
; 概率与上限解耦(案②令牌池+提前退役):概率位=档+令牌数、已拿上限=curio_cap_*(拿了才计);
|
||||
; 曝光槽=三选一 2 格全池+1 格优秀档以上子池(子池空回退全池);凭证不进子池。
|
||||
; 前史 TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
|
||||
; 普通 70/稀有 25/罕见 3 = 98 张(罕见档原列「道具件 2」随 treasure_item 池种退役收窄——台账自决披露项);
|
||||
; 洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭;
|
||||
; 种类次数上限退役(池消耗即上限;货架库存同账本=牌库剩余令牌数)。
|
||||
|
||||
@@ -277,7 +277,7 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔档位最高 rarest>rare>common〕、2 级=最左;
|
||||
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔迭代 20 四级:legendary>rare>excellent>common〕、2 级=最左;
|
||||
# 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。
|
||||
# 成型画像(迭代 12 T7——relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一
|
||||
# 〔贪心捕捞→特性入局→成型画像全流派覆盖——R3 乙单锚承载〕;off=relic 与属性加成型特性同规则竞争)
|
||||
@@ -296,9 +296,11 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction
|
||||
var t: Dictionary = tok as Dictionary
|
||||
var rank := 0
|
||||
match String(t.get("tier", "")):
|
||||
"rarest":
|
||||
rank = 3
|
||||
"legendary":
|
||||
rank = 4
|
||||
"rare":
|
||||
rank = 3
|
||||
"excellent":
|
||||
rank = 2
|
||||
"common":
|
||||
rank = 1
|
||||
|
||||
+19
-17
@@ -218,15 +218,17 @@ func _smoke_adventure_table() -> void:
|
||||
"clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct))
|
||||
for tier in _db.adventure.TIERS:
|
||||
check(int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]),
|
||||
"档位配额结构锚(v2.2 追认 b):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
|
||||
check(int(_db.adventure.deck_total()) == 98, "令牌池总量 = 98 张(普通 70/稀有 25/罕见 3——罕见档随道具件池种退役收窄)")
|
||||
check(int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "rare")) == 50
|
||||
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "rare")) == 2
|
||||
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "rare")) == 10
|
||||
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "rare")) == 2,
|
||||
"数值对(v2.2):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2")
|
||||
check(int(_db.adventure.curio_value("heal", "rare")) == 6 and int(_db.adventure.curio_value("ap", "rarest")) == 1,
|
||||
"恢复件 +6 当即回血(新入池种类,rare 档)· AP +1(rarest 档)")
|
||||
"档位配额结构锚(四级 70/25/3/0 直映射承袭——迭代 20 T2):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
|
||||
check(int(_db.adventure.deck_total()) == 98, "令牌池总量 = 98 张(四级 普通 70/优秀 25/稀有 3/传说 0——直映射承袭零偏离)")
|
||||
check(int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
|
||||
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
|
||||
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
|
||||
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2,
|
||||
"数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2")
|
||||
check(int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1,
|
||||
"恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕")
|
||||
check(int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99,
|
||||
"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕")
|
||||
check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3
|
||||
and Attunement.slots(_db, null) == 0,
|
||||
"同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)")
|
||||
@@ -427,8 +429,8 @@ func _smoke_adventure() -> void:
|
||||
var k := 5.0 # hp+400 ⇒ 调试浮窗百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
|
||||
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
|
||||
# TP0 数值对后 max_hp 件有大生命 +10(rare 档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽)
|
||||
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "rare")) + 15) * k)))
|
||||
# 大生命 +10(迭代 20 T2 四级换代:优秀档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽承袭)
|
||||
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if int(v) < exp_base or int(v) > hp_ceiling:
|
||||
@@ -640,14 +642,14 @@ func _smoke_feats() -> void:
|
||||
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 feat_tokens := 0
|
||||
var rarest_tokens := 0
|
||||
var rare_tier_tokens := 0
|
||||
for t in a_r0._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
feat_tokens += 1
|
||||
if String((t as Dictionary).get("tier", "")) == "rarest":
|
||||
rarest_tokens += 1
|
||||
check(feat_tokens == 27 and rarest_tokens == 6 and a_r0._curio_deck.size() == 125,
|
||||
"规则改写型特性带入池:27 件(rare 档 21 + rarest 锚点 6)——牌库 98+27=125")
|
||||
if String((t as Dictionary).get("tier", "")) == "rare":
|
||||
rare_tier_tokens += 1
|
||||
check(feat_tokens == 27 and rare_tier_tokens == 20 and a_r0._curio_deck.size() == 125,
|
||||
"规则改写型特性带入池:27 件(transitional 直映射——旧 rare 名直通 14+旧 rarest 回退 6=稀有档 20、旧 common 7=普通档)——牌库 98+27=125〔专长批 V14 换代〕")
|
||||
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic 魔能共鸣 dig_reveal 订阅在案)")
|
||||
# ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag;内容数据驱动扩展批 T3 断言面随迁:
|
||||
# 行=triggers 事件订阅形态,挂点投影断言换代=效果引擎订阅装载序〔feats 表序〕)
|
||||
@@ -682,7 +684,7 @@ func _smoke_feats() -> void:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
deck_relic += 1
|
||||
check(deck_relic == 5 and a_r._curio_deck.size() == 103,
|
||||
"规则改写型特性带入池:5 行 relic 每件 1 张(common/rare→稀有档 rarest→罕见档)——牌库 98+5=103")
|
||||
"规则改写型特性带入池:5 行 relic 每件 1 张(transitional——旧 common/rare→优秀档)——牌库 98+5=103")
|
||||
# T4 渠道白名单断言(迭代 15 卡②——渠道轴定位:通关渠道只出专长/节点渠道零专长)
|
||||
var feat_seen: Dictionary = {}
|
||||
var node_relic_seen := false
|
||||
|
||||
@@ -45,6 +45,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面)
|
||||
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0}, # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
|
||||
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 池结构案D 行为单测(迭代 20 T2——需求卡片 V6/V7/V8 验收条目)。
|
||||
# V6 令牌三态(拿了消耗/没拿回池/只刷出不计);V7 曝光槽 2+1+子池空回退+凭证不进子池;
|
||||
# V8 概率与上限解耦(改 cap 不动概率位/改概率位不动 cap/cap 满提前退役只影响候选视图)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/pool_case_d_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[pool_case_d_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# ---- V7 曝光槽:直改 _curio_deck 档位构成后直调候选抽签面 ----
|
||||
var a = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v7(a, db)
|
||||
|
||||
# ---- V6 令牌三态:真牌库 + 停点直调 pick_curio ----
|
||||
var b = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v6(b, db)
|
||||
|
||||
# ---- V8 解耦:cap 独立可设 + 提前退役只影响候选视图 ----
|
||||
var c = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v8(c, db)
|
||||
|
||||
quit(Kit.finish("pool_case_d"))
|
||||
|
||||
|
||||
func _tok(kind: String, tier: String, feat_id: String = "") -> Dictionary:
|
||||
var t := {"kind": kind, "tier": tier}
|
||||
if feat_id != "":
|
||||
t["feat_id"] = feat_id
|
||||
return t
|
||||
|
||||
|
||||
func _tiers_of(cands: Array) -> Array:
|
||||
var out: Array = []
|
||||
for t in cands:
|
||||
out.append(String((t as Dictionary).get("tier", "")))
|
||||
return out
|
||||
|
||||
|
||||
func check_v7(a, db) -> void:
|
||||
if not a.start_adventure(201, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V7 前置:冒险启动")
|
||||
return
|
||||
# 构成①:全池含优秀档以上——曝光槽应出 1 格高档(rank≥excellent)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("armor", "common"), _tok("heal", "excellent"), _tok("refresh", "excellent"), _tok("ap", "rare")]
|
||||
var hi_seen := false
|
||||
for i in 40:
|
||||
var cands: Array = a._draw_curio_candidates("reward_node")
|
||||
if cands.size() != 3:
|
||||
continue
|
||||
var hi := 0
|
||||
for t in cands:
|
||||
if db.adventure.tier_rank(String(t.get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
|
||||
hi += 1
|
||||
if hi >= 1:
|
||||
hi_seen = true
|
||||
Kit.check_quiet(hi_seen, "曝光槽:全池含高档时三选一呈现 ≥1 格优秀档以上〔专长批 V7〕")
|
||||
# 构成②:高档子池空——该格回退全池(候选仍 3 格全普通)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
var ok_fb := true
|
||||
for i in 20:
|
||||
var cands2: Array = a._draw_curio_candidates("reward_node")
|
||||
if cands2.size() != 3:
|
||||
ok_fb = false
|
||||
break
|
||||
for t in cands2:
|
||||
if String(t.get("tier", "")) != "common":
|
||||
ok_fb = false
|
||||
Kit.check_quiet(ok_fb, "子池空回退全池:全普通池候选恒 3 格普通档〔专长批 V7〕")
|
||||
# 构成③:凭证不进子池也不进全池视图(独立计数体系——只经门票注入出现)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("feat_ticket", "excellent")]
|
||||
var ticket_seen := false
|
||||
for i in 30:
|
||||
for t in a._draw_curio_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "feat_ticket":
|
||||
ticket_seen = true
|
||||
Kit.check_quiet(not ticket_seen, "凭证不进抽签池(feat_ticket 只经保底注入通道)〔专长批 V7〕")
|
||||
# 构成④:池空=零候选合法态(同族兜底入口)
|
||||
a._curio_deck = []
|
||||
Kit.check_quiet(a._draw_curio_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
|
||||
|
||||
func check_v6(b, db) -> void:
|
||||
if not b.start_adventure(202, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V6 前置:冒险启动")
|
||||
return
|
||||
# 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点
|
||||
b._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]
|
||||
var gp_before: int = b.pool_stock("gp")
|
||||
b._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
Kit.check_quiet(b.pick_curio("gp", "common") and b.pool_stock("gp") == gp_before - 1,
|
||||
"拿了消耗:pick_curio 后牌库摘牌(选中踢池)〔专长批 V6〕")
|
||||
# 没拿回池:抽候选不摘牌——重复抽签同令牌可再现
|
||||
var deck_size: int = b._curio_deck.size()
|
||||
var seen_again := false
|
||||
for i in 30:
|
||||
var cands: Array = b._draw_curio_candidates("reward_node")
|
||||
if cands.size() == 2 and b._curio_deck.size() == deck_size:
|
||||
seen_again = true
|
||||
Kit.check_quiet(seen_again, "没拿回池:抽签不摘牌(未选候选再次可现,牌库余量不变)〔专长批 V6〕")
|
||||
# 只刷出不计:抽签后 _curio_taken 零变化
|
||||
var taken_before: Dictionary = b._curio_taken.duplicate()
|
||||
for i in 10:
|
||||
b._draw_curio_candidates("reward_node")
|
||||
var unchanged := true
|
||||
for k in taken_before.keys():
|
||||
if int(b._curio_taken.get(k, 0)) != int(taken_before[k]):
|
||||
unchanged = false
|
||||
Kit.check_quiet(unchanged, "只刷出不计:抽签不改已拿账〔专长批 V6〕")
|
||||
|
||||
|
||||
func check_v8(c, db) -> void:
|
||||
if not c.start_adventure(203, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V8 前置:冒险启动")
|
||||
return
|
||||
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
|
||||
var cap_field_before := int(db.adventure.curio_cap_gp)
|
||||
db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
|
||||
var gp_tokens: int = c.pool_stock("gp")
|
||||
Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20,
|
||||
"改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens)
|
||||
# cap 只在拿了才计:刷出不计
|
||||
c._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
for i in 20:
|
||||
c._draw_curio_candidates("reward_node")
|
||||
Kit.check_quiet(bool(c._curio_cap_ok("gp", "")) and int(c._curio_taken.get("gp", 0)) == 0,
|
||||
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
|
||||
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
|
||||
c._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
Kit.check_quiet(c.pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
|
||||
Kit.check_quiet(not bool(c._curio_cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
|
||||
var gp_in_deck := 0
|
||||
for t in c._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "gp":
|
||||
gp_in_deck += 1
|
||||
var gp_cand := false
|
||||
for i in 30:
|
||||
for t in c._draw_curio_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "gp":
|
||||
gp_cand = true
|
||||
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
|
||||
"提前退役:cap 满后候选视图零 gp(牌库本体张数不动=%d——概率位独立存活)〔专长批 V8〕" % gp_in_deck)
|
||||
db.adventure.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
|
||||
Reference in new issue
Block a user