From ac3cee7daa731d031438ab8e41f2859f89beb5c2 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 5 Oct 2026 03:50:19 +0800 Subject: [PATCH] =?UTF-8?q?MC-1=20=E5=95=86=E5=BA=97=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E6=9C=BA=E5=88=B6=EF=BC=88UI=E8=90=BD?= =?UTF-8?q?=E7=A0=81=E7=AC=AC=E4=B8=89=E6=B3=A2=E6=94=B6=E5=AE=98=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=89=B9=EF=BC=89=E2=80=94=E2=80=94level=5Frunner?= =?UTF-8?q?=E6=96=B0=E5=A2=9Eshop=5Frefresh/free=5Fleft/cost=E4=B8=89?= =?UTF-8?q?=E9=9D=A2(GP=E6=89=A3=E8=B4=B9+=E8=B4=A7=E6=9E=B6=E6=95=B4?= =?UTF-8?q?=E6=9E=B6=E9=87=8D=E6=8A=BDshop=5Fdraw=E5=90=8C=E7=AD=BE+?= =?UTF-8?q?=E4=BB=98=E8=B4=B9=E8=AE=A1=E6=95=B0=E8=BF=9B=E7=A6=BB=E6=B8=85?= =?UTF-8?q?=E9=9B=B6)+adventure=5Ftable=E4=B8=89=E6=97=8B=E9=92=AE(?= =?UTF-8?q?=E5=85=8D=E8=B4=B90/=E5=9F=BA=E4=BB=B720/=E6=9B=B2=E7=BA=BF1.8?= =?UTF-8?q?=20provisional)+shop=5Foverlay=E5=88=B7=E6=96=B0=E9=92=AE?= =?UTF-8?q?=E8=BD=AC=E6=AD=A3(set=5Frefresh=5Fdisplay=E6=8A=95=E5=96=82?= =?UTF-8?q?=E9=9D=A2)+presenter=20on=5Fshop=5Frefresh+main=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF+=E5=86=92=E7=83=9F5=E6=96=AD=E8=A8=80(752/0)+?= =?UTF-8?q?=E5=85=B3=E5=8D=A1=E8=8A=82=E7=82=B9=E5=9C=B0=E5=9B=BEv1.6?= =?UTF-8?q?=E6=8D=A2=E4=BB=A3+=E5=86=92=E9=99=A9=E7=BB=93=E6=9E=84v0.15?= =?UTF-8?q?=E5=9B=9E=E5=86=99=E3=80=82=E6=9C=BA=E5=88=B6=3DP-C2+Q2?= =?UTF-8?q?=E8=A3=81(=E5=9F=BA=E7=A1=80=E5=85=8D=E8=B4=B90/=E5=A4=A9?= =?UTF-8?q?=E8=B5=8B=E5=A2=9E=E7=9B=8A=E4=BD=8D=E9=A2=84=E7=95=99/GP=20cei?= =?UTF-8?q?l(20=C3=971.8^n)/=E6=B1=A0=E6=BA=90=E5=90=8C=E6=BA=90)=E3=80=82?= =?UTF-8?q?=E9=97=B8=3D=E5=86=92=E7=83=9F752/0+probe=5Fgate=E5=85=A8?= =?UTF-8?q?=E7=BB=BF+layout=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/systems/冒险循环/关卡节点地图.md | 2 +- design/gdd/systems/冒险循环/冒险结构.md | 2 +- game/core/level_runner.gd | 28 ++++++++++++++++++++ game/data/schemas/adventure_table.gd | 3 +++ game/presentation/camp_shop_presenter.gd | 14 ++++++++++ game/presentation/main.gd | 2 ++ game/presentation/views/shop_overlay_view.gd | 26 +++++++++++++++--- game/smoke/smoke_all.gd | 11 ++++++++ 8 files changed, 83 insertions(+), 5 deletions(-) diff --git a/design/gdd/systems/冒险循环/关卡节点地图.md b/design/gdd/systems/冒险循环/关卡节点地图.md index 50ac3692..f4ff5761 100644 --- a/design/gdd/systems/冒险循环/关卡节点地图.md +++ b/design/gdd/systems/冒险循环/关卡节点地图.md @@ -58,7 +58,7 @@ | Boss | 恒为图终点;行进至此开 Boss 战;胜利即关卡收束点 | | 奖励 | 行进至此触发节点奖励(兑付见下节);反馈呈现后回到选路 | | 恢复 | 行进至此向冒险结构发治疗封顶回报;反馈呈现后回到选路 | -| 商店 | 行进至此打开商店覆盖层,用 GP(显示名「金币」,战斗掉落的冒险内货币)购买属性提升;离开后兑付确认回选路;不携带战斗行。**v1.5 兑付换代(pool 模式现行)**:进店从共享池抽 3 展示、买 1 后三件全重抽(未买不保留、无独立刷新动作——重抽绑购买)、货架买空=空架合法态;账本与定价归[冒险结构](冒险结构.md)共享池账本节 | +| 商店 | 行进至此打开商店覆盖层,用 GP(显示名「金币」,战斗掉落的冒险内货币)购买属性提升;离开后兑付确认回选路;不携带战斗行。**v1.5 兑付换代(pool 模式现行)**:进店从共享池抽 3 展示、买 1 后三件全重抽(未买不保留)、货架买空=空架合法态。**v1.6(2026-10-04 P-C2+Q2 裁/MC-1)**:新增**独立刷新**——基础免费 0 次(天赋/营地侧刷新次数增益位预留)+ GP 付费刷新 ceil(20×1.8^已付费刷) provisional;重抽=共享池同源(shop_draw 同签),推翻本行旧「无独立刷新动作——重抽绑购买」口径;账本与定价归[冒险结构](冒险结构.md)共享池账本节 | 商店房的编排规则(2026-09-20 用户拍板):每关**恰 1 个**商店房,位置 = **关底 Boss 前一位**——哈迪斯式的"决战前最后一个补给点":买到的属性还有整段战斗可消费,错过的成长也没有第二家店可补。Boss 前一位同时是**必经位**:图拓扑收束使所有入口→Boss 的路径都穿过它,成长保底成为图结构事实而非玩家自觉。这条位置约束不由加载校验承载——图合法性六检零新增,商店房的"每关恰 1 + 必经"由冒烟断言把关。 diff --git a/design/gdd/systems/冒险循环/冒险结构.md b/design/gdd/systems/冒险循环/冒险结构.md index 27da6831..512a0dc6 100644 --- a/design/gdd/systems/冒险循环/冒险结构.md +++ b/design/gdd/systems/冒险循环/冒险结构.md @@ -151,7 +151,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 掉落掷定消费 gp_drop 封闭随机流(主种子派生、可回放)。**商店货架抽签消费 shop_draw 封闭随机流**(v0.13 扩签——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰)。掉落与价格的现行行值读商店表(registry 为准),数值所有权归[难度曲线与Boss](难度曲线与Boss.md)曲线调参批——本批骨架先行,全部数值 provisional。 -**共享池账本(v0.13 P1 共享池单点;v0.14 令牌库换代)**:商店货架与提升三选一是**同一个池**——货架候选 = 池∖{GP 袋}(用钱买钱恒负交易)= {攻击, 生命上限, 护甲, 行动点} 四种(魔法道具件已随同调批出池);**库存 = 剩余令牌数**(TP0——「池消耗即上限」,种类次数上限退役,与奖励兑付摘牌同一令牌库承载,全冒险一次性跨关卡不补给):商店买一件,奖励面就少一枚;「无限购买」收敛为**钱尽或池空为止**(池空=硬闸、A 链爬价=软闸),空货架合法态。货架 = **令牌级抽签**(同 kind 去重每 kind 至多一卡,价格公式 tier-blind 维持批②裁定,购买消费在架令牌本体 kind+tier)。两渠道不得各自私记库存——消费走同一入口写同一令牌库(实现=AdventureRunner._bonus_deck 单点,冒险结构 owner;kind 维度派生计数 _bonus_taken 承载断点附随件与读数)。 +**共享池账本(v0.13 P1 共享池单点;v0.14 令牌库换代)**:商店货架与提升三选一是**同一个池**——货架候选 = 池∖{GP 袋}(用钱买钱恒负交易)= {攻击, 生命上限, 护甲, 行动点} 四种(魔法道具件已随同调批出池);**库存 = 剩余令牌数**(TP0——「池消耗即上限」,种类次数上限退役,与奖励兑付摘牌同一令牌库承载,全冒险一次性跨关卡不补给):商店买一件,奖励面就少一枚;「无限购买」收敛为**钱尽或池空为止**(池空=硬闸、A 链爬价=软闸),空货架合法态。货架 = **令牌级抽签**(同 kind 去重每 kind 至多一卡,价格公式 tier-blind 维持批②裁定,购买消费在架令牌本体 kind+tier)。两渠道不得各自私记库存——消费走同一入口写同一令牌库(实现=AdventureRunner._bonus_deck 单点,冒险结构 owner;kind 维度派生计数 _bonus_taken 承载断点附随件与读数)。**货架独立刷新(v0.15 MC-1 2026-10-04——P-C2+Q2 裁)**:进店免费刷新次数(基础 0,天赋/营地侧增益位预留)用尽后 GP 付费刷新(ceil(20×1.8^已付费刷) provisional——调参表旋钮 shop_refresh_*);重抽=同 pool_draw 共享池抽签(shop_draw 恰一新 scope),池空=不可刷新合法态;进店/离开双向清零付费刷计数。 ### 关卡大奖励:通关回血 + 提升三选一(钥石退役出大奖励) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 93be736e..eb1be7f5 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -73,6 +73,7 @@ var _pool_deck: Array = [] # 共享池令牌库共享引用(Ad var _pool_consume := Callable() # 池消费 handler(AdventureRunner._consume_pool_purchase——_apply_bonus_pick 同口写加算区+账本+摘牌) var _shop_shelf: Array = [] # 当前货架(进店抽 3/买后全重抽——令牌字典 {"kind","tier"} 序,同 kind 去重每 kind 至多一卡) var _shop_visit_buys := 0 # 本次进店已购次数(A 链定价输入——离开清零) +var _shop_paid_refreshes := 0 # 本次进店已付费刷新次数(MC-1——GP 刷新价曲线输入,离开清零) var hero_atk_pct := 0 # 测试面板覆盖值(切片专用) var hero_hp_pct := 0 @@ -653,6 +654,7 @@ func continue_after_node() -> bool: if t == "shop": _shop_shelf = [] # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态——迭代 11 第②批 T2) _shop_visit_buys = 0 + _shop_paid_refreshes = 0 # MC-1 return _present_choices() @@ -742,6 +744,7 @@ func set_shop_pool(deck_ref: Array, consume_handler: Callable) -> void: # 进店开店(_enter_node shop 分支 pool 模式):进店购买次计数清零 + 抽 3 落架(恰一 shop_draw scope) func _shop_open_pool() -> void: _shop_visit_buys = 0 + _shop_paid_refreshes = 0 # MC-1——进店付费刷新计数清零 _shop_draw_shelf() @@ -848,6 +851,31 @@ func shop_shelf() -> Array: return out +# 商店独立刷新(MC-1——P-C2 裁 2026-10-04 推翻 v1.5「无独立刷新」):免费次数(天赋增益位,基础 +# knob=0——Q2 裁「基础免费 0 次」)用尽后 GP 付费刷新;刷新=货架整架重抽(shop_draw 恰一新 scope, +# 与进店/买后重抽同签——重抽同源共享池);池空/非池模式=不可刷新合法态。 +func shop_refresh_free_left() -> int: + return maxi(0, int(db.adventure.shop_refresh_free)) + + +func shop_refresh_cost() -> int: + return int(ceil(float(db.adventure.shop_refresh_cost_base) + * pow(db.adventure.shop_refresh_cost_curve, float(_shop_paid_refreshes)))) + + +func shop_refresh() -> bool: + if not _shop_pool_mode or _shop_shelf.is_empty(): + return false + if shop_refresh_free_left() <= 0: + var cost := shop_refresh_cost() + if gp < cost: + return false + gp -= cost + _shop_paid_refreshes += 1 + _shop_draw_shelf() + return true + + # 购买(pool 模式唯一购买口):在架(kind→在架令牌)/余额前置校验零副作用 → handler 消费在架 # 令牌本体(kind+tier——_apply_bonus_pick 同口写加算区+账本+摘牌,跨渠道共库)→ 出账 → 进店购买 # 次计数 → wallet 事件 → 三件全重抽(未买不保留——重抽绑购买,无独立刷新动作) diff --git a/game/data/schemas/adventure_table.gd b/game/data/schemas/adventure_table.gd index 634f9546..82adf06a 100644 --- a/game/data/schemas/adventure_table.gd +++ b/game/data/schemas/adventure_table.gd @@ -31,6 +31,9 @@ const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3} # —— 三选一候选数(2026-10-02 修复批 T2 魔法数结构化)—— @export var bonus_candidate_count: int = 3 # [1,6] knob——令牌洗牌不放回抽取张数上界 +@export var shop_refresh_free: int = 0 # [0,9] knob(MC-1 provisional——进店免费刷新次数;基础 0=Q2 裁,天赋增益位预留) +@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 档位构成)—— @export var token_attack_common: int = 18 # [0,99] diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 1a47bbda..da7e89ad 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -153,6 +153,7 @@ func on_shop_buy(product_id: String) -> void: toast.show_toast("金币不足——买不起这件商品", "warn") return shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) + shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) _refresh_presentations.call() return if not runner.buy_product(product_id): @@ -164,6 +165,19 @@ func on_shop_buy(product_id: String) -> void: _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(): + return + if not runner.shop_refresh(): + toast.show_toast("金币不足——刷新失败", "warn") + return + shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) + shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) + _refresh_presentations.call() + + func on_shop_leave() -> void: _close_modal.call(shop_overlay) var runner = _runner_get.call() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 5c04377c..feba9646 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -289,6 +289,7 @@ func _build_ui() -> void: board_zone.toast, _start_overlay, logger, func() -> Variant: return runner, _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button) shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy) + shop_overlay.refresh_requested.connect(_camp_pres.on_shop_refresh) # MC-1 shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave) camp_overlay.buy_requested.connect(_camp_pres.on_camp_buy) camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle) @@ -520,6 +521,7 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: _open_modal(shop_overlay) if runner.is_shop_pool(): shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) + shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) # MC-1 else: shop_overlay.open_shop(runner.shop_products(), int(runner.gp)) log_view.append_bb(Narrator.shop_line(int(runner.gp))) diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 84873258..69be6d6f 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -18,7 +18,8 @@ const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": const KIND_LABELS := {"attack": "攻击", "max_hp": "生命上限", "armor": "护甲", "ap": "行动点"} # 货架件值文案轴(共享池 kind 词表——迭代 11 第②批) var _gp_label: Label -var _refresh_btn: Button # M6 刷新呈现位(禁用占位——机制由 MC-1 核心批接线) +var _refresh_btn: Button # M6 呈现位——MC-1 起由 set_refresh_display 投喂启用 +signal refresh_requested # MC-1:刷新意图(pool 面;组合根接 presenter.on_shop_refresh) 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 面卡片(买后整架重建=重抽反馈) @@ -40,8 +41,8 @@ func _init() -> void: _refresh_btn = UiKit.button("") _refresh_btn.icon = UiKit.load_tex(UiKit.TEX_ICON_REFRESH) _refresh_btn.custom_minimum_size = Vector2(44, 44) - _refresh_btn.tooltip_text = "刷新货架(机制接入中——商店刷新核心批 MC-1)" - _refresh_btn.disabled = true + _refresh_btn.disabled = true # MC-1 投喂后启用(set_refresh_display) + _refresh_btn.pressed.connect(_emit_refresh_requested) top.add_child(_refresh_btn) var stock_badge := UiKit.label("0", 12, true, UiKit.INK_ON_GOLD) stock_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -154,6 +155,21 @@ func refresh_shelf_state(gp: int) -> void: for card in _shelf_rows: card.btn.disabled = gp < int(card.price) card.card.modulate = Color(1, 1, 1, 1) if gp >= int(card.price) else Color(1, 1, 1, 0.55) + if _refresh_btn != null and not _refresh_btn.disabled: + _refresh_btn.disabled = gp < _refresh_gp_cost + + +# MC-1 刷新呈现投喂(free_left=天赋位免费剩余[基础 0]/gp_cost=本次 GP 刷新价/can_afford=余额可付) +var _refresh_gp_cost := 0 + + +func set_refresh_display(free_left: int, gp_cost: int, can_afford: bool) -> void: + _refresh_gp_cost = gp_cost + for lab in _refresh_btn.get_children(): + if lab is Label: + (lab as Label).text = str(free_left) + _refresh_btn.tooltip_text = "刷新货架——换一批商品\n免费 %d 次(天赋增益位,基础 0)\nGP 刷新 %d 金币(provisional)\n池源=共享池,与奖励三选一同源" % [free_left, gp_cost] + _refresh_btn.disabled = not can_afford and free_left <= 0 # 货架卡(bcard 竖卡:类型色带+图标+名+件值描述+价签+购买钮——M6 同构卡;demo ⑤) @@ -181,3 +197,7 @@ func _build_shelf_card(holder: Control, c: Dictionary) -> Dictionary: func _emit_shelf_buy(kind: String) -> void: buy_requested.emit(kind) + + +func _emit_refresh_requested() -> void: + refresh_requested.emit() diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index eaed7315..a8b8f687 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -3729,6 +3729,17 @@ func _smoke_shop_pool() -> void: 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()) + # 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() + check(cost0 == int(_db.adventure.shop_refresh_cost_base), "刷新首价=基价(%d)" % cost0) + check(r8.shop_refresh_free_left() == 0, "基础免费 0 次(Q2——天赋增益位后续添加)") + var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true) + check(r8.shop_refresh(), "GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0]) + check(int(r8.gp) == gp0 - cost0, "GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)]) + check(int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.adventure.shop_refresh_cost_curve))), "刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost())) var same_shelf := first_shelf_a.size() == first_shelf_b.size() if same_shelf: for i in first_shelf_a.size():