diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 1e33d01d..3cc3817f 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -41,7 +41,7 @@ var hero_hp: int: var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) get: return _wallet.gp set(v): _wallet.gp = v -var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主) +var shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主) # 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口 var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) var treasure_inventory: TreasureItems = null # 秘宝 关卡域库存(TreasureItems;跨战斗延续, @@ -96,10 +96,10 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v _hero_armor_type = String(ctx.get("hero_armor_type", "")) _voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0))) _voucher_time_sand = maxi(0, int(ctx.get("voucher_time_sand", 0))) - _shop = ShopSession.new(db, rng) + shop = ShopSession.new(db, rng) var pool: Dictionary[Variant, Variant] = ctx.get("pool_wiring", {}) as Dictionary if String(ctx.get("shop_mode", "")) == "pool" and not pool.is_empty(): - _shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable, + shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable, pool.get("ticket", Callable()) as Callable, pool.get("cap", Callable()) as Callable) _bind_shop() @@ -302,8 +302,8 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]: "node_id": current_node_id(), "visited_ids": visited_ids(), "aux_items": treasure_snapshot, - "shop_shelf": _shop.shop_bp_shelf(), - "shop_visit_buys": _shop.shop_bp_visit_buys(), + "shop_shelf": shop.shop_bp_shelf(), + "shop_visit_buys": shop.shop_bp_visit_buys(), } @@ -504,8 +504,8 @@ func _enter_node(node: Dictionary) -> bool: # 商店房兑付:到达即开店(覆盖层归呈现层),购买出账/ # 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口 var payload := {"gp": gp} # legacy 固定三件(off 对照承载——零 RNG 零池消费) - if _shop.pool_mode: - _shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签) + if shop.pool_mode: + shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签) payload = {"gp": gp, "shelf": true} node_entered.emit(node, payload) return true @@ -701,7 +701,7 @@ func continue_after_node() -> bool: if t != "reward" and t != "rest" and t != "shop": return false if t == "shop": - _shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态) + shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态) return _present_choices() @@ -726,7 +726,7 @@ func close_wallet() -> void: # 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用) func _bind_shop() -> void: - _shop.bind({ + shop.bind({ "wallet": _wallet, "journal": _journal, "level_row": level_row, @@ -749,80 +749,80 @@ func _gain_max_hp_flat(flat: int) -> void: func shop_products() -> Array: - return _shop.shop_products() + return shop.shop_products() func current_price(prow: Dictionary) -> int: - return _shop.current_price(prow) + return shop.current_price(prow) func purchased_tier(product_id: String) -> int: - return _shop.purchased_tier(product_id) + return shop.purchased_tier(product_id) func buy_product(product_id: String) -> bool: - return _shop.buy_product(product_id) + return shop.buy_product(product_id) func player_request_refresh() -> bool: - return _shop.player_request_refresh() + return shop.player_request_refresh() # 散折价单点(测试取数面沿用本类调用名) func _spend_discounted(price: int) -> int: - return _shop._spend_discounted(price) + return shop._spend_discounted(price) # 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 + # 池消费 handler + 专长凭证判定 + cap 检查回调(cap 满 kind 剔出货架候选视图)。 func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void: - _shop.set_shop_pool(deck_ref, consume_handler, ticket_consider, cap_check) + shop.set_shop_pool(deck_ref, consume_handler, ticket_consider, cap_check) # 货架抽签白盒入口(测试取数面沿用本类调用名) func _shop_draw_shelf() -> void: - _shop._draw_shelf() + shop._draw_shelf() func pool_stock(kind: String) -> int: - return _shop.pool_stock(kind) + return shop.pool_stock(kind) func pool_price(kind: String) -> int: - return _shop.pool_price(kind) + return shop.pool_price(kind) func shop_shelf() -> Array: - return _shop.shop_shelf() + return shop.shop_shelf() func shop_refresh_free_left() -> int: - return _shop.shop_refresh_free_left() + return shop.shop_refresh_free_left() func shop_refresh_cost() -> int: - return _shop.shop_refresh_cost() + return shop.shop_refresh_cost() func shop_refresh() -> bool: - return _shop.shop_refresh() + return shop.shop_refresh() func spend_gp(amount: int) -> bool: - return _shop.spend_gp(amount) + return shop.spend_gp(amount) func buy_pool_item(kind: String) -> bool: - return _shop.buy_pool_item(kind) + return shop.buy_pool_item(kind) func is_shop_pool() -> bool: - return _shop.is_shop_pool() + return shop.is_shop_pool() func shop_bp_shelf() -> Array: - return _shop.shop_bp_shelf() + return shop.shop_bp_shelf() func shop_bp_visit_buys() -> int: - return _shop.shop_bp_visit_buys() + return shop.shop_bp_visit_buys() diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index a17d329c..aab5c1eb 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -170,37 +170,37 @@ func on_shop_buy(product_id: String) -> void: var runner = _runner_get.call() if runner == null: return - if runner.is_shop_pool(): + if runner.shop.is_shop_pool(): # pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用) - if not runner.buy_pool_item(product_id): + if not runner.shop.buy_pool_item(product_id): logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) + shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) + shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 + shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost()) _refresh_presentations.call() return - if not runner.buy_product(product_id): + if not runner.shop.buy_product(product_id): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) toast.show_toast("GP不足——买不起这件商品", "warn") return - shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) + shop_overlay.refresh_state(runner.shop.shop_products(), int(runner.gp)) _refresh_presentations.call() # MC-1:刷新意图(shop_overlay.refresh_requested——核心 shop_refresh 受理后整架重投喂=重抽反馈) func on_shop_refresh() -> void: var runner = _runner_get.call() - if runner == null or not runner.is_shop_pool(): + if runner == null or not runner.shop.is_shop_pool(): return - if not runner.shop_refresh(): + if not runner.shop.shop_refresh(): toast.show_toast("GP不足——刷新失败", "warn") return - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) + shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp)) + shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行 + shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost()) _refresh_presentations.call() diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index df3f20fc..cad30dee 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -160,7 +160,7 @@ func on_refresh() -> void: if not refreshed: # 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用) var cost: int = adventure.refresh_bank().gp_cost() - if runner == null or not runner.spend_gp(cost): + if runner == null or not runner.shop.spend_gp(cost): ctx.views.toast.show_toast("GP不足——刷新失败", "warn") return refreshed = adventure.stop().refresh_curio_paid() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 50ba97ef..11aa6eb6 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -770,7 +770,7 @@ func _on_refresh_confirmed() -> void: return var ok: bool = false if run_scope.level_active and run_scope.runner != null: - ok = run_scope.runner.player_request_refresh() + ok = run_scope.runner.shop.player_request_refresh() elif run_scope.settlement != null: ok = run_scope.settlement.player_request_refresh() logger.log_action("refresh", Vector2i(-1, -1), ok) diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index 66cbe3c2..56d2e192 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -78,12 +78,12 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载) root._open_modal(root.shop_overlay) - if root.run_scope.runner.is_shop_pool(): - root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp)) - root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 - root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1 + if root.run_scope.runner.shop.is_shop_pool(): + root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) + root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 + root.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 else: - root.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp)) + root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp))) root.logger.log_node(node, info, root.run_scope.runner.node_index) diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index ff74ed03..e59e3499 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -132,10 +132,10 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic if String(event) == "gain": root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) if root.shop_overlay.visible and root.run_scope.runner != null: - if root.run_scope.runner.is_shop_pool(): + if root.run_scope.runner.shop.is_shop_pool(): root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - root.shop_overlay.refresh_state(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp)) + root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root._refresh_presentations() diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 8ed5458e..6b946b49 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -85,7 +85,7 @@ func _xnum(v: float) -> String: return "×%d" % int(round(v)) if absf(v - roundf(v)) < 0.005 else "×%.2f" % v -# 打开商店(products = runner.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}]) +# 打开商店(products = runner.shop.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}]) func open_shop(products: Array, gp: int) -> void: for child in _cards_box.get_children(): child.queue_free() @@ -149,7 +149,7 @@ func _emit_buy_requested(p: Dictionary) -> void: # ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)---- -# 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。 +# 打开/重刷货架(cards = runner.shop.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(): diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 61d2597c..60f23e6b 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -332,13 +332,13 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr # 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的 # 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走 -# runner.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 +# runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 static func _autoplay_shop(r, st: Dictionary) -> void: var guard := 30 while guard > 0: guard -= 1 var affordable: Array = [] - for p in r.shop_products(): + for p in r.shop.shop_products(): if r.gp >= int(p.get("price", 1 << 30)): affordable.append(p) if affordable.is_empty(): @@ -347,20 +347,20 @@ static func _autoplay_shop(r, st: Dictionary) -> void: for p in affordable: if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)): pick = p - if not r.buy_product(String(pick.get("product_id", ""))): + if not r.shop.buy_product(String(pick.get("product_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 # 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买 -# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.buy_pool_item +# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.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(): + for c in r.shop.shop_shelf(): if r.gp >= int(c.get("price", 1 << 30)): affordable.append(c) if affordable.is_empty(): @@ -369,7 +369,7 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void: 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", ""))): + if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 @@ -415,10 +415,10 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int: s = 10.0 var cheapest := 1 << 30 if String(st.get("shop_mode", "legacy")) == "pool": - for c in runner.shop_shelf(): # pool 面:货架现价 + for c in runner.shop.shop_shelf(): # pool 面:货架现价 cheapest = mini(cheapest, int(c.get("price", 1 << 30))) else: - for p in runner.shop_products(): + for p in runner.shop.shop_products(): cheapest = mini(cheapest, int(p.get("price", 1 << 30))) if cheapest < (1 << 30) and runner.gp >= cheapest: s = 80.0 diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index b73b0265..320ecbad 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -361,7 +361,7 @@ func _smoke_adventure() -> void: if int(pb.get("level_index", -1)) == 2: l2_price_bonus = int(pb.get("bonus", 0)) Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus) - Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up")) + Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.shop.buy_product("hp_up")) var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子) Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3) var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index 48932e26..ca6fb88e 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -49,9 +49,9 @@ func _init() -> void: # ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)---- var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) lr.gp = 30 - Kit.check_quiet(lr.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用") - Kit.check_quiet(lr.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用") - Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)") + Kit.check_quiet(lr.shop.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用") + Kit.check_quiet(lr.shop.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用") + Kit.check_quiet(lr.shop.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)") # ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)---- var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1} diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index 74a25f21..4102292b 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -57,13 +57,13 @@ func _init() -> void: # ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)---- runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例) - var products: Array = runner.shop_products() + var products: Array = runner.shop.shop_products() var bought := false for p in products: if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "": var attr := String(p.get("attr", "")) var before: int = runner.journal().bonus_total(attr) - bought = runner.buy_product(String(p.get("product_id", ""))) + bought = runner.shop.buy_product(String(p.get("product_id", ""))) if bought: Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)), "hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)") diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 98e9f1b2..aace162a 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1025,17 +1025,17 @@ func _smoke_shop_room() -> void: return Kit.check("到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)", not runner.awaiting_choice() and runner.next_candidate_ids().is_empty()) # 现价 = 基价 + price_level_bonus(L2)=2 + 步长 × 已购档数 - var prods: Array = runner.shop_products() + var prods: Array = runner.shop.shop_products() Kit.check("三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)", prods.size() == 3) var by_id := _shop_products_by_id(prods) Kit.check("L2 首购现价 = 基价 + 关级加值 2(atk_up 6 / hp_up 4 / armor_up 6)", int(by_id["atk_up"]["price"]) == 6 and int(by_id["hp_up"]["price"]) == 4 and int(by_id["armor_up"]["price"]) == 6) # 购买 atk_up:钱包 −6 · 加算区 atk +1 · 二购现价 8 var gp0: int = runner.gp - Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up")) + Kit.check("购买 atk_up 受理(现价 6)", runner.shop.buy_product("atk_up")) Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6) # 加算区住加算日记 Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0) - by_id = _shop_products_by_id(runner.shop_products()) + by_id = _shop_products_by_id(runner.shop.shop_products()) Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1) # 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道) runner.set_hero_override(0, 0) @@ -1043,28 +1043,28 @@ func _smoke_shop_room() -> void: # hp_up:上限与当前生命同步 +3(封顶新上限) var hp_before: int = runner.hero_hp var mhp_before: int = runner.effective_max_hp() - Kit.check("购买 hp_up 受理(现价 4)", runner.buy_product("hp_up")) + Kit.check("购买 hp_up 受理(现价 4)", runner.shop.buy_product("hp_up")) Kit.check("钱包扣 4(%d)" % runner.gp, runner.gp == gp0 - 6 - 4) Kit.check("生命上限 +3(%d → %d——加算区 max_hp 轴)" % [mhp_before, runner.effective_max_hp()], runner.effective_max_hp() == mhp_before + 3) Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp())) # armor_up:AC 10+1 - Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up")) + Kit.check("购买 armor_up 受理(现价 6)", runner.shop.buy_product("armor_up")) Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1) # 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝 var guard_drain := 20 var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环 while guard_drain > 0: guard_drain -= 1 - var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) + var hp_price: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"]) if runner.gp < hp_price: break - if not runner.buy_product("hp_up"): + if not runner.shop.buy_product("hp_up"): break hp_buys += 1 var gp_dry: int = runner.gp - var tier_dry: int = runner.purchased_tier("hp_up") - var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) - Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry) + var tier_dry: int = runner.shop.purchased_tier("hp_up") + var hp_dry: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"]) + Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.shop.buy_product("hp_up") and runner.gp == gp_dry and runner.shop.purchased_tier("hp_up") == tier_dry) # 离开 → 回选路口(候选 = 该店出边——恰一条,指向终点 Boss) Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs), runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs)) @@ -1253,7 +1253,7 @@ func _smoke_shop_pool() -> void: 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) + first_shelf_a = (a8.runner().shop.shop_shelf() as Array).duplicate(true) break Kit.check("池模式商店房场景可达(seed=%s)" % str(scen.get("s", -1)), not scen.is_empty()) if not scen.is_empty(): @@ -1262,19 +1262,19 @@ func _smoke_shop_pool() -> void: a8b.set_hero_override(900, 400) if a8b.start_adventure(int(scen["s"]), {"shop_mode": "pool"}) \ and _adv_drive_to_shop(a8b, _adv_st(), autoplay): - first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true) + first_shelf_b = (a8b.runner().shop.shop_shelf() as Array).duplicate(true) Kit.check("进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size(), first_shelf_a.size() == 3) # MC-1 商店独立刷新(P-C2+Q2 裁——基础免费 0;GP 刷新 ceil(20×1.8^n) provisional;重抽同源) if not scen.is_empty(): var r8 = scen["r"] var gp0: int = int(r8.gp) - var cost0: int = r8.shop_refresh_cost() + var cost0: int = r8.shop.shop_refresh_cost() Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base)) - Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0) - var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true) - Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh()) + Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop.shop_refresh_free_left() == 0) + var shelf_pre: Array = (r8.shop.shop_shelf() as Array).duplicate(true) + Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop.shop_refresh()) Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0) - Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve)))) + Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop.shop_refresh_cost()), int(r8.shop.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve)))) var same_shelf := first_shelf_a.size() == first_shelf_b.size() if same_shelf: for i in first_shelf_a.size(): @@ -1290,10 +1290,10 @@ func _smoke_shop_pool() -> void: # ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算) var l2 := int(r8.level_row.level_index) # 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致) - var relic_price_now: int = r8.pool_price("relic") + var relic_price_now: int = r8.shop.pool_price("relic") var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0)) Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre) - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): # T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉 # (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价) var ckind := String(c.get("kind_id", "")) @@ -1308,7 +1308,7 @@ func _smoke_shop_pool() -> void: Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p) # ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体) var buy_c: Dictionary = {} - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): if int(c.get("price", 1 << 30)) <= int(r8.gp): buy_c = c break @@ -1320,7 +1320,7 @@ func _smoke_shop_pool() -> void: var gp0: int = int(r8.gp) var taken0: int = int(a8._curio.taken.get(kind, 0)) var val0: int = a8.journal().bonus_total(String(val_map[kind])) - Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind)) + Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.shop.buy_pool_item(kind)) Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0))) Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier))) Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1) @@ -1328,7 +1328,7 @@ func _smoke_shop_pool() -> void: # A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为) r8.gp = int(r8.gp) + 64 var second: Dictionary = {} - for c in r8.shop_shelf(): + for c in r8.shop.shop_shelf(): if int(c.get("price", 1 << 30)) <= int(r8.gp): second = c break @@ -1336,14 +1336,14 @@ func _smoke_shop_pool() -> void: var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", ""))) var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a)) Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2) - Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", "")))) + Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.shop.buy_pool_item(String(second.get("kind_id", "")))) else: Kit.check("A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败", false) # ④ 切边零入店(白-box 重抽 40 次——GP 袋/规则改写型特性永不在架;重抽消费 shop_draw 签,场景尾不回写主时间线) var cut_seen := false for i in 40: - r8._shop_draw_shelf() - for c in r8.shop_shelf(): + r8.shop._draw_shelf() + for c in r8.shop.shop_shelf(): if String(c.get("kind_id", "")) == "gp" or String(c.get("kind_id", "")) == "treasure_item": cut_seen = true Kit.check("切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)", not cut_seen) @@ -1353,8 +1353,8 @@ func _smoke_shop_pool() -> void: a8._curio.deck.remove_at(di8) 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"): + r8.shop._draw_shelf() + if (r8.shop.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"): attack_seen = true break Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index f1660fa9..0b3360c8 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -732,12 +732,12 @@ func _smoke_tree_rules() -> void: var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004), {"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散未激活:商品标价原价(无 economy_spend 键)", - int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) + int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), {"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)", - int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) - and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) + int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) + and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) # 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×) var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:基线编排启动(无增益侧)", diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index a00b593a..86aabf8d 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -42,9 +42,9 @@ func _init() -> void: Kit.check_quiet(not e.stop().skip_empty_curio(), "空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕") var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) - lr.set_shop_pool([], Callable(), Callable(), Callable()) - lr._shop_draw_shelf() - Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(), + lr.shop.set_shop_pool([], Callable(), Callable(), Callable()) + lr.shop._draw_shelf() + Kit.check_quiet((lr.shop.shop_bp_shelf() as Array).is_empty(), "空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕") quit(Kit.finish("pool_case_d")) diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 13db7148..4f232f6a 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -226,11 +226,11 @@ func _init() -> void: # ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加) var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) - Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free), + Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free), "刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free)) lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(), {"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役) - Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2, + Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2, "刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)") # ⑪ 天眼(3×3 真视+安全格直开/危险格不开/封印格跳过) diff --git a/game/tests/unit/core/tree_rules_v11_test.gd b/game/tests/unit/core/tree_rules_v11_test.gd index ad233a8b..8ecd5030 100644 --- a/game/tests/unit/core/tree_rules_v11_test.gd +++ b/game/tests/unit/core/tree_rules_v11_test.gd @@ -3,7 +3,7 @@ # 在册词表组合;本测试直构 rebuild 同构合成行(与 effect_engine.gd rebuild 树规则段逐字段同源) # 调 validate_trigger_row 断零违例。实现侧扩闭集未呈报时,本断言以在册词表为准转红。 # 聚散两键(economy_hoard/economy_spend)不入事件面:消费点=tree_rules 成员判定 -# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / level_runner._spend_discounted 商品折价), +# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / shop_session._spend_discounted 商品折价), # rebuild 树规则段对两键零合成行——此状态在此被显式断言(旧版对两键取空夹具静默跳过=假绿): # rebuild 实装面行数对照——四键逐键与夹具同行数(rebuild 分支被删/漂移即转红), # 两键断零行(将来被加合成行即转红,提示先复核效果闭集再同步夹具与实现)。