迭代11第②批:GP商店房重构——三选一同构化(P1共享池账本+抽3买1全重抽+A/b乘法定价ceil+断点扩键+货架同构呈现+MC off/on分层;冒烟671/0+off对拍off_n60逐位PASS+探针三件绿)

This commit is contained in:
DKChan committed 2026-10-03 18:19:57 +08:00
1 parent bc17b66c00
commit d79afd2241
18 files changed
+557 -45

No files matched your search

+42 -1
View File
@@ -79,13 +79,14 @@ var attune_charges := {} # 同调主动件冒险充能账本(
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
# ---- 三选一停点态(迭代 10 奖励重构批)----
var _bonus_taken := {} # 件获取次数计数(attack/max_hp/armor/ap——次数上限判定不可由加算区累加值反推,牌库剔除态依赖)
var _bonus_taken := {} # 件获取次数计数(attack/max_hp/armor/ap——次数上限判定不可由加算区累加值反推,牌库剔除态依赖);迭代 11 第②批起兼作共享池库存账本(stock=cap−taken,商店购买同账本消费)
var _bonus_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付)
var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
var _last_draw_aux: Array[String] = [] # 最近一次抽签掷定的道具件(掷定前置——卡片直显具体道具;追加批二)
var _node_bonus_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
var _shop_mode := "legacy" # 商店模式(迭代 11 第②批 T3——"legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -116,6 +117,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_pending_next_level = false
_base_override = _normalize_override(params.get("hero_override", {}))
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 迭代 11 第②批 T3——缺省 legacy(off 对照零扰动)
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
@@ -177,6 +179,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)), maxi(1,
int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0))))
_first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 迭代 11 第②批 T3——续玩参数面同 start_adventure(缺省 legacy)
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
@@ -322,6 +325,38 @@ func _apply_bonus_pick(id: String, value: int) -> bool:
return true
# ---- 商店共享池消费面(迭代 11 第②批 T3——P1 共享池单点账本)----
# 库存余量(stock = cap − taken;gp 无上限种返回大数——货架词表封闭不含 gp,本口仅供账面/防御读)。
# 「上限即库存」(P1-a):奖励三选一达上限剔牌 与 商店购买扣库存 由同一 taken 账本承载——两渠道
# 跨关共享 11 份有限库存(属性 3+3+3+AP 2),全冒险一次性(P1-b)。
func pool_stock(kind: String) -> int:
var cap := int(db.adventure.bonus_cap(kind))
if cap == db.adventure.BONUS_CAP_UNLIMITED:
return 1 << 30
return cap - int(_bonus_taken.get(kind, 0))
# 商店购买消费 handler(LevelRunner.buy_pool_item 经 Callable 调用——ADR-0001 S6 方法引用):
# 校验有限库存种且有库存 → 走 _apply_bonus_pick 同一入口写加算区+账本(与奖励三选一真单点——
# 买一件=奖励面少一件的库存语义由同账本成立)。gp 件/未知件拒绝(货架词表封闭的前置防御)。
func _consume_pool_purchase(kind: String) -> bool:
if not db.adventure.BONUS_IDS.has(kind) or pool_stock(kind) <= 0:
push_error("[冒险结构] 商店购买消费拒绝——件不在有限库存池或库存尽: %s" % kind)
return false
return _apply_bonus_pick(kind, int(db.adventure.bonus_value(kind)))
# 终局池快照(_settle 清账前调用——_reset_bonus_state 会重置 _bonus_taken,摘要 additive 键
# pool_stock 须在此前取数;货架四种 stock=cap−taken,读数面 = 商店买走 vs 奖励领走 vs 枯竭)
func _pool_stock_snapshot() -> Dictionary:
var out := {}
for row in db.shop_pool_rows():
var kind := String(row.get("kind_id", ""))
out[kind] = pool_stock(kind)
return out
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)。
# 掷定前置(追加批二):具体道具随候选抽定(_last_draw_aux / _bonus_pending.aux_detail)——本函数消费预掷值,
# 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。
@@ -665,6 +700,9 @@ func _start_current_level() -> bool:
if ok:
_runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
_runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
if _shop_mode == "pool":
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动)
_runner.set_shop_pool(_bonus_taken, Callable(self, "_consume_pool_purchase"))
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
@@ -739,6 +777,7 @@ func _settle(result: String) -> void:
"master_seed": master_seed,
"aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷)
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源)
"pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
}
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
@@ -795,6 +834,8 @@ func _snapshot_breakpoint() -> void:
},
"gp": int(ledger.get("gp", 0)),
"shop_purchased": purchased.duplicate(true),
"shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 迭代 11 第②批 T2——P2 甲货架入断点(离开店/非店停点 = 空架合法态)
"shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 迭代 11 第②批 T2——本次进店已购次数(A 链定价输入,P2 甲扩列)
"attunement_charges": attune_charges.duplicate(true), # 迭代 11 第①批 T6——同调主动件充能余量随断点(save v4 域)
"template_pick": _runner.active_template_index if _runner != null else -1,
"master_seed": master_seed,
+23
View File
@@ -279,6 +279,29 @@ func shop_product_row(product_id: String) -> Dictionary:
return {}
# ── 共享池货架访问器(M-4 迭代 11 第②批 T1——消费面一律走访问器,禁直读行集)──
# 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序)
func shop_pool_rows() -> Array:
return _shop_rows("pool_shelf")
func shop_pool_row(kind_id: String) -> Dictionary:
for row in _shop_rows("pool_shelf"):
if String(row.get("kind_id", "")) == kind_id:
return row
return {}
# 乘法价格因子(provisional 投值——0 = 表未投值,消费方按 fail-fast 域校验拦截)
func shop_price_a() -> float:
return float(shop.price_a) if shop != null else 0.0
func shop_price_b() -> float:
return float(shop.price_b) if shop != null else 0.0
# 行族数组读取(ShopTable 属性名 = 行族名;表未落籍/属性缺失 = 空集非错误态)
func _shop_rows(family: String) -> Array:
if shop == null:
+14
View File
@@ -98,6 +98,20 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
if skt != null and en != null:
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
push_diag.call("error", e.category, e.message)
# ⑥ 商店货架 × 冒险共享池跨表校验(M-4 迭代 11 第②批 T1——次数上限域):
# pool_shelf kind 须命中 adventure.BONUS_IDS 且为有限库存种(cap ∈ [1,99]——gp 无上限种/牌库外种
# 入货架 = 库存账本无锚,加载致命;gp 袋负交易切边/机制件三轴切边由 ShopTable 词表封闭另拦)
var adv = loaded.get("adventure")
var shp = loaded.get("shop")
if adv != null and shp != null:
for row in shp.pool_shelf:
var kind: String = String(row.get("kind_id", ""))
if not adv.BONUS_IDS.has(kind):
push_diag.call("error", "shelf_dangling_kind", "ShopTable.pool_shelf kind %s 不在 AdventureTable.BONUS_IDS(共享池牌库外种)" % kind)
continue
var cap: int = int(adv.bonus_cap(kind))
if cap < 1 or cap > 99:
push_diag.call("error", "shelf_cap_domain", "ShopTable.pool_shelf kind %s 次数上限越域 [1,99]: %d(货架件须有限库存种)" % [kind, cap])
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
+122 -1
View File
@@ -67,6 +67,12 @@ var _aux_unlocked := {} # aux 解锁门控集(迭代 7
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动)
var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_aux 扣减直改同引用)
# 关卡末清空+快照计数入 level_finished 载荷)
# ---- 商店共享池模式态(迭代 11 第②批 T3——缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费)----
var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注入时置位——AdventureRunner 按 params.shop_mode 下传)
var _pool_taken: Dictionary = {} # 共享池库存账本共享引用(AdventureRunner._bonus_taken——stock=cap−taken 单点)
var _pool_consume := Callable() # 池消费 handler(AdventureRunner._consume_pool_purchase——_apply_bonus_pick 同口写加算区+账本)
var _shop_shelf: Array[String] = [] # 当前货架(进店抽 3/买后全重抽——kind_id 序)
var _shop_visit_buys := 0 # 本次进店已购次数(A 链定价输入——离开清零)
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
var hero_hp_pct := 0
@@ -441,7 +447,11 @@ func _enter_node(node: Dictionary) -> bool:
"shop":
# 商店房兑付(GP+商店房实施批 2026-09-21):到达即开店(覆盖层归呈现层),购买出账/
# 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口
node_entered.emit(node, {"gp": gp})
if _shop_pool_mode:
_shop_open_pool() # 迭代 11 第②批 T3——进店抽 3(恰一 shop_draw scope;买后全重抽同签)
node_entered.emit(node, {"gp": gp, "shelf": true})
return true
node_entered.emit(node, {"gp": gp}) # legacy 固定三件(off 对照承载——零 RNG 零池消费)
return true
push_error("[关卡节点地图] 未知节点类型: %s" % str(node))
return false
@@ -615,6 +625,9 @@ func continue_after_node() -> bool:
var t := String(current_node().get("type", ""))
if t != "reward" and t != "rest" and t != "shop":
return false
if t == "shop":
_shop_shelf = [] # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态——迭代 11 第②批 T2)
_shop_visit_buys = 0
return _present_choices()
@@ -689,3 +702,111 @@ func player_request_refresh() -> bool:
if _settlement == null:
return false
return _settlement.player_request_refresh()
# ---- 商店共享池模式(迭代 11 第②批 T3——P1 共享池/P4 ceil;legacy 固定三件路径上方原样保留为 off 对照承载)----
# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):库存账本共享引用 +
# 池消费 handler。legacy 模式不注入(_shop_pool_mode 恒 false——off 态零 RNG 零池消费零扰动)。
func set_shop_pool(taken_ref: Dictionary, consume_handler: Callable) -> void:
_pool_taken = taken_ref
_pool_consume = consume_handler
_shop_pool_mode = true
# 进店开店(_enter_node shop 分支 pool 模式):进店购买次计数清零 + 抽 3 落架(恰一 shop_draw scope)
func _shop_open_pool() -> void:
_shop_visit_buys = 0
_shop_draw_shelf()
# 货架抽签(进店 + 每次买后全重抽共用):牌库 = 货架词表四种中库存 >0 者(库存=上限即库存——
# 与奖励三选一达上限剔牌同一账本承载);E-S weight-key 加权洗牌不放回取 3(数学同奖励洗牌族,
# 自包含实现防奖励侧 RNG 序漂移);消费 shop_draw 封闭流恰一 scope(每次抽 3 恰一取号)。牌库空
# = 空货架合法态(池空硬闸——买光即枯竭既定后果)。
func _shop_draw_shelf() -> void:
_shop_shelf = []
var deck: Array = []
for row in db.shop_pool_rows():
var kind := String(row.get("kind_id", ""))
if pool_stock(kind) > 0:
deck.append(kind)
if deck.is_empty():
return
var stream: RandomNumberGenerator = rng.open_stream("shop_draw")
var keyed: Array = []
for kind in deck:
var w := float(maxi(1, int(db.adventure.bonus_weight(kind))))
keyed.append({"id": kind, "key": pow(stream.randf(), 1.0 / w)})
keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"]))
for i in mini(3, keyed.size()):
_shop_shelf.append(String(keyed[i]["id"]))
# 库存余量(账本单点读——gp 无上限种不入货架,防御读返回大数)
func pool_stock(kind: String) -> int:
var cap := int(db.adventure.bonus_cap(kind))
if cap == db.adventure.BONUS_CAP_UNLIMITED:
return 1 << 30
return cap - int(_pool_taken.get(kind, 0))
# 现价 = ceil(基价 × b^(关序−1) × A^(本次进店已购次数))(P4 甲 ceil 向上单一取整——玩家不利向;
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)
func pool_price(kind: String) -> int:
var row: Dictionary = db.shop_pool_row(kind)
var base := float(maxi(1, int(row.get("base_price", 1))))
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \
* pow(db.shop_price_a(), float(_shop_visit_buys))
return int(ceil(price_f))
# 货架只读投影(呈现层数据源——kind/显示名/件值/现价/库存余量齐备,UI 零复算)。
# 件值=奖励件本体值(db.adventure.bonus_value——池件与三选一件同一本体,买一件=奖励面少一件)。
func shop_shelf() -> Array:
var out: Array[Dictionary] = []
for kind in _shop_shelf:
var row: Dictionary = db.shop_pool_row(kind)
out.append({
"kind_id": kind,
"display_name": String(row.get("display_name", kind)),
"value": int(db.adventure.bonus_value(kind)),
"price": pool_price(kind),
"stock_left": pool_stock(kind),
})
return out
# 购买(pool 模式唯一购买口):在架/余额前置校验零副作用 → handler 消费池库存(_apply_bonus_pick
# 同口写加算区+账本——跨渠道共账)→ 出账 → 进店购买次计数 → wallet 事件 → 三件全重抽(未买
# 不保留——重抽绑购买,无独立刷新动作)
func buy_pool_item(kind: String) -> bool:
if not _shop_pool_mode or not _shop_shelf.has(kind):
push_error("[关卡节点地图] 商店购买拒绝——非池模式或件不在架: %s" % kind)
return false
var price := pool_price(kind)
if gp < price:
push_warning("[关卡节点地图] 购买拒绝——余额不足: %s(需 %d,钱包 %d)" % [kind, price, gp])
return false
if _pool_consume.is_null() or not bool(_pool_consume.call(kind)):
push_error("[关卡节点地图] 购买拒绝——池库存消费失败: %s" % kind)
return false
gp -= price
_shop_visit_buys += 1
wallet_changed.emit("buy", gp, {"product": kind, "price": price})
_shop_draw_shelf() # 买 1 → 三件全重抽(恰一 shop_draw scope——同进店取号签)
return true
# 池模式谓词(呈现层组合根分发用——货架面 vs legacy 商品面)
func is_shop_pool() -> bool:
return _shop_pool_mode
# 断点快照读面(P2 甲货架+账本入断点——AdventureRunner._snapshot_breakpoint 消费;离开店即空架)
func shop_bp_shelf() -> Array:
return _shop_shelf.duplicate()
func shop_bp_visit_buys() -> int:
return _shop_visit_buys
+1 -1
View File
@@ -15,7 +15,7 @@ class_name RngServiceCore
extends Node
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动)
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(M-4 迭代 11 第②批 T1——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动)
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15