迭代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
@@ -214,9 +214,9 @@
|
||||
|
||||
**现状**:现行商店(2026-09-20 GP+商店房批落地)= 固定品类陈列、可重复购买、无刷新(09-20 裁决「三件全陈列、无刷新、无三选一」,底稿 git 提交 9ac1540 可考(已按生命周期条款退役))。**2026-10-02 用户裁决取代 09-20 无刷新裁决**——商店与三选一奖励同构成同一「三选一」交互:①进店从随机池抽 3 展示;②买 1 → **三个全重抽**(未买两件不保留);③无独立刷新动作(重抽绑定购买,每次「钓鱼」=真实购买一次);④价格 = 基价 × b^(关序−1) × A^(本次进店已购次数)——A=店内递增(方向 1.5-2)≫ b=跨关递增(方向 1.1),**具体系数归 game-balancer 研究**(对照 roguelike 商店定价惯例+本作 GP 供给面)出提案值后用户拍板,provisional 归调参批;⑤商店池维持**纯数值道具**(flat 攻/血/甲族)——机制件永不入店(2026-09-22 同调裁决 1「局内只许商店式数值道具」锁死,不随本裁松动);⑥无限购买加深 GP 沉淀,与关间大奖励/回复供给的对账窗口随批重开。三选一奖励本身本质不变(一次免费「购买」后结束);其刷新增强(X/Y 双源)已随同调实施批任务书 v1.1 落文([任务书-魔法道具同调实施批-2026-10-02.md](../research/任务书-魔法道具同调实施批-2026-10-02.md) §五 P1)。
|
||||
|
||||
**归属**:GP 商店房重构批(主责:商店 UI+购买/重抽逻辑+定价表+读数复测)+ 元进度/关卡节点地图/难度曲线与Boss 随批连带;A/b 系数研究归 game-balancer 工作流随批执行。
|
||||
**归属**:~~GP 商店房重构批(主责:商店 UI+购买/重抽逻辑+定价表+读数复测)+ 元进度/关卡节点地图/难度曲线与Boss 随批连带;A/b 系数研究归 game-balancer 工作流随批执行~~(已落籍,见状态)
|
||||
|
||||
**状态**:已拍板待立项(2026-10-02;排期建议=Alpha 读数后 M-4 迭代 11 评估窗口,与同调实施批/反制批并列排序——动奖励经济必扰通关率读数,迭代 10 刚锚 80000);**裁决面全闭·任务书 v1.3 定稿**(2026-10-02,[任务书-GP商店房重构-三选一同构化-2026-10-02.md](../research/任务书-GP商店房重构-三选一同构化-2026-10-02.md) §五)——P1 改裁**共享池**:商店池=奖励池同一池(六种现行池,跨渠道消耗,库存=上限吸收「达上限剔牌库」语义),魔法道具件/GP 袋对店面切边(三轴 doctrine/负交易,翻案须用户明示);P2 甲货架+账本入断点/P3 甲程序占位/P4 甲 ceil/P5 甲随批 game-balancer;**子项双甲已裁**:P1-a 库存=现行上限口径(属性 3/AP 2)/P1-b 全冒险一次性(L1 买光则后关属性件枯竭=池空硬闸既定后果,用户知情)——**开工=用户发令;已编入迭代 11、序位第二(2026-10-03 用户裁)——随同调批后落,共享池建在 aux 卡退役后的终局池上,通关率读数只锚一次**
|
||||
**状态**:**已落籍(2026-10-03 迭代 11 第②批开发批收官,worktree m4-iter11b-shop-20261003/合并落 master——出口人闸后移迭代 12 随第①批一并)**:P1 共享池账本落地(货架候选=池∖{GP 袋}∖{机制件}=四种,库存=上限即库存全冒险一次性,两渠道同一账本 `_bonus_taken` 单点,池空硬闸+空架合法态);shop_draw 扩签(每次抽 3 恰一 scope);价格=ceil(基价×b^(关序−1)×A^(进店已购次数)),**A=1.8/b=1.1 provisional 投值**(原 P5 甲「game-balancer 研究→用户拍板」依 2026-10-03 零人工令改 provisional+后置追认——追认单随迭代 11 出口包,研究复核归调参批);P2 甲断点扩键 shop_shelf/shop_visit_buys(save v4 域内,货架续玩=确定性重放承载);P3 甲货架三选一同构程序占位(UiKit);MC off 对拍 off_n60 逐位(legacy 对照承载)+on 新锚 N=60 双策略(终局池读数只测不判,锚留盘 sim-data);连带修订=冒险结构 v0.13(共享池账本段+定价公式换代)/关卡节点地图 v1.5/难度曲线与Boss v0.22/node-reward v2.3/registry(shop_table 扩列+shop_draw)。遗留=shop=on 带平移判读+换算率登记+UI 视觉终稿归调参批/表现批;off 锚换代时 products 行族+legacy 模式随行退役。
|
||||
|
||||
### 关卡节点地图
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
| Boss | 恒为图终点;行进至此开 Boss 战;胜利即关卡收束点 |
|
||||
| 奖励 | 行进至此触发节点奖励(兑付见下节);反馈呈现后回到选路 |
|
||||
| 恢复 | 行进至此向冒险结构发治疗封顶回报;反馈呈现后回到选路 |
|
||||
| 商店 | 行进至此打开商店覆盖层,用 GP(显示名「金币」,战斗掉落的冒险内货币)购买属性提升;离开后兑付确认回选路;不携带战斗行 |
|
||||
| 商店 | 行进至此打开商店覆盖层,用 GP(显示名「金币」,战斗掉落的冒险内货币)购买属性提升;离开后兑付确认回选路;不携带战斗行。**v1.5 兑付换代(pool 模式现行)**:进店从共享池抽 3 展示、买 1 后三件全重抽(未买不保留、无独立刷新动作——重抽绑购买)、货架买空=空架合法态;账本与定价归[冒险结构](冒险结构.md)共享池账本节 |
|
||||
|
||||
商店房的编排规则(2026-09-20 用户拍板):每关**恰 1 个**商店房,位置 = **关底 Boss 前一位**——哈迪斯式的"决战前最后一个补给点":买到的属性还有整段战斗可消费,错过的成长也没有第二家店可补。Boss 前一位同时是**必经位**:图拓扑收束使所有入口→Boss 的路径都穿过它,成长保底成为图结构事实而非玩家自觉。这条位置约束不由加载校验承载——图合法性六检零新增,商店房的"每关恰 1 + 必经"由冒烟断言把关。
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
呈现侧口径(2026-09-17 用户拍板):节点图的画面表达走**方块地牢观感**——节点画成地牢房间的方块、连线画成通道——但逻辑拓扑就是本文的节点图,不产生任何额外选路或探索语义。关卡画面本体即节点链进度的呈现载体(选路覆盖层的常驻形态,节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 封印黑描边(旗朱→封印紫→封印黑 #000000,2026-09-28 用户拍板现行)),不另设进度条组件;视觉规格归 [HUD 框架](../表现与支撑/HUD框架.md)。
|
||||
|
||||
**商店覆盖层呈现**(呈现 spec:[design/gdd/ux/shop.md](../../ux/shop.md) v1.0,2026-09-30):兑付 = 打开商店覆盖层(模态)——三商品卡全陈列、GP 余额读数、购买/离开;离开后兑付确认回选路。本节锁编排面(何时开/何时关/回选路),呈现细节归 spec;定价数值归[难度曲线与Boss](难度曲线与Boss.md)(shop.tres)。
|
||||
**商店覆盖层呈现**(呈现 spec:[design/gdd/ux/shop.md](../../ux/shop.md) v1.0,2026-09-30;**v1.5 注记**——pool 模式货架面=共享池三选一同构卡〔进店抽 3/买 1 全重抽/库存余量就近/空架售罄态〕,与三选一奖励同一交互语言;legacy 固定三件面随对照模式保留):兑付 = 打开商店覆盖层(模态)——货架卡陈列、GP 余额读数、购买/离开;离开后兑付确认回选路。本节锁编排面(何时开/何时关/回选路),呈现细节归 spec;定价数值归[难度曲线与Boss](难度曲线与Boss.md)(shop.tres)。
|
||||
|
||||
### 行进与选路:兑付后确认,选路就是行进
|
||||
|
||||
@@ -99,7 +99,7 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
|
||||
| 战斗 / Boss | 组装定位信息(关卡序号 + 节点编号),移交[战斗结算](../战斗循环/战斗结算.md)开战;战斗结果(胜负与战后生命)经战斗结果上报回流冒险结构(契约语义归[冒险结构](冒险结构.md),本文不复述) |
|
||||
| 恢复 | 向冒险结构发治疗封顶回报(载荷 = 该关恢复比例,读本关配置行);按比例回血并封顶的执行归冒险结构 |
|
||||
| 奖励 | 触发节点奖励兑付:提升三选一(与通关三选一共享同池——六种加权抽 3 点选一件,同次数上限;见下节) |
|
||||
| 商店 | 打开商店覆盖层(商品陈列与定价读商店表);购买即时生效、写统一冒险加算区(与关卡大奖励同一本账——账本语义归[冒险结构](冒险结构.md),本文不复述);离开覆盖层即兑付确认。商店房不携带战斗行:零敌人、零棋盘、零战斗初始化 |
|
||||
| 商店 | 打开商店覆盖层(pool 模式:进店抽 3〔shop_draw 封闭流恰一 scope〕+买 1 全重抽;legacy 对照模式:商品陈列读商店表);购买即时生效、经共享池账本写统一冒险加算区(与关卡大奖励同一本账——账本语义归[冒险结构](冒险结构.md),本文不复述);离开覆盖层即兑付确认。商店房不携带战斗行:零敌人、零棋盘、零战斗初始化 |
|
||||
|
||||
**遭遇构成的编排面**——本文对"放什么敌人"的全部职责只有三条:
|
||||
|
||||
@@ -190,3 +190,4 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
|
||||
| v1.2 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `../../../dev/architecture/adr-0006-board-node2d-rendering.md` 与 `../../../dev/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 行进循环改停点前移口径——关卡开始即呈现节点图选路停点(关卡画面 = 选路覆盖层常驻化,入口节点不自动进入,第一次选路就是"出发"),选路确认进节点,兑付确认后返回关卡画面再选路(战后回图);兑付后确认、零自动推进两条纪律语义不变,只改承载位;② 呈现侧口径补进度承载条款——关卡画面本体即节点链进度呈现载体(节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件(战斗屏 A 区进度条随批退役);③ P0 闭环两条、依赖表 HUD 框架行、验收标准「行进自主」条同步。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行;③ 关卡画面进度呈现由选路覆盖层节点态承载(登记面见 HUD框架 v0.8) |
|
||||
| v1.3 | 2026-10-01 | **图合法性第七检(T17 目检用户拍板「节点出度最大为 2」)**:`validate` 新增 `fanout_violation`(单节点出边 >2 加载致命);规则连带层宽上限 4(原 5 与出度约束不相容,废止)——生成侧规则与模板池重建归[关卡节点图生成](关卡节点图生成.md) v0.6 同批 |
|
||||
| v1.4 | 2026-10-03 | **奖励节点道具渠道退役注记(迭代 11 第①批同调实施批——与 node-reward.md v2.2 / registry 同批)**:奖励节点的魔法道具件渠道随停点渠道退役拆除(BONUS_IDS 词表除名 aux_item,池五种;三选一刷新件面新增见 node-reward.md v2.2)——奖励节点兑付面=提升三选一(属性/GP/AP 五种)+刷新,零结构改动(节点类型词表/图合法性六检零新增) |
|
||||
| v1.5 | 2026-10-03 | **商店房兑付换代注记(迭代 11 第②批 GP 商店房重构批——任务书 v1.3 全裁;冒险结构 v0.13 / registry / 难度曲线与Boss 同批)**:商店节点类型词表与编排规则零改动(每关恰 1/Boss 前一位必经位/图合法性六检零新增);兑付面换代 = pool 模式进店抽 3(shop_draw 封闭流)+买 1 全重抽+重抽绑购买+空架合法态,货架候选与库存账本归冒险结构共享池账本节;覆盖层呈现面=货架三选一同构卡(legacy 固定三件面随对照模式保留注记) |
|
||||
@@ -142,14 +142,16 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
|
||||
| 收口 | 方向 | 规则 |
|
||||
|---|---|---|
|
||||
| 战斗完成 | 入 | 每完成一场战斗统一到账一次:金额 = 掉落掷定 + 关级掉落加值 + 本战战斗内缓冲,随战斗结束信号携带的 gp_dropped 一笔入账 |
|
||||
| 商店购买 | 出 | 商店房购买按现行价扣减钱包,余额不足拒绝;价格 = 基价 + 关级价格加成 + 步长 × 已购档数 |
|
||||
| 商店购买 | 出 | 商店房购买按现行价扣减钱包,余额不足拒绝。价格公式(**v0.13 换代——pool 模式现行**):ceil(基价 × b^(关序−1) × A^(本次进店已购次数)),A=店内递增/b=跨关递增表因子(provisional 1.8/1.1),ceil 向上单一取整(玩家不利向,杜绝「差 1 GP 反复钓」套利角);legacy 固定三件模式(shop=off 对照承载)沿用旧式 = 基价 + 关级价格加成 + 步长 × 已购档数 |
|
||||
| 冒险收束 | 清零 | 完成 / 失败 / 放弃三种结局都清零;重开后钱包从零开始 |
|
||||
|
||||
**战斗内缓冲隔离**:战斗进行中从格面兑付拿到的 GP(金币袋正向、破财负向——字典归[地图战斗资源](../战斗循环/地图战斗资源.md))不直接进钱包,而是记入战斗域的中间态缓冲;战斗完成时缓冲随掉落掷定与关级加值一并折进 gp_dropped 统一到账。钱包只在战斗边界变动,战斗域自身零钱包状态——这条隔离让战斗结算保持"折算归我、账本归冒险结构"的既有边界。
|
||||
|
||||
**统一冒险加算区**:商店购买的属性提升与提升三选一的属性件记**同一本账**——加算区四轴 {攻击, 生命上限, 护甲等级, 行动点},入战参数 = 基础表值 + 加算区累加,经战斗开始入参的覆盖值通道生效(与生命账本的上参同一条通道)。购买生命上限时,当前生命与上限同步 +Δ(生命账本的连带写入口)。属性轴累加源 = 商店购买与三选一属性件两类;行动点轴累加源 = 三选一 AP 件一类(每场战斗开始的首轮行动点基线 +1,语义细节归[行动经济](../排雷循环/行动经济.md)跨冒险加成轴节;全冒险获取次数上限最紧,provisional 2 次)。
|
||||
**统一冒险加算区**:商店购买的属性提升与提升三选一的属性件记**同一本账**——加算区四轴 {攻击, 生命上限, 护甲等级, 行动点},入战参数 = 基础表值 + 加算区累加,经战斗开始入参的覆盖值通道生效(与生命账本的上参同一条通道)。购买生命上限时:pool 模式(v0.13——货架件=奖励件本体)只加上限不回当前生命(与三选一件同语义);legacy 模式当前生命与上限同步 +Δ(生命账本的连带写入口)。属性轴累加源 = 商店购买与三选一属性件两类;行动点轴累加源 = 三选一 AP 件与商店 AP 件两类(每场战斗开始的首轮行动点基线 +1,语义细节归[行动经济](../排雷循环/行动经济.md)跨冒险加成轴节;全冒险获取次数上限最紧,provisional 2 次——pool 模式下商店购买与三选一共用该库存,见下)。
|
||||
|
||||
掉落掷定消费 gp_drop 封闭随机流(主种子派生、可回放)。掉落与价格的现行行值读商店表(registry 为准),数值所有权归[难度曲线与Boss](难度曲线与Boss.md)曲线调参批——本批骨架先行,全部数值 provisional。
|
||||
掉落掷定消费 gp_drop 封闭随机流(主种子派生、可回放)。**商店货架抽签消费 shop_draw 封闭随机流**(v0.13 扩签——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰)。掉落与价格的现行行值读商店表(registry 为准),数值所有权归[难度曲线与Boss](难度曲线与Boss.md)曲线调参批——本批骨架先行,全部数值 provisional。
|
||||
|
||||
**共享池账本(v0.13——P1 共享池单点)**:商店货架与提升三选一是**同一个池**——货架候选 = 池∖{GP 袋}(用钱买钱恒负交易)∖{魔法道具件}(三轴 doctrine,机制件永不入店)= {攻击, 生命上限, 护甲, 行动点} 四种;库存 = 上限即库存(属性件各 3、AP 件 2——与「达上限剔牌库」同一账本承载,全冒险一次性跨关卡不补给):商店买一件,奖励面就少一件;「无限购买」收敛为**钱尽或池空为止**(池空=硬闸、A 链爬价=软闸),空货架合法态。两渠道不得各自私记库存——消费走同一入口写同一账本(实现=AdventureRunner._bonus_taken 单点,冒险结构 owner)。
|
||||
|
||||
### 关卡大奖励:通关回血 + 提升三选一(钥石退役出大奖励)
|
||||
|
||||
@@ -199,7 +201,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
|
||||
冒险进行中随时关游戏走人(**中断**),下次启动可从最近停点继续——承载 = 存档 `breakpoint` 域(格式归[存档序列化](../表现与支撑/存档序列化.md),内容口径归本文)。
|
||||
|
||||
- **停点粒度**:快照只挂选路停点(入口呈现/战斗胜利后/兑付确认后的选路态)——这是唯一可恢复停点形态。战斗进行中不写快照:中断恢复后**当场战斗从头打**,棋盘中局不入档。
|
||||
- **七字段拍板面**:关卡序号 / 节点位 / 生命 / 本关加成区 / GP 钱包 / 商店已购档 / 已抽模板。附随件(续玩正确性必需,随批登记):主种子 + 取数计数器(rng 快照契约回填——续玩从断点号位精确续接原随机时间线,已抽模板零重抽)、钥石累计(Boss 掉落逐杀入账的中途值,终局摘要不失真)、已到节点序(关卡画面图条重建)、魔法道具库存(奖励节点已选件防静默丢失)、魔法道具库存(奖励节点已选件防静默丢失)、**奖励选择态**(三选一停点中断:已选件 / 同调位替换在途——续玩已选不丢失;候选一致性=快照现状非强求 feature,2026-10-01 用户裁决)、**件获取次数计数**(攻击/生命上限/护甲/行动点四件各自已获取次数——次数上限判定不可由加算区累加值反推,牌库剔除态依赖它,迭代 10 扩)。
|
||||
- **七字段拍板面**:关卡序号 / 节点位 / 生命 / 本关加成区 / GP 钱包 / 商店已购档 / 已抽模板。附随件(续玩正确性必需,随批登记):主种子 + 取数计数器(rng 快照契约回填——续玩从断点号位精确续接原随机时间线,已抽模板零重抽)、钥石累计(Boss 掉落逐杀入账的中途值,终局摘要不失真)、已到节点序(关卡画面图条重建)、魔法道具库存(奖励节点已选件防静默丢失)、魔法道具库存(奖励节点已选件防静默丢失)、**奖励选择态**(三选一停点中断:已选件 / 同调位替换在途——续玩已选不丢失;候选一致性=快照现状非强求 feature,2026-10-01 用户裁决)、**件获取次数计数**(攻击/生命上限/护甲/行动点四件各自已获取次数——次数上限判定不可由加算区累加值反推,牌库剔除态依赖它,迭代 10 扩)、**商店货架态**(当前货架三件 + 本次进店已购次数——P2 甲扩列随迭代 11 第②批;离开店=空架合法态,货架续玩由确定性重放承载:重进店同种子同货架逐位)。
|
||||
- **中断与退出两口径分离**(2026-09-29 用户裁决):中断(关游戏)= 留断点可续;**退出冒险**(放弃流程)= 失败同型结算 + **清除断点**——退出后「继续上局」不再出现。
|
||||
- **续玩**:自断点重建到最近停点(账面逐字段回填 + 随机流回填),停点处玩家重新选路。三结局(通关/失败/放弃)一律清断点——断点只活在一个进行中的冒险上。
|
||||
- 与测量面的关系:断点面只在注入存档承载的正式冒险路径生效;模拟/冒烟不注入 = 整面静默,MC 对拍零扰动。
|
||||
@@ -278,7 +280,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
|
||||
| 通关回血比例 | int [0,100],冒险表单行全局 | adventure_table clear_heal_pct(provisional 20) | 通关部分回血回复生效上限的百分比;封顶上限不回满 |
|
||||
| 同调位过渡键 | int [0,9],冒险表单行全局 | adventure_table transitional_sync_slots(provisional 1) | 道具持有上限过渡读数源;正主 = 同调位里程碑谓词表(同调实施批,切换义务在册) |
|
||||
| Boss 钥石掉落 | int [0,999],meta 表逐关行 | meta.tres boss_keystone 首杀 3 / 复杀 2(provisional) | 逐杀实时入账;精英怪掉 1 预留;消费方归元进度 |
|
||||
| GP 经济参数 | 商店表逐行 | 见 shop.tres gp_drops / level_gp_bonus / products / price_level_bonus(provisional,registry 为准) | 掉落均值与区间(普通/精英/Boss)、关级掉落加值、商品首价与步长、关级价格加成;公式语义归本文,数值所有权归难度曲线与Boss曲线调参批 |
|
||||
| GP 经济参数 | 商店表逐行 | 见 shop.tres gp_drops / level_gp_bonus / pool_shelf / price_a / price_b(provisional,registry 为准;legacy products / price_level_bonus 随对照模式保留) | 掉落均值与区间(普通/精英/Boss)、关级掉落加值、货架基价、A/b 价格因子;公式语义归本文,数值所有权归难度曲线与Boss曲线调参批 |
|
||||
|
||||
结构冻结(非调参项):序列线性无跳关、关底 Boss 恒为末节点、倒下即冒险终、大奖励构成 = 通关部分回血 + 提升三选一(钥石退役出大奖励,改 Boss/精英击杀掉落——2026-09-30 拍板重构)、一冒险一主种子、收束点先清库后宣布的次序。
|
||||
|
||||
@@ -326,3 +328,4 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
|
||||
| v0.10 | 2026-09-27 | **统一随机流批口径对齐(ADR-0007 裁丙——rng 架构批随批)**:主种子节「可复现域 = 主种子 + 各随机流计数器」改「主种子 + 全局取数计数器」(语义权威指数据配置);本文规则面零改动——主种子恰一次注入、重开即全新一轮、摘要五件事均不变。代码实证与七消费点迁移见数据配置 v0.5 |
|
||||
| v0.11 | 2026-09-29 | **冒险断点批(迭代 8 卡①③——中断可续玩/退出即结算两口径分离)**:核心规则新增「冒险断点」节(停点粒度/七字段拍板面+附随件/中断退出两口径/续玩语义/测量面静默);依赖表存档序列化行补断点域 owner 登记;验收标准 GP 不留痕条补断点域豁免口径。实现落点 = AdventureRunner 快照/清除/续玩 + SaveManager breakpoint 域 + rng restore_adventure |
|
||||
| v0.12 | 2026-10-01 | **关卡大奖励重构批(迭代 10——用户 2026-09-30 拍板案丙 + P-A~P-E 全裁毕;冒险表扩列 / meta.tres 重锚 / registry / 行动经济 / 元进度 / 01-核心循环 / 03-内容规模 / 难度曲线与Boss / 关卡节点地图 / 06 同批落文)**:①大奖励「固定三件套」退役改两层兑付——非末关通关 = 部分回血(clear_heal_pct 20 provisional)+ 提升三选一(池六种:攻击/生命上限/护甲/GP 袋/魔法道具/行动点件;加权洗牌不放回 E-S weight-key 消费 reward_pick 封闭流;属性件上限各 3、AP 件上限 2,达上限先剔牌库);②统一冒险加算区三轴扩四轴 +行动点(AP 件 = 每场战斗开始首轮基线 +1);③钥石退役出大奖励改 Boss/精英击杀逐杀掉落(boss_keystone 重锚首杀 3/复杀 2,末关照常入账,精英掉 1 预留);④魔法道具件受同调位约束(过渡键 transitional_sync_slots=1 provisional,正主 = 同调实施批谓词表;超位替换交互 N+1 选 N、战斗内渠道沉没不入);⑤断点附随件扩奖励选择态 + 件获取次数计数;⑥选择停点零自动推进/可中断。渲染层验证冻结(用户 2026-10-01 提前开工拍板——不合入 master、目检待批),目检清单随批交付 |
|
||||
| v0.13 | 2026-10-03 | **GP 商店房重构批(迭代 11 第②批——任务书 v1.3 全裁 P1 共享池/P1-a/P1-b 双甲/P2 甲/P3 甲/P4 ceil/P5 甲;06「GP 商店房重构」条 2026-10-02 用户裁决取代 09-20 无刷新裁决)**:①GP 冒险账本节商店购买行换代——价格公式 = ceil(基价 × b^(关序−1) × A^(本次进店已购次数))(A/b provisional 1.8/1.1;legacy 旧式随对照模式保留注记);②新增「共享池账本」段——商店货架 = 池∖{GP 袋}∖{魔法道具件} 四种,库存=上限即库存全冒险一次性,两渠道同一账本(买一件=奖励面少一件),池空硬闸+空架合法态;③shop_draw 扩签登记;④统一冒险加算区段——pool 模式买上限不回血(池件=奖励件本体)、AP 件累加源并商店;⑤断点附随件扩商店货架态(shop_shelf/shop_visit_buys,P2 甲;确定性重放承载注记);⑥调参表 GP 经济参数行随表换代。实现=AdventureRunner/LevelRunner 池模式(缺省 legacy 对照承载,真实入口投 pool) |
|
||||
@@ -156,21 +156,23 @@ GP(显示名「金币」)与商店房随 2026-09-20 GP+商店房批落地,
|
||||
|
||||
```
|
||||
每战 GP 掉落 = 掷定(均值 ± 方差,按战斗节点身份查 gp_drops 行) + 关级掉落加值 + 战斗内缓冲
|
||||
商品价格 = 基价 + 关级价格加成 + 步长 × 已购档数
|
||||
商品价格(pool 现行,v0.22) = ceil(基价 × b^(关序−1) × A^(本次进店已购次数)) —— A=1.8 / b=1.1 provisional 投值
|
||||
商品价格(legacy 对照) = 基价 + 关级价格加成 + 步长 × 已购档数
|
||||
```
|
||||
|
||||
定价锚定口径:商品价格按"正常运气可购档数 / 运气极好可购档数"两读数的数学期望制定(期望账进商店表批注)——正常运气的玩家买得到成长保底档,运气极好也不至于全表买穿,两个失速态都不出现。
|
||||
定价锚定口径:商品价格按"正常运气可购档数 / 运气极好可购档数"两读数的数学期望制定(期望账进商店表批注)——正常运气的玩家买得到成长保底档,运气极好也不至于全表买穿,两个失速态都不出现。**v0.22 换代注记(迭代 11 第②批)**:A=店内递增 ≫ b=跨关递增的方向裁决随 06「GP 商店房重构」条落定;A/b 具体系数原裁决=game-balancer 研究后用户拍板(P5 甲),迭代 11 第②批依零人工令改 provisional 投值 1.8/1.1+后置追认(追认单随迭代 11 出口包,研究归调参批复核);ceil 向上单一取整(玩家不利向)。
|
||||
|
||||
| 杠杆位 | 口径 | 说明 |
|
||||
|---|---|---|
|
||||
| GP 掉落均值与方差 | 普通 / 精英(预留行)/ Boss 三行掷定区间 | 掉落量级与战斗节奏同族;格面 gp 型(金币袋,低概率)是"惊喜意外获取"的补充供给 |
|
||||
| 关级掉落加值 | 逐关一行 | 关间供给抬升的半边 |
|
||||
| 关级价格加成 | 逐关一行 | 关间价格上涨对应关间掉落上升的另一半——两轴同向联动的对子 |
|
||||
| 商品首价与步长 | 商品三行(攻击 / 生命上限 / 护甲等级 flat 档) | 重复购买逐档递增,堆单一属性自然衰减 |
|
||||
| 关级价格加成 | 逐关一行 | 关间价格上涨对应关间掉落上升的另一半——两轴同向联动的对子(legacy 模式轴;pool 模式由 b 因子承载同一杠杆位) |
|
||||
| 货架基价 | pool_shelf 四行(攻击 / 生命上限 / 护甲 / 行动点件) | **v0.22 换代**:重复购买爬价由 A 链承载(同店买越多越贵)+ b 跨关递增(后关整体更贵),步长逐档递增语义退役随 legacy 模式保留 |
|
||||
| 价格因子 A / b | 表因子 price_a / price_b | A=店内递增(进店已购次数幂)、b=跨关递增(关序−1 幂);provisional 1.8/1.1,方向裁决 A ≫ b |
|
||||
|
||||
公式语义与钱包账本归[冒险结构](冒险结构.md)(GP 冒险账本节),商店房编排归[关卡节点地图](关卡节点地图.md);行值读 `data/tables/shop.tres`(registry 为准),全部 provisional 归本文曲线调参批。
|
||||
|
||||
**分层验收 defer(2026-09-20 用户拍板成文)**:按"商店存在与否"分层的通关率验收——MC 双格(shop=off 现行基线 / shop=on 购买画像)、shop=on 的带平移量、商店属性提升的换算率登记(换算率强条目纪律同类)——**全部随曲线调参批执行**,与「基线通关率越带」同窗(原「L2 Boss 承伤微越带」「大奖励数值随难度曲线联动」两 06 条目已收口移除——前者 2026-09-28 读数自愈、后者随迭代 10 大奖励退役失义)。执行前 shop=off 续用现行带判读,shop=on 读数只登记不判读、不得用 shop=off 读数外推 shop=on。L3 首遇绿档冒烟断言仍按成长代理档口径,不因商店批改判。
|
||||
**分层验收 defer(2026-09-20 用户拍板成文)**:按"商店存在与否"分层的通关率验收——MC 双格(shop=off 现行基线 / shop=on 购买画像)、shop=on 的带平移量、商店属性提升的换算率登记(换算率强条目纪律同类)——**全部随曲线调参批执行**,与「基线通关率越带」同窗(原「L2 Boss 承伤微越带」「大奖励数值随难度曲线联动」两 06 条目已收口移除——前者 2026-09-28 读数自愈、后者随迭代 10 大奖励退役失义)。执行前 shop=off 续用现行带判读,shop=on 读数只登记不判读、不得用 shop=off 读数外推 shop=on。L3 首遇绿档冒烟断言仍按成长代理档口径,不因商店批改判。**v0.22 注记(迭代 11 第②批)**:shop=off 对照 = legacy 固定三件模式(MC 对拍 off_n60 逐位,迭代 11 第②批 T5);shop=on 首锚 = 共享池货架面 N=60 双策略(`runsum_m4iter11b_t5shopon_n60_20261003.jsonl`,attune=off 同种子配对)——带平移判读仍归调参批,终局池读数(pool_stock)随行只测不判。
|
||||
|
||||
### Boss 设计:食人魔酋长与关底血量锚
|
||||
|
||||
@@ -302,7 +304,7 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
|
||||
| 注入概率初值 / 衰减 | 读表 | 注入机制归[地图战斗资源](../战斗循环/地图战斗资源.md) |
|
||||
| 清障炸药威力区间 | 读表 | 清图槽当场引爆 |
|
||||
| 状态强度 | 读 `status_effects.tres`(掷点轴——2026-09-16 换装,原减伤退役) | 联动评估归本文,禁独立调参 |
|
||||
| GP 掉落与商店价格参数 | 读 `shop.tres`(gp_drops 均值±方差 / level_gp_bonus / products 首价与步长 / price_level_bonus) | GP+商店房批新增;公式语义归[冒险结构](冒险结构.md),数值全部 provisional-pending-曲线调参批,与分层带联动禁独立调参 |
|
||||
| GP 掉落与商店价格参数 | 读 `shop.tres`(gp_drops 均值±方差 / level_gp_bonus / pool_shelf 货架基价四行 / price_a / price_b;legacy products 首价与步长 / price_level_bonus 随对照模式保留) | GP+商店房批新增,pool 换代随迭代 11 第②批;公式语义归[冒险结构](冒险结构.md),数值全部 provisional-pending-曲线调参批(A/b 投值 1.8/1.1 后置追认),与分层带联动禁独立调参 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
@@ -347,3 +349,4 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
|
||||
| v0.19 | 2026-09-23 | **基线通关率带测量口径退役(用户拍板单关直入口口径退役——与单关试玩入口退役同批)**:原带(第 1 关 [35,55] / 第 2 关 [25,45],v0.16 分带批)锚定直入口单关读数——满血零上下文进关,与真实冒险的条件通关率脱节(实证:直入口 L2 普通 5% 与营地检查点 L2 条件 90% 同期并存,曾误导曲线调参)。逐关读数改冒险条件口径(balance_verifier 冒险段派生:通关数/到达数);新带值重锚归曲线调参批,重锚前只测不判;调参表「基线通关率带」行与验收标准通关率带条同步改口径退役注记。行值零改动 |
|
||||
| v0.20 | 2026-09-28 | 判读口径落档+回滚阶梯收编+Boss 锚漂移登记 |
|
||||
| v0.21 | 2026-09-30 | 普通档改带 L1 [35,55]/L2 [10,15]+带表与验收标准回写现行口径 |
|
||||
| v0.22 | 2026-10-03 | **商店定价公式换代(迭代 11 第②批 GP 商店房重构批——任务书 v1.3 P1 共享池/P4 ceil/P5 甲;06 条 2026-10-02 用户裁决)**:商店与 GP 经济杠杆节公式换代 = ceil(基价 × b^(关序−1) × A^(进店已购次数)),A/b provisional 投值 1.8/1.1(原 P5 甲 game-balancer 研究拍板改后置追认——零人工令);杠杆位表换货架基价四行+价格因子 A/b 两行;分层验收 defer 节补 v0.22 注记(shop=off=legacy 对照 off_n60 逐位 / shop=on 首锚 N=60 双策略带平移归调参批/终局池读数只测不判)。行值零结构改动(shop.tres 扩列不退役) |
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
## 口径
|
||||
|
||||
- **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。
|
||||
- **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。**v2.3 起第三渠道并账**:商店货架=同池付费渠道(买一件=奖励面少一件),库存=上限即库存,账本语义与切边注记归[冒险结构](../systems/冒险循环/冒险结构.md)共享池账本节。
|
||||
- ~~道具件入本关库存~~(v2.2 退役——魔法道具局内不掉落机制道具,三轴 doctrine 锁死)。
|
||||
- 「池空直通」合法态随共享池退役:池含无次数上限的 GP 件,牌库恒非空。
|
||||
- 「池空直通」合法态随共享池退役:池含无次数上限的 GP 件,牌库恒非空(GP 袋对商店面切边、奖励面照常——奖励牌库恒非空结构保证不变)。
|
||||
|
||||
## 版本历史
|
||||
|
||||
@@ -31,3 +31,4 @@
|
||||
| v2.1 | 2026-10-01 | **掷定前置(目检反馈批——用户反馈「抽到魔法道具不显示详情不直观」,推荐案获批)**:道具件具体道具随候选抽定(reward_pick 流序内挪,消费数不变),卡片直显「魔法道具 / 道具名 · 效果摘要」;断点停点快照随载 aux_detail(续玩卡片详情不丢);node_bonus_candidates 返回扩 dict {candidates, aux_detail} |
|
||||
| v2.2 | 2026-10-03 | **池种退役+刷新件面(迭代 11 第①批同调实施批 T7/T5b——P1 甲)**:①aux_item 池种退役出三选一牌库(BONUS_IDS 除名+权重/件数/掷定前置字段随批退役)——魔法道具改走营地同调装配面(三轴 doctrine),v2.1 直显/满位替换两面随渠道退役(机械惰性承载零投放);②三选一刷新件面新增(X/Y 双源:刷新件每碰三选一重给优先消耗不结转+营地刷新升级冒险周期池;重抽消费 reward_pick 既有洗牌流池规则全保留;autoplay 默认不刷) |
|
||||
| v2.2 | 2026-10-03 | **三档位令牌池(用户全裁——待 M-4 数值批实施,现行 v2.1 平铺池维持到实施)**:池改固定配额令牌池(普通 70/稀有 25/罕见 5=100 张;洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭);档位构成=普通〔小GP 18/小增伤 18/小护甲 17/小生命 17〕+稀有〔大GP/大增伤/大护甲/大生命/恢复 各 5〕+罕见〔AP 3/道具 2〕;数值对 20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1、道具 1 件;**种类次数上限退役**(池消耗即上限;同调位上限保留);恢复件=新入池种类;MC 策略改判 1 级最稀有/2 级最左(确定性)——设计全稿与执行口径见 [06 待办「奖励池三档位令牌池重设计」条](../06-待办清单.md);卡片呈现面(本 spec 交互节)零改动 |
|
||||
| v2.3 | 2026-10-03 | **商店并账注记(迭代 11 第②批 GP 商店房重构批——P1 共享池)**:口径节「同池同上限」扩第三渠道=商店货架(pool 模式付费渠道,买一件=奖励面少一件;库存=上限即库存全冒险一次性);本 spec 交互面零改动(商店货架面归 shop.md 与 ShopOverlay,非三选一覆盖层);GP 袋对商店面切边(负交易)、奖励面照常(牌库恒非空结构保证不变) |
|
||||
@@ -21,11 +21,11 @@ entities:
|
||||
attributes:
|
||||
kind: autoload
|
||||
stream_architecture: **统一流(ADR-0007 裁丙 2026-09-27——用户两轮复裁终选「统一用一个流」并拍「全面执行」,七流并一+全事件日志)**:唯一主种子 + 单盐派生 + 唯一全局取数计数器;历史注记 = 2026-09-12 encounter 增流〔遭遇随机化批〕/ 2026-09-16 armor_roll〔护甲掷点批〕+ behavior_roll〔敌人技能池批〕/ 2026-09-20 variant_place〔地形变异批〕/ 2026-09-21 gp_drop〔GP+商店房批〕——分盐分流架构随并流批整体退役;combat_var 已于 2026-09-07 full 评审删除(玩法随机不预置基础设施)
|
||||
purpose_tags: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place, gp_drop](封闭词表 = 并流前七流名全保留;扩签需改代码 + 评审;结算表 armor_roll_stream 字段降格为纯标签)
|
||||
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〕+ reward_pick〔迭代 10 奖励重构〕+ shop_draw〔迭代 11 第②批 T1——商店货架抽签,每次抽 3 恰一 scope〕;扩签需改代码 + 评审;结算表 armor_roll_stream 字段降格为纯标签)
|
||||
stream_facade: open_stream(purpose) 取号(purpose 只进事件日志不参与派生;全局计数器 +1 → 单盐派生 → 返回已播种实例 → 落日志);derive_at(scope) 纯函数供测试/预览/日志回放(不推计数器);open_scope 显式位取号门随子流派生向量退役(behavior_roll 逐敌顺序取号替代)
|
||||
event_log: 冒险会话制全事件落盘——user://logs/rng_log_<主种子>.jsonl,会话行 + 逐次取数行(scope / 用途标签 / 派生种子);写盘失败告警降级不致命;号位全序化后不再单独回答「第 N 次某类事件」(路线依赖为已裁决代价),单点定位由日志承载
|
||||
new_adventure: 冒险开始重播种入口——调用权 = AdventureRunner.start_adventure 唯一正式调用方(切片单关测试路径 LevelRunner.start_level 除外;冒险结构实现批 2026-09-21——主种子全冒险恰一次注入,冒险内不重置不逐关播种);并流批加重播种同时开日志会话
|
||||
tag_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(地图战斗资源);encounter=每次战斗实例化(战斗结算 begin_battle 实例化期、扫雷开始前——**实证先于 board_gen 取号**);armor_roll=每次掷点(战斗结算 1d20);behavior_roll=每敌每轮开轮摇号(战斗结算——并流后顺序取号,spawn_index×STRIDE 派生向量退役);variant_place=每次装板(战斗结算 布置段);gp_drop=每场战斗结束掉落掷定(战斗结算)——标签绑定只描述取号时机语义,不再承载派生隔离
|
||||
tag_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(地图战斗资源);encounter=每次战斗实例化(战斗结算 begin_battle 实例化期、扫雷开始前——**实证先于 board_gen 取号**);armor_roll=每次掷点(战斗结算 1d20);behavior_roll=每敌每轮开轮摇号(战斗结算——并流后顺序取号,spawn_index×STRIDE 派生向量退役);variant_place=每次装板(战斗结算 布置段);gp_drop=每场战斗结束掉落掷定(战斗结算);reward_pick=每次三选一抽签恰一 scope(冒险结构——通关/节点两停点+刷新重抽);shop_draw=每次货架抽 3 恰一 scope(LevelRunner pool 模式——进店+每次买后全重抽,shop=off 零消费零扰动)——标签绑定只描述取号时机语义,不再承载派生隔离
|
||||
unseeded_access: 致命中止
|
||||
snapshot_contract: 续玩/复盘快照 = master_seed + 全局取数计数器(消费方 存档序列化;并流前「各流计数器」口径退役——现行存档未持久化计数器,契约面登记变更)
|
||||
referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化, 关卡节点图生成]
|
||||
@@ -595,10 +595,10 @@ entities:
|
||||
type: schema_data
|
||||
owner: design/gdd/systems/冒险循环/冒险结构.md
|
||||
attributes:
|
||||
table: ShopTable(数据配置 承载;**2026-09-20 GP+商店房批新建**——商店房商品与 GP 经济的行集;冒险结构 回填 owner,公式语义归其 GP 冒险账本节,数值所有权归 难度曲线与Boss 曲线调参批)
|
||||
fields: gp_drops 三行(键 = 战斗节点身份 {normal, elite, boss}——普通 均值 3 区间 [2,4] / 精英 均值 5 区间 [4,6] 预留行 / Boss 均值 8 区间 [6,10],掷定 + 均值方差口径)· level_gp_bonus(逐关——L1 0 / L2 1 / L3 2)· products 三行(atk_up 基础攻击 +1 首价 4 / hp_up 生命上限 +3 首价 2 / armor_up 护甲等级 +1 首价 4,步长均 2)· price_level_bonus(逐关——L1 0 / L2 2 / L3 4)
|
||||
formulas: 每战 GP 掉落 = 掷定 + 关级掉落加值 + 战斗内缓冲(战斗完成随 battle_finished.gp_dropped 统一入钱包——冒险结构 GP 冒险账本三收口);商品价格 = 基价 + 关级价格加成 + 步长 × 已购档数;定价锚 = 「正常运气可购档数 / 运气极好可购档数」两读数的数学期望(期望账进表批注)
|
||||
value_status: 全部行值 provisional-pending-曲线调参批(用户 2026-09-20 拍板「骨架先行、数值整体后置」)——shop=off/on 分层 MC、shop=on 带平移、换算率登记全部随曲线调参批执行(难度曲线与Boss v0.18 商店与 GP 经济杠杆节)
|
||||
table: ShopTable(数据配置 承载;**2026-09-20 GP+商店房批新建,2026-10-03 迭代 11 第②批扩列**——商店房商品与 GP 经济的行集;冒险结构 回填 owner,公式语义归其 GP 冒险账本节(共享池账本段 v0.13),数值所有权归 难度曲线与Boss 曲线调参批)
|
||||
fields: gp_drops 三行(键 = 战斗节点身份 {normal, elite, boss}——普通 均值 3 区间 [2,4] / 精英 均值 5 区间 [4,6] 预留行 / Boss 均值 8 区间 [6,10],掷定 + 均值方差口径)· level_gp_bonus(逐关——L1 0 / L2 1 / L3 2)· **pool_shelf 四行(迭代 11 第②批 T1——kind_id 词表封闭 {attack, max_hp, armor, ap}=四切边后种〔GP 袋负交易切边/机制件三轴切边〕:attack 基价 4 / max_hp 4 / armor 4 / ap 5 provisional;库存=上限即库存,账本在 AdventureRunner._bonus_taken 单点共享,行族不记库存)· price_a=1.8 / price_b=1.1(乘法价格因子 provisional,后置追认)** · products 三行(atk_up +1 首价 4 / hp_up +3 首价 2 / armor_up +1 首价 4,步长均 2——legacy 对照模式承载,退役归 off 锚换代批)· price_level_bonus(逐关——L1 0 / L2 2 / L3 4,legacy 同上)
|
||||
formulas: 每战 GP 掉落 = 掷定 + 关级掉落加值 + 战斗内缓冲(战斗完成随 battle_finished.gp_dropped 统一入钱包——冒险结构 GP 冒险账本三收口);**货架现价(pool 现行)= ceil(基价 × b^(关序−1) × A^(本次进店已购次数))**;商品价格(legacy 对照)= 基价 + 关级价格加成 + 步长 × 已购档数;定价锚 = 「正常运气可购档数 / 运气极好可购档数」两读数的数学期望(期望账进表批注)
|
||||
value_status: 全部行值 provisional-pending-曲线调参批(用户 2026-09-20 拍板「骨架先行、数值整体后置」;A/b 投值 1.8/1.1 系迭代 11 第②批零人工令改 provisional+后置追认)——shop=off/on 分层 MC(off=legacy 对照 off_n60 逐位 / on=共享池首锚 N=60 双策略)、shop=on 带平移、换算率登记全部随曲线调参批执行(难度曲线与Boss v0.22 商店与 GP 经济杠杆节)
|
||||
rule4_status: 进度成长表(数值随关卡与商品档变化,无 Rule 4 豁免)
|
||||
referenced_by: [冒险结构, 关卡节点地图, 难度曲线与Boss, 战斗结算, 数据配置]
|
||||
|
||||
|
||||
@@ -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