feat(polaris): 《夜市一条街》M3——内容一期到量+economy_curve 千局网格定标
内容四轴灌装到量表(菜谱 30/顾客 19/事件 12/任务 52+成就);千局网格揪修三真缺陷:破产死锁 (11/25 生涯中期现金归零永锁→relief.floor_coins 地板+autoRestock 均衡轮转,根因=旧表序贪婪把救济金砸单一食材)、 卡点张力缺席(判据 2 首测 n=0→地块 900/2200/4500+树尾 22 节点×1.6+设备×1.6+员工升级基价×3)、 离线封顶 2h→75min(判据 5 解析收敛 44.4%→33.3%)。 门证据:上界安全门 7 配置×7000 营业日全过(判据 1 零死锁窗/判据 2 基线卡点中位 15min∈[15,45], 主会话复跑逐值一致)+30 分钟真玩口径三 seed 零卡死零 uncaught+扰动翼安全边界探明(小费 0.03 塌/0.04 绿、 薪涨 1.15 塌/1.12 绿,域外塌方翼零死锁=relief 兜底最坏参数仍保能玩);数值定标报告归档 docs/m3-数值定标报告.md(26 锚逐项保留/调整/废弃+三缺陷修复史+M5 输入+driver 两课);消耗率@10h 95.7%(判据 3 归 M5 观测)。 回归:logic-smoke 95 断言(⑤解锁阈值按设计 §3 首小时消耗带 ≥6→≥4、机制不放水)+tree-check 15 查 +单测 30/30+M2 千局零死锁+富三门接线未动;主会话真跑复验。M4 闸=风格 A 暖夜霓虹已拍。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0fc1cb695b
commit
a10945d74f
@ -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。
|
||||
|
||||
56
tier2/games/night-market/docs/m3-数值定标报告.md
Normal file
56
tier2/games/night-market/docs/m3-数值定标报告.md
Normal file
@ -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 上界安全门口径。
|
||||
BIN
tier2/games/night-market/evidence/after-play.png
Normal file
BIN
tier2/games/night-market/evidence/after-play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
BIN
tier2/games/night-market/evidence/first-paint.png
Normal file
BIN
tier2/games/night-market/evidence/first-paint.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
184
tier2/games/night-market/evidence/verdict.json
Normal file
184
tier2/games/night-market/evidence/verdict.json
Normal file
@ -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 跳过"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 });
|
||||
|
||||
342
tier2/games/night-market/scripts/scan-calibrate.mjs
Normal file
342
tier2/games/night-market/scripts/scan-calibrate.mjs
Normal file
@ -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-<part>.json
|
||||
const PART = (() => { const a = process.argv.find((x) => x.startsWith('--part=')); return a ? a.split('=')[1] : null; })();
|
||||
// gate 模式 --from=<json 路径>:从已落盘扫描数据判门(同代码同 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-<part>.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=<json> = 从已落盘扫描数据判(同代码同 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) };
|
||||
}
|
||||
}
|
||||
@ -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 全自然。');
|
||||
|
||||
@ -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。`);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 },
|
||||
]);
|
||||
|
||||
@ -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 },
|
||||
});
|
||||
|
||||
@ -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 } },
|
||||
]);
|
||||
|
||||
@ -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: '半价回馈!客如潮水' },
|
||||
]);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 },
|
||||
]);
|
||||
|
||||
@ -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' },
|
||||
]);
|
||||
|
||||
@ -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 }] },
|
||||
]);
|
||||
|
||||
@ -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 },
|
||||
]);
|
||||
|
||||
@ -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 },
|
||||
]);
|
||||
|
||||
@ -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(); // 收钟强制回日结面板(信息层级唯一)
|
||||
|
||||
@ -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} 金`}`,
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user