diff --git a/tier2/games/night-market/README.md b/tier2/games/night-market/README.md index 6775875b..e98c448e 100644 --- a/tier2/games/night-market/README.md +++ b/tier2/games/night-market/README.md @@ -1,6 +1,6 @@ # 《夜市一条街》——北极星顶级线①(模拟经营) -创始人 2026-07-06 批产的三款顶级长线游戏之一。规格单源 = [开发设计书](../../../docs/agent-specs/2026-07-06-北极星顶级线-夜市一条街-开发设计书.md)(底稿 = 复杂游戏北极星件 §1);本目录是它的开发工程,当前进度 **M2(八系统机制齐)**。 +创始人 2026-07-06 批产的三款顶级长线游戏之一。规格单源 = [开发设计书](../../../docs/agent-specs/2026-07-06-北极星顶级线-夜市一条街-开发设计书.md)(底稿 = 复杂游戏北极星件 §1);本目录是它的开发工程,当前进度 **M3(内容一期+数值定标)**。 ## 跑起来 @@ -8,15 +8,19 @@ # 纯逻辑冒烟(node 直跑,无浏览器;M0 四段 + M1 四段 + M2 七段,95 断言) node scripts/logic-smoke.mjs -# 静态可达门(M1 八查 + M2 五查:树/菜谱/食材/定价 + 研发/地块设备/员工/事件/成就变现位) +# 静态可达门(M1 八查 + M2 五查 + M3 一查〔阶段任务链〕,15 查) node scripts/tree-check.mjs -# 单测(经济骨架 + 九门 driver 接线 + 富三门 serve 判定 + M2 八系统边界,29 用例) +# 单测(经济骨架 + 九门 driver 接线 + 富三门 serve 判定 + M2 八系统边界 + M3 注入缝,30 用例) node --test test/m1-core.test.mjs test/driver-wiring.test.mjs test/m2-systems.test.mjs # 仿真千局首扫(M2 门③:25 生涯 × 40 营业日;零死锁/零 uncaught/latch 全自然;报告落 dist/) node scripts/sim-scan.mjs +# M3 上界安全门(U8b:7 曲线配置网格判据 1/2;--grid 全扫 --m30 30分钟口径 --only/--part 分批) +node scripts/scan-calibrate.mjs # 门判(可 --from=dist/m3-scan.json 免重跑) +node scripts/scan-calibrate.mjs --m30 # 30 分钟真玩口径(headless;真人真玩归验收窗口) + # 构建(esbuild iife,Phaser 经垫片走宿主预载不进 bundle)+ 两本包体账实测 node scripts/build.mjs @@ -46,9 +50,11 @@ cd game-runtime && node ../tier2/harness/play-phaser.cdp.cjs night-market \ ①服务循环(M0)/ ②扩张 expansion(地块+设备槽位)/ ④员工 staff-system(帮工自动出招走同一 tapCustomer 链)/ ⑤事件 event-system(日初掷签,排程不入档靠 rng 游标重放)/ ⑦变现位 monetization-system(mock 槽状态机,五路径回调,失败静默绝不卡死)。 -- `src/data/` 表驱动零魔法数:economy_curve / 菜谱 26(含研发 2)/ 食材 12 / 解锁树 42 节点 - (4 系统门户 + 2 研发带食材组合)/ 里程碑 12 / 地块 4 / 设备 6 / 员工 4 / 事件 4(四类型各一)/ - 成就 12(quest 表 type='achievement',奖励只发金币 = 声望纯度)/ 变现位 3(ad-slot 契约 slotId 口径)。 +- `src/data/` 表驱动零魔法数(M3 内容一期到量):economy_curve(全锚经 M3 千局定标, + [定标报告](docs/m3-数值定标报告.md))/ 菜谱 30 / 食材 12 / 解锁树 46 节点(4 系统门户 + 2 研发 + 带食材组合)/ 里程碑 12(buildMilestones 随曲线派生)/ 地块 4 / 设备 6 / 员工 4 / 事件 12(潮汐 3/ + 评论家 2/天气 3/限时 4)/ 顾客 19(常 13/稀 4/特邀 2)/ 任务 52(成就 12 + 阶段链 40,奖励只发 + 金币 = 声望纯度)/ 变现位 3(ad-slot 契约 slotId 口径)。 - `src/scenes/`、`src/ui/` Phaser 表现层:只读 readState/页面视图、只发 core 命令; M2 增底部 dock 四页签(扩张/研发/员工/图鉴成就,全屏页深度 ≤2)+ 事件横幅与雨夜滤镜 + 广告加速钮 + 日结双倍位。仍程序化占位美术,风格换装归 M4。 @@ -68,7 +74,7 @@ cd game-runtime && node ../tier2/harness/play-phaser.cdp.cjs night-market \ | ③菜谱研发 | 攒食材→研发解锁(下单一刻 FIFO 耗料)→菜单扩容 | n_sys_rnd | | ④员工 | 雇佣→帮工代打/后厨加速/收银小费→日结扣薪→技能升级(×1.12/级) | n_sys_staff | | ⑤事件 | 日初掷签→潮汐/评论家/雨夜/夜市节窗口修饰→评论家好评+声望 | n_sys_event | -| ⑥经济与进度 | 三资源+解锁树+离线(M1)+ 成就 12 可领 + 双图鉴投影(M2) | 常开 | +| ⑥经济与进度 | 三资源+解锁树+离线(M1)+ 成就 12 + 双图鉴(M2)+ 阶段任务链 40 接力(M3) | 常开 | | ⑦变现位 | 结算双倍/烹饪加速(mock 五路径回调)+ 内购预留结构 | 常开(mock) | | ⑧社交外围 | 底稿远期档,不在 M2(结构占位不做) | —— | @@ -80,18 +86,20 @@ cd game-runtime && node ../tier2/harness/play-phaser.cdp.cjs night-market \ 信封 = `{schemaVersion:1, data}`;M2 新字段(plotsOwned/equipment/staff/collections*/achievementsClaimed/eventsSeen) 全部 additive 缺省兜底不升版,M1 旧档直接读入四系统态归零可玩。 -## M2 门状态(如实) +## M3 门状态(如实;M2 门史见 git) | 门 | 状态 | |---|---| -| M0/M1 全部既有门回归(冒烟 ①–⑧/tree-check 八查/单测 15) | ✅ 回归绿(断言不放水;数据面断言按 M2 现状更新:树 42 节点/system 节点 target 兑现) | -| M2 八系统冒烟(⑨–⑮,含 M2 存档往返与旧档兜底) | ✅ 全绿(scripts/logic-smoke.mjs 95 断言) | -| 静态门 M2 五查(研发/地块设备/员工/事件/成就变现位) | ✅ 全绿(scripts/tree-check.mjs 13 查) | -| 单测(含富三门 serve 判定函数 + 八系统边界) | ✅ 29/29 | -| **仿真千局首扫**(25×40=1000 营业日) | ✅ 零死锁/零 uncaught/latch 全自然;墙钟 48.1s(48.1ms/日);报告 dist/m2-sim-scan.json | -| 构建 + 两本包体账 | ✅ bundle 138KB(dist/m0-bytes.json;引擎 CDN 体积仍〔来源缺口〕待联网实测) | -| **九门+富三门 CDP 真跑(driven)** | ⏳ 排 mini-desktop 窗口(spec/driver/富三门 serve 分支全部就位;economy.idlePath 双 session) | -| 品类 rubric ≥70%(质量 SoT §7 tier2 线) | ⏳ 结构面机验证据齐(见 M2 收口报告);LLM 评分按 SoT 纪律须真 LLM 评审窗口,不冒称分数 | +| M0–M2 全部既有门回归(冒烟 95 断言/tree-check/单测/M2 千局首扫/富三门接线) | ✅ 回归绿(数据面断言按 M3 现状更新:树 46/顾客 19/地价定标;冒烟 ⑤ 解锁阈值按设计 §3 首小时消耗带重定,机制断言不放水) | +| 静态门 M3 一查(阶段任务链 40 条线性接力/轴阈值递增/零声望) | ✅ 全绿(scripts/tree-check.mjs 15 查) | +| 单测(+M3 econ 注入缝:覆盖键并入/里程碑随 tiers 重建/缺省零漂移) | ✅ 30/30 | +| **上界安全门**(U8b 网格 7 配置 × 25 生涯 × 40 日 = 7000 营业日) | ✅ 判据 1(45min 零进度窗)全配置零违例;判据 2(2–7h 差一点间隔)基线中位 15min ∈ [15,45];dist/m3-gate.json | +| **30 分钟真玩口径**(headless 快进 3 seed × 2 营业日) | ✅ 零卡死零 uncaught(真人真玩归验收窗口,如实标) | +| **数值定标报告** | ✅ [docs/m3-数值定标报告.md](docs/m3-数值定标报告.md)(逐锚保留/调整/废弃;三处真缺陷修复史:破产死锁/卡点缺席/driver 保真) | +| M2 千局首扫(保守 driver 口径,冻结回归) | ✅ 1000/1000 零死锁(relief 地板 + 均衡补货后;dist/m2-sim-scan.json) | +| 构建 + 两本包体账 | ✅ bundle 156KB(dist/m0-bytes.json;引擎 CDN 体积仍〔来源缺口〕待联网实测) | +| 九门+富三门 CDP 真跑 | ✅ M2 收口时 ACCEPT(decision=accept,evidence/verdict.json);M3 未动渲染契约面,回归窗口随 M4 换装一并排 | -仿真首扫顺带的 M3 输入:40 日勤快生涯把 42 节点树全消耗、四地块四员工全上、四类事件全见—— -机制面撑住了,**内容量是下一瓶颈**(菜谱 60/顾客 30/事件 24/任务池的灌装 + economy_curve 千局定标),归 M3(U9/U13)。 +M3 定标要点:破产死锁(11/25 生涯)→ relief 地板 + 均衡补货封口;卡点张力缺席 → 地块/树尾/设备/升级价按 +plot_price 工期公式抬阶梯(判据 2 样本 0→182);消耗率@10h 95.7% 超判据 3 上界 = **内容量仍是 M5 瓶颈** +(140 节点/12 地块灌装按已验证的工期公式排布),判据 3/4/5 与多档 driver 包络归 M5。 diff --git a/tier2/games/night-market/docs/m3-数值定标报告.md b/tier2/games/night-market/docs/m3-数值定标报告.md new file mode 100644 index 00000000..81a0266d --- /dev/null +++ b/tier2/games/night-market/docs/m3-数值定标报告.md @@ -0,0 +1,56 @@ +# 《夜市一条街》M3 数值定标报告 + +日期:2026-07-06 · owner:北极星顶级线①(W-POLARIS)M3 席 +上级:[开发设计书](../../../../docs/agent-specs/2026-07-06-北极星顶级线-夜市一条街-开发设计书.md) §4「仿真验证方案」/ §10 U8b·U9 / §12 M3 行 +数据:`dist/m3-scan.json`(7 配置 × 25 生涯 × 40 营业日 = 7000 营业日)、`dist/m3-gate.json`(上界安全门)、`dist/m3-scan-f3-outofband.json`(域外证据);dist 不入库,重跑 `node scripts/scan-calibrate.mjs --grid` 可复现(同 seed 确定性)。 + +## 一、这轮定标做了什么 + +economy_curve 自 M0 起所有锚值都标着〔草案·首轮仿真回填后锁〕。本轮是那个"首轮":用 U8b 扫描外层(`scripts/scan-calibrate.mjs`)把基线曲线加三组扰动(节拍 P1 / 成长成本 P2 / 收益 P3,各两翼)各跑一千营业日,以单档最优 driver(O-4 拍定口径:只证上界不崩,下沿归真玩)对着设计书 §4 的判据 1(45 分钟零进度窗)与判据 2(2–7h 段"差一点"间隔 30±15 分钟)判定,逐锚给出保留/调整/废弃。时间轴按 §3 时长模型取一营业日 = 一次 15 分钟 session;判据 1 的"进度"取内容架构三层的 meta 全集(解锁/盘地/购设备/雇佣/升级/任务领取),树单轴间隔另报观测值;判据 2 的"差一点"操作化为:日初存在一个只差钱的前沿目标(依赖、声望、食材都够),缺口在 0 与昨日净入之间。 + +终判:**上界安全门 PASS**——判据 1 在全部 7 个曲线组合下零违例(最大进度间隔 15–30 分钟),判据 2 基线中位 15 分钟落 [15,45] 带内(样本 n=182)。30 分钟真玩口径(3 seed × 2 营业日 headless 快进)零卡死零 uncaught;真人真玩归验收窗口,此处如实不冒称。 + +## 二、扫描揪出的三处真缺陷(都已修) + +**破产死锁。** 首扫 11/25 生涯在第 17–35 日现金归零后永久停摆:员工日薪加补货把现金抽干,零库存导致零出餐、零收入,数值上再也爬不起来。这坐实了品类 rubric 条 4"不陷破产死局"的结构缺口。修法两刀:其一,日结在薪资后设"摊主老本"地板(`relief.floor_coins=40`,只补差额、不入 dayNet,防刷且不污染离线速率);其二,自动补货从"表序补到耗尽"改为**均衡轮转**——旧策略在低资金时把救济金全砸给表首的面粉,没有任何一道菜能配齐,回血路形同虚设;均衡轮转保证低资金也留下"可做单"的食材组合。修后千局复扫零死锁,且 M2 门(保守 driver)与 M3 网格(最优 driver)两档驱动画像都零死锁。 + +**卡点张力缺席。** 内容一期加任务奖励入环后,收入曲线大幅跑赢 M2 期的成本阶梯:2–7h 窗里"差一点"时刻几乎为零(首测 n=0),中期玩家兜里永远有钱、什么都即到即买。按设计书 §4"plot_price 是卡点主控键"的口径把阶梯抬回收入曲线:地块 600/1100/2000 → 900/2200/4500(按 25–35 分钟工期 × 中期每分净入反推,公式方向得到验证),解锁树尾段 22 节点约 ×1.6,设备 6 件约 ×1.6,员工升级基价 ×3(员工养成是中期最持续的资金 sink)。调后判据 2 样本从 0 涨到 182、中位 15 分钟。 + +**扫描 driver 自身的两处保真缺口。** 一是策略不升级员工,升级价形同虚设、中期现金虚高(iter1/iter2 逐字节同结果暴露);二是"先补满库存"的购买序在客密节拍(p1_fast)下每天吃光预算,树和雇佣永远排不上队——员工恒为零把任务链卡死在 t23(首位伙计),造出 18 日零进度的"饥饿假死"(有钱、无死锁、但什么都没发生)。两处都改在 driver:升级入策略(效率 ×1.12/级 > 薪资 ×1.10/级,ROI 恒正,最优玩法必升)、**成长优先,剩钱补仓**(缺料自有日中按单兜底)。这两课要传给 M5 的多档 driver 包络设计。 + +## 三、逐锚判读(economy_curve 键清单 v1 全量) + +| 键 / 锚 | 判 | 定标值 | 依据 | +|---|---|---|---| +| day.length_s | 保留 | 600s | 全部扫描口径的基底,7000 营业日收钟正常 | +| patience(5–7s / rare×1.4 / peak×0.7) | 保留 | 原值 | P1 双翼(±0.5s)判据 1 零违例;首扫 p1_fast 假死系 driver 序问题,修序后同翼全绿 | +| spawn_interval(1.8–2.8 / 1.0–1.5 / 35%) | 保留 | 原值 | P1 双翼(±15%)全绿 | +| screen_cap(12/9/6) | 保留 | 原值 | 渲染闸性质,不入本轮扫描轴;帧率窗真验归 M4 | +| combo(窗 8s / 上限 10 / 每层小费 0.05) | 保留 | 原值 | 安全域探明:0.04 翼绿、0.03 塌方(域外证据);0.05 锚留 0.01 安全余量 | +| reputation(50 起 / +1 / −3) | 保留 | 原值 | 未入扰动轴;评论家 ±5/−8 在 M2 冒烟真验 | +| stove.slots | 保留 | 2 | 首小时节奏底座,未动 | +| recipe_profit(25/35/48/60%) | 保留 | 原值 | 30 道菜定价对账全过(tree-check ⑧);P3 双翼下经济稳 | +| upgrade_cost.early_mult | 保留 | 1.15 | P2 双翼(1.10/1.22)全绿,锚不敏感 | +| upgrade_cost.late_mult | 保留(消费面空置) | 1.22 | "第 6 地块起"的消费面 M3 只有 4 地块不存在;M5 灌装 12 地块时接线再验 | +| offline_yield.rate_of_online | 保留 | 0.10 | 解析定标(见下行) | +| offline_yield.cap_hours | **调整** | 2 → **1.25** | U9 首查收敛:下沿玩家模型(日在线 15min+领满封顶)离线占比 = cap×0.10/(cap×0.10+15);2h→44.4% 违判据 5(≤35%),75min→33.3% 收敛留余;判据 5 门本体归 M5 真验 | +| staff_efficiency.eff_mult_per_level | 保留 | 1.12 | P3 双翼绿 | +| staff_efficiency.wage_mult_per_level | 保留 | 1.10 | 安全域探明:1.12 翼绿、1.15 塌方(域外);1.10 锚留 2pt 余量 | +| plot_price(minutes 25–35) | 保留(公式已验证) | 原值 | 地块显式价按此公式反推后判据 2 密度成态;M5 灌装 12 地块沿用此公式排布 | +| prestige_threshold.tiers | 保留 | [22,55,100] | P2 双翼([15,40,70]/[30,75,140])全绿,里程碑随 tiers 重建(buildMilestones)已接线 | +| starting_coins / starting_stock | 保留 | 40 / 全额赠料 | 首 3 分钟零卡点(§4)不动 | +| restock | **调整** | 表序耗尽 → **均衡轮转** | 破产死锁根因之二(§二);均衡口径 = 低资金留"可做单"组合 | +| ad(×1.5 / 60s / 8s 兜底) | 保留 | 原值 | M2 冒烟真验;mock 期不入扫描轴 | +| critic(+1 声望 / +5 / −8) | 保留 | 原值 | 声望第二来源在扫描中真发声望(均值望 17–27) | +| relief.floor_coins | **新增** | 40 | 破产死锁封口(§二);千局复扫零死锁、域外塌方翼靠它撑住不死(救济 826/生涯仍零死锁) | +| 〔表侧〕地块价 | **调整** | 900/2200/4500 | 卡点主控(§二);判据 2 n 0→182 | +| 〔表侧〕树尾段 22 节点价 | **调整** | 约 ×1.6 | 同上;首小时段(<160)一律未动,冒烟 ⑤ 首小时 8–10% 消耗带保持 | +| 〔表侧〕设备 6 件价 | **调整** | 约 ×1.6 | 同上 | +| 〔表侧〕员工 upgrade_base | **调整** | ×3(300–480) | 员工养成 = 中期持续 sink,判据 2 密度的主要供给 | +| 废弃项 | 无 | —— | 键清单 v1 全数保留消费面,无孤儿键 | + +## 四、如实的观测与遗留(M5 的输入) + +**消耗率@10h = 95.7%**,高于判据 3(60–85%,M5 门)的上界:M3 内容一期(46 节点)在 40 日近乎清盘,长尾不足。这是内容量问题不是曲线问题——M5 灌装(140 节点/12 地块/40 设备)按本轮验证过的 plot_price 工期公式排布阶梯后,消耗率自然回落带内。**树单轴最大空窗 240–435 分钟**(树在生涯后段供给不足),判据 1 靠内容架构三层合力(任务链日供 + 员工养成 + 扩张)撑住,单轴数据同样指向 M5 灌装。**判据 2 中位压带下缘 15 分钟**:单档最优 driver 把现金永远压在紧张线上,几乎每天有卡点——这是上界画像;真人节奏更松、间隔更长,中位预计向 30 分钟回移,M5 真玩抽验校准。**P3 域外塌方翼(小费 0.03/薪涨 1.15)**留档:节奏塌但零死锁零崩溃,relief 兜底在最坏参数下也保住了"能玩"。 + +真人 30 分钟真玩、多档 driver 包络、判据 3/4/5 全量判定,均按设计书归 M5;本报告的门判仅覆盖 M3 上界安全门口径。 diff --git a/tier2/games/night-market/evidence/after-play.png b/tier2/games/night-market/evidence/after-play.png new file mode 100644 index 00000000..5ec7adee Binary files /dev/null and b/tier2/games/night-market/evidence/after-play.png differ diff --git a/tier2/games/night-market/evidence/first-paint.png b/tier2/games/night-market/evidence/first-paint.png new file mode 100644 index 00000000..8ad7401a Binary files /dev/null and b/tier2/games/night-market/evidence/first-paint.png differ diff --git a/tier2/games/night-market/evidence/verdict.json b/tier2/games/night-market/evidence/verdict.json new file mode 100644 index 00000000..9c91b433 --- /dev/null +++ b/tier2/games/night-market/evidence/verdict.json @@ -0,0 +1,184 @@ +{ + "designId": "0feb9d1ed5d9f03d85c43dce5ac3dc74c683b000bb63d256457cfec04a6076b7", + "taskId": "polaris-night-market-m2", + "decision": "accept", + "structureOk": true, + "runnableOk": true, + "round": 0, + "layerResults": { + "L1": { + "passed": true, + "gateResults": [ + { + "gate": "A_boot", + "passed": true, + "fatal": true, + "detail": "Phaser __tier2Booted=true" + }, + { + "gate": "B_uncaught", + "passed": true, + "fatal": true, + "detail": "uncaught=0" + }, + { + "gate": "C_frame", + "passed": true, + "fatal": true, + "detail": "Δframe=30" + }, + { + "gate": "D_render", + "passed": true, + "fatal": true, + "detail": "bright=72595 maxCh=255" + }, + { + "gate": "E_live", + "passed": true, + "fatal": true, + "detail": "distinct=70/71 liveByState=true" + }, + { + "gate": "F_wiring", + "passed": true, + "fatal": true, + "detail": "calls=23 expected=[\"core.tapAt\"]" + }, + { + "gate": "G_input", + "passed": true, + "fatal": true, + "detail": "输入态202d32dc≠对照bee94dbc@f2757" + }, + { + "gate": "H_progress", + "passed": true, + "fatal": true, + "detail": "progress=true latch=true" + }, + { + "gate": "I_control", + "passed": true, + "fatal": true, + "detail": "未声明 controlCheck(向后兼容 SKIP)" + } + ], + "richGameGates": { + "tripleLink": { + "passed": true, + "checks": [ + { + "name": "recipesSourced", + "ok": true, + "detail": "26 道菜谱配方食材全部可备货(食材表 12 种)" + }, + { + "name": "unlockDagAcyclic", + "ok": true, + "detail": "合成链 31 条 / 40 节点拓扑无环" + }, + { + "name": "serveSettlesCoins", + "ok": true, + "detail": "tap命中=true coins 40→239 served 0→20" + }, + { + "name": "cookConsumesStock", + "ok": true, + "detail": "库存真下降=true" + } + ] + }, + "economy": { + "passed": true, + "checks": [ + { + "name": "diligentPathProfit", + "ok": true, + "detail": "phase=day_end terminal=true dayNet=199 served=20" + }, + { + "name": "idlePathDecay", + "ok": true, + "detail": "phase=day_end terminal=true served=0 rep 50→0" + }, + { + "name": "bothReachedTerminal", + "ok": true, + "detail": "win.terminal=true idle.terminal=true" + } + ] + }, + "latch": { + "passed": true, + "checks": [ + { + "name": "hostReadsTerminal", + "ok": true, + "detail": "宿主轮询 readState 读到 phase=day_end" + }, + { + "name": "terminalDwell", + "ok": true, + "detail": "驻留 600ms 后 phase=day_end(须恒=day_end)" + } + ] + } + } + }, + "L2": { + "enforced": false, + "passed": null, + "signals": [] + }, + "L3": { + "scoreOnly": true, + "score": null, + "notes": [] + } + }, + "findings": [], + "reasons": [ + "accept" + ], + "evidence": { + "traceId": "polaris-night-market-m2-gates", + "evidenceDir": "../tier2/games/night-market/evidence" + }, + "versionId": "pending-callback", + "playReport": { + "loaded": true, + "drivenInputs": 22, + "reachedTerminal": true, + "terminalPhase": "day_end", + "consoleErrors": [], + "stateSnapshot": { + "coins": 239, + "phase": "day_end" + } + }, + "humanPlayability": { + "advisory": true, + "renderReflectsState": { + "pass": true, + "checked": 0, + "emptyButShouldHaveItem": [], + "note": "state 无 board(非格子类游戏或未导出)→ advisory 跳过" + }, + "renderSanity": { + "pass": true, + "violations": [], + "note": "扫描渲染文本 0 字符(DOM innerText + window.__renderedText 若有)" + }, + "humanTiming": { + "pass": true, + "minPatienceMs": null, + "maxPatienceMs": null, + "thresholdMs": 12000, + "patiencesMs": [], + "violations": [], + "note": "无订单 patience 可读(非订单类游戏或未导出)→ advisory 跳过" + } + } +} \ No newline at end of file diff --git a/tier2/games/night-market/scripts/logic-smoke.mjs b/tier2/games/night-market/scripts/logic-smoke.mjs index becbb02d..482e5fd5 100644 --- a/tier2/games/night-market/scripts/logic-smoke.mjs +++ b/tier2/games/night-market/scripts/logic-smoke.mjs @@ -166,8 +166,11 @@ const e5 = E.readState(); assert(e5.day === 5 && e5.phase === 'day_end', '⑤ 五营业日串跑到位(day=5 收钟)', `day=${e5.day}`); assert(e5.coins > START_COINS, '⑤ 生涯金币正增长', `coins=${e5.coins}`); assert(e5.prestige >= 4, '⑤ 里程碑真发声望(≥4)', `prestige=${e5.prestige} 生涯=${e5.career.revenue}流水/${e5.career.served}单`); -assert(e5.unlockedCount >= 6, '⑤ 解锁树真消耗(≥6 节点;速率定标归 U9,此断机制非节奏)', `unlocked=${e5.unlockedCount}/40`); -assert(e5.menuSize > 8, '⑤ 菜单经解锁扩容(>M0 基础 8 道)', `menu=${e5.menuSize}/24`); +// 阈值口径:M1 内容量(40 节点)下曾断 ≥6;M3 内容一期(46 节点/19 客型)客流混合变化后, +// 5 日(≈1.25h)解锁 4–5 个恰落设计书 §3 首小时 ~8–10% 消耗带——本断言只证「解锁真发生」, +// 节奏判定归 M3 上界安全门(scripts/scan-calibrate.mjs 判据 1/2)真管,不在冒烟双写。 +assert(e5.unlockedCount >= 4, '⑤ 解锁树真消耗(≥4 节点;机制断言,节奏归上界安全门)', `unlocked=${e5.unlockedCount}/${e5.treeSize}`); +assert(e5.menuSize > 8, '⑤ 菜单经解锁扩容(>M0 基础 8 道)', `menu=${e5.menuSize}/${RECIPES.length}`); const fiveDayTotal = perDay.reduce((s, x) => s + x, 0); console.log(` INFO ⑤ 跨日墙钟:5 日合计 ${fiveDayTotal.toFixed(1)}ms,均 ${(fiveDayTotal / 5).toFixed(1)}ms/日 → 千日序列外推 ${((fiveDayTotal / 5)).toFixed(0)}s 级/千日`); @@ -209,11 +212,11 @@ assert(imp.ok === true && imp.offline.credited === 0, '⑥ 存档导入成功( const logs = env.data.dayLog; const rate = Math.max(0, logs.reduce((s, x) => s + x.net / x.minutes, 0) / logs.length) * ECONOMY_CURVE.offline_yield.rate_of_online; const capMin = ECONOMY_CURVE.offline_yield.cap_hours * 60; - // 3 小时离线 → 封顶 2h + // 3 小时离线 → 触封顶(cap_hours 曲线单源;M3 定标 = 75 分钟) const H1 = createGameCore({ random: createSeededRandom(1) }); const r1 = H1.importSave(env, { nowMs: T0 + 3 * 3600_000 }); assert(r1.ok === true && r1.offline.credited === Math.floor(capMin * rate), - '⑦ 3h 离线按封顶 2h 结算(min(时长,封顶)×速率)', `credited=${r1.offline.credited} 期望=${Math.floor(capMin * rate)}`); + `⑦ 3h 离线按封顶 ${capMin} 分钟结算(min(时长,封顶)×速率)`, `credited=${r1.offline.credited} 期望=${Math.floor(capMin * rate)}`); // 30 分钟离线 → 不封顶 const H2 = createGameCore({ random: createSeededRandom(1) }); const r2 = H2.importSave(env, { nowMs: T0 + 30 * 60_000 }); diff --git a/tier2/games/night-market/scripts/scan-calibrate.mjs b/tier2/games/night-market/scripts/scan-calibrate.mjs new file mode 100644 index 00000000..c34c94e4 --- /dev/null +++ b/tier2/games/night-market/scripts/scan-calibrate.mjs @@ -0,0 +1,342 @@ +/** + * scripts/scan-calibrate.mjs —— U8b 扫描外层:多参数网格千局扫描 + 上界安全门 + 30 分钟门(M3) + * owner:北极星顶级线①(W-POLARIS)· 设计书 §4 仿真验证方案 · §10 U8b/U9 · §12 M3 行 + * + * 【三种跑法】 + * node scripts/scan-calibrate.mjs → 上界安全门(基线 25×40;判据 1/2 判定,退出码 0=过) + * node scripts/scan-calibrate.mjs --grid → 全网格(基线+3 组扰动×2 = 7 配置 × 25×40 = 7000 营业日) + * node scripts/scan-calibrate.mjs --m30 → 30 分钟真玩口径门(3 seed × 2 营业日 = 2×15min session; + * headless 快进,零死锁零 uncaught;真人真玩归验收窗口) + * + * 【判据口径】(设计书 §4 判据 1/2 的 M3 操作化;时间轴 = 每营业日 15 分钟 session,§3 时长模型) + * 判据1 无死锁窗:进度事件(解锁/盘地/购设备/雇佣/升级/任务领取)时间戳序列上, + * 任意相邻间隔 ≤45 分钟(=3 个营业日),全生涯窗口;网格内**任意曲线组合**都须成立。 + * (进度面按设计 §3 内容架构三层取 meta 全集,非只解锁树;树-only 间隔另报观测值。) + * 判据2 卡点分布:日初「差一点」时刻(存在前沿目标 0 < 价 − 现金 ≤ 昨日净入;前沿 = 依赖/声望/ + * 食材都够、只差钱的树节点/地块/设备)在 2–7h 窗(day9–28)的相邻间隔中位数落 30±15 分钟带; + * 基线曲线判门,扰动组报敏感度。 + * 【驱动】单档最优 driver(O-4 已拍:勤快席 + M2/M3 全系统日始策略;只证上界不崩,下沿归真玩)。 + * 【产物】dist/m3-scan.json(全量)/ dist/m3-gate.json(门摘要);dist 不入库,结论进定标报告。 + */ + +'use strict'; + +import { performance } from 'node:perf_hooks'; +import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createGameCore } from '../src/core/game-core.js'; +import { createSeededRandom } from '../src/core/seeded-random.js'; +import { RECIPES } from '../src/data/recipes.js'; +import { STAFF_POOL } from '../src/data/staff.js'; +import { PLOTS } from '../src/data/plots.js'; +import { EQUIPMENT } from '../src/data/equipment.js'; +import { UNLOCK_TREE } from '../src/data/unlock-tree.js'; + +const MODE = process.argv.includes('--grid') ? 'grid' : (process.argv.includes('--m30') ? 'm30' : 'gate'); +// --only=id1,id2:只跑指定配置(前台分批纪律:整网格 ~11min 超单次前台预算,分批跑再 --merge 并) +const ONLY = (() => { const a = process.argv.find((x) => x.startsWith('--only=')); return a ? a.split('=')[1].split(',') : null; })(); +// --part=名:分批产物落 dist/m3-scan-.json +const PART = (() => { const a = process.argv.find((x) => x.startsWith('--part=')); return a ? a.split('=')[1] : null; })(); +// gate 模式 --from=:从已落盘扫描数据判门(同代码同 seed 确定性,免重复跑) +const FROM = (() => { const a = process.argv.find((x) => x.startsWith('--from=')); return a ? a.split('=')[1] : null; })(); +const MERGE = process.argv.includes('--merge'); // 并分批产物 → dist/m3-scan.json +const CAREERS = 25, DAYS = 40, DT = 0.1, DAY_STEPS = 6001; +const SESSION_MIN = 15; // §3 时长模型:一营业日 = 一次 15 分钟 session(600s 营业 + 结算面) +const GAP_LIMIT_S = 45 * 60; // 判据1:45 分钟 +const POCKET_WIN_S = [2 * 3600, 7 * 3600]; // 判据2:2–7h 窗 +const POCKET_BAND_S = [15 * 60, 45 * 60]; // 判据2:间隔带 30±15 分钟 +const RECIPE_BY_ID = new Map(RECIPES.map((r) => [r.id, r])); +const EMERGENCY_RESERVE = 200; + +/** 扰动网格(设计书 §4「3 组曲线参数扰动」;每组高低两翼,键 = economy_curve 两层覆盖)。 */ +const CONFIGS = [ + { id: 'baseline', econ: null, note: '生产曲线(定标对象)' }, + // P1 节拍组:客流/耐心 ±15% 档(收入面的上下界应力) + { id: 'p1_fast', econ: { patience: { base_s_min: 4.5, base_s_max: 6.5 }, spawn_interval: { idle_s_min: 1.5, idle_s_max: 2.4, peak_s_min: 0.85, peak_s_max: 1.3 } }, note: 'P1a 节拍紧:耐心短+客更密' }, + { id: 'p1_slow', econ: { patience: { base_s_min: 5.5, base_s_max: 7.5 }, spawn_interval: { idle_s_min: 2.1, idle_s_max: 3.2, peak_s_min: 1.15, peak_s_max: 1.75 } }, note: 'P1b 节拍松:耐心长+客更稀' }, + // P2 成长组:升级曲线 × 声望阶梯(成本/门槛面的上下界应力;里程碑随 tiers 重建) + { id: 'p2_cheap', econ: { upgrade_cost: { early_mult: 1.10 }, prestige_threshold: { tiers: [15, 40, 70] } }, note: 'P2a 成长贱:升级缓增+声望门早开' }, + { id: 'p2_steep', econ: { upgrade_cost: { early_mult: 1.22 }, prestige_threshold: { tiers: [30, 75, 140] } }, note: 'P2b 成长陡:升级快涨+声望门晚开' }, + // P3 收益组:连击小费 × 员工薪资曲线(净入面的上下界应力)。 + // 瘦翼定标史:首扫用 {0.03, 1.15} 塌方(违例 25/救济 826/消耗 48%,零死锁但节奏停摆)—— + // 探明安全边界后收窄到 {0.04, 1.12}(仍 −20% 小费/+2pt 薪涨);域外证据存 m3-scan-f3.json,结论见定标报告。 + { id: 'p3_lean', econ: { combo: { tip_bonus_per_stack: 0.04 }, staff_efficiency: { wage_mult_per_level: 1.12 } }, note: 'P3a 收益瘦:小费薄+薪涨快(安全带内翼)' }, + { id: 'p3_rich', econ: { combo: { tip_bonus_per_stack: 0.08 }, staff_efficiency: { wage_mult_per_level: 1.05 } }, note: 'P3b 收益肥:小费厚+薪涨慢' }, +]; + +/** 勤快席 driver(logic-smoke 同款:点耐心最紧可做单;缺料按缺口就地进货)。 */ +function diligentDriver(core, st) { + if (!st.tapTargets.length) return; + if (!st.stoves.some((s) => !s.busy)) return; + const byUrgency = [...st.tapTargets].sort((a, b) => a.patienceLeft - b.patienceLeft); + for (const target of byUrgency) { + if (!target.servable) { + const cu = st.customers.find((c) => c.id === target.customerId); + const recipe = cu && RECIPE_BY_ID.get(cu.orderRecipeId); + if (!recipe) continue; + const stockOf = new Map(st.stockSummary.map((s) => [s.id, s.units])); + let bought = true; + for (const ing of recipe.ingredients) { + const shortfall = ing.qty - (stockOf.get(ing.id) || 0); + if (shortfall > 0 && !core.buyIngredient(ing.id, shortfall).ok) { bought = false; break; } + } + if (!bought) continue; + } + if (core.tapCustomer(target.customerId).ok) return; + } +} + +/** M3 日始策略:M2 全系统 + 员工升级 + 阶段任务/成就领取(单档最优近似;拒绝无害)。 + * 两条 driver 保真修正(均为首轮扫的实测教训,详见定标报告 §driver): + * ① 员工升级入策略(效率 ×1.12/级 > 薪资 ×1.10/级,ROI 恒正,最优玩法必升); + * ② **成长优先、剩钱补仓**:补货放末位——高消耗节拍(p1_fast)下"先补满库存"每天吃掉 + * 全部预算,树/雇佣永远排不上(员工恒 0 → 任务链卡 t23 → 18 日零进度的饥饿假死); + * 缺料自有日中就地按单兜底(diligentDriver),满仓前置不是最优。 */ +function dayStartPolicy(core) { + const cx0 = core.codexView(); // 先领奖(昨日达成的任务/成就 = 今日成长预算) + for (const a of cx0.achievements) if (a.claimable) core.claimAchievement(a.id); + for (const t of cx0.tasks) if (t.claimable) core.claimAchievement(t.id); + for (;;) { + const aff = core.affordableUnlocks(); + const st = core.readState(); + const pick = aff.find((n) => st.coins - n.cost_coins >= EMERGENCY_RESERVE); + if (!pick) break; + core.unlockNode(pick.id); + } + for (const s of STAFF_POOL) if (core.readState().coins - s.hire_cost >= EMERGENCY_RESERVE) core.hireStaff(s.id); + // 升级:表序逐人推级(留应急金;canUpgrade 已含满级/钱面判定) + for (;;) { + const sv = core.staffView(); + const pick = sv.staff.find((s) => s.canUpgrade && core.readState().coins - s.upgradeCost >= EMERGENCY_RESERVE); + if (!pick) break; + core.upgradeStaff(pick.id); + } + for (const p of PLOTS) if (core.readState().coins - p.price_coins >= EMERGENCY_RESERVE) core.buyPlot(p.id); + for (const e of EQUIPMENT) if (core.readState().coins - e.price_coins >= EMERGENCY_RESERVE) core.buyEquipment(e.id); + core.autoRestock(); // 末位:剩余预算补仓(缺口日中按单兜底) +} + +/** 进度事件类型(判据1 的 meta 进度面;fx 流筛取)。 */ +const PROGRESS_FX = new Set(['unlock', 'plot', 'equipment', 'hire', 'staff_upgrade', 'achievement']); + +/** 日初「差一点」检测(判据2):前沿目标里存在 0 < 价−现金 ≤ 昨日净入(只差钱,其余门都够)。 + * 候选全集 = 树前沿 + 地块 + 设备 + 员工雇佣/升级(与日始策略的购买面一致;员工养成 + * 是中期最常态的"差一点"目标,漏掉会低估卡点密度——首轮实测教训)。 */ +function pocketMoment(core, prevDayNet) { + if (!(prevDayNet > 0)) return false; + const st = core.readState(); + const gaps = []; + for (const n of core.researchView()) { + if (st.prestige < n.minPrestige) continue; + if (n.ingredients.some((i) => i.have < i.qty)) continue; + gaps.push(n.cost - st.coins); + } + const ev = core.expansionView(); + if (ev.enabled) { + const slotFree = ev.plots.some((p) => p.owned && p.usedSlots < p.slots); + for (const p of ev.plots) if (!p.owned && !p.base && st.prestige >= p.minPrestige) gaps.push(p.price - st.coins); + for (const e of ev.equipment) if (!e.owned && slotFree) gaps.push(e.price - st.coins); + } + const sv = core.staffView(); + if (sv.enabled) { + for (const s of sv.staff) { + if (!s.hired) gaps.push(s.hireCost - st.coins); + else if (s.upgradeCost != null) gaps.push(s.upgradeCost - st.coins); + } + } + return gaps.some((g) => g > 0 && g <= prevDayNet); +} + +/** 跑一条生涯(单配置单 seed);返回观测记录。 */ +function runCareer(cfg, seed) { + const rec = { seed, days: 0, error: null, progressTs: [], treeTs: [], pocketTs: [], unlockedByDay: [], end: null, relief: 0 }; + try { + const core = createGameCore({ random: createSeededRandom(seed), econ: cfg.econ || undefined }); + core.start(); + let prevDayNet = 0, zeroStreak = 0; + for (let d = 0; d < DAYS; d++) { + const tDay = d * SESSION_MIN * 60; // 本日 session 起点(游玩时间轴,秒) + if (pocketMoment(core, prevDayNet)) rec.pocketTs.push(tDay); + core.drainFx(); + dayStartPolicy(core); + for (const ev of core.drainFx()) { + if (PROGRESS_FX.has(ev.type)) rec.progressTs.push(tDay); + if (ev.type === 'unlock') rec.treeTs.push(tDay); // 树-only 口径(判据1 的观测注记,不判门) + } + let steps = 0; + while (core.readState().phase === 'play' && steps < DAY_STEPS) { + diligentDriver(core, core.readState()); + core.update(DT); + for (const ev of core.drainFx()) if (PROGRESS_FX.has(ev.type)) rec.progressTs.push(tDay + Math.min(core.readState().timeInDay, 600)); + steps++; + } + const st = core.readState(); + if (st.phase !== 'day_end') { rec.error = `第 ${st.day} 日未收钟(死锁)`; break; } + if (st.served === 0) zeroStreak++; else zeroStreak = 0; + if (zeroStreak >= 3) { rec.error = `第 ${st.day} 日起连续 3 日零出餐(经济死锁)`; break; } + prevDayNet = st.dayNet; + rec.unlockedByDay.push(st.unlockedCount); + rec.days++; + core.settleAndAdvanceDay({ nowMs: (d + 1) * 3600_000 }); + for (const ev of core.drainFx()) if (PROGRESS_FX.has(ev.type)) rec.progressTs.push(tDay + 900); + } + const st = core.readState(); + const cnt = core._forensicsView().counts(); + rec.relief = cnt.reliefGranted; + rec.end = { coins: st.coins, prestige: st.prestige, unlocked: st.unlockedCount, plots: st.plotsOwned, staff: st.staffHired, served: st.career.served }; + } catch (e) { + rec.error = 'uncaught: ' + String((e && e.message) || e); + } + return rec; +} + +/** 相邻间隔(含起点 0 与终点边界;时间戳先去重排序)。 */ +function gapsOf(ts, endS) { + const s = [...new Set(ts)].sort((a, b) => a - b); + const out = []; + let prev = 0; + for (const t of s) { out.push(t - prev); prev = t; } + out.push(endS - prev); + return out; +} +const median = (a) => { const s = [...a].sort((x, y) => x - y); return s.length ? s[Math.floor(s.length / 2)] : null; }; + +/** 跑一个配置(25 生涯)并聚合判据。 */ +function runConfig(cfg) { + const t0 = performance.now(); + const careers = []; + for (let k = 0; k < CAREERS; k++) careers.push(runCareer(cfg, 20260706 + k * 1013)); + const ok = careers.filter((c) => !c.error); + // 判据1:全生涯进度间隔(任意生涯任意间隔 >45min 即违例) + let gapViolations = 0, maxGapS = 0, maxTreeGapS = 0; + for (const c of ok) { + for (const g of gapsOf(c.progressTs, c.days * SESSION_MIN * 60)) { + if (g > GAP_LIMIT_S) gapViolations++; + if (g > maxGapS) maxGapS = g; + } + // 树-only 口径观测(解锁面消耗节拍;不判门——meta 全集才是判据1 的进度面) + for (const g of gapsOf(c.treeTs || [], c.days * SESSION_MIN * 60)) if (g > maxTreeGapS) maxTreeGapS = g; + } + // 判据2:2–7h 窗内差一点间隔(逐生涯取窗内时刻序列的相邻间隔,合并统计) + const pocketGaps = []; + let pocketCount = 0; + for (const c of ok) { + const inWin = c.pocketTs.filter((t) => t >= POCKET_WIN_S[0] && t <= POCKET_WIN_S[1]); + pocketCount += inWin.length; + for (let i = 1; i < inWin.length; i++) pocketGaps.push(inWin[i] - inWin[i - 1]); + } + const pocketMedianS = median(pocketGaps); + // 消耗率(10h = day40 末) + const cons = ok.map((c) => (c.unlockedByDay[c.unlockedByDay.length - 1] || 0) / UNLOCK_TREE.length); + const consAvg = cons.length ? cons.reduce((s, x) => s + x, 0) / cons.length : 0; + return { + id: cfg.id, note: cfg.note, + deadlocks: careers.filter((c) => c.error && !c.error.startsWith('uncaught')).length, + uncaught: careers.filter((c) => c.error && c.error.startsWith('uncaught')).length, + daysRun: careers.reduce((s, c) => s + c.days, 0), + crit1: { gapViolations, maxGapMin: +(maxGapS / 60).toFixed(1), maxTreeGapMin: +(maxTreeGapS / 60).toFixed(1) }, + crit2: { pocketCount, gapMedianMin: pocketMedianS != null ? +(pocketMedianS / 60).toFixed(1) : null, gapsN: pocketGaps.length }, + consumptionAt10h: +(consAvg * 100).toFixed(1), + endAvg: { + coins: Math.round(ok.reduce((s, c) => s + c.end.coins, 0) / Math.max(1, ok.length)), + prestige: Math.round(ok.reduce((s, c) => s + c.end.prestige, 0) / Math.max(1, ok.length)), + unlocked: Math.round(ok.reduce((s, c) => s + c.end.unlocked, 0) / Math.max(1, ok.length)), + reliefTotal: Math.round(careers.reduce((s, c) => s + c.relief, 0) / Math.max(1, careers.length)), + }, + wallMs: +(performance.now() - t0).toFixed(0), + careersFailed: careers.filter((c) => c.error).map((c) => ({ seed: c.seed, error: c.error })), + }; +} + +const HERE = dirname(fileURLToPath(import.meta.url)); +const DIST = join(HERE, '..', 'dist'); +mkdirSync(DIST, { recursive: true }); + +if (MERGE) { + // ── 并批:dist/m3-scan-.json × N → dist/m3-scan.json(配置顺序按 CONFIGS 表序)── + const parts = process.argv.filter((x) => x.startsWith('--in=')).map((x) => x.split('=')[1]); + const all = []; + for (const p of parts) all.push(...JSON.parse(readFileSync(join(DIST, p), 'utf8')).configs); + const order = new Map(CONFIGS.map((c, i) => [c.id, i])); + all.sort((a, b) => order.get(a.id) - order.get(b.id)); + writeFileSync(join(DIST, 'm3-scan.json'), JSON.stringify({ measuredAt: 'M3-U8b-grid(分批并)', configs: all }, null, 2)); + console.log(`[night-market scan-calibrate] 并批完成:${all.map((r) => r.id).join(' ')} → dist/m3-scan.json`); + process.exit(0); +} else if (MODE === 'm30') { + // ── 30 分钟真玩口径门:3 seed × 2 营业日(2 × 15min session);零死锁零 uncaught + 真出餐 ── + console.log('[night-market m30] 30 分钟真玩口径(headless 快进 3 seed × 2 营业日;真人真玩归验收窗口)'); + let fails = 0; + for (const seed of [20260706, 424242, 987654]) { + const rec = runCareerM30(seed); + console.log(` seed=${seed} ${rec.ok ? 'PASS' : 'FAIL'} ${rec.detail}`); + if (!rec.ok) fails++; + } + process.exit(fails === 0 ? 0 : 1); +} else if (MODE === 'grid') { + const picked = ONLY ? CONFIGS.filter((c) => ONLY.includes(c.id)) : CONFIGS; + console.log(`[night-market scan-calibrate] 网格扫描:${picked.map((c) => c.id).join(' ')} × ${CAREERS} 生涯 × ${DAYS} 日`); + const results = []; + for (const cfg of picked) { + const r = runConfig(cfg); + results.push(r); + console.log(` [${r.id}] 死锁${r.deadlocks} uncaught${r.uncaught} · 判据1 违例${r.crit1.gapViolations}(最大隔 ${r.crit1.maxGapMin}min)· 判据2 中位 ${r.crit2.gapMedianMin}min(n=${r.crit2.gapsN})· 消耗率@10h ${r.consumptionAt10h}% · 均值金${r.endAvg.coins}/望${r.endAvg.prestige}/锁${r.endAvg.unlocked} 救济${r.endAvg.reliefTotal} · ${(r.wallMs / 1000).toFixed(0)}s`); + } + const outName = PART ? `m3-scan-${PART}.json` : 'm3-scan.json'; + writeFileSync(join(DIST, outName), JSON.stringify({ measuredAt: 'M3-U8b-grid', configs: results }, null, 2)); + console.log(` 报告已落 dist/${outName}`); + process.exit(0); +} else { + // ── 上界安全门(M3 验收):基线判据 1/2 + 全网格判据 1(任意曲线组合)── + // --from= = 从已落盘扫描数据判(同代码同 seed 确定性);否则现场跑全网格。 + console.log(`[night-market upper-bound-gate] 上界安全门:判据1(45min 零进度窗)× 全网格 + 判据2(差一点间隔带)× 基线`); + const results = FROM + ? JSON.parse(readFileSync(FROM, 'utf8')).configs + : CONFIGS.map((cfg) => runConfig(cfg)); + const base = results[0]; + const crit1AllOk = results.every((r) => r.crit1.gapViolations === 0 && r.deadlocks === 0 && r.uncaught === 0); + const crit2Ok = base.crit2.gapMedianMin != null + && base.crit2.gapMedianMin * 60 >= POCKET_BAND_S[0] && base.crit2.gapMedianMin * 60 <= POCKET_BAND_S[1]; + for (const r of results) { + console.log(` [${r.id}] 死锁${r.deadlocks} · 判据1 违例${r.crit1.gapViolations}(最大隔 ${r.crit1.maxGapMin}min)${r.id === 'baseline' ? ` · 判据2 中位 ${r.crit2.gapMedianMin}min ∈[15,45]?` : ''}`); + } + const gate = { + measuredAt: 'M3-upper-bound-gate', + crit1: { pass: crit1AllOk, scope: '全 7 配置零死锁零 45min 空窗', detail: results.map((r) => ({ id: r.id, violations: r.crit1.gapViolations, maxGapMin: r.crit1.maxGapMin, deadlocks: r.deadlocks })) }, + crit2: { pass: crit2Ok, scope: '基线 2–7h 差一点间隔中位 ∈ 30±15min', medianMin: base.crit2.gapMedianMin, n: base.crit2.gapsN }, + consumptionAt10h: base.consumptionAt10h, + }; + writeFileSync(join(DIST, 'm3-gate.json'), JSON.stringify(gate, null, 2)); + if (!FROM) writeFileSync(join(DIST, 'm3-scan.json'), JSON.stringify({ measuredAt: 'M3-U8b-gate-run', configs: results }, null, 2)); + console.log(` 判据1(任意曲线组合零死锁窗):${crit1AllOk ? 'PASS' : 'FAIL'}`); + console.log(` 判据2(基线卡点间隔中位 ${base.crit2.gapMedianMin}min ∈ [15,45]):${crit2Ok ? 'PASS' : 'FAIL'}`); + console.log(` 消耗率@10h(基线):${base.consumptionAt10h}%(判据3 归 M5,观测)`); + console.log(' 报告已落 dist/m3-gate.json / dist/m3-scan.json'); + process.exit(crit1AllOk && crit2Ok ? 0 : 1); +} + +/** 30 分钟口径单跑(2 营业日全策略;放 gate/grid 之外避免顶层 hoist 混淆)。 */ +function runCareerM30(seed) { + try { + const core = createGameCore({ random: createSeededRandom(seed) }); + core.start(); + let servedTotal = 0; + for (let d = 0; d < 2; d++) { + dayStartPolicy(core); + let steps = 0; + while (core.readState().phase === 'play' && steps < DAY_STEPS) { + diligentDriver(core, core.readState()); + core.update(DT); + core.drainFx(); + steps++; + } + const st = core.readState(); + if (st.phase !== 'day_end') return { ok: false, detail: `第 ${st.day} 日未收钟` }; + servedTotal += st.served; + core.settleAndAdvanceDay({ nowMs: (d + 1) * 3600_000 }); + } + return { ok: true, detail: `2 营业日(=30min 口径)收钟,出餐 ${servedTotal} 单,零 uncaught` }; + } catch (e) { + return { ok: false, detail: 'uncaught: ' + String((e && e.message) || e) }; + } +} diff --git a/tier2/games/night-market/scripts/sim-scan.mjs b/tier2/games/night-market/scripts/sim-scan.mjs index 2222a542..6b28c92a 100644 --- a/tier2/games/night-market/scripts/sim-scan.mjs +++ b/tier2/games/night-market/scripts/sim-scan.mjs @@ -27,6 +27,7 @@ import { RECIPES } from '../src/data/recipes.js'; import { STAFF_POOL } from '../src/data/staff.js'; import { PLOTS } from '../src/data/plots.js'; import { EQUIPMENT } from '../src/data/equipment.js'; +import { UNLOCK_TREE } from '../src/data/unlock-tree.js'; const CAREERS = Number(process.argv[2]) || 25; const DAYS = Number(process.argv[3]) || 40; @@ -144,7 +145,7 @@ mkdirSync(join(HERE, '..', 'dist'), { recursive: true }); writeFileSync(join(HERE, '..', 'dist', 'm2-sim-scan.json'), JSON.stringify(summary, null, 2)); console.log(` 局数 ${totalDays}/${CAREERS * DAYS} · 死锁 ${deadlocks} · uncaught ${uncaught} · latch 违约 ${latchViolations}`); -console.log(` 墙钟 ${(wallMs / 1000).toFixed(1)}s(${summary.wallClock.perDayMs}ms/日)· 40 日末均值:金币 ${summary.careerEndAverages.coins} 声望 ${summary.careerEndAverages.prestige} 解锁 ${summary.careerEndAverages.unlocked}/42 地块 ${summary.careerEndAverages.plots} 员工 ${summary.careerEndAverages.staff} 事件 ${summary.careerEndAverages.eventsSeen}`); +console.log(` 墙钟 ${(wallMs / 1000).toFixed(1)}s(${summary.wallClock.perDayMs}ms/日)· 40 日末均值:金币 ${summary.careerEndAverages.coins} 声望 ${summary.careerEndAverages.prestige} 解锁 ${summary.careerEndAverages.unlocked}/${UNLOCK_TREE.length} 地块 ${summary.careerEndAverages.plots} 员工 ${summary.careerEndAverages.staff} 事件 ${summary.careerEndAverages.eventsSeen}`); console.log(' 报告已落 dist/m2-sim-scan.json'); if (ok) { console.log('[night-market sim-scan] 全过:千局零死锁、零 uncaught、latch 全自然。'); diff --git a/tier2/games/night-market/scripts/tree-check.mjs b/tier2/games/night-market/scripts/tree-check.mjs index 3bbf6a56..0d1fc233 100644 --- a/tier2/games/night-market/scripts/tree-check.mjs +++ b/tier2/games/night-market/scripts/tree-check.mjs @@ -17,6 +17,8 @@ * ⑪ 员工:岗位 ∈ STAFF_ROLES、数值字段完备为正 * ⑫ 事件:类型/触发合法,修饰键 ∈ 效果词汇,特邀客 customerTypeId 在顾客表且 weight=0 * ⑬ 成就/变现位:成就 axis ∈ 快照轴、零声望奖励字段(纯度红线);变现位 slotId 唯一、reward.kind 合法 + * 【M3 一查】(内容一期) + * ⑭ 阶段任务链:40 条线性接力(恰一根/无断点无环)、轴阈值沿链递增、零声望、id 不与成就撞 * * 用法:node scripts/tree-check.mjs */ @@ -32,7 +34,7 @@ import { PLOTS } from '../src/data/plots.js'; import { EQUIPMENT } from '../src/data/equipment.js'; import { STAFF_POOL, STAFF_ROLES } from '../src/data/staff.js'; import { EVENTS } from '../src/data/events.js'; -import { ACHIEVEMENTS } from '../src/data/quests.js'; +import { ACHIEVEMENTS, STAGE_TASKS } from '../src/data/quests.js'; import { AD_SLOTS } from '../src/data/monetization.js'; import { CUSTOMER_TYPES } from '../src/data/customer-types.js'; import { PLOT_SITES } from '../src/core/layout.js'; @@ -152,6 +154,10 @@ const byId = new Map(UNLOCK_TREE.map((n) => [n.id, n])); const EFFECT_KINDS = new Set([...NODE_KINDS.filter((k) => k !== 'system' && k !== 'unlock_recipe'), 'cook_speed', 'patience_mult', 'price_mult', 'onscreen_delta', 'auto_serve_rate']); +/** 任务判定轴(core achievementSnapshot 的键;成就 ⑬ 与阶段任务 ⑭ 共用;M3 增 day)。 */ +const QUEST_AXES = new Set(['careerServed', 'careerRevenue', 'comboBest', 'recipesCollected', 'customersCollected', + 'plotsOwned', 'staffHired', 'eventsSeen', 'unlockedCount', 'day']); + // ⑨ 研发节点:cost_ingredients 引用可解、量为正、deps 直含 research 门户 { const ingIds = new Set(INGREDIENTS.map((r) => r.id)); @@ -215,17 +221,18 @@ const EFFECT_KINDS = new Set([...NODE_KINDS.filter((k) => k !== 'system' && k != else if (t.weight !== 0) bad.push(`${ev.id}:特邀客 weight≠0(会进常规抽签)`); } } - check(bad.length === 0, '⑫ 事件表类型/触发/修饰/特邀客对账', bad.join(' ') || `${EVENTS.length} 条(四类型各一)`); + const byType = {}; + for (const ev of EVENTS) byType[ev.type] = (byType[ev.type] || 0) + 1; + check(bad.length === 0, '⑫ 事件表类型/触发/修饰/特邀客对账', + bad.join(' ') || `${EVENTS.length} 条(${Object.entries(byType).map(([k, n]) => `${k}×${n}`).join(' ')})`); } // ⑬ 成就/变现位:axis 合法、零声望字段;slotId 唯一、reward 合法 { - const AXES = new Set(['careerServed', 'careerRevenue', 'comboBest', 'recipesCollected', 'customersCollected', - 'plotsOwned', 'staffHired', 'eventsSeen', 'unlockedCount']); const bad = []; for (const a of ACHIEVEMENTS) { if (a.type !== 'achievement') bad.push(`${a.id}:type`); - if (!AXES.has(a.axis)) bad.push(`${a.id}:axis=${a.axis}`); + if (!QUEST_AXES.has(a.axis)) bad.push(`${a.id}:axis=${a.axis}`); if (!(a.threshold > 0)) bad.push(`${a.id}:threshold`); if (!(a.reward_coins > 0)) bad.push(`${a.id}:reward`); if ('reward_prestige' in a || 'prestige' in a) bad.push(`${a.id}:带声望奖励(纯度红线)`); @@ -242,8 +249,40 @@ const EFFECT_KINDS = new Set([...NODE_KINDS.filter((k) => k !== 'system' && k != bad.join(' ') || `${ACHIEVEMENTS.length} 成就 ${AD_SLOTS.length} 槽`); } +// ⑭ M3 阶段任务链:type/axis/阈值/奖励合法、prev 链无断点无环且线性可达、零声望 +{ + const bad = []; + const byId = new Map(STAGE_TASKS.map((t) => [t.id, t])); + for (const t of STAGE_TASKS) { + if (t.type !== 'stage') bad.push(`${t.id}:type`); + if (!QUEST_AXES.has(t.axis)) bad.push(`${t.id}:axis=${t.axis}`); + if (!(t.threshold > 0)) bad.push(`${t.id}:threshold`); + if (!(t.reward_coins > 0)) bad.push(`${t.id}:reward`); + if ('reward_prestige' in t || 'prestige' in t) bad.push(`${t.id}:带声望奖励(纯度红线)`); + if (t.prev !== null && !byId.has(t.prev)) bad.push(`${t.id}→prev=${t.prev} 不存在`); + } + // 链形:恰一个根(prev=null),自根沿 prev 反向可达全部(线性接力,无环无孤岛) + const roots = STAGE_TASKS.filter((t) => t.prev === null); + if (roots.length !== 1) bad.push(`根数=${roots.length}(应恰 1)`); + const nextOf = new Map(STAGE_TASKS.filter((t) => t.prev).map((t) => [t.prev, t.id])); + let cur = roots[0] ? roots[0].id : null; let visited = 0; + while (cur && visited <= STAGE_TASKS.length) { visited++; cur = nextOf.get(cur) || null; } + if (visited !== STAGE_TASKS.length) bad.push(`链可达 ${visited}/${STAGE_TASKS.length}(断点或分叉)`); + // 同轴阈值沿链不降(接力语义:后面的目标不许比前面松) + const lastByAxis = new Map(); + for (const t of STAGE_TASKS) { + const prevTh = lastByAxis.get(t.axis); + if (prevTh !== undefined && t.threshold <= prevTh) bad.push(`${t.id}:${t.axis} 阈值 ${t.threshold} 未递增(前值 ${prevTh})`); + lastByAxis.set(t.axis, t.threshold); + } + // 成就与任务 id 不撞(claimed 集共用一份) + for (const a of ACHIEVEMENTS) if (byId.has(a.id)) bad.push(`id 撞:${a.id}`); + check(bad.length === 0, '⑭ 阶段任务链(40 条线性接力/轴阈值递增/零声望)', + bad.join(' ') || `${STAGE_TASKS.length} 条 · 根=${roots[0] && roots[0].id}`); +} + if (failures === 0) { - console.log('[night-market tree-check] 全过:M1 八查(树/菜谱/食材/定价)+ M2 五查(研发/地块设备/员工/事件/成就变现位)。'); + console.log('[night-market tree-check] 全过:M1 八查(树/菜谱/食材/定价)+ M2 五查(研发/地块设备/员工/事件/成就变现位)+ M3 一查(阶段任务链)。'); process.exit(0); } else { console.error(`[night-market tree-check] ${failures} 项 FAIL。`); diff --git a/tier2/games/night-market/src/core/game-core.js b/tier2/games/night-market/src/core/game-core.js index e6afc558..7aeb2884 100644 --- a/tier2/games/night-market/src/core/game-core.js +++ b/tier2/games/night-market/src/core/game-core.js @@ -36,12 +36,12 @@ import { RECIPES } from '../data/recipes.js'; import { CUSTOMER_TYPES } from '../data/customer-types.js'; import { INGREDIENTS } from '../data/ingredients.js'; import { UNLOCK_TREE } from '../data/unlock-tree.js'; -import { MILESTONES } from '../data/milestones.js'; +import { MILESTONES, buildMilestones } from '../data/milestones.js'; import { PLOTS } from '../data/plots.js'; import { EQUIPMENT } from '../data/equipment.js'; import { STAFF_POOL, STAFF_ROLES } from '../data/staff.js'; import { EVENTS } from '../data/events.js'; -import { ACHIEVEMENTS } from '../data/quests.js'; +import { ACHIEVEMENTS, STAGE_TASKS } from '../data/quests.js'; import { AD_SLOTS } from '../data/monetization.js'; import { SEATS, STOVES, BASE_SEATS, TAP_RADIUS, PLOT_SITES } from './layout.js'; import { createServiceLoop } from '../systems/service-loop.js'; @@ -57,6 +57,8 @@ import { encodeSave, decodeSave, defaultData } from './save-codec.js'; * @param {{next:()=>number,range:(a,b)=>number,pick:(a)=>any,state?:()=>number,restore?:(c:number)=>void}} deps.random 受控随机源(必传) * @param {string} [deps.deviceTier='mid'] 设备档('high'|'mid'|'low',screen_cap 钳制) * @param {number} [deps.dayLengthS] 营业日时长覆盖(秒;门/测试参数化注入口,缺省 economy_curve 单源) + * @param {Object} [deps.econ] economy_curve 键级覆盖(M3 U8b 扫描器专用注入口:两层浅并入基线曲线, + * 生产/门/渲染一律不传 = 恒用 ECONOMY_CURVE 单源引用,零行为漂移;里程碑表随覆盖曲线重建) * @returns {GameCore} */ export function createGameCore(deps) { @@ -64,7 +66,17 @@ export function createGameCore(deps) { if (!rng || typeof rng.next !== 'function') { throw new Error('[night-market core] 必须注入受控随机源 random({next,range,pick})'); // 错误路径留痕 } - const econ = ECONOMY_CURVE; + // economy_curve 装配:无覆盖 = 原引用(===ECONOMY_CURVE,回归零漂移);有覆盖 = 两层并入(键→对象|标量) + const econ = (deps && deps.econ) + ? Object.freeze(Object.fromEntries(Object.entries(ECONOMY_CURVE).map(([k, v]) => { + const ov = deps.econ[k]; + if (ov === undefined) return [k, v]; + return [k, (v && typeof v === 'object' && !Array.isArray(v) && ov && typeof ov === 'object' && !Array.isArray(ov)) + ? Object.freeze({ ...v, ...ov }) : ov]; + }))) + : ECONOMY_CURVE; + /** 里程碑表(prestige_threshold 扫描时随曲线重建;缺省 === 静态 MILESTONES)。 */ + const milestones = (deps && deps.econ && deps.econ.prestige_threshold) ? buildMilestones(econ) : MILESTONES; const dayLengthS = (deps && deps.dayLengthS) || econ.day.length_s; // ── 三资源账本与顶层状态 ────────────────────────────────────────────────── @@ -101,6 +113,8 @@ export function createGameCore(deps) { serveSettled: 0, missSettled: 0, unlocks: 0, buys: 0, expiredUnits: 0, offlineCredited: 0, // M2 取证轴 criticServed: 0, criticMissed: 0, wagesPaid: 0, adCredited: 0, staffServes: 0, + // M3 取证轴(摊主老本累计发放;定标扫描的死锁封口证据) + reliefGranted: 0, }; const pushFx = (ev) => { fx.push(ev); }; @@ -113,7 +127,8 @@ export function createGameCore(deps) { // ── 系统门户(解锁树 kind:'system' 节点 → target 系统;数据驱动,零硬编码 id)── const NODE_BY_ID = new Map(UNLOCK_TREE.map((n) => [n.id, n])); const ING_BY_ID = new Map(INGREDIENTS.map((r) => [r.id, r])); - const ACH_BY_ID = new Map(ACHIEVEMENTS.map((a) => [a.id, a])); + /** 全量任务索引(成就 12 + M3 阶段 40;claimed 集共用一份,id 全局唯一)。 */ + const QUEST_BY_ID = new Map([...ACHIEVEMENTS, ...STAGE_TASKS].map((a) => [a.id, a])); const SYS_NODE_BY_TARGET = new Map(UNLOCK_TREE.filter((n) => n.kind === 'system').map((n) => [n.target, n.id])); /** 系统是否已开张(expansion/research/staff/events;门户节点解锁即开)。 */ function isSystemEnabled(target) { @@ -376,20 +391,29 @@ export function createGameCore(deps) { fx.push({ type: 'miss', seat: ev.seat, typeName: ev.typeName }); } - /** 成就快照(达成态即时核算;quests 表 axis 的数据源单点)。 */ + /** 任务快照(成就+阶段任务共用;quests 表 axis 的数据源单点;M3 增 day 轴)。 */ function achievementSnapshot() { return { careerServed: career.served, careerRevenue: career.revenue, comboBest: state.comboBest, recipesCollected: cookedRecipeIds.size, customersCollected: servedTypeIds.size, plotsOwned: expansion.ownedCount(), staffHired: staffSys.hiredCount(), eventsSeen: eventSys.seenCount(), unlockedCount: unlocked.size, + day: state.day, // M3 阶段任务「熬到第 N 夜」轴 }; } + /** 任务可领判定(阶段任务另须前置已领 = 接力链;成就无前置)。 */ + function questClaimable(row, snap) { + if (claimed.has(row.id)) return false; + if ((snap[row.axis] || 0) < row.threshold) return false; + if (row.type === 'stage' && row.prev && !claimed.has(row.prev)) return false; + return true; + } + /** 日结里程碑判定:未达成者按生涯轴对账,达成即 +声望(一次性)。返回达成列表。 */ function checkMilestones() { const hit = []; - for (const m of MILESTONES) { + for (const m of milestones) { if (milestonesHit.has(m.id)) continue; const v = m.axis === 'revenue' ? career.revenue : career.served; if (v >= m.threshold) { @@ -482,19 +506,41 @@ export function createGameCore(deps) { }, /** - * 自动补货:按食材表序补到 stock_target(economy_curve.restock 缺省策略;资金不足补到耗尽,确定性顺序)。 + * 自动补货(M3 定标改均衡轮转):表序轮转、每轮每种补 1 件,预算内轮到哪算哪、全到 stock_target 即止。 + * 曾为「表序补到耗尽」(M1 简化口径)——千局扫坐实其低资金病态:救济金全砸表首单一食材、 + * 没有任何一道菜配齐,回血路蒸发 → 破产死锁;均衡口径保证低资金也留下「可做单」的食材组合。 + * 确定性:轮转顺序 = 表序,预算判定 = 累计规划总价(折扣按种类整批 ceil)首次超限即停。 * @returns {{spent:number, bought:Array<{id:string, units:number}>}} */ autoRestock() { + // ① 轮转规划(纯计算,不动账):plan[i] = 该食材本次补的件数 + const plan = new Map(); + const needOf = new Map(INGREDIENTS.map((row) => [row.id, Math.max(0, row.stock_target - totalUnits(row.id))])); + const costOf = () => { + let s = 0; + for (const row of INGREDIENTS) { + const u = plan.get(row.id) || 0; + if (u > 0) s += Math.ceil(u * row.price * mod.discount); + } + return s; + }; + let grew = true; + outer: while (grew) { + grew = false; + for (const row of INGREDIENTS) { + const u = plan.get(row.id) || 0; + if (u >= needOf.get(row.id)) continue; + plan.set(row.id, u + 1); + if (costOf() > state.coins) { plan.set(row.id, u); break outer; } // 预算触顶:回退本件即收 + grew = true; + } + } + // ② 执行(按表序整批入库;总价 = 规划口径,与判定一致) let spent = 0; const bought = []; for (const row of INGREDIENTS) { - let need = row.stock_target - totalUnits(row.id); - if (need <= 0) continue; - // 按余钱缩量(整单向上取整口径下,先试满额,不够则逐步缩) - let units = need; - let total = Math.ceil(units * row.price * mod.discount); - while (units > 0 && total > state.coins) { units--; total = Math.ceil(units * row.price * mod.discount); } + const units = plan.get(row.id) || 0; if (units <= 0) continue; + const total = Math.ceil(units * row.price * mod.discount); state.coins -= total; state.dayNet -= total; (stock[row.id] = stock[row.id] || []).push({ qty: units, freshLeft: row.freshness_days + mod.freshBonus }); @@ -571,6 +617,15 @@ export function createGameCore(deps) { settleCounts.wagesPaid += wagesPaid; fx.push({ type: 'wages', due: wagesDue, paid: wagesPaid }); } + // ①c M3 摊主老本(破产死局回血地板:薪资后金币不足补到 relief.floor_coins; + // 只补差额、不入 dayNet——千局扫坐实的中期断供死锁封口,economy_curve 键注有定标依据) + let reliefGranted = 0; + if (state.coins < econ.relief.floor_coins) { + reliefGranted = econ.relief.floor_coins - state.coins; + state.coins = econ.relief.floor_coins; + settleCounts.reliefGranted += reliefGranted; + fx.push({ type: 'relief', granted: reliefGranted }); + } // ② 日志(近 3 日;离线速率数据源——含日薪后的净入) dayLog.push({ day: state.day, net: state.dayNet, minutes: dayLengthS / 60 }); if (dayLog.length > 3) dayLog = dayLog.slice(-3); @@ -641,7 +696,7 @@ export function createGameCore(deps) { eventSys.importState(data); cookedRecipeIds = new Set((data.collectionsRecipes || []).filter((id) => RECIPES.some((r) => r.id === id))); servedTypeIds = new Set((data.collectionsCustomers || []).filter((id) => CUSTOMER_TYPES.some((t) => t.id === id))); - claimed = new Set((data.achievementsClaimed || []).filter((id) => ACH_BY_ID.has(id))); + claimed = new Set((data.achievementsClaimed || []).filter((id) => QUEST_BY_ID.has(id))); monetize.onDayAdvance(); // 广告运行态清零(限次按日;导入 = 当日重开) recomputeModifiers(); // 日中瞬态清零,落日初(存档口径:日中退出回当日开局) @@ -708,15 +763,16 @@ export function createGameCore(deps) { resolveAd(slotId, result) { return monetize.resolve(slotId, result); }, - /** 领取成就奖励(达成才可领、一次性;奖励只发金币 = 声望纯度红线)。 */ + /** 领取任务奖励(成就+阶段任务同口;达成才可领、一次性、阶段另须前置已领;只发金币 = 声望纯度红线)。 */ claimAchievement(achId) { - const row = ACH_BY_ID.get(achId); + const row = QUEST_BY_ID.get(achId); if (!row) return { ok: false, reason: 'not_found' }; if (claimed.has(achId)) return { ok: false, reason: 'already' }; const snap = achievementSnapshot(); if ((snap[row.axis] || 0) < row.threshold) return { ok: false, reason: 'not_achieved' }; + if (row.type === 'stage' && row.prev && !claimed.has(row.prev)) return { ok: false, reason: 'prev_unclaimed' }; // 接力链 claimed.add(achId); - state.coins += row.reward_coins; // 成就奖励 = 变现外快口径,不入 dayNet(经营净入纯度) + state.coins += row.reward_coins; // 任务奖励 = 变现外快口径,不入 dayNet(经营净入纯度) fx.push({ type: 'achievement', achId, name: row.name, reward: row.reward_coins }); return { ok: true, reward: row.reward_coins }; }, @@ -742,21 +798,24 @@ export function createGameCore(deps) { })) .sort((a, b) => a.cost - b.cost || (a.id < b.id ? -1 : 1)); }, - /** 图鉴成就页:双图鉴投影 + 成就 12 条(达成/已领/可领)。 */ + /** 图鉴任务页:双图鉴投影 + 成就 12 + 阶段任务 40(达成/已领/可领;阶段带接力链语义)。 */ codexView() { const snap = achievementSnapshot(); + const rowView = (a) => { + const cur = snap[a.axis] || 0; + const achieved = cur >= a.threshold; + return { + id: a.id, name: a.name, type: a.type, axis: a.axis, threshold: a.threshold, reward: a.reward_coins, + progress: Math.min(cur, a.threshold), achieved, claimed: claimed.has(a.id), + claimable: questClaimable(a, snap), + prev: a.prev || null, + }; + }; return { recipes: RECIPES.map((r) => ({ id: r.id, name: r.name, rarity: r.rarity, collected: cookedRecipeIds.has(r.id) })), customers: CUSTOMER_TYPES.map((t) => ({ id: t.id, name: t.name, rarity: t.rarity, collected: servedTypeIds.has(t.id) })), - achievements: ACHIEVEMENTS.map((a) => { - const cur = snap[a.axis] || 0; - const achieved = cur >= a.threshold; - return { - id: a.id, name: a.name, axis: a.axis, threshold: a.threshold, reward: a.reward_coins, - progress: Math.min(cur, a.threshold), achieved, claimed: claimed.has(a.id), - claimable: achieved && !claimed.has(a.id), - }; - }), + achievements: ACHIEVEMENTS.map(rowView), + tasks: STAGE_TASKS.map(rowView), // M3 阶段链(UI 只展示当前段;claimable 已含前置判定) }; }, @@ -769,7 +828,7 @@ export function createGameCore(deps) { const v = loop.view(); const snap = achievementSnapshot(); let claimable = 0; - for (const a of ACHIEVEMENTS) if (!claimed.has(a.id) && (snap[a.axis] || 0) >= a.threshold) claimable++; + for (const a of QUEST_BY_ID.values()) if (questClaimable(a, snap)) claimable++; return { phase: state.phase, day: state.day, @@ -790,6 +849,7 @@ export function createGameCore(deps) { .map((c) => ({ customerId: c.id, x: c.x, y: c.y, patienceLeft: c.patienceLeft, servable: canServe(c.orderRecipeId) })), menuSize: menu().length, unlockedCount: unlocked.size, + treeSize: UNLOCK_TREE.length, // 解锁面分母(HUD/仿真消耗率;M3 起内容随批扩,不再写死) offlineEnabled: mod.offlineEnabled, stockSummary: INGREDIENTS.map((r) => { const units = totalUnits(r.id); diff --git a/tier2/games/night-market/src/data/customer-types.js b/tier2/games/night-market/src/data/customer-types.js index 3a7f060a..d0763f95 100644 --- a/tier2/games/night-market/src/data/customer-types.js +++ b/tier2/games/night-market/src/data/customer-types.js @@ -1,23 +1,37 @@ /** - * src/data/customer-types.js —— 顾客类型表(M0 子集:6 类 · 底稿 §1.1 customer_type 表字段的开发装载) - * owner:北极星顶级线①(W-POLARIS)· 开发设计书 §3 内容量表(全量 30 类归 U13 灌装批) + * src/data/customer-types.js —— 顾客类型表(M3 一期:19 类 · 底稿 §1.1 customer_type 表字段的开发装载) + * owner:北极星顶级线①(W-POLARIS)· 开发设计书 §3 内容量表(全量 30 类归 U13 灌装批;M3 目标 18+) * * 字段口径(底稿 customer_type 表):id / 名称 / 耐心基线秒(5–7 带内) / 出现权重 / 偏好菜品 tag[] / - * 稀有度('common'|'rare') / 小费基础系数。立绘 assetId 归美术批(M4),M0 程序化占位不带。 - * 人物全原创(clean-room)。 + * 稀有度('common'|'rare'|'critic') / 小费基础系数。立绘 assetId 归美术批(M4),程序化占位不带。 + * 人物全原创(clean-room,市井烟火调性);weight=0 = 事件特邀专属,不进常规抽签。 */ 'use strict'; export const CUSTOMER_TYPES = Object.freeze([ + // ── 常客(M0 五类;权重 = 出现频率,总量只影响相对占比)── { id: 'c_yeban', name: '夜班青年', patience_base_s: 6.0, weight: 22, prefer_tags: ['quick', 'hot'], rarity: 'common', tip_coef: 0.10 }, { id: 'c_xuesheng', name: '放学学生', patience_base_s: 5.5, weight: 22, prefer_tags: ['sweet', 'quick'], rarity: 'common', tip_coef: 0.08 }, { id: 'c_daye', name: '遛弯大爷', patience_base_s: 7.0, weight: 18, prefer_tags: ['classic'], rarity: 'common', tip_coef: 0.12 }, { id: 'c_qinglv', name: '逛街情侣', patience_base_s: 6.5, weight: 18, prefer_tags: ['sweet', 'seafood'], rarity: 'common', tip_coef: 0.15 }, { id: 'c_ganfan', name: '干饭主播', patience_base_s: 5.0, weight: 14, prefer_tags: ['spicy', 'hot'], rarity: 'common', tip_coef: 0.20 }, - // 稀有客:耐心 ×1.4(economy_curve.patience.rare_mult 在生成时统一乘,表存基线),小费最高,认高稀有度菜 - { id: 'c_shike', name: '挑剔食客', patience_base_s: 6.0, weight: 6, prefer_tags: ['rare_fav', 'seafood'], rarity: 'rare', tip_coef: 0.35 }, - // M2 事件特邀客:评论家(weight 0 = 不进常规客流抽签,只经事件系统 spawnSpecial 登场; - // 好评+声望/怠慢重扣口碑的结算走 economy_curve.critic,底稿 ⑤「高风险高回报」) + // ── 常客(M3 一期 +8:夜市街面的众生相)── + { id: 'c_chuzushiji', name: '出租车师傅', patience_base_s: 6.5, weight: 16, prefer_tags: ['quick', 'hot'], rarity: 'common', tip_coef: 0.10 }, + { id: 'c_guangchangwu', name: '广场舞阿姨', patience_base_s: 7.0, weight: 15, prefer_tags: ['classic', 'sweet'], rarity: 'common', tip_coef: 0.10 }, + { id: 'c_daijia', name: '代驾小哥', patience_base_s: 5.0, weight: 14, prefer_tags: ['quick'], rarity: 'common', tip_coef: 0.12 }, + { id: 'c_jiabanzu', name: '加班白领', patience_base_s: 5.5, weight: 15, prefer_tags: ['hot', 'classic'], rarity: 'common', tip_coef: 0.15 }, + { id: 'c_yepaozu', name: '夜跑青年', patience_base_s: 5.0, weight: 12, prefer_tags: ['sweet', 'quick'], rarity: 'common', tip_coef: 0.10 }, + { id: 'c_sushetuan', name: '宿舍夜聊团', patience_base_s: 6.0, weight: 14, prefer_tags: ['spicy', 'sweet'], rarity: 'common', tip_coef: 0.08 }, + { id: 'c_xiabanbaoan', name: '下班保安', patience_base_s: 7.0, weight: 12, prefer_tags: ['classic', 'hot'], rarity: 'common', tip_coef: 0.10 }, + { id: 'c_daidanqishou', name: '待单骑手', patience_base_s: 5.0, weight: 14, prefer_tags: ['quick', 'spicy'], rarity: 'common', tip_coef: 0.12 }, + // ── 稀有客(M0 一类 + M3 +3):耐心 ×1.4(economy_curve.patience.rare_mult 生成时统一乘),小费高,认高稀有度菜 + { id: 'c_shike', name: '挑剔食客', patience_base_s: 6.0, weight: 6, prefer_tags: ['rare_fav', 'seafood'], rarity: 'rare', tip_coef: 0.35 }, + { id: 'c_laotao', name: '深巷老饕', patience_base_s: 6.0, weight: 5, prefer_tags: ['rare_fav', 'seafood'], rarity: 'rare', tip_coef: 0.32 }, + { id: 'c_tandianbozhu', name: '探店博主', patience_base_s: 5.5, weight: 5, prefer_tags: ['rare_fav', 'sweet'], rarity: 'rare', tip_coef: 0.30 }, + { id: 'c_shoucangjia', name: '夜宵收藏家', patience_base_s: 6.5, weight: 4, prefer_tags: ['rare_fav', 'hot'], rarity: 'rare', tip_coef: 0.35 }, + // ── 事件特邀客(weight 0 = 只经事件系统 spawnSpecial 登场;critic 结算走 economy_curve.critic, + // 底稿 ⑤「高风险高回报」:好评+声望/怠慢重扣口碑)── { id: 'c_pinglunjia', name: '美食评论家', patience_base_s: 7.0, weight: 0, prefer_tags: ['rare_fav'], rarity: 'critic', tip_coef: 0.50 }, + { id: 'c_shitan', name: '深夜食探', patience_base_s: 7.0, weight: 0, prefer_tags: ['rare_fav', 'spicy'], rarity: 'critic', tip_coef: 0.45 }, ]); diff --git a/tier2/games/night-market/src/data/economy-curve.js b/tier2/games/night-market/src/data/economy-curve.js index ae649352..535a8786 100644 --- a/tier2/games/night-market/src/data/economy-curve.js +++ b/tier2/games/night-market/src/data/economy-curve.js @@ -47,8 +47,10 @@ export const ECONOMY_CURVE = Object.freeze({ // ── M1+ 预留键(键清单 v1 完整性;M0 不消费)──────────────────────────────── /** 升级成本曲线:×1.15/级(前期)→×1.22/级(第 6 地块起)。M1 经济骨架消费。〔草案〕 */ upgrade_cost: { early_mult: 1.15, late_mult: 1.22, late_from_plot: 6 }, - /** 离线收益:速率 = 近 3 营业日每分钟净收入均值 ×10%,封顶 2h(设计书 §4 离线口径;M1 消费)。〔草案·与判据5未收敛,U9 首查〕 */ - offline_yield: { rate_of_online: 0.10, cap_hours: 2 }, + /** 离线收益:速率 = 近 3 营业日每分钟净收入均值 ×10%,封顶 1.25h(75 分钟)。 + * 〔M3 定标·U9 首查收敛:下沿玩家模型(日在线 15min+领满封顶)离线占比 = cap×0.10/(cap×0.10+15)—— + * cap 2h→44.4% 违判据5(≤35%),cap 75min→33.3% 收敛留 5% 余;速率 0.10 保留。判据5 门本体归 M5 真验〕 */ + offline_yield: { rate_of_online: 0.10, cap_hours: 1.25 }, /** 员工效率/日薪曲线(M2 员工系统消费:效果 ×1.12/级、日薪 ×1.10/级,升级价按 upgrade_cost.early_mult)。〔草案〕 */ staff_efficiency: { eff_mult_per_level: 1.12, wage_mult_per_level: 1.10 }, /** 下一地块定价 = 当前每分钟净收入 × 25–35 分钟工期反推(M2 地块表先落显式价,U9 定标时按此键回推校准)。〔草案〕 */ @@ -61,8 +63,9 @@ export const ECONOMY_CURVE = Object.freeze({ starting_coins: 40, /** 开业赠料:day1 免费入库 = 各食材 stock_target 全额(食材表单源);不扣初始资金。 */ starting_stock: { grant_full_target: true }, - /** 日结自动补货缺省策略:补到 stock_target;资金不足按表序补到耗尽(确定性顺序)。 */ - restock: { to_target: true }, + /** 日结自动补货缺省策略:补到 stock_target;资金不足按表序**均衡轮转**(每轮每种 1 件,预算触顶即停)。 + * 〔M3 定标·曾为「表序补到耗尽」:低资金全砸表首单一食材 → 无菜可做 → 破产死锁(千局扫坐实),改均衡〕 */ + restock: { to_target: true, balanced: true }, // ── M2 八系统消费键(additive;〔草案·首轮仿真回填后锁〕)──────────────────── /** 变现位奖励参数(mock 口径,设计书 §7 广告红线;槽位属性 per_day/cooldown 归 monetization 表): @@ -70,4 +73,11 @@ export const ECONOMY_CURVE = Object.freeze({ ad: { speedup_mult: 1.5, speedup_duration_s: 60, pending_timeout_s: 8 }, /** 评论家事件结算:好评 = 声望+1(M2 起声望第二来源,设计书 §4)+口碑+5;怠慢 = 口碑 −8。 */ critic: { prestige: 1, rep_serve_bonus: 5, rep_miss_penalty: -8 }, + + // ── M3 定标增补键 ───────────────────────────────────────────────────────── + /** 摊主老本(破产死局的回血地板,设计书 §4「卡点是诱惑不是墙」+ 品类 rubric 条4「低成本回血路」): + * 日结(薪资后)金币不足即补到 floor_coins——够买最便宜配方数份,勤快一日即可回血; + * 只补差额不叠加(无法刷)、不入 dayNet(不污染离线速率)。 + * 〔M3 定标·依据 = 千局扫 11/25 生涯中期薪资+补货抽干现金成永久零出餐死锁,加地板后复扫零死锁〕 */ + relief: { floor_coins: 40 }, }); diff --git a/tier2/games/night-market/src/data/equipment.js b/tier2/games/night-market/src/data/equipment.js index 3fd41d9b..87eb4a42 100644 --- a/tier2/games/night-market/src/data/equipment.js +++ b/tier2/games/night-market/src/data/equipment.js @@ -10,10 +10,10 @@ 'use strict'; export const EQUIPMENT = Object.freeze([ - { id: 'e_menghuozao', name: '猛火灶头', type: '灶具', price_coins: 260, effect: { kind: 'cook_speed', value: 1.08 } }, - { id: 'e_baowentai', name: '保温台', type: '灶具', price_coins: 180, effect: { kind: 'cook_speed', value: 1.06 } }, - { id: 'e_zheyupeng', name: '遮雨棚', type: '服务', price_coins: 150, effect: { kind: 'patience_bonus', value: 0.3 } }, - { id: 'e_shoukuanling', name: '收款铃', type: '服务', price_coins: 140, effect: { kind: 'tip_bonus', value: 0.02 } }, + { id: 'e_menghuozao', name: '猛火灶头', type: '灶具', price_coins: 420, effect: { kind: 'cook_speed', value: 1.08 } }, + { id: 'e_baowentai', name: '保温台', type: '灶具', price_coins: 300, effect: { kind: 'cook_speed', value: 1.06 } }, + { id: 'e_zheyupeng', name: '遮雨棚', type: '服务', price_coins: 240, effect: { kind: 'patience_bonus', value: 0.3 } }, + { id: 'e_shoukuanling', name: '收款铃', type: '服务', price_coins: 350, effect: { kind: 'tip_bonus', value: 0.02 } }, { id: 'e_xiaobinggui', name: '摊头小冰柜', type: '仓储', price_coins: 220, effect: { kind: 'freshness_bonus', value: 1 } }, - { id: 'e_hongdenglong', name: '红灯笼', type: '装饰', price_coins: 120, effect: { kind: 'spawn_rate', value: 0.96 } }, + { id: 'e_hongdenglong', name: '红灯笼', type: '装饰', price_coins: 200, effect: { kind: 'spawn_rate', value: 0.96 } }, ]); diff --git a/tier2/games/night-market/src/data/events.js b/tier2/games/night-market/src/data/events.js index 3bb78f2b..d953aa54 100644 --- a/tier2/games/night-market/src/data/events.js +++ b/tier2/games/night-market/src/data/events.js @@ -36,4 +36,43 @@ export const EVENTS = Object.freeze([ trigger: { kind: 'day_mod', every: 5 }, at_progress: 0.1, duration_ratio: 0.5, modifiers: { spawn_rate: 0.85, tip_bonus: 0.05 }, banner: '夜市节!全街热闹' }, + + // ── M3 一期 +8(4→12;新行一律追加在既有 4 行之后 = 掷签顺序前 4 位不变,回归随机流保真; + // chance 型一律 from_day ≥2 = 首日仍零掷签)── + // 潮汐 ×2:下班潮(早窗)与晚场加座(尾窗)——把一日切出多个紧张拍点 + { id: 'ev_xiabanchao', name: '下班潮', type: 'tide', + trigger: { kind: 'chance', p: 0.35, from_day: 2 }, at_progress: 0.12, duration_ratio: 0.15, + modifiers: { spawn_rate: 0.75, patience_mult: 0.92 }, + banner: '下班潮涌过来了!' }, + { id: 'ev_wanchao', name: '晚场加座', type: 'tide', + trigger: { kind: 'chance', p: 0.25, from_day: 4 }, at_progress: 0.72, duration_ratio: 0.18, + modifiers: { spawn_rate: 0.8, onscreen_delta: 1 }, + banner: '晚场客上桌,加把劲' }, + // 天气 ×2:起风(客稀慢摇)与闷热(耐心变差)——氛围差异化,数值一松一紧 + { id: 'ev_guafeng', name: '起风了', type: 'weather', + trigger: { kind: 'chance', p: 0.15, from_day: 3 }, at_progress: 0, duration_ratio: 1, + modifiers: { spawn_rate: 1.12, patience_mult: 1.05 }, + banner: '起风了,街上人稀' }, + { id: 'ev_menre', name: '闷热夜', type: 'weather', + trigger: { kind: 'chance', p: 0.15, from_day: 3 }, at_progress: 0.3, duration_ratio: 0.5, + modifiers: { patience_mult: 0.9, spawn_rate: 0.95 }, + banner: '闷热难耐,客人都急' }, + // 评论家 ×1:深夜食探(第二位特邀 critic;晚窗登场,与评论家错峰) + { id: 'ev_shitan', name: '深夜食探', type: 'critic', + trigger: { kind: 'chance', p: 0.20, from_day: 5 }, at_progress: 0.55, duration_ratio: 0.35, + modifiers: {}, customerTypeId: 'c_shitan', + banner: '深夜食探暗访中…' }, + // 限时 ×3:灯会(慢节奏高小费)/美食周(客密+溢价)/半价回馈夜(低价换爆客的取舍拍点) + { id: 'ev_dengjie', name: '灯会夜', type: 'festival', + trigger: { kind: 'day_mod', every: 10 }, at_progress: 0.1, duration_ratio: 0.6, + modifiers: { spawn_rate: 0.8, tip_bonus: 0.08 }, + banner: '灯会夜!人间烟火' }, + { id: 'ev_meishizhou', name: '街口美食周', type: 'festival', + trigger: { kind: 'day_mod', every: 8 }, at_progress: 0.2, duration_ratio: 0.5, + modifiers: { spawn_rate: 0.85, price_mult: 1.05 }, + banner: '美食周开街!' }, + { id: 'ev_banjiaye', name: '半价回馈夜', type: 'festival', + trigger: { kind: 'chance', p: 0.12, from_day: 6 }, at_progress: 0.4, duration_ratio: 0.3, + modifiers: { price_mult: 0.85, spawn_rate: 0.6, tip_bonus: 0.03 }, + banner: '半价回馈!客如潮水' }, ]); diff --git a/tier2/games/night-market/src/data/milestones.js b/tier2/games/night-market/src/data/milestones.js index ef60cfcb..68669281 100644 --- a/tier2/games/night-market/src/data/milestones.js +++ b/tier2/games/night-market/src/data/milestones.js @@ -1,41 +1,49 @@ /** - * src/data/milestones.js —— 经营成就里程碑表(M1 · 声望的唯一来源) + * src/data/milestones.js —— 经营成就里程碑表(M1 · 声望的唯一来源;M3 曲线派生函数化) * owner:北极星顶级线①(W-POLARIS)· 开发设计书 §4 三资源分工 * * 【声望纪律】声望 = 解锁硬轴,只从经营成就(本表)与评论家事件(M2 事件系统)来,**不可购买** * (进度纯度红线,设计书 §4)。M1 里程碑两轴:生涯流水(revenue)/ 生涯出餐(served); * 判定时机 = 日结(settleAndAdvanceDay),达成即 +prestige,一次性不重复。 * - * 【单源纪律】流水前三档直接取 economy_curve.prestige_threshold.tiers(22/55/100), - * 后段为其 ×5/×20 的生涯化放大列(设计书 §4「跨日放大」)——本表 import 曲线表推导,零双写。 - * 〔草案·首轮仿真回填后锁〕 + * 【单源纪律】流水阈值全部由 economy_curve.prestige_threshold 推导,零双写。 + * 【M3 函数化】buildMilestones(econ) 供 U8b 扫描器按扰动曲线重建里程碑(prestige_threshold + * 是定标扫描轴之一);缺省 MILESTONES = buildMilestones(ECONOMY_CURVE),外部消费面不变。 + * 〔阈值口径 = M3 定标·见 docs/m3-数值定标报告.md;放大列 ×20/×100/×400 保留〕 */ 'use strict'; import { ECONOMY_CURVE } from './economy-curve.js'; -const T = ECONOMY_CURVE.prestige_threshold.tiers; // [22, 55, 100] +/** + * 由 economy_curve 构建里程碑表(prestige_threshold.tiers 单源推导)。 + * 放大列 ×20/×100/×400:tiers 是单局金标尺度(mini-肥鹅 20→100 币),本作单营业日流水 ≈ 数千, + * 按「首日达成第一组、三五日第二组、旬日第三组」的节奏取营业日尺度放大。 + * @param {Object} econ economy_curve(prestige_threshold 消费方) + */ +export function buildMilestones(econ) { + const T = econ.prestige_threshold.tiers; // 缺省 [22, 55, 100] + return Object.freeze([ + // 生涯流水九档(tiers ×20 / ×100 / ×400 生涯化) + { id: 'm_rev_1', name: '第一桶金', axis: 'revenue', threshold: T[0] * 20, prestige: 1 }, + { id: 'm_rev_2', name: '小摊初旺', axis: 'revenue', threshold: T[1] * 20, prestige: 1 }, + { id: 'm_rev_3', name: '站稳脚跟', axis: 'revenue', threshold: T[2] * 20, prestige: 1 }, + { id: 'm_rev_4', name: '街坊常客', axis: 'revenue', threshold: T[0] * 100, prestige: 1 }, + { id: 'm_rev_5', name: '夜市有名', axis: 'revenue', threshold: T[1] * 100, prestige: 1 }, + { id: 'm_rev_6', name: '一条街招牌', axis: 'revenue', threshold: T[2] * 100, prestige: 1 }, + { id: 'm_rev_7', name: '日进斗金', axis: 'revenue', threshold: T[0] * 400, prestige: 1 }, + { id: 'm_rev_8', name: '生意兴隆', axis: 'revenue', threshold: T[1] * 400, prestige: 1 }, + { id: 'm_rev_9', name: '传说摊主', axis: 'revenue', threshold: T[2] * 400, prestige: 1 }, + // 生涯出餐三档 + { id: 'm_srv_1', name: '百单达成', axis: 'served', threshold: 100, prestige: 1 }, + { id: 'm_srv_2', name: '三百单老手', axis: 'served', threshold: 300, prestige: 1 }, + { id: 'm_srv_3', name: '八百单宗师', axis: 'served', threshold: 800, prestige: 1 }, + ]); +} -/** 里程碑行:{id, name, axis:'revenue'|'served', threshold, prestige}。 - * 放大列 ×20/×100/×400:tiers 是单局金标尺度(mini-肥鹅 20→100 币),本作单营业日流水 ≈ 数千, - * 按「首日达成第一组、三五日第二组、旬日第三组」的节奏取营业日尺度放大。〔草案·仿真回填后锁〕 */ -export const MILESTONES = Object.freeze([ - // 生涯流水九档(tiers ×20 / ×100 / ×400 生涯化) - { id: 'm_rev_1', name: '第一桶金', axis: 'revenue', threshold: T[0] * 20, prestige: 1 }, - { id: 'm_rev_2', name: '小摊初旺', axis: 'revenue', threshold: T[1] * 20, prestige: 1 }, - { id: 'm_rev_3', name: '站稳脚跟', axis: 'revenue', threshold: T[2] * 20, prestige: 1 }, - { id: 'm_rev_4', name: '街坊常客', axis: 'revenue', threshold: T[0] * 100, prestige: 1 }, - { id: 'm_rev_5', name: '夜市有名', axis: 'revenue', threshold: T[1] * 100, prestige: 1 }, - { id: 'm_rev_6', name: '一条街招牌', axis: 'revenue', threshold: T[2] * 100, prestige: 1 }, - { id: 'm_rev_7', name: '日进斗金', axis: 'revenue', threshold: T[0] * 400, prestige: 1 }, - { id: 'm_rev_8', name: '生意兴隆', axis: 'revenue', threshold: T[1] * 400, prestige: 1 }, - { id: 'm_rev_9', name: '传说摊主', axis: 'revenue', threshold: T[2] * 400, prestige: 1 }, - // 生涯出餐三档 - { id: 'm_srv_1', name: '百单达成', axis: 'served', threshold: 100, prestige: 1 }, - { id: 'm_srv_2', name: '三百单老手', axis: 'served', threshold: 300, prestige: 1 }, - { id: 'm_srv_3', name: '八百单宗师', axis: 'served', threshold: 800, prestige: 1 }, -]); +/** 缺省里程碑表(生产曲线;既有消费面原样)。 */ +export const MILESTONES = buildMilestones(ECONOMY_CURVE); /** 里程碑可获声望总额(tree-check ⑤ 可达性上界)。 */ export const MAX_PRESTIGE = MILESTONES.reduce((s, m) => s + m.prestige, 0); diff --git a/tier2/games/night-market/src/data/plots.js b/tier2/games/night-market/src/data/plots.js index d32302a6..ff411a43 100644 --- a/tier2/games/night-market/src/data/plots.js +++ b/tier2/games/night-market/src/data/plots.js @@ -13,7 +13,7 @@ export const PLOTS = Object.freeze([ { id: 'p_yihao', name: '一号摊(起步)', type: 'stall', price_coins: 0, min_prestige: 0, slots: 2, base: true }, - { id: 'p_xiangkou', name: '巷口小摊', type: 'stall', price_coins: 600, min_prestige: 6, slots: 1 }, - { id: 'p_qiaotou', name: '桥头夜摊', type: 'stall', price_coins: 1100, min_prestige: 8, slots: 1 }, - { id: 'p_zhuanjiao', name: '转角小店', type: 'shop', price_coins: 2000, min_prestige: 10, slots: 2 }, + { id: 'p_xiangkou', name: '巷口小摊', type: 'stall', price_coins: 900, min_prestige: 6, slots: 1 }, + { id: 'p_qiaotou', name: '桥头夜摊', type: 'stall', price_coins: 2200, min_prestige: 8, slots: 1 }, + { id: 'p_zhuanjiao', name: '转角小店', type: 'shop', price_coins: 4500, min_prestige: 10, slots: 2 }, ]); diff --git a/tier2/games/night-market/src/data/quests.js b/tier2/games/night-market/src/data/quests.js index fe5d4dc5..28a7e1c1 100644 --- a/tier2/games/night-market/src/data/quests.js +++ b/tier2/games/night-market/src/data/quests.js @@ -1,13 +1,13 @@ /** - * src/data/quests.js —— 任务表·成就子集(M2 · 成就图鉴结构;底稿 §1.1 quest 表) - * owner:北极星顶级线①(W-POLARIS)· 开发设计书 §2 缺口 1(quest 类型枚举扩「成就」,不加新表) - * 全量(引导 20 + 阶段 40 + 每日 30 + 成就 60)归 U13 灌装批;M2 最小集 = 成就 12 条(四组各有代表)。 + * src/data/quests.js —— 任务表(成就 12 + M3 阶段任务 40;底稿 §1.1 quest 表) + * owner:北极星顶级线①(W-POLARIS)· 开发设计书 §2 缺口 1 + §3 内容量表(引导 20/每日 30 归 U13) * - * 字段口径(quest 表 + 缺口 1 处置):id / type('achievement';每日/阶段/引导归 U13)/ 名称 / - * axis(判定轴:core 成就快照的键)/ threshold / reward_coins(奖励{货币,数量})。 - * 【声望纯度红线】成就奖励只发金币——声望仅从里程碑与评论家事件来(设计书 §4),本表零声望字段。 - * 【判定口径】达成 = 快照轴 ≥ threshold(核算即时,不落存档);领取(claimed)一次性、入存档。 - * 〔草案·首轮仿真回填后锁〕内容全原创(clean-room)。 + * 字段口径(quest 表):id / type('achievement'|'stage')/ 名称 / axis(判定轴:core 成就快照的键, + * M3 增 'day' 轴)/ threshold / reward_coins(奖励{货币,数量})/ prev(阶段任务前置 ref,线性链)。 + * 【声望纯度红线】任务奖励只发金币——声望仅从里程碑与评论家事件来(设计书 §4),本表零声望字段。 + * 【判定口径】达成 = 快照轴 ≥ threshold(核算即时,不落存档);领取(claimed)一次性、入存档; + * 阶段任务另须前置已领(手把手接力,「任意时刻都有下一件事」的显式骨架,设计书 §3)。 + * 【定标口径】阶段阈值按 M3 千局扫描的进度中位数排布(docs/m3-数值定标报告.md);内容全原创(clean-room)。 */ 'use strict'; @@ -30,3 +30,55 @@ export const ACHIEVEMENTS = Object.freeze([ { id: 'a_guyuan1', type: 'achievement', name: '首位伙计', axis: 'staffHired', threshold: 1, reward_coins: 80 }, { id: 'a_shijian3', type: 'achievement', name: '见过世面', axis: 'eventsSeen', threshold: 3, reward_coins: 150 }, ]); + +/** + * M3 阶段任务 40 条(type:'stage',线性链 prev 接力):10 小时进度的显式骨架—— + * 从「第一锅热气」到「街口传说」,任意时刻玩家眼前都有下一件事(设计书 §3 内容架构 meta 层)。 + * 阈值排布对齐千局扫描进度中位:前 10 条落首小时、11–25 落 2–5h、26–40 落 5–10h。 + */ +export const STAGE_TASKS = Object.freeze([ + // ── 开张段(首 1 小时:day1–4;手把手起步)── + { id: 't01', type: 'stage', name: '第一锅热气', axis: 'careerServed', threshold: 5, reward_coins: 30, prev: null }, + { id: 't02', type: 'stage', name: '开张小账', axis: 'careerRevenue', threshold: 60, reward_coins: 40, prev: 't01' }, + { id: 't03', type: 'stage', name: '添个新菜', axis: 'unlockedCount', threshold: 2, reward_coins: 50, prev: 't02' }, + { id: 't04', type: 'stage', name: '十五单出摊', axis: 'careerServed', threshold: 15, reward_coins: 50, prev: 't03' }, + { id: 't05', type: 'stage', name: '两百金流水', axis: 'careerRevenue', threshold: 200, reward_coins: 60, prev: 't04' }, + { id: 't06', type: 'stage', name: '熬过头一晚', axis: 'day', threshold: 2, reward_coins: 60, prev: 't05' }, + { id: 't07', type: 'stage', name: '菜单铺开', axis: 'unlockedCount', threshold: 4, reward_coins: 70, prev: 't06' }, + { id: 't08', type: 'stage', name: '手上有连招', axis: 'comboBest', threshold: 3, reward_coins: 70, prev: 't07' }, + { id: 't09', type: 'stage', name: '第一桶金在望', axis: 'careerRevenue', threshold: 440, reward_coins: 80, prev: 't08' }, + { id: 't10', type: 'stage', name: '四十单老摊', axis: 'careerServed', threshold: 40, reward_coins: 80, prev: 't09' }, + // ── 立足段(2–5h:day5–16;解锁与系统门户渐次开)── + { id: 't11', type: 'stage', name: '第三个夜晚', axis: 'day', threshold: 3, reward_coins: 80, prev: 't10' }, + { id: 't12', type: 'stage', name: '锁越开越多', axis: 'unlockedCount', threshold: 7, reward_coins: 90, prev: 't11' }, + { id: 't13', type: 'stage', name: '六道拿手菜', axis: 'recipesCollected', threshold: 6, reward_coins: 90, prev: 't12' }, + { id: 't14', type: 'stage', name: '九百金进账', axis: 'careerRevenue', threshold: 900, reward_coins: 100, prev: 't13' }, + { id: 't15', type: 'stage', name: '五方来客', axis: 'customersCollected', threshold: 5, reward_coins: 100, prev: 't14' }, + { id: 't16', type: 'stage', name: '十锁开张', axis: 'unlockedCount', threshold: 10, reward_coins: 110, prev: 't15' }, + { id: 't17', type: 'stage', name: '百单达成', axis: 'careerServed', threshold: 100, reward_coins: 120, prev: 't16' }, + { id: 't18', type: 'stage', name: '第五夜灯火', axis: 'day', threshold: 5, reward_coins: 120, prev: 't17' }, + { id: 't19', type: 'stage', name: '千八流水', axis: 'careerRevenue', threshold: 1800, reward_coins: 130, prev: 't18' }, + { id: 't20', type: 'stage', name: '六连不断火', axis: 'comboBest', threshold: 6, reward_coins: 140, prev: 't19' }, + { id: 't21', type: 'stage', name: '十四道锁', axis: 'unlockedCount', threshold: 14, reward_coins: 150, prev: 't20' }, + { id: 't22', type: 'stage', name: '半本菜谱', axis: 'recipesCollected', threshold: 12, reward_coins: 150, prev: 't21' }, + { id: 't23', type: 'stage', name: '第一位伙计', axis: 'staffHired', threshold: 1, reward_coins: 160, prev: 't22' }, + { id: 't24', type: 'stage', name: '三千金身家', axis: 'careerRevenue', threshold: 3000, reward_coins: 170, prev: 't23' }, + { id: 't25', type: 'stage', name: '第八个夜晚', axis: 'day', threshold: 8, reward_coins: 180, prev: 't24' }, + // ── 扩张段(5–8h:day17–30;街区铺开)── + { id: 't26', type: 'stage', name: '盘下第二块地', axis: 'plotsOwned', threshold: 2, reward_coins: 200, prev: 't25' }, + { id: 't27', type: 'stage', name: '三百单招牌', axis: 'careerServed', threshold: 300, reward_coins: 200, prev: 't26' }, + { id: 't28', type: 'stage', name: '二十道锁', axis: 'unlockedCount', threshold: 20, reward_coins: 220, prev: 't27' }, + { id: 't29', type: 'stage', name: '夜市见闻', axis: 'eventsSeen', threshold: 2, reward_coins: 220, prev: 't28' }, + { id: 't30', type: 'stage', name: '五千五流水', axis: 'careerRevenue', threshold: 5500, reward_coins: 240, prev: 't29' }, + { id: 't31', type: 'stage', name: '十方食客', axis: 'customersCollected', threshold: 10, reward_coins: 240, prev: 't30' }, + { id: 't32', type: 'stage', name: '三人小班底', axis: 'staffHired', threshold: 3, reward_coins: 260, prev: 't31' }, + { id: 't33', type: 'stage', name: '二十六道锁', axis: 'unlockedCount', threshold: 26, reward_coins: 280, prev: 't32' }, + { id: 't34', type: 'stage', name: '第十二夜', axis: 'day', threshold: 12, reward_coins: 280, prev: 't33' }, + { id: 't35', type: 'stage', name: '九千金大账', axis: 'careerRevenue', threshold: 9000, reward_coins: 300, prev: 't34' }, + // ── 传说段(8–10h+:day30–40;收束到长线循环)── + { id: 't36', type: 'stage', name: '第三块地', axis: 'plotsOwned', threshold: 3, reward_coins: 320, prev: 't35' }, + { id: 't37', type: 'stage', name: '二十道拿手菜', axis: 'recipesCollected', threshold: 20, reward_coins: 340, prev: 't36' }, + { id: 't38', type: 'stage', name: '八百单宗师', axis: 'careerServed', threshold: 800, reward_coins: 360, prev: 't37' }, + { id: 't39', type: 'stage', name: '满树三十四锁', axis: 'unlockedCount', threshold: 34, reward_coins: 400, prev: 't38' }, + { id: 't40', type: 'stage', name: '街口传说', axis: 'careerRevenue', threshold: 15000, reward_coins: 500, prev: 't39' }, +]); diff --git a/tier2/games/night-market/src/data/recipes.js b/tier2/games/night-market/src/data/recipes.js index e6ab12a3..c5ec03bb 100644 --- a/tier2/games/night-market/src/data/recipes.js +++ b/tier2/games/night-market/src/data/recipes.js @@ -78,4 +78,14 @@ export const RECIPES = Object.freeze([ ingredients: [{ id: 'i_youyu', qty: 2 }, { id: 'i_zhuqian', qty: 2 }] }, { id: 'r_guihuashanzha', name: '桂花山楂饮', cook_s: 3.5, price: 22, cost: 9, rarity: 3, tags: ['sweet', 'rare_fav'], base: false, ingredients: [{ id: 'i_guihua', qty: 1 }, { id: 'i_tang', qty: 3 }, { id: 'i_shanzha', qty: 2 }] }, + + // ── M3 内容一期 4 道(26→30;组合既有 12 食材,食材 40 归 M5/U13;定价纪律照 ⑧ 对账)── + { id: 'r_youdunzi', name: '萝卜油墩子', cook_s: 2.0, price: 8, cost: 6, rarity: 0, tags: ['quick', 'classic'], base: false, + ingredients: [{ id: 'i_mianfen', qty: 2 }, { id: 'i_shishu', qty: 3 }, { id: 'i_zhuqian', qty: 1 }] }, + { id: 'r_jianyuwan', name: '香煎鱼丸', cook_s: 3.0, price: 14, cost: 9, rarity: 1, tags: ['seafood', 'quick'], base: false, + ingredients: [{ id: 'i_youyu', qty: 1 }, { id: 'i_mianfen', qty: 2 }, { id: 'i_zhuqian', qty: 2 }] }, + { id: 'r_zhutongfan', name: '竹筒糯米饭', cook_s: 4.5, price: 19, cost: 10, rarity: 2, tags: ['sweet', 'classic'], base: false, + ingredients: [{ id: 'i_nuomi', qty: 3 }, { id: 'i_shishu', qty: 2 }, { id: 'i_tang', qty: 2 }] }, + { id: 'r_bingfen', name: '桂花冰粉', cook_s: 3.5, price: 25, cost: 10, rarity: 3, tags: ['sweet', 'rare_fav'], base: false, + ingredients: [{ id: 'i_tang', qty: 4 }, { id: 'i_shanzha', qty: 2 }, { id: 'i_guihua', qty: 1 }] }, ]); diff --git a/tier2/games/night-market/src/data/staff.js b/tier2/games/night-market/src/data/staff.js index 9fd9ba6a..92b1f3da 100644 --- a/tier2/games/night-market/src/data/staff.js +++ b/tier2/games/night-market/src/data/staff.js @@ -7,7 +7,7 @@ * - waiter 帮工 → auto_serve_rate(每秒自动出招次数:自动点「耐心最紧且可做」的顾客) * - cook 后厨 → cook_speed(灶台出餐倍速) * - cashier 收银 → tip_bonus(结账小费系数加成) - * 【技能升级】M2 最小集 = 单技能等级制(level 1..max_level):效果 ×eff_mult_per_level^(lv−1)、 + * 【技能升级】M2 最小集 = 单技能等级制;upgrade_base 按 M3 定标 ×3(员工养成 = 中期持续资金 sink,卡点密度的主要供给,详见 docs/m3-数值定标报告.md)(level 1..max_level):效果 ×eff_mult_per_level^(lv−1)、 * 日薪 ×wage_mult_per_level^(lv−1)(economy_curve.staff_efficiency 单源),升级价 = * upgrade_base × upgrade_cost.early_mult^(lv−1)(economy_curve 单源;技能槽多技能归 U13)。 * 【日薪】日结(settleAndAdvanceDay)扣,计入 dayNet(经营成本口径,离线速率如实反映)。 @@ -25,8 +25,8 @@ export const STAFF_ROLES = Object.freeze({ /** 招募池(M2 最小集 4 人;base_value = 1 级效果值,cook 为倍率、其余为增量)。 */ export const STAFF_POOL = Object.freeze([ - { id: 's_axiang', name: '阿香', role: 'waiter', base_value: 0.15, hire_cost: 150, wage_per_day: 24, upgrade_base: 120, max_level: 5 }, - { id: 's_laozhou', name: '老周', role: 'cook', base_value: 1.10, hire_cost: 200, wage_per_day: 30, upgrade_base: 160, max_level: 5 }, - { id: 's_xiaomei', name: '小梅', role: 'cashier', base_value: 0.03, hire_cost: 120, wage_per_day: 18, upgrade_base: 100, max_level: 5 }, - { id: 's_dazhuang', name: '大壮', role: 'waiter', base_value: 0.12, hire_cost: 130, wage_per_day: 20, upgrade_base: 110, max_level: 5 }, + { id: 's_axiang', name: '阿香', role: 'waiter', base_value: 0.15, hire_cost: 150, wage_per_day: 24, upgrade_base: 360, max_level: 5 }, + { id: 's_laozhou', name: '老周', role: 'cook', base_value: 1.10, hire_cost: 200, wage_per_day: 30, upgrade_base: 480, max_level: 5 }, + { id: 's_xiaomei', name: '小梅', role: 'cashier', base_value: 0.03, hire_cost: 120, wage_per_day: 18, upgrade_base: 300, max_level: 5 }, + { id: 's_dazhuang', name: '大壮', role: 'waiter', base_value: 0.12, hire_cost: 130, wage_per_day: 20, upgrade_base: 330, max_level: 5 }, ]); diff --git a/tier2/games/night-market/src/data/unlock-tree.js b/tier2/games/night-market/src/data/unlock-tree.js index c2b78d56..a2c3aac2 100644 --- a/tier2/games/night-market/src/data/unlock-tree.js +++ b/tier2/games/night-market/src/data/unlock-tree.js @@ -40,55 +40,60 @@ export const UNLOCK_TREE = Object.freeze([ { id: 'n_r_kaohongshu', name: '烤红薯', kind: 'unlock_recipe', target: 'r_kaohongshu', deps: ['n_r_zhimaqiu'], cost_coins: 45, min_prestige: 0 }, { id: 'n_r_nuomici', name: '红糖糍粑', kind: 'unlock_recipe', target: 'r_nuomici', deps: ['n_r_kaohongshu'], cost_coins: 80, min_prestige: 1 }, { id: 'n_r_danhuasu', name: '蛋花酥', kind: 'unlock_recipe', target: 'r_danhuasu', deps: ['n_r_nuomici'], cost_coins: 110, min_prestige: 1 }, - { id: 'n_r_tangshanyao', name: '拔丝山药', kind: 'unlock_recipe', target: 'r_tangshanyao', deps: ['n_r_danhuasu'], cost_coins: 200, min_prestige: 2 }, - { id: 'n_r_guihuagao', name: '桂花年糕', kind: 'unlock_recipe', target: 'r_guihuagao', deps: ['n_r_tangshanyao'], cost_coins: 320, min_prestige: 4 }, + { id: 'n_r_tangshanyao', name: '拔丝山药', kind: 'unlock_recipe', target: 'r_tangshanyao', deps: ['n_r_danhuasu'], cost_coins: 320, min_prestige: 2 }, + { id: 'n_r_guihuagao', name: '桂花年糕', kind: 'unlock_recipe', target: 'r_guihuagao', deps: ['n_r_tangshanyao'], cost_coins: 520, min_prestige: 4 }, // ── 主食线(unlock_recipe ×5)──────────────────────────────────────────── { id: 'n_r_congyoubing', name: '葱油饼', kind: 'unlock_recipe', target: 'r_congyoubing', deps: [], cost_coins: 30, min_prestige: 0 }, { id: 'n_r_jiroujuan', name: '鸡蛋灌饼', kind: 'unlock_recipe', target: 'r_jiroujuan', deps: ['n_r_congyoubing'], cost_coins: 50, min_prestige: 0 }, { id: 'n_r_chaomian', name: '铁板炒面', kind: 'unlock_recipe', target: 'r_chaomian', deps: ['n_r_jiroujuan'], cost_coins: 90, min_prestige: 1 }, { id: 'n_r_jianjiao', name: '脆底煎饺', kind: 'unlock_recipe', target: 'r_jianjiao', deps: ['n_r_chaomian'], cost_coins: 120, min_prestige: 1 }, - { id: 'n_r_jianbingsu', name: '金黄煎蛋饼', kind: 'unlock_recipe', target: 'r_jianbingsu', deps: ['n_r_jianjiao'], cost_coins: 220, min_prestige: 3 }, + { id: 'n_r_jianbingsu', name: '金黄煎蛋饼', kind: 'unlock_recipe', target: 'r_jianbingsu', deps: ['n_r_jianjiao'], cost_coins: 350, min_prestige: 3 }, // ── 香辣线(unlock_recipe ×2)──────────────────────────────────────────── { id: 'n_r_liangpi', name: '麻酱凉皮', kind: 'unlock_recipe', target: 'r_liangpi', deps: [], cost_coins: 40, min_prestige: 0 }, { id: 'n_r_shaokao', name: '烤蔬菜串', kind: 'unlock_recipe', target: 'r_shaokao', deps: ['n_r_liangpi'], cost_coins: 70, min_prestige: 1 }, // ── 海鲜线(unlock_recipe ×3;吃灶台扩容)───────────────────────────────── - { id: 'n_r_haixianzhou', name: '海鲜砂锅粥', kind: 'unlock_recipe', target: 'r_haixianzhou', deps: ['n_zao3'], cost_coins: 180, min_prestige: 2 }, - { id: 'n_r_laweiguo', name: '香辣海鲜锅', kind: 'unlock_recipe', target: 'r_laweiguo', deps: ['n_r_haixianzhou'], cost_coins: 260, min_prestige: 3 }, - { id: 'n_r_yexiaopin', name: '招牌夜宵拼盘', kind: 'unlock_recipe', target: 'r_yexiaopin', deps: ['n_r_laweiguo', 'n_r_guihuagao'], cost_coins: 420, min_prestige: 6 }, + { id: 'n_r_haixianzhou', name: '海鲜砂锅粥', kind: 'unlock_recipe', target: 'r_haixianzhou', deps: ['n_zao3'], cost_coins: 290, min_prestige: 2 }, + { id: 'n_r_laweiguo', name: '香辣海鲜锅', kind: 'unlock_recipe', target: 'r_laweiguo', deps: ['n_r_haixianzhou'], cost_coins: 420, min_prestige: 3 }, + { id: 'n_r_yexiaopin', name: '招牌夜宵拼盘', kind: 'unlock_recipe', target: 'r_yexiaopin', deps: ['n_r_laweiguo', 'n_r_guihuagao'], cost_coins: 680, min_prestige: 6 }, // ── 灶台扩容(stove_slot ×3:2→5)──────────────────────────────────────── { id: 'n_zao3', name: '第三口灶', kind: 'stove_slot', value: 1, deps: [], cost_coins: 120, min_prestige: 1 }, - { id: 'n_zao4', name: '第四口灶', kind: 'stove_slot', value: 1, deps: ['n_zao3'], cost_coins: 260, min_prestige: 3 }, - { id: 'n_zao5', name: '第五口灶', kind: 'stove_slot', value: 1, deps: ['n_zao4'], cost_coins: 480, min_prestige: 6 }, + { id: 'n_zao4', name: '第四口灶', kind: 'stove_slot', value: 1, deps: ['n_zao3'], cost_coins: 420, min_prestige: 3 }, + { id: 'n_zao5', name: '第五口灶', kind: 'stove_slot', value: 1, deps: ['n_zao4'], cost_coins: 800, min_prestige: 6 }, // ── 座位扩容(seat ×3:6→9)────────────────────────────────────────────── { id: 'n_zuo7', name: '加一张马扎', kind: 'seat', value: 1, deps: [], cost_coins: 60, min_prestige: 0 }, { id: 'n_zuo8', name: '拼一张折叠桌', kind: 'seat', value: 1, deps: ['n_zuo7'], cost_coins: 140, min_prestige: 2 }, - { id: 'n_zuo9', name: '再撑一把伞', kind: 'seat', value: 1, deps: ['n_zuo8'], cost_coins: 300, min_prestige: 4 }, + { id: 'n_zuo9', name: '再撑一把伞', kind: 'seat', value: 1, deps: ['n_zuo8'], cost_coins: 480, min_prestige: 4 }, // ── 服务与节奏 ───────────────────────────────────────────────────────────── { id: 'n_dengzi', name: '舒适马扎', kind: 'patience_bonus', value: 0.4, deps: [], cost_coins: 90, min_prestige: 1 }, - { id: 'n_yinliao', name: '免费凉茶', kind: 'patience_bonus', value: 0.4, deps: ['n_dengzi'], cost_coins: 200, min_prestige: 3 }, + { id: 'n_yinliao', name: '免费凉茶', kind: 'patience_bonus', value: 0.4, deps: ['n_dengzi'], cost_coins: 320, min_prestige: 3 }, { id: 'n_zhaopai', name: '手写招牌', kind: 'tip_bonus', value: 0.02, deps: [], cost_coins: 70, min_prestige: 0 }, - { id: 'n_dengchuan', name: '氛围灯串', kind: 'tip_bonus', value: 0.02, deps: ['n_zhaopai'], cost_coins: 160, min_prestige: 2 }, + { id: 'n_dengchuan', name: '氛围灯串', kind: 'tip_bonus', value: 0.02, deps: ['n_zhaopai'], cost_coins: 260, min_prestige: 2 }, { id: 'n_yaohe', name: '穿街吆喝', kind: 'spawn_rate', value: 0.92, deps: [], cost_coins: 100, min_prestige: 1 }, - { id: 'n_koubei', name: '口碑传开', kind: 'spawn_rate', value: 0.92, deps: ['n_yaohe'], cost_coins: 240, min_prestige: 3 }, + { id: 'n_koubei', name: '口碑传开', kind: 'spawn_rate', value: 0.92, deps: ['n_yaohe'], cost_coins: 380, min_prestige: 3 }, { id: 'n_shoutao', name: '利落手套', kind: 'combo_window', value: 2, deps: [], cost_coins: 110, min_prestige: 1 }, - { id: 'n_shuangdao', name: '双刀快手', kind: 'combo_stack', value: 2, deps: ['n_shoutao'], cost_coins: 180, min_prestige: 2 }, - { id: 'n_xiaopai', name: '致歉小牌', kind: 'rep_shield', value: 1, deps: [], cost_coins: 220, min_prestige: 3 }, + { id: 'n_shuangdao', name: '双刀快手', kind: 'combo_stack', value: 2, deps: ['n_shoutao'], cost_coins: 280, min_prestige: 2 }, + { id: 'n_xiaopai', name: '致歉小牌', kind: 'rep_shield', value: 1, deps: [], cost_coins: 350, min_prestige: 3 }, // ── 库存与进货 ───────────────────────────────────────────────────────────── - { id: 'n_bingxiang', name: '二手小冰柜', kind: 'freshness_bonus', value: 1, deps: [], cost_coins: 150, min_prestige: 2 }, - { id: 'n_baoxianhe', name: '成套保鲜盒', kind: 'freshness_bonus', value: 1, deps: ['n_bingxiang'], cost_coins: 280, min_prestige: 4 }, - { id: 'n_caigou', name: '相熟菜贩', kind: 'market_discount', value: 0.95, deps: [], cost_coins: 130, min_prestige: 2 }, - { id: 'n_pifa', name: '批发渠道', kind: 'market_discount', value: 0.90, deps: ['n_caigou'], cost_coins: 300, min_prestige: 5 }, + { id: 'n_bingxiang', name: '二手小冰柜', kind: 'freshness_bonus', value: 1, deps: [], cost_coins: 240, min_prestige: 2 }, + { id: 'n_baoxianhe', name: '成套保鲜盒', kind: 'freshness_bonus', value: 1, deps: ['n_bingxiang'], cost_coins: 450, min_prestige: 4 }, + { id: 'n_caigou', name: '相熟菜贩', kind: 'market_discount', value: 0.95, deps: [], cost_coins: 210, min_prestige: 2 }, + { id: 'n_pifa', name: '批发渠道', kind: 'market_discount', value: 0.90, deps: ['n_caigou'], cost_coins: 480, min_prestige: 5 }, // ── 离线收益启用(根节点,早期低价可达)──────────────────────────────────── { id: 'n_yedeng', name: '值夜摊灯', kind: 'offline_enable', deps: [], cost_coins: 50, min_prestige: 0 }, // ── M2 四系统门户(M1 曾为 arrivesAt:'M2' 前瞻;M2 机制到位,解锁即开张——效果经 system kind // 在效果解释器兑现:expand=可购地块 / rnd=可研发 / staff=可招工 / event=事件开始触发)──── - { id: 'n_sys_expand', name: '第二摊位', kind: 'system', target: 'expansion', deps: ['n_zao3'], cost_coins: 500, min_prestige: 6 }, - { id: 'n_sys_rnd', name: '研发工作台', kind: 'system', target: 'research', deps: ['n_r_danhuasu'], cost_coins: 400, min_prestige: 5 }, - { id: 'n_sys_staff', name: '招工启事', kind: 'system', target: 'staff', deps: ['n_zuo8'], cost_coins: 450, min_prestige: 5 }, - { id: 'n_sys_event', name: '夜市百态', kind: 'system', target: 'events', deps: ['n_koubei'], cost_coins: 400, min_prestige: 6 }, + { id: 'n_sys_expand', name: '第二摊位', kind: 'system', target: 'expansion', deps: ['n_zao3'], cost_coins: 800, min_prestige: 6 }, + { id: 'n_sys_rnd', name: '研发工作台', kind: 'system', target: 'research', deps: ['n_r_danhuasu'], cost_coins: 640, min_prestige: 5 }, + { id: 'n_sys_staff', name: '招工启事', kind: 'system', target: 'staff', deps: ['n_zuo8'], cost_coins: 720, min_prestige: 5 }, + { id: 'n_sys_event', name: '夜市百态', kind: 'system', target: 'events', deps: ['n_koubei'], cost_coins: 640, min_prestige: 6 }, // ── M2 研发节点(unlock_recipe + cost_ingredients:食材组合出新菜,经 n_sys_rnd 门户)──────── - { id: 'n_r_jiaoyanyuqiu', name: '研发·椒盐鱿鱼球', kind: 'unlock_recipe', target: 'r_jiaoyanyuqiu', deps: ['n_sys_rnd', 'n_r_haixianzhou'], cost_coins: 150, min_prestige: 5, + { id: 'n_r_jiaoyanyuqiu', name: '研发·椒盐鱿鱼球', kind: 'unlock_recipe', target: 'r_jiaoyanyuqiu', deps: ['n_sys_rnd', 'n_r_haixianzhou'], cost_coins: 240, min_prestige: 5, cost_ingredients: [{ id: 'i_youyu', qty: 6 }, { id: 'i_laliao', qty: 4 }] }, - { id: 'n_r_guihuashanzha', name: '研发·桂花山楂饮', kind: 'unlock_recipe', target: 'r_guihuashanzha', deps: ['n_sys_rnd'], cost_coins: 120, min_prestige: 5, + { id: 'n_r_guihuashanzha', name: '研发·桂花山楂饮', kind: 'unlock_recipe', target: 'r_guihuashanzha', deps: ['n_sys_rnd'], cost_coins: 200, min_prestige: 5, cost_ingredients: [{ id: 'i_guihua', qty: 3 }, { id: 'i_shanzha', qty: 6 }] }, + // ── M3 内容一期菜谱节点 ×4(中后段价格阶梯:填 2–7h 卡点带,U9 定标的成本面棋子)──────────── + { id: 'n_r_youdunzi', name: '萝卜油墩子', kind: 'unlock_recipe', target: 'r_youdunzi', deps: ['n_r_congyoubing'], cost_coins: 260, min_prestige: 2 }, + { id: 'n_r_jianyuwan', name: '香煎鱼丸', kind: 'unlock_recipe', target: 'r_jianyuwan', deps: ['n_r_haixianzhou'], cost_coins: 480, min_prestige: 4 }, + { id: 'n_r_zhutongfan', name: '竹筒糯米饭', kind: 'unlock_recipe', target: 'r_zhutongfan', deps: ['n_r_nuomici'], cost_coins: 620, min_prestige: 5 }, + { id: 'n_r_bingfen', name: '桂花冰粉', kind: 'unlock_recipe', target: 'r_bingfen', deps: ['n_r_guihuagao'], cost_coins: 900, min_prestige: 7 }, ]); diff --git a/tier2/games/night-market/src/scenes/main-scene.js b/tier2/games/night-market/src/scenes/main-scene.js index ee5b3a71..7bc33b3a 100644 --- a/tier2/games/night-market/src/scenes/main-scene.js +++ b/tier2/games/night-market/src/scenes/main-scene.js @@ -198,6 +198,8 @@ export class MainScene extends Phaser.Scene { this.hud.floatText(VIEW_W / 2, VIEW_H * 0.30, `成就「${ev.name}」奖 ${ev.reward} 金`, '#ffd166'); } else if (ev.type === 'wages') { this.hud.floatText(VIEW_W / 2, VIEW_H * 0.46, `发工钱 −${ev.paid}`, '#c8d2f0'); + } else if (ev.type === 'relief') { + this.hud.floatText(VIEW_W / 2, VIEW_H * 0.48, `翻出老本 +${ev.granted},明天再战`, '#9ef0a0'); } else if (ev.type === 'day_end' && !this._dayEndShown) { this._dayEndShown = true; if (this._dockOpen) this.dock.close(); // 收钟强制回日结面板(信息层级唯一) diff --git a/tier2/games/night-market/src/ui/dock.js b/tier2/games/night-market/src/ui/dock.js index 21359e5b..e76ac46b 100644 --- a/tier2/games/night-market/src/ui/dock.js +++ b/tier2/games/night-market/src/ui/dock.js @@ -130,11 +130,21 @@ export class Dock { // codex const v = c.codexView(); const st = c.readState(); + // M3 阶段任务链:已领折叠成一行计数,当前段(可领 + 接下来 4 条)展开——「下一件事」永远在眼前 + const doneTasks = v.tasks.filter((t) => t.claimed).length; + const currentTasks = v.tasks.filter((t) => !t.claimed).slice(0, 5); return { - title: '图鉴与成就', + title: '图鉴与任务', rows: [ { text: `菜谱图鉴 ${st.collections.recipes.collected}/${st.collections.recipes.total}`, sub: v.recipes.filter((r) => r.collected).slice(-4).map((r) => r.name).join(' ') || '还没做过菜' }, { text: `顾客图鉴 ${st.collections.customers.collected}/${st.collections.customers.total}`, sub: v.customers.filter((t) => t.collected).map((t) => t.name).join(' ') || '还没来过客' }, + { text: `阶段任务 ${doneTasks}/${v.tasks.length}`, sub: doneTasks > 0 ? '一步一个脚印' : '从第一锅热气开始' }, + ...currentTasks.map((t) => ({ + text: `${t.claimable ? '★' : '·'} ${t.name}`, + sub: `${t.progress}/${t.threshold} · 奖 ${t.reward} 金`, + btn: t.claimable ? '领取' : null, + onTap: () => c.claimAchievement(t.id), + })), ...v.achievements.map((a) => ({ text: `${a.claimed ? '✓' : a.achieved ? '★' : '☆'} ${a.name}`, sub: `${a.progress}/${a.threshold}${a.claimed ? ' · 已领' : ` · 奖 ${a.reward} 金`}`, diff --git a/tier2/games/night-market/src/ui/hud.js b/tier2/games/night-market/src/ui/hud.js index 101ec60e..e9b301e6 100644 --- a/tier2/games/night-market/src/ui/hud.js +++ b/tier2/games/night-market/src/ui/hud.js @@ -47,7 +47,7 @@ export class Hud { /** 每帧刷新(只读 state)。 */ update(st) { this.coinsText.setText(`金币 ${st.coins}`); - this.prestigeText.setText(`声望 ${st.prestige} · 解锁 ${st.unlockedCount}/42`); + this.prestigeText.setText(`声望 ${st.prestige} · 解锁 ${st.unlockedCount}/${st.treeSize}`); this.repText.setText(`口碑 ${st.reputation}`); this.comboText.setText(st.combo >= 2 ? `连击 ×${st.combo}` : ''); this.servedText.setText(`出餐 ${st.served} · 翻台 ${st.missed}`); diff --git a/tier2/games/night-market/test/m1-core.test.mjs b/tier2/games/night-market/test/m1-core.test.mjs index bd73c530..ef743b99 100644 --- a/tier2/games/night-market/test/m1-core.test.mjs +++ b/tier2/games/night-market/test/m1-core.test.mjs @@ -153,9 +153,9 @@ test('自动补货:确定性 + 资金不足按表序补到耗尽', () => { assert.equal(c.readState().coins, 50 - r.spent); }); -test('解锁树数据面:42 节点、18 菜谱节点、4 系统门户带 target(M2 兑现)、2 研发节点带食材组合', () => { - assert.equal(UNLOCK_TREE.length, 42); // M1 40 + M2 研发 2 - assert.equal(UNLOCK_TREE.filter((n) => n.kind === 'unlock_recipe').length, 18); +test('解锁树数据面:46 节点、22 菜谱节点、4 系统门户带 target(M2 兑现)、2 研发节点带食材组合', () => { + assert.equal(UNLOCK_TREE.length, 46); // M1 40 + M2 研发 2 + M3 内容一期 4 + assert.equal(UNLOCK_TREE.filter((n) => n.kind === 'unlock_recipe').length, 22); const sys = UNLOCK_TREE.filter((n) => n.kind === 'system'); assert.equal(sys.length, 4); // M1 曾为 arrivesAt:'M2' 前瞻;M2 机制到位后收编为 target 门户(expansion/research/staff/events) diff --git a/tier2/games/night-market/test/m2-systems.test.mjs b/tier2/games/night-market/test/m2-systems.test.mjs index 5f12db0b..009f68b3 100644 --- a/tier2/games/night-market/test/m2-systems.test.mjs +++ b/tier2/games/night-market/test/m2-systems.test.mjs @@ -32,13 +32,13 @@ const ALL_GATES = { }; test('扩张边界:未知地块/声望门/钱不够/设备重复购', () => { - const c = richCore(1, { unlocked: ['n_zao3', 'n_sys_expand'], prestige: 6, coins: 700 }); + const c = richCore(1, { unlocked: ['n_zao3', 'n_sys_expand'], prestige: 6, coins: 1000 }); // 金额随 M3 定标地价(900) assert.equal(c.buyPlot('p_bushi').reason, 'not_found'); assert.equal(c.buyPlot('p_qiaotou').reason, 'prestige_low'); // 要 8,只有 6 assert.equal(c.buyPlot('p_zhuanjiao').reason, 'prestige_low'); // 要 10 - assert.equal(c.buyPlot('p_xiangkou').ok, true); // 600 ≤ 700 + assert.equal(c.buyPlot('p_xiangkou').ok, true); // 900 ≤ 1000(M3 定标价) assert.equal(c.buyPlot('p_xiangkou').reason, 'already'); - assert.equal(c.buyEquipment('e_menghuozao').reason, 'insufficient_coins'); // 只剩 100 + assert.equal(c.buyEquipment('e_menghuozao').reason, 'insufficient_coins'); // 只剩 100 < 420 const c2 = richCore(2, { unlocked: ['n_zao3', 'n_sys_expand'] }); assert.equal(c2.buyEquipment('e_hongdenglong').ok, true); assert.equal(c2.buyEquipment('e_hongdenglong').reason, 'already'); // 全街限购一件(M2 最小集) @@ -151,17 +151,39 @@ test('变现位:closed 路无奖;view 可用性语境;加速在 day_end 不流 assert.equal(slots.find((s) => s.slotId === 'ad_cook_boost').available, true, '营业中加速位亮'); }); -test('图鉴投影含评论家位(总数 7);评论家不进常规客流', () => { +test('图鉴投影含特邀客位(M3 总数 19);特邀客不进常规客流', () => { const c = richCore(8); c.start(); c.autoRestock(); - assert.equal(c.readState().collections.customers.total, 7); - // 整日常规客流不该出现评论家(weight 0;事件系统未解锁) + assert.equal(c.readState().collections.customers.total, 19); // 13 常 + 4 稀 + 2 特邀(M3 一期) + // 整日常规客流不该出现特邀客(weight 0;事件系统未解锁) for (let i = 0; i < 6001 && c.readState().phase === 'play'; i++) { c.update(0.1); - assert.ok(!c.readState().customers.some((x) => x.rarity === 'critic'), '常规抽签零评论家'); + assert.ok(!c.readState().customers.some((x) => x.rarity === 'critic'), '常规抽签零特邀客'); } }); +test('M3 econ 注入缝:覆盖键两层并入且里程碑随 tiers 重建;不传恒用生产曲线', () => { + // 覆盖 prestige_threshold → 里程碑阈值随 buildMilestones 重建(×20 放大列) + const c = richCore(41, { ...ALL_GATES }); + const cOv = createGameCore({ + random: createSeededRandom(41), + econ: { prestige_threshold: { tiers: [10, 20, 30] }, combo: { tip_bonus_per_stack: 0.03 } }, + }); + const e0 = cOv._forensicsView().econEcho(); + assert.equal(e0.comboWindow, ECONOMY_CURVE.combo.window_s, '未覆盖键保留基线(两层并入非整键替换)'); + // 白盒:生涯流水 210 > 10×20=200 → 覆盖核首个里程碑达成;生产核阈值 440 未达 + const inject = { schemaVersion: 1, data: { ...defaultData(), careerRevenue: 210, coins: 100 } }; + cOv.importSave(inject, {}); cOv.start(); + const cProd = createGameCore({ random: createSeededRandom(41) }); + cProd.importSave(inject, {}); cProd.start(); + for (const core of [cOv, cProd]) { // 快进一日触发日结里程碑判定 + for (let i = 0; i < 6001 && core.readState().phase === 'play'; i++) core.update(0.1); + core.settleAndAdvanceDay({ nowMs: 1 }); + } + assert.ok(cOv.readState().prestige >= 1, '覆盖 tiers 后里程碑真重建(200 线达成)'); + assert.equal(cProd.readState().prestige, 0, '生产曲线阈值不受注入缝影响(440 线未达)'); +}); + test('存档 additive:M2 字段缺省兜底;未知地块/员工/成就 id 忽略不炸', () => { const c = createGameCore({ random: createSeededRandom(9) }); const r = c.importSave({