迭代11第②批:GP商店房重构——三选一同构化(P1共享池账本+抽3买1全重抽+A/b乘法定价ceil+断点扩键+货架同构呈现+MC off/on分层;冒烟671/0+off对拍off_n60逐位PASS+探针三件绿)
This commit is contained in:
1 parent
bc17b66c00
commit
d79afd2241
18 files changed
+557
-45
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
# products = 商品行(键 product_id;三件全陈列无刷新无摇号——2026-09-20 拍板);
|
||||
# 现价公式 = base_price + price_level_bonus(关卡) + price_increment × 该商品已购档数(已购档数在 冒险结构)。
|
||||
# price_level_bonus = 关级定价加值行(level_index 连续从 1 递增)。
|
||||
# pool_shelf = 共享池货架行(M-4 迭代 11 第②批 T1——任务书 v1.3 P1 共享池/P4 ceil):键 kind_id 词表
|
||||
# 封闭 = 四切边后种(SHELF_KINDS:attack/max_hp/armor/ap——GP 袋负交易切边、机制件三轴 doctrine 切边);
|
||||
# 库存=上限即库存(账本在 冒险结构 _bonus_taken 单点共享,行族不记库存);price_a/price_b = 乘法价格
|
||||
# 因子(公式 ceil(base × b^(关序−1) × A^(本次进店已购次数)),provisional 投值——P5 甲研究拍板后置追认)。
|
||||
# products 行族 + 加法定价随 legacy 模式(shop=off 对照承载)保留,退役归 off 锚换代批。
|
||||
# 行值全部 provisional 归 难度曲线与Boss(用户拍板骨架先行、数值整体后置)。
|
||||
class_name ShopTable
|
||||
extends Resource
|
||||
@@ -17,15 +22,19 @@ extends Resource
|
||||
@export var level_gp_bonus: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
|
||||
@export var products: Array[Dictionary] = [] # Array[Dictionary] 键 product_id
|
||||
@export var price_level_bonus: Array[Dictionary] = [] # Array[Dictionary] 键 level_index
|
||||
@export var pool_shelf: Array[Dictionary] = [] # Array[Dictionary] 键 kind_id(迭代 11 第②批 T1)
|
||||
@export var price_a: float = 0.0 # 进店已购次数爬价因子(provisional——0 = 未投值即致命域)
|
||||
@export var price_b: float = 0.0 # 跨关递增因子(provisional)
|
||||
|
||||
const GP_TIERS := ["normal", "elite", "boss"]
|
||||
const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"]
|
||||
const SHELF_KINDS := ["attack", "max_hp", "armor", "ap"] # 货架候选词表封闭(四切边后种——切边面翻案须用户)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
# 行族空护栏(四轴皆为 ConfigDB 消费面——缺族即致命)
|
||||
for family in ["gp_drops", "level_gp_bonus", "products", "price_level_bonus"]:
|
||||
# 行族空护栏(legacy 四轴 + pool_shelf 皆为 ConfigDB 消费面——缺族即致命)
|
||||
for family in ["gp_drops", "level_gp_bonus", "products", "price_level_bonus", "pool_shelf"]:
|
||||
if get(family).is_empty():
|
||||
errs.append({"category": "empty_table", "message": "ShopTable 行族为空: %s" % family})
|
||||
# gp_drops:tier 词表封闭 + 掷定域 + 均值锚 + tier 唯一
|
||||
@@ -87,4 +96,23 @@ func validate() -> Array:
|
||||
var inc: int = int(row.get("price_increment", -1))
|
||||
if inc < 0 or inc > 999:
|
||||
errs.append({"category": "domain", "message": "%s price_increment 越域 [0,999]: %d" % [pid, inc]})
|
||||
# pool_shelf:kind_id 词表封闭(四切边后种)+ 唯一 + display_name + base_price 域 [1,999](迭代 11 第②批 T1)
|
||||
var seen_kind := {}
|
||||
for row in pool_shelf:
|
||||
var kind: String = row.get("kind_id", "")
|
||||
if not SHELF_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "pool_shelf kind_id 出域(四切边后种封闭词表): %s" % kind})
|
||||
if seen_kind.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "pool_shelf kind_id 重复: %s" % kind})
|
||||
seen_kind[kind] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "pool_shelf %s display_name 空" % kind})
|
||||
var base: int = int(row.get("base_price", 0))
|
||||
if base < 1 or base > 999:
|
||||
errs.append({"category": "domain", "message": "pool_shelf %s base_price 越域 [1,999]: %d" % [kind, base]})
|
||||
# 价格因子域(乘法语义——A/b 均 ≥1 单调不降;上界防手滑投值;provisional 值本体归难度曲线与Boss)
|
||||
if price_a < 1.0 or price_a > 3.0:
|
||||
errs.append({"category": "domain", "message": "price_a 越域 [1.0,3.0]: %f" % price_a})
|
||||
if price_b < 1.0 or price_b > 2.0:
|
||||
errs.append({"category": "domain", "message": "price_b 越域 [1.0,2.0]: %f" % price_b})
|
||||
return errs
|
||||
@@ -65,6 +65,25 @@ price_level_bonus = [{
|
||||
"level_index": 3,
|
||||
"bonus": 4
|
||||
}]
|
||||
pool_shelf = [{
|
||||
"kind_id": "attack",
|
||||
"display_name": "攻击锻升",
|
||||
"base_price": 4
|
||||
}, {
|
||||
"kind_id": "max_hp",
|
||||
"display_name": "生命锻升",
|
||||
"base_price": 4
|
||||
}, {
|
||||
"kind_id": "armor",
|
||||
"display_name": "护甲锻升",
|
||||
"base_price": 4
|
||||
}, {
|
||||
"kind_id": "ap",
|
||||
"display_name": "行动点件",
|
||||
"base_price": 5
|
||||
}]
|
||||
price_a = 1.8
|
||||
price_b = 1.1
|
||||
|
||||
; GP+商店房实施批(2026-09-20 用户拍板,设计权威 design/research/proposal-gp-shop-room-2026-09-20.md 裁决记录):
|
||||
; 掉落=均值±方差掷定(gp_drop 封闭流)+关级加值;精英行为预留行(当前无消费方,随精英节点内容批启用);
|
||||
@@ -73,3 +92,10 @@ price_level_bonus = [{
|
||||
; 期望账:L2 商店前可得 GP 期望 ≈ 35–40(L1 全程 22 ± 惊喜 + L2 商店前五战 21 ± ),三件首购总价 L2 =
|
||||
; (4+2)+(2+2)+(4+2)=16、全购欲望 ≈ 26–34——取舍空间成立;量级与方差全部 provisional-pending-曲线调参批
|
||||
;(用户拍板骨架先行、数值整体后置,行值归 难度曲线与Boss)。
|
||||
; GP 商店房重构批(M-4 迭代 11 第②批 T1,2026-10-03——任务书 v1.3 全裁 P1 共享池/P4 ceil/P5 甲):
|
||||
; pool_shelf 行族 = 共享池货架(kind_id 词表封闭四切边后种 attack/max_hp/armor/ap——GP 袋负交易切边、
|
||||
; 机制件三轴 doctrine 切边;库存=上限即库存,账本在 冒险结构 _bonus_taken 单点共享,行族不记库存);
|
||||
; 价格公式换代(pool 模式)= ceil(base_price × b^(关序−1) × A^(本次进店已购次数)),price_a=1.8/price_b=1.1
|
||||
; provisional 投值(零人工令改 provisional+后置追认,原 P5 甲研究拍板后置);products 行族 + 加法定价
|
||||
; 随 legacy 模式(shop=off 对照承载)保留不退役,退役归 off 锚换代批。货架行值全部 provisional 归
|
||||
; 难度曲线与Boss。
|
||||
@@ -447,7 +447,7 @@ func _start_adventure() -> void:
|
||||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
# 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签
|
||||
var first_script: bool = not save_manager.is_first_adventure_done()
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux.unlocked_set()}):
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux.unlocked_set(), "shop_mode": "pool"}):
|
||||
_fatal_screen("冒险启动失败——见输出日志")
|
||||
return
|
||||
|
||||
@@ -496,10 +496,14 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
|
||||
log_view.append_bb(Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
|
||||
elif t == "shop":
|
||||
# 商店房兑付(GP+商店房实施批):到达即开店——商品快照/余额经 runner 只读投影喂入;
|
||||
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进
|
||||
# 商店房兑付(GP+商店房实施批):到达即开店——商品/货架快照与余额经 runner 只读投影喂入;
|
||||
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
|
||||
# pool 面 = 共享池货架三选一同构卡(迭代 11 第②批 T4);legacy 面 = 固定三件(off 对照承载)
|
||||
_open_modal(shop_overlay)
|
||||
shop_overlay.open_shop(runner.shop_products(), int(runner.gp))
|
||||
if runner.is_shop_pool():
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
else:
|
||||
shop_overlay.open_shop(runner.shop_products(), int(runner.gp))
|
||||
log_view.append_bb(Narrator.shop_line(int(runner.gp)))
|
||||
logger.log_node(node, info, runner.node_index)
|
||||
_refresh_presentations()
|
||||
@@ -1084,6 +1088,15 @@ func _on_camp_attune_toggle(item_id: String) -> void:
|
||||
func _on_shop_buy(product_id: String) -> void:
|
||||
if runner == null:
|
||||
return
|
||||
if runner.is_shop_pool():
|
||||
# pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用)
|
||||
if not runner.buy_pool_item(product_id):
|
||||
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
|
||||
board_zone.toast.show_toast("金币不足——买不起这件商品", "warn")
|
||||
return
|
||||
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
|
||||
_refresh_presentations()
|
||||
return
|
||||
if not runner.buy_product(product_id):
|
||||
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||||
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
|
||||
@@ -1107,7 +1120,10 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void:
|
||||
if String(event) == "gain":
|
||||
_last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain)
|
||||
if shop_overlay.visible and runner != null:
|
||||
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
|
||||
if runner.is_shop_pool():
|
||||
shop_overlay.refresh_shelf_state(int(runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
|
||||
else:
|
||||
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-21(GP+商店房实施批——商店房覆盖层呈现)
|
||||
# 商店覆盖层(模态):标题「商店」/ 金币余额 / 三商品卡全陈列(显示名·档位文案·现价·已购档数)/
|
||||
# 购买按钮(余额不足禁用置灰)/ 离开按钮。模态挂接方式仿既有覆盖层(legend_overlay 现成参照——
|
||||
# dim + CenterContainer + box,组合根 _register_modal 收口层级)。
|
||||
# 高内聚边界:纯视图 setter 面——商品快照与余额经组合根喂入(runner.shop_products() 只读投影,
|
||||
# 现价/已购档数由 runner 现算),购买/离开意图经信号外发——零核心写调用、零 runner 持有。
|
||||
# 商店覆盖层(模态):标题「商店」/ 金币余额 / 商品卡陈列 / 购买按钮(余额不足禁用置灰)/ 离开按钮。
|
||||
# 模态挂接方式仿既有覆盖层(legend_overlay 现成参照——dim + CenterContainer + box,组合根
|
||||
# _register_modal 收口层级)。
|
||||
# 双面承载(M-4 迭代 11 第②批 T4):legacy 面 = 三商品卡全陈列(open_shop/refresh_state——shop=off
|
||||
# 对照承载原样保留);pool 面 = 共享池货架三选一同构卡(open_shelf——进店抽 3 + 买 1 全重抽反馈 +
|
||||
# 库存余量就近呈现,买后组合根重投喂货架整架重建)。高内聚边界:纯视图 setter 面——商品/货架快照
|
||||
# 与余额经组合根喂入(runner 投影现算现价,视图零复算定价公式),购买/离开意图经信号外发——
|
||||
# 零核心写调用、零 runner 持有。
|
||||
class_name ShopOverlay
|
||||
extends Control
|
||||
|
||||
signal buy_requested(product_id: String)
|
||||
signal buy_requested(product_id: String) # legacy=product_id / pool=kind_id(组合根按模式分发)
|
||||
signal leave_pressed
|
||||
|
||||
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表)
|
||||
const KIND_LABELS := {"attack": "攻击", "max_hp": "生命上限", "armor": "护甲", "ap": "行动点"} # 货架件值文案轴(共享池 kind 词表——迭代 11 第②批)
|
||||
|
||||
var _gp_label: Label
|
||||
var _cards_box: VBoxContainer
|
||||
var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]——refresh_state 就地重刷(零重建回流)
|
||||
var _shelf_rows: Array[Dictionary] = [] # [{kind_id, btn}]——pool 面卡片(买后整架重建=重抽反馈)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -95,3 +100,55 @@ func _emit_leave_pressed() -> void:
|
||||
|
||||
func _emit_buy_requested(p: Dictionary) -> void:
|
||||
buy_requested.emit(String(p.product_id))
|
||||
|
||||
|
||||
# ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)----
|
||||
|
||||
# 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。
|
||||
# 买后组合根重投喂新货架 = 整架重建呈现(三件全重抽反馈——未买不保留);空架 = 池空硬闸合法态。
|
||||
func open_shelf(cards: Array, gp: int) -> void:
|
||||
for child in _cards_box.get_children():
|
||||
child.queue_free()
|
||||
_shelf_rows.clear()
|
||||
for c in cards:
|
||||
_shelf_rows.append(_build_shelf_card(c))
|
||||
if cards.is_empty():
|
||||
var empty := UiKit.label("货架售罄——共享池已买空(属性件全冒险共 3+3+3 份 / AP 件 2 份)", 14, false, UiKit.WARM_FAINT)
|
||||
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_cards_box.add_child(empty)
|
||||
refresh_shelf_state(gp)
|
||||
visible = true
|
||||
|
||||
|
||||
# 货架余额重刷(就地改禁用态——买后重建路径经 open_shelf 覆盖,本口供 wallet 变化联动)
|
||||
func refresh_shelf_state(gp: int) -> void:
|
||||
_gp_label.text = "金币 %d" % gp
|
||||
for card in _shelf_rows:
|
||||
card.btn.disabled = gp < int(card.price)
|
||||
|
||||
|
||||
# 货架卡(显示名+件值文案 | 现价 | 库存余量 | 购买钮)——三选一同构语言:买 1 = 一次真实购买
|
||||
func _build_shelf_card(c: Dictionary) -> Dictionary:
|
||||
var kind := String(c.get("kind_id", ""))
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.GOLD_DIM, 4, 1, Vector4(12, 6, 12, 6)))
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 14)
|
||||
card.add_child(row)
|
||||
var name_l := UiKit.label("%s(%s %+d)" % [String(c.get("display_name", kind)), String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))], 15, true)
|
||||
name_l.custom_minimum_size = Vector2(190, 0)
|
||||
row.add_child(name_l)
|
||||
var price_l := UiKit.label("现价 %d 金币" % int(c.get("price", 0)), 14, false, UiKit.WARM_SOFT)
|
||||
price_l.custom_minimum_size = Vector2(110, 0)
|
||||
row.add_child(price_l)
|
||||
var stock_l := UiKit.label("库存余 %s" % str(int(c.get("stock_left", 0))) if int(c.get("stock_left", 0)) < (1 << 20) else "库存余 ∞", 14, false, UiKit.WARM_FAINT)
|
||||
row.add_child(stock_l)
|
||||
var btn := UiKit.button("购买")
|
||||
btn.pressed.connect(_emit_shelf_buy.bind(kind))
|
||||
row.add_child(btn)
|
||||
_cards_box.add_child(card)
|
||||
return {"kind_id": kind, "price": int(c.get("price", 0)), "btn": btn}
|
||||
|
||||
|
||||
func _emit_shelf_buy(kind: String) -> void:
|
||||
buy_requested.emit(kind)
|
||||
@@ -38,6 +38,10 @@ func _initialize() -> void:
|
||||
var attune_mode := "off"
|
||||
if args.size() >= 5 and args[4] in ["off", "on"]:
|
||||
attune_mode = args[4]
|
||||
# 商店分层开关(迭代 11 第②批 T5——off=legacy 对照缺省零扰动;on=共享池货架面,终局池读数随行)
|
||||
var shop_mode := "off"
|
||||
if args.size() >= 6 and args[5] in ["off", "on"]:
|
||||
shop_mode = args[5]
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
@@ -81,7 +85,8 @@ func _initialize() -> void:
|
||||
for i in range(n):
|
||||
var seed_val := seed0 + _run_cursor
|
||||
_run_cursor += 1
|
||||
var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route, {}, attune_profile)
|
||||
var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route, {}, attune_profile,
|
||||
{"mode": "pool"} if shop_mode == "on" else {})
|
||||
var sum: Dictionary = st["summary"]
|
||||
var rec := {
|
||||
"policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route,
|
||||
@@ -103,6 +108,11 @@ func _initialize() -> void:
|
||||
"level_templates": st.get("level_templates", []),
|
||||
"wallet_count": (st["wallet_events"] as Array).size(),
|
||||
}
|
||||
if shop_mode == "on":
|
||||
# pool 面增量字段(迭代 11 第②批 T5——off 行零新增保对拍逐位干净)
|
||||
rec["shop_mode"] = "pool"
|
||||
rec["shop_buys"] = int(st.get("shop_buys", 0))
|
||||
rec["pool_stock"] = st.get("pool_stock", {})
|
||||
var rs := String(sum.get("result", ""))
|
||||
tally[rs] = int(tally.get(rs, 0)) + 1
|
||||
print("RUNSUM " + JSON.stringify(rec))
|
||||
|
||||
@@ -114,7 +114,7 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
|
||||
# (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。
|
||||
static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false,
|
||||
logger = null, policy: String = "normal", route: String = "min",
|
||||
hero_override: Dictionary = {}, attune: Dictionary = {}) -> Dictionary:
|
||||
hero_override: Dictionary = {}, attune: Dictionary = {}, shop: Dictionary = {}) -> Dictionary:
|
||||
var own_session := logger == null
|
||||
if own_session:
|
||||
logger = load("res://presentation/playtest_logger.gd").new()
|
||||
@@ -154,6 +154,11 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
if not attune.is_empty():
|
||||
adv_params["attunement"] = attune.get("equipped", [])
|
||||
adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0))
|
||||
# 商店分层注入(迭代 11 第②批 T5——缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面)
|
||||
var pool_shop: bool = String(shop.get("mode", "")) == "pool"
|
||||
if pool_shop:
|
||||
adv_params["shop_mode"] = "pool"
|
||||
st["shop_mode"] = "pool"
|
||||
if not adr.start_adventure(seed_val, adv_params): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数)
|
||||
st.result = "start_failed"
|
||||
if own_session:
|
||||
@@ -168,6 +173,10 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
st.timeout = true
|
||||
if adr.runner() != null:
|
||||
adr.runner().restore_hero_base()
|
||||
if pool_shop:
|
||||
# 终局池读数(迭代 11 第②批 T5——摘要 additive 键 pool_stock:_settle 清账前快照,
|
||||
# 商店买走 vs 奖励领走 vs 枯竭的终局面;事后读 adr.pool_stock 会被收束清账重置=满库假象)
|
||||
st["pool_stock"] = (st.get("summary", {}).get("pool_stock", {}) as Dictionary).duplicate(true)
|
||||
st.log_path = logger.log_path()
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
@@ -215,7 +224,10 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
r.continue_after_node()
|
||||
elif t == "shop":
|
||||
st.shop_gp_seq.append(int(info.get("gp", -1)))
|
||||
_autoplay_shop(r, st) # 贪心买齐(2026-10-01——旧零购离开退役:GP 局内死钱,真人实证两店全购)
|
||||
if String(st.get("shop_mode", "legacy")) == "pool":
|
||||
_autoplay_shop_pool(r, st) # 共享池货架贪心(迭代 11 第②批 T5——买一件=奖励面少一件的机会成本画像维持贪心语义)
|
||||
else:
|
||||
_autoplay_shop(r, st) # 贪心买齐(2026-10-01——旧零购离开退役:GP 局内死钱,真人实证两店全购)
|
||||
r.continue_after_node()
|
||||
if logger != null:
|
||||
logger.log_node(node, info, r.node_index)
|
||||
@@ -279,6 +291,28 @@ static func _autoplay_shop(r, st: Dictionary) -> void:
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
|
||||
# 商店共享池贪心(迭代 11 第②批 T5):货架三选一同构面贪心买齐——affordable 集挑最便宜反复买
|
||||
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.buy_pool_item
|
||||
# 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。
|
||||
static func _autoplay_shop_pool(r, st: Dictionary) -> void:
|
||||
var guard := 30
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
var affordable: Array = []
|
||||
for c in r.shop_shelf():
|
||||
if r.gp >= int(c.get("price", 1 << 30)):
|
||||
affordable.append(c)
|
||||
if affordable.is_empty():
|
||||
return
|
||||
var pick: Dictionary = affordable[0]
|
||||
for c in affordable:
|
||||
if int(c.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
|
||||
pick = c
|
||||
if not r.buy_pool_item(String(pick.get("kind_id", ""))):
|
||||
return
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
|
||||
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
|
||||
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.summary = summary.duplicate(true)
|
||||
@@ -319,8 +353,12 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int:
|
||||
elif t == "shop":
|
||||
s = 10.0
|
||||
var cheapest := 1 << 30
|
||||
for p in runner.shop_products():
|
||||
cheapest = mini(cheapest, int(p.get("price", 1 << 30)))
|
||||
if String(st.get("shop_mode", "legacy")) == "pool":
|
||||
for c in runner.shop_shelf(): # pool 面:货架现价(迭代 11 第②批 T5)
|
||||
cheapest = mini(cheapest, int(c.get("price", 1 << 30)))
|
||||
else:
|
||||
for p in runner.shop_products():
|
||||
cheapest = mini(cheapest, int(p.get("price", 1 << 30)))
|
||||
if cheapest < (1 << 30) and runner.gp >= cheapest:
|
||||
s = 80.0
|
||||
elif t == "reward":
|
||||
|
||||
@@ -106,6 +106,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_gp_escrow()
|
||||
_smoke_shop_room()
|
||||
_smoke_shop_graph()
|
||||
_smoke_shop_pool()
|
||||
_smoke_nine_types_gp_stream()
|
||||
_smoke_adventure_table()
|
||||
_smoke_adventure_api()
|
||||
@@ -3347,6 +3348,135 @@ func _smoke_shop_graph() -> void:
|
||||
"L1 行集零改动(单链 Boss (1,4)——教学期最小修订面)")
|
||||
|
||||
|
||||
# ---- ⑤ 商店共享池(M-4 迭代 11 第②批 T5——三选一同构/乘法价/跨渠道共账/切边/断点扩键)----
|
||||
|
||||
func _smoke_shop_pool() -> void:
|
||||
print("--- 商店共享池(抽3买1全重抽 · 乘法定价 ceil · 池空硬闸 · 切边零入店)---")
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var shelf_kinds: Array = []
|
||||
for row in _db.shop_pool_rows():
|
||||
shelf_kinds.append(String(row.get("kind_id", "")))
|
||||
# ① 池模式场景可达(_adv_drive_to_shop 复用——L1 全程+L2 推进到商店房;pool 模式进店即抽架)
|
||||
var scen := {}
|
||||
var first_shelf_a: Array = []
|
||||
var first_shelf_b: Array = []
|
||||
for s in range(42, 92):
|
||||
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
a8.set_hero_override(900, 400)
|
||||
if not a8.start_adventure(s, {"first_script": true, "shop_mode": "pool"}):
|
||||
continue
|
||||
if not _adv_drive_to_shop(a8, _adv_st(), autoplay):
|
||||
continue
|
||||
scen = {"a": a8, "s": s, "r": a8.runner()}
|
||||
first_shelf_a = (a8.runner().shop_shelf() as Array).duplicate(true)
|
||||
break
|
||||
check(not scen.is_empty(), "池模式商店房场景可达(seed=%s)" % str(scen.get("s", -1)))
|
||||
if not scen.is_empty():
|
||||
# 同种子再跑一遍采第二份首架(确定性对拍——断点货架续玩=确定性重放承载的实证面)
|
||||
var a8b = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(int(scen["s"])))
|
||||
a8b.set_hero_override(900, 400)
|
||||
if a8b.start_adventure(int(scen["s"]), {"first_script": true, "shop_mode": "pool"}) \
|
||||
and _adv_drive_to_shop(a8b, _adv_st(), autoplay):
|
||||
first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true)
|
||||
check(first_shelf_a.size() == 3, "进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size())
|
||||
var same_shelf := first_shelf_a.size() == first_shelf_b.size()
|
||||
if same_shelf:
|
||||
for i in first_shelf_a.size():
|
||||
if String(first_shelf_a[i].get("kind_id", "")) != String(first_shelf_b[i].get("kind_id", "")) \
|
||||
or int(first_shelf_a[i].get("price", -1)) != int(first_shelf_b[i].get("price", -1)):
|
||||
same_shelf = false
|
||||
break
|
||||
check(same_shelf, "同种子同货架逐位(确定性重放承载——断点货架续玩语义)")
|
||||
if scen.is_empty():
|
||||
return
|
||||
var a8 = scen["a"]
|
||||
var r8 = scen["r"]
|
||||
# ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算)
|
||||
var l2 := int(r8.level_row.level_index)
|
||||
for c in r8.shop_shelf():
|
||||
check(shelf_kinds.has(String(c.get("kind_id", ""))), "货架 kind 在封闭词表(%s)" % str(c.get("kind_id", "")))
|
||||
var prow: Dictionary = _db.shop_pool_row(String(c.get("kind_id", "")))
|
||||
var expect_p: int = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
|
||||
check(int(c.get("price", -1)) == expect_p,
|
||||
"首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [str(c.get("kind_id", "")), int(c.get("price", -1)), expect_p])
|
||||
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1)
|
||||
var buy_c: Dictionary = {}
|
||||
for c in r8.shop_shelf():
|
||||
if int(c.get("price", 1 << 30)) <= int(r8.gp):
|
||||
buy_c = c
|
||||
break
|
||||
check(not buy_c.is_empty(), "可购卡在架(GP %d——种子扫描保证首购空间)" % int(r8.gp))
|
||||
if not buy_c.is_empty():
|
||||
var kind := String(buy_c.get("kind_id", ""))
|
||||
var val_map := {"attack": "atk", "max_hp": "max_hp", "armor": "armor_class", "ap": "ap"}
|
||||
var gp0: int = int(r8.gp)
|
||||
var taken0: int = int(a8._bonus_taken.get(kind, 0))
|
||||
var val0: int = int(a8.bonus.get(String(val_map[kind]), -999))
|
||||
check(r8.buy_pool_item(kind), "购买受理(pool 口——%s)" % kind)
|
||||
check(int(r8.gp) == gp0 - int(buy_c.get("price", 0)), "钱包扣现价(%d → %d)" % [gp0, int(r8.gp)])
|
||||
check(int(a8.bonus.get(String(val_map[kind]), -999)) == val0 + int(_db.adventure.bonus_value(kind)),
|
||||
"加算区 +件值(池件=奖励件本体——%s %+d)" % [kind, int(_db.adventure.bonus_value(kind))])
|
||||
check(int(a8._bonus_taken.get(kind, 0)) == taken0 + 1, "共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._bonus_taken.get(kind, 0))])
|
||||
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
|
||||
var second: Dictionary = {}
|
||||
for c in r8.shop_shelf():
|
||||
if int(c.get("price", 1 << 30)) <= int(r8.gp):
|
||||
second = c
|
||||
break
|
||||
if not second.is_empty():
|
||||
var prow2: Dictionary = _db.shop_pool_row(String(second.get("kind_id", "")))
|
||||
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * _db.shop_price_a()))
|
||||
check(int(second.get("price", -1)) == expect_p2,
|
||||
"A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2)
|
||||
check(r8.buy_pool_item(String(second.get("kind_id", ""))), "同 visit 第二购受理(A 链软闸在价不在限)")
|
||||
# ④ 切边零入店(白-box 重抽 40 次——GP 袋/机制件永不在架;重抽消费 shop_draw 签,场景尾不回写主时间线)
|
||||
var cut_seen := false
|
||||
for i in 40:
|
||||
r8._shop_draw_shelf()
|
||||
for c in r8.shop_shelf():
|
||||
if String(c.get("kind_id", "")) == "gp" or String(c.get("kind_id", "")) == "aux_item":
|
||||
cut_seen = true
|
||||
check(not cut_seen, "切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)")
|
||||
# ⑤ 池空硬闸(库存尽剔出牌库):white-box 计满 attack 后重抽零 attack + 消费口拒绝
|
||||
a8._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack)
|
||||
var attack_seen := false
|
||||
for i in 40:
|
||||
r8._shop_draw_shelf()
|
||||
if (r8.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"):
|
||||
attack_seen = true
|
||||
break
|
||||
check(not attack_seen, "库存尽剔牌库(attack 计满 %d——上限即库存跨渠道共账 reward→shop 向)" % int(_db.adventure.bonus_cap_attack))
|
||||
var st_consume: int = int(a8._bonus_taken["attack"])
|
||||
check(not a8._consume_pool_purchase("attack") and int(a8._bonus_taken["attack"]) == st_consume,
|
||||
"池空消费拒绝(handler 零副作用——买光即枯竭硬闸)")
|
||||
# ⑥ 跨渠道共账 shop→reward 向:消费口写账本后奖励牌库同步剔牌
|
||||
a8._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack) - 1
|
||||
check(a8._consume_pool_purchase("attack"), "商店消费口受理(_apply_bonus_pick 同口——真单点账本)")
|
||||
check(not (a8._draw_bonus_candidates() as Array).has("attack"),
|
||||
"商店购买进奖励牌库剔除(买一件=奖励面少一件——shop→reward 向共账)")
|
||||
# ⑦ 断点扩键(P2 甲——save v4 域内:shop_shelf/shop_visit_buys 入快照;离开店=空架合法态)
|
||||
var sm = load("res://core/save_manager.gd").new("user://_smoke_shop_pool_save.json")
|
||||
sm.load_from_disk()
|
||||
a8.set_save_manager(sm)
|
||||
check(r8.continue_after_node() and r8.awaiting_choice(), "离开商店 → 回选路口(货架态清零落档——零自动推进收口)")
|
||||
var bp: Dictionary = sm.breakpoint_snapshot()
|
||||
check(bp.has("shop_shelf") and (bp["shop_shelf"] as Array).is_empty(),
|
||||
"断点扩键 shop_shelf 在案(离开店=空架合法态——P2 甲扩列)")
|
||||
check(bp.has("shop_visit_buys") and int(bp["shop_visit_buys"]) == 0,
|
||||
"断点扩键 shop_visit_buys 在案(离开店清零——A 链输入不入档语义)")
|
||||
sm.clear_breakpoint()
|
||||
# ⑧ 终局池读数(run_adventure pool 面——商店与奖励跨渠道消耗后的终局余量出讫)
|
||||
var st_pool: Dictionary = autoplay.run_adventure(_db, 42, 0, 0, false, null, "normal", "min", {}, {}, {"mode": "pool"})
|
||||
var ps: Dictionary = st_pool.get("pool_stock", {})
|
||||
check(ps.size() == 4, "终局池读数四键出讫(run_adventure pool 面——读数只锚一次载体)")
|
||||
var ps_ok := true
|
||||
for k in ps.keys():
|
||||
var cap := int(_db.adventure.bonus_cap(String(k)))
|
||||
if int(ps[k]) < 0 or int(ps[k]) > cap:
|
||||
ps_ok = false
|
||||
check(ps_ok, "终局池余量在域 [0,cap](%s)" % str(ps))
|
||||
|
||||
|
||||
# 入口→Boss 全路径枚举(DFS 迭代栈——图合法性校验保证单入口无环,路径有限)
|
||||
func _all_entry_boss_paths(nodes: Array) -> Array:
|
||||
var by_id := {}
|
||||
|
||||
Reference in new issue
Block a user