From b32ba6d5954fc207c69ac46e8f693ab474f3ee7d Mon Sep 17 00:00:00 2001 From: manpengan Date: Sat, 28 Mar 2026 23:10:24 +0800 Subject: [PATCH] =?UTF-8?q?content:=206=20=E5=9F=8E=E5=B8=82=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=20+=20=E5=86=B3=E7=AD=96=E9=94=81=E5=AE=9A=20+=20?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - js/content/cities/: 北京/东京/曼谷/首尔/新加坡/伊斯坦布尔完整 CityManifest - 01-charter: 5 项设计决策标记 ✅ 已锁定 - 00-process: Phase 1/4 已完成,Phase 7 进行中 - 收录 Codex 产出的 04/05 技术设计文档 Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/00-project-process.md | 8 +- docs/01-project-charter.md | 10 +- docs/04-city-content-system.md | 411 ++++++++++++++++++++++++++++ docs/05-difficulty-generator.md | 457 ++++++++++++++++++++++++++++++++ js/content/cities/bangkok.js | 64 +++++ js/content/cities/beijing.js | 65 +++++ js/content/cities/istanbul.js | 65 +++++ js/content/cities/seoul.js | 64 +++++ js/content/cities/singapore.js | 64 +++++ js/content/cities/tokyo.js | 65 +++++ 10 files changed, 1264 insertions(+), 9 deletions(-) create mode 100644 docs/04-city-content-system.md create mode 100644 docs/05-difficulty-generator.md create mode 100644 js/content/cities/bangkok.js create mode 100644 js/content/cities/beijing.js create mode 100644 js/content/cities/istanbul.js create mode 100644 js/content/cities/seoul.js create mode 100644 js/content/cities/singapore.js create mode 100644 js/content/cities/tokyo.js diff --git a/docs/00-project-process.md b/docs/00-project-process.md index c16dbca..1b8077d 100644 --- a/docs/00-project-process.md +++ b/docs/00-project-process.md @@ -3,7 +3,7 @@ > 创建日期:2026-03-28 > 项目仓库:https://github.com/manpengan/wechat-minigame > 标准流程:~/pro/kb/workflows/standard-dev-process/SKILL.md -> 当前阶段:Phase 1 立项(待开始) +> 当前阶段:Phase 7 开发实现(进行中) --- @@ -38,13 +38,13 @@ Phase 6 ──→ Phase 7 ──→ Phase 8 ──→ Phase 9 ──→ Phase 10 | 阶段 | 状态 | 产出文档 | 备注 | |------|------|----------|------| -| Phase 1 立项 | ⏳ 待开始 | docs/01-project-charter.md | | +| Phase 1 立项 | ✅ 已完成 | docs/01-project-charter.md | 方向确定为城市抓猫猫,5 项决策已锁定 | | Phase 2 市场调研 | 🔒 未开始 | docs/02-market-research.md | | | Phase 3 需求分析 | 🔒 未开始 | docs/03-prd.md | | -| Phase 4 技术方案 | 🔒 未开始 | docs/04-technical-design.md | | +| Phase 4 技术方案 | ✅ 已完成 | docs/04-technical-design.md | 04-city-content-system + 05-difficulty-generator | | Phase 5 项目计划 | 🔒 未开始 | docs/05-project-plan.md | | | Phase 6 设计阶段 | 🔒 未开始 | docs/06-design-spec.md | | -| Phase 7 开发实现 | 🔒 未开始 | docs/07-dev-notes.md | | +| Phase 7 开发实现 | 🔧 进行中 | docs/07-dev-notes.md | Codex 落代码骨架 js/content/* | | Phase 8 测试验收 | 🔒 未开始 | docs/08-test-report.md | | | Phase 9 发布上线 | 🔒 未开始 | docs/09-release-checklist.md | | | Phase 10 运营迭代 | 🔒 未开始 | docs/10-operations.md | | diff --git a/docs/01-project-charter.md b/docs/01-project-charter.md index d122940..572b293 100644 --- a/docs/01-project-charter.md +++ b/docs/01-project-charter.md @@ -178,11 +178,11 @@ | # | 决策项 | 建议方向 | 决策依据 | 状态 | |---|--------|---------|---------|------| -| 1 | 消除机制 | 沿用抓大鹅堆叠三消,不做大变体 | 机制成熟、用户认知成本低、开发风险小 | 待确认 | -| 2 | 首页层级 | MVP 简化为"洲 → 城市"二级(去掉国家层) | 减少导航深度,MVP 只有 1 洲 6 城市,三级无意义 | 待确认 | -| 3 | 首批 6 城市 | 北京、东京、曼谷、首尔、新加坡、伊斯坦布尔 | 文化辨识度高、素材丰富、地理分布覆盖亚洲主要区域 | 待确认 | -| 4 | 美术风格 | 扁平冰箱贴风 | 辨识度高、AI 生成友好、适合小尺寸 Canvas 渲染 | 待确认 | -| 5 | 素材生产 | AI 出图 + 人工修正 | 单城市 2-3 天产出,成本可控,质量可接受 | 待确认 | +| 1 | 消除机制 | 沿用抓大鹅堆叠三消,不做大变体 | 机制成熟、用户认知成本低、开发风险小 | ✅ 已锁定 | +| 2 | 首页层级 | MVP 简化为"洲 → 城市"二级(去掉国家层) | 减少导航深度,MVP 只有 1 洲 6 城市,三级无意义 | ✅ 已锁定 | +| 3 | 首批 6 城市 | 北京、东京、曼谷、首尔、新加坡、伊斯坦布尔 | 文化辨识度高、素材丰富、地理分布覆盖亚洲主要区域 | ✅ 已锁定 | +| 4 | 美术风格 | 扁平冰箱贴风 | 辨识度高、AI 生成友好、适合小尺寸 Canvas 渲染 | ✅ 已锁定 | +| 5 | 素材生产 | AI 出图 + 人工修正 | 单城市 2-3 天产出,成本可控,质量可接受 | ✅ 已锁定 | ## 13. 下一步产出 diff --git a/docs/04-city-content-system.md b/docs/04-city-content-system.md new file mode 100644 index 0000000..44fc4ee --- /dev/null +++ b/docs/04-city-content-system.md @@ -0,0 +1,411 @@ +# 04 City Content System — 城市抓猫猫 + +## 1. 目标 + +城市内容系统负责把"北京/东京/曼谷"这类城市内容变成稳定、可校验、可扩展、可分包加载的数据资产。 + +它解决 4 个问题: + +- 新增一个城市时,尽量只新增配置和资源,不改玩法代码 +- 运行时能快速拿到城市列表、封面、元素、猫猫、关卡参数 +- 内容出错时能在入库前被校验出来,而不是上线后炸在运行时 +- 内容包和玩家进度解耦,后续扩城市、扩洲、做活动都不改存档结构 + +## 2. 设计原则 + +### 2.1 配置驱动 + +- 玩法代码不写死城市名、元素名、解锁顺序 +- 城市、洲、猫猫、护照、分享卡片都从 JS module 配置读取 +- 运行时只依赖稳定 id,不依赖中文展示名 + +### 2.2 稳定标识优先 + +- `continentId`、`cityId`、`elementId`、`catId` 一旦上线不可重命名 +- 展示文案、美术资源可迭代,稳定 id 不变 +- 玩家存档只记录 id 和进度,不直接记录资源路径 + +### 2.3 内容与进度解耦 + +- 内容是只读配置 +- 进度来自 `playerState` +- UI 展示由 `content + playerState` 合成,不把内容字段写回存档 + +### 2.4 可校验优先于可编辑 + +- 城市配置必须显式、冗余、容易校验 +- 不追求“最短配置”,追求“新手也不容易配错” + +## 3. 逻辑模块 + +| 模块 | 职责 | +|------|------| +| `ContinentRegistry` | 管理洲索引、城市顺序、预下载策略 | +| `CityRegistry` | 加载单个城市配置、提供城市查询接口 | +| `ContentResolver` | 根据 id 解析封面、猫猫、元素、护照、分享资源 | +| `ContentValidator` | 在入库前做结构校验、语义校验、跨文件校验 | +| `ProgressProjector` | 用 `playerState` 投影出解锁态、通关态、图鉴态 | +| `BundleResolver` | 决定某个城市资源在主包还是分包,何时预加载 | + +## 4. 数据层级 + +运行时的数据层级固定为: + +```text +ContinentManifest + -> CityManifest + -> ElementSpec[] + -> CatSpec + -> LevelPreset[] + -> PassportSpec + -> ShareCardSpec +``` + +### 4.1 ContinentManifest + +洲配置只描述“入口”和“顺序”,不重复城市详情。 + +```javascript +module.exports = { + id: 'asia', + name: '亚洲', + sortOrder: 1, + themeColor: '#FF6B6B', + cityIds: ['beijing', 'tokyo', 'bangkok', 'seoul', 'singapore', 'istanbul'], + unlockOrder: ['beijing', 'tokyo', 'bangkok', 'seoul', 'singapore', 'istanbul'], + bundle: { + packId: 'asia-rest', + preloadOnEnter: true, + }, +} +``` + +约束: + +- `cityIds` 和 `unlockOrder` 必须一一对应 +- MVP 只启用 `asia` +- 后续新增洲时,`sortOrder` 决定大地图排序 + +### 4.2 CityManifest + +城市配置是运行时的主实体。 + +```javascript +module.exports = { + id: 'beijing', + contentVersion: 1, + continentId: 'asia', + sortOrder: 1, + unlockAfterCityId: null, + bundle: { + packId: 'main', + preload: 'startup', + }, + display: { + name: '北京', + nameEn: 'Beijing', + bgColor: '#CC2936', + tagline: '天安门前看猫猫', + funFact: '北京是世界上拥有最多宫殿的城市', + }, + cover: { + catImage: 'images/cats/cat_beijing.png', + catThumb: 'images/cats/cat_beijing_thumb.png', + shareAccent: '#F6D365', + }, + cat: { + id: 'cat_beijing', + name: '京京', + baseColor: '#F28C28', + pattern: 'tabby', + accessory: '虎头帽', + }, + passport: { + stampId: 'stamp_beijing', + stampLabel: '北京', + }, + elements: [ + // 12-15 个 + ], + levelPresets: [ + // 6 个基础关卡 + ], + shareCard: { + titleUnlocked: '我解锁了北京猫!', + titlePassport: '北京护照盖章完成', + }, +} +``` + +### 4.3 ElementSpec + +元素是关卡和图鉴的最小单位。 + +```javascript +{ + id: 'beijing_01', + name: '糖葫芦', + category: 'food', + rarity: 'core', + image: 'images/elements/beijing/beijing_01.png', + tags: ['sweet', 'street-food', 'red'], +} +``` + +字段约束: + +- `id` 全局唯一 +- `category` 只能来自固定枚举:`landmark` / `food` / `culture` / `item` / `nature` +- `rarity` 先保留 `core` / `accent` 两档,MVP 默认都可用 `core` +- `tags` 给后续活动筛选、图鉴展示、搜索扩展留接口 + +### 4.4 LevelPreset + +关卡不直接保存所有物件坐标,只保存生成输入。 + +```javascript +{ + id: 1, + difficultyTier: 'intro', + seedBase: 11001, + elementCount: 6, + piecesPerElement: 3, + layers: 2, + density: 'low', + targetPassRate: 0.95, + targetDurationSec: [90, 150], +} +``` + +设计原则: + +- 关卡配置是“生成器输入 + 目标指标” +- 真正布局由 `difficulty generator` 按 `seed` 生成 +- 同一关卡可复现实例,不手工存静态坐标表 + +## 5. 推荐目录结构 + +系统层推荐的逻辑目录如下: + +```text +js/content/ + continents/ + asia.js + index.js + cities/ + beijing.js + tokyo.js + ... + registry/ + continent-registry.js + city-registry.js + bundle-resolver.js + progress-projector.js + validation/ + validate-continent.js + validate-city.js + validate-assets.js +``` + +说明: + +- `docs/03` 里的 `cities/*.js`、`continents/*.js` 是逻辑格式,不要求现在就按这个目录实现 +- 真的写代码时,建议统一挂到 `js/content/` 下,避免根目录膨胀 + +## 6. 运行时读取流程 + +### 6.1 启动阶段 + +1. 读取 `continents/index` +2. 初始化 `ContinentRegistry` +3. 读取主包内首城市 `beijing` +4. 生成城市页卡片数据 +5. 用 `playerState` 投影出解锁态和通关态 + +### 6.2 进入城市页 + +1. 取亚洲 `unlockOrder` +2. 读取所有已在本地可用的 `CityManifest` 摘要 +3. 对未下载但已可见的城市展示灰态卡片 +4. 对“下一目标城市”预触发分包预下载 + +### 6.3 进入单城市 + +1. 检查 `bundle.packId` +2. 若分包未加载,则先下载/加载 +3. 加载 `CityManifest` +4. 加载该城市封面猫猫、元素图标、分享卡资源 +5. 生成 6 个关卡入口及对应种子 + +### 6.4 完成城市 + +1. `levelProgress` 全部满 6 关 +2. 写入 `collectedCats` +3. 写入 `passportStamps` +4. 解锁 `unlockAfterCityId` 的后续城市 +5. 预下载下一个城市内容包 + +## 7. 内容与存档的映射关系 + +内容系统不直接写存档,它只定义映射规则。 + +| 内容字段 | 存档字段 | 用途 | +|---------|---------|------| +| `city.id` | `unlockedCities[]` | 城市解锁 | +| `levelPresets[].id` | `levelProgress[cityId][levelId]` | 关卡进度 | +| `city.id` | `collectedCats[]` | 猫猫图鉴完成态 | +| `city.id` | `passportStamps[]` | 护照盖章完成态 | +| `bundle.packId` | 不入存档 | 运行时加载决策 | +| `contentVersion` | 不入存档 | 内容升级和兼容判定 | + +规则: + +- 存档永远记录 `cityId`,不记录中文名 +- `cat.id` 和 `passport.stampId` 只作为配置内标识,MVP 存档阶段继续只记 `cityId` +- 这样后面猫猫重命名、护照表现样式调整、城市文案微调都不影响旧存档 + +## 8. 分包与资源边界 + +### 8.1 包划分原则 + +- 主包只放启动必需内容 +- 分包按“玩家最可能连续访问的内容”组织,而不是按文件类型组织 +- 对 MVP 来说,推荐: + - 主包:框架、通用 UI、北京城市包 + - 亚洲分包:其余 5 个城市 + +### 8.2 Bundle 元数据 + +每个城市都带自己的包信息: + +```javascript +bundle: { + packId: 'asia-rest', + preload: 'on-city-page', +} +``` + +`preload` 枚举建议: + +- `startup`:随启动主包一起可用 +- `on-city-page`:进入城市页即预下载 +- `on-demand`:用户点击城市时再拉取 + +### 8.3 资源命名规则 + +- 猫猫封面:`images/cats/cat_{cityId}.png` +- 猫猫缩略图:`images/cats/cat_{cityId}_thumb.png` +- 元素图标:`images/elements/{cityId}/{cityId}_{seq}.png` +- 分享资源如需额外图层:`images/share/{cityId}_*.png` + +## 9. 校验体系 + +### 9.1 结构校验 + +结构校验回答“字段有没有、类型对不对”。 + +校验项: + +- 必填字段完整 +- 枚举值合法 +- `levelPresets` 固定为 6 个 +- `elements.length` 在 12-15 之间 +- `piecesPerElement` 为 3 的倍数 + +### 9.2 语义校验 + +语义校验回答“内容合理不合理”。 + +校验项: + +- 5 类目配比满足:`landmark >= 2`、`food >= 3`、`culture >= 2`、`item >= 2`、`nature >= 2` +- 同城市元素名称不能重复 +- `unlockAfterCityId` 必须和洲内顺序一致 +- `display.bgColor` 与封面资源不能缺失 +- `cat.id` 与 `city.id` 一一对应 + +### 9.3 跨文件校验 + +跨文件校验回答“城市、洲、资源之间有没有断链”。 + +校验项: + +- `continent.cityIds` 里的每个城市文件都存在 +- `city.continentId` 必须回指到合法洲 +- `cover.catImage`、`cover.catThumb`、`elements[].image` 必须存在 +- `unlockOrder` 和 `sortOrder` 不冲突 + +### 9.4 校验输出 + +校验输出分两类: + +- `error`:阻止入库,比如资源缺失、id 重复、类目不达标 +- `warning`:允许入库,但需要人工确认,比如 `funFact` 过长、色彩接近、标签过少 + +推荐接口: + +```javascript +validateContinent(continentConfig) +validateCity(cityConfig, allContinentConfigs) +validateAssetPaths(cityConfig, assetIndex) +``` + +## 10. 内容扩展流程 + +新增一个城市时,流程固定为: + +1. 创建 `CityManifest` +2. 准备 12-15 个元素图标 +3. 准备猫猫封面和缩略图 +4. 补齐护照和分享文案 +5. 跑内容校验 +6. 跑难度生成器验证 6 个基础关卡 +7. 把城市 id 挂到对应洲的 `cityIds` 和 `unlockOrder` +8. 再跑一次跨文件校验 + +只有第 7 步会影响洲索引,其余步骤都不该动玩法代码。 + +## 11. 版本与兼容策略 + +### 11.1 contentVersion + +- `contentVersion` 用来标记城市配置版本 +- 仅当字段结构变化或默认行为变化时递增 +- 纯文案或美术替换不必改版本 + +### 11.2 删除与下线 + +上线后不建议真正删除城市 id。 + +若后续城市下线: + +- 内容层标为 `disabled: true` +- 入口层不再展示 +- 存档里旧 `cityId` 仍然保留,避免脏存档 + +## 12. 对工程实现的要求 + +内容系统落地时需要优先保证这些接口稳定: + +```javascript +getContinentList() +getContinent(continentId) +getCity(cityId) +getCityCardView(cityId, playerState) +getLevelPreset(cityId, levelId) +ensureCityBundle(cityId) +validateContent() +``` + +这批接口一旦稳定,UI、地图、图鉴、护照、分享页都能复用同一层内容数据。 + +## 13. 本文档不负责的事情 + +以下内容不在 04 文档内定义: + +- 具体关卡布局算法 +- overlap graph 结构 +- solver / validator 的实现 +- 道具状态机细节 + +这些交给 [05-difficulty-generator.md](./05-difficulty-generator.md)。 diff --git a/docs/05-difficulty-generator.md b/docs/05-difficulty-generator.md new file mode 100644 index 0000000..eb9135f --- /dev/null +++ b/docs/05-difficulty-generator.md @@ -0,0 +1,457 @@ +# 05 Difficulty Generator — 城市抓猫猫 + +## 1. 目标 + +可控难度生成器负责把 `CityManifest.levelPresets` 变成可玩的单局棋盘,并保证: + +- 同一关卡能通过 `seed` 复现 +- 关卡默认可解,不能依赖广告道具救不可能局 +- 不同城市和关卡能被放进统一的难度带 +- 生成器输出不仅是“布局”,还是“布局质量评估结果” + +## 2. 非目标 + +这套系统暂时不做这些事: + +- 不做人类级最优解搜索 +- 不做手工逐关摆盘编辑器 +- 不把难度完全交给在线热更新动态生成 +- 不在 MVP 阶段做实时个性化难度 + +MVP 的目标是:`离线可复现 + 可校验 + 能调参`。 + +## 3. 设计原则 + +### 3.1 关卡必须先“无道具可过” + +道具是纠错和商业化入口,不是不可解关卡的补丁。 + +要求: + +- 所有基础关卡必须存在无道具通关路径 +- 关 1-4 的目标通关率以无道具为准 +- 关 5-6 可以允许少量玩家依赖道具,但不能依赖道具才有解 + +### 3.2 同一 seed 必须得到同一布局 + +- 便于复现 bug +- 便于分享每日挑战 +- 便于离线验证和埋点对齐 + +### 3.3 “难”来自信息压力,不来自脏规则 + +- 难度主要来自种类数、可见物件密度、遮挡深度、误点代价 +- 不做肉眼不可识别的遮挡判定 +- 不做首屏几乎无可操作空间的恶意布局 + +## 4. 核心数据模型 + +### 4.1 DifficultyProfile + +```javascript +{ + cityId: 'beijing', + levelId: 1, + seed: 11001, + elementCount: 6, + piecesPerElement: 3, + layers: 2, + density: 'low', + targetPassRate: 0.95, + targetDurationSec: [90, 150], +} +``` + +### 4.2 PieceInstance + +```javascript +{ + id: 'piece_0001', + elementId: 'beijing_01', + layer: 1, + x: 220, + y: 340, + width: 64, + height: 64, + rotation: -4, + removed: false, +} +``` + +### 4.3 BoardState + +```javascript +{ + boardId: 'beijing-1-11001', + cityId: 'beijing', + levelId: 1, + seed: 11001, + pieces: [], + overlapGraph: {}, + metrics: { + totalPieces: 18, + initialClickableRatio: 0.39, + simulatedPassRate: 0.97, + avgTurnsToFinish: 15.2, + toolFreePassRate: 0.97, + toolAssistPassRate: 1.0, + }, +} +``` + +### 4.4 RuntimeState + +```javascript +{ + slot: [], // 最多 7 个 + bypass: [], // Remove 使用,最多 3 个 + removedPieceIds: [], +} +``` + +## 5. 坐标系与布局空间 + +### 5.1 画布布局区 + +棋盘不直接自由散点,而是在“布局安全区”中放置: + +- 上方预留标题和城市信息区 +- 下方预留 7 格槽位和道具按钮区 +- 中间主区域作为堆叠区 + +建议生成器使用一套固定的 `anchor grid`: + +- 列数:6-8 +- 行数:7-9 +- 每个 anchor 可带随机偏移 +- 每层共享同一批 anchor,但偏移不同 + +这样做的目的: + +- 让视觉上看起来是自然堆叠 +- 实现上仍然可控、可校验 + +### 5.2 顶层回退落点 + +Undo 不恢复原位,而是回到“场景顶层空闲位置”。这个位置必须由生成器提前预留安全落点: + +- `returnLanes` 是一组不与标题区、按钮区重叠的顶层 anchor +- 回退件只落在 `returnLanes` +- 回退后不能让“初始唯一可点击物件”全部被堵死 + +## 6. Overlap Graph + +Overlap Graph 是生成器和运行时共同使用的核心结构。 + +### 6.1 定义 + +- 每个物件是一个节点 +- 若 A 遮挡 B,则存在有向边 `A -> B` +- 节点入度为 0 表示当前可点击 + +### 6.2 遮挡判定 + +两个物件存在遮挡关系,需同时满足: + +- `A.layer > B.layer` +- 包围盒重叠面积超过 `B` 面积的阈值 +- 且重叠区域命中了 `B` 的中心活跃区 + +推荐阈值: + +- `overlapRatio >= 0.2` +- 中心活跃区为物件中心 `50% x 50%` + +这样可以避免“边角轻微擦到就算完全遮挡”的脏判定。 + +### 6.3 图结构接口 + +```javascript +buildOverlapGraph(pieces) +getClickablePieces(boardState) +rebuildGraphAfterShuffle(boardState) +rebuildGraphAfterUndo(boardState) +``` + +## 7. 可点击判定 + +运行时的点击条件统一为: + +1. 物件未被移除 +2. 物件不在暂存槽和旁路寄存区 +3. 在 `overlapGraph` 中入度为 0 + +不引入额外“半可点”状态,避免玩家心智混乱。 + +## 8. 生成流水线 + +### 8.1 输入 + +- `CityManifest` +- `LevelPreset` +- `seed` + +### 8.2 输出 + +- `BoardState` +- 评估指标 `metrics` +- 若生成失败则返回错误原因和重试次数 + +### 8.3 流程 + +```text +1. 依据 level preset 生成元素池 +2. 根据 seed 洗牌元素池 +3. 计算 layerDistribution +4. 在 anchor grid 中逐层放置物件 +5. 构建 overlap graph +6. 计算初始可点击集合 +7. 跑 solver / validator +8. 若指标不达标则调参重试 +9. 输出最终 board + metrics +``` + +## 9. 元素池生成 + +### 9.1 规则 + +- `elementCount` 决定本关启用多少种元素 +- `piecesPerElement` 可以是单值,也可以是数组 +- 所有值必须是 3 的倍数 + +### 9.2 采样原则 + +- 优先从城市 `elements` 中选取高辨识度元素 +- 尽量覆盖多类目,不让一关全是美食或全是建筑 +- 同一城市 6 关的元素组合需要有节奏变化,不应完全重复 + +推荐策略: + +- 先抽每个类目至少 1 个 +- 剩余名额按权重补齐 + +## 10. 分层与密度模型 + +### 10.1 layerDistribution + +按 `density` 决定每层占比。 + +建议默认分布: + +| density | 分层建议 | +|--------|---------| +| `low` | `[0.40, 0.35, 0.25]` | +| `medium` | `[0.30, 0.30, 0.25, 0.15]` | +| `medium_high` | `[0.28, 0.27, 0.25, 0.20]` | +| `high` | `[0.25, 0.25, 0.25, 0.25]` | + +注意: + +- 实际层数少于数组长度时,截断并重新归一化 +- 最顶层占比不能低到“首屏没有明显可点物件” + +### 10.2 初始可见性约束 + +基础约束: + +- 初始可点击物件占比 `>= 0.30` +- 初始可点击集合中,至少存在 `2` 组可形成潜在三消的元素链 +- 首关至少有 `1` 组直接三消机会 + +这三条是“避免开局恶心人”的底线。 + +## 11. 布局放置规则 + +### 11.1 放置策略 + +- 从底层到顶层放置 +- 每层物件先选 anchor,再施加轻微随机偏移 +- 同类元素默认不要 3 个全堆在同一小区域 + +### 11.2 同类分散规则 + +同类元素放置时遵循: + +- 至少 1 个在首屏可点击层附近 +- 不允许 3 个完全同层紧邻,避免白给三消过多 +- 也不允许 3 个全部深埋,避免“直到残局才第一次看到” + +这是影响手感最明显的隐藏规则之一。 + +### 11.3 冲突规避 + +放置时要避开: + +- 顶部标题安全区 +- 底部槽位和道具按钮区 +- Undo 回退通道 `returnLanes` + +## 12. Seed 机制 + +### 12.1 来源 + +- 常规关:`hash(cityId + ':' + levelId + ':' + revision)` +- 每日挑战:`hash(utcDate + ':daily:' + cityId)` +- 调试复现:可直接手输 seed + +### 12.2 用途 + +- 复现线上问题 +- 让同一日挑战全量玩家拿到同一盘 +- 把埋点和具体布局绑定起来 + +### 12.3 revision + +如果某关布局难度整体偏离目标,不直接改 `levelId`,而是增加 `revision`。 + +这样可以区分: + +- `levelId`:设计上的第几关 +- `revision`:该关当前采用的生成版本 + +## 13. Solver 设计 + +生成器不能只靠一条贪心策略,否则评估噪音太大。 + +### 13.1 Solver 策略集 + +至少跑 3 类策略: + +- `match-first` + 优先选择能立刻三消的物件 +- `slot-build` + 优先补齐槽里已有 1-2 个的类型 +- `risk-averse` + 避免把槽位铺得过散,优先减少槽中类型数 + +### 13.2 单次模拟流程 + +```text +1. 初始化 board / slot / bypass +2. 获取 clickable pieces +3. 按当前策略给每个可点物件打分 +4. 选择得分最高的候选 +5. 放入槽位并执行三消 +6. 若触发 bypass 回归,则先回归再重新判定槽位 +7. 重建可点击集合 +8. 直到清盘成功或失败 +``` + +### 13.3 输出指标 + +每批模拟需要统计: + +- `toolFreePassRate` +- `avgTurnsToFinish` +- `avgPeakSlotUsage` +- `openingStability` + 定义为前 5 步中失败样本占比 +- `stuckRate` + 定义为进入硬死局的比例 + +## 14. 死局分类 + +### 14.1 Hard Deadlock + +满足以下条件时,判为硬死局: + +- 暂存槽已满 7 格 +- 当前无任何可形成三消的点击路径 +- 旁路寄存区为空或回归后仍失败 +- 无道具情况下不可能继续 + +### 14.2 Soft Deadlock + +满足以下条件时,判为软死局: + +- 当前直接继续大概率失败 +- 但使用 `Undo`、`Remove` 或 `Shuffle` 可恢复可玩态 + +设计原则: + +- 生成器允许出现软死局 +- 生成器不允许把基础通关路径设计成必经硬死局 + +## 15. 验收门槛 + +### 15.1 关卡级门槛 + +| 关卡 | `toolFreePassRate` | `toolAssistPassRate` | `initialClickableRatio` | +|------|--------------------|----------------------|-------------------------| +| 1 | `>= 0.95` | `1.00` | `>= 0.38` | +| 2 | `>= 0.90` | `>= 0.98` | `>= 0.35` | +| 3 | `>= 0.80` | `>= 0.92` | `>= 0.33` | +| 4 | `>= 0.70` | `>= 0.88` | `>= 0.32` | +| 5 | `>= 0.60` | `>= 0.82` | `>= 0.30` | +| 6 | `0.50 - 0.60` | `>= 0.75` | `>= 0.30` | + +说明: + +- `toolFreePassRate` 是核心门槛 +- `toolAssistPassRate` 用来衡量道具价值,不是救火指标 + +### 15.2 批量生成门槛 + +如果某个 `LevelPreset` 连续重试 `50` 次仍无法达到目标区间,应判为参数设计有问题,而不是继续无限重试。 + +## 16. 调参顺序 + +当某关过难时,按这个顺序调: + +1. 降低 `density` +2. 降低 `layers` +3. 降低 `elementCount` +4. 调整 `layerDistribution` +5. 调整同类元素分散规则 + +当某关过易时,反向调整。 + +不要优先改槽位大小,不要动基础规则。 + +## 17. 埋点与闭环 + +线上埋点至少记录: + +- `cityId` +- `levelId` +- `revision` +- `seed` +- `result`(win / fail / revive / quit) +- `peakSlotUsage` +- `toolUsage` +- `timeSpentSec` +- `firstFailStep` + +这些数据用来做三件事: + +- 验证离线模拟是否接近真实玩家 +- 找出异常 seed +- 调整不同关卡的目标通关率 + +## 18. 推荐接口 + +```javascript +generateBoard(cityId, levelId, seed) +buildOverlapGraph(pieces) +getClickablePieces(boardState) +simulateBoard(boardState, policy) +evaluateBoard(boardState, levelPreset) +classifyDeadlock(runtimeState, boardState) +regenerateWithTweaks(levelPreset, failureReason) +``` + +## 19. 与 04 文档的边界 + +05 文档消费 [04-city-content-system.md](./04-city-content-system.md) 提供的这些输入: + +- `CityManifest.elements` +- `CityManifest.levelPresets` +- `cityId / levelId / revision` + +05 文档输出给运行时的是: + +- 单局布局 +- 评估指标 +- 可复现 seed + +内容系统负责“有什么内容”,难度生成器负责“这些内容如何排成一盘可玩的局”。 diff --git a/js/content/cities/bangkok.js b/js/content/cities/bangkok.js new file mode 100644 index 0000000..993e7d1 --- /dev/null +++ b/js/content/cities/bangkok.js @@ -0,0 +1,64 @@ +module.exports = { + id: 'bangkok', + contentVersion: 1, + continentId: 'asia', + sortOrder: 3, + unlockAfterCityId: 'tokyo', + bundle: { + packId: 'asia-rest', + preload: 'on-city-page', + }, + display: { + name: '曼谷', + nameEn: 'Bangkok', + bgColor: '#FFB347', + tagline: '湄南河畔追猫猫', + funFact: '曼谷的全名有168个字母,是世界最长城市名', + }, + cover: { + catImage: 'images/cats/cat_bangkok.png', + catThumb: 'images/cats/cat_bangkok_thumb.png', + shareAccent: '#C9A96E', + }, + cat: { + id: 'cat_bangkok', + name: '暹暹', + intro: '戴泰式花环的暹罗猫,最爱在水上市场闲逛', + baseColor: '#FAF0E6', + pattern: 'siamese', + patternColor: '#6B4226', + accessory: '泰式花环', + expression: 'curious', + }, + passport: { + stampId: 'stamp_bangkok', + stampLabel: '曼谷', + }, + elements: [ + { id: 'bangkok_01', name: '冬阴功', category: 'food', rarity: 'core', image: 'images/elements/bangkok/bangkok_01.png', tags: ['soup', 'spicy', 'shrimp'] }, + { id: 'bangkok_02', name: '嘟嘟车', category: 'item', rarity: 'core', image: 'images/elements/bangkok/bangkok_02.png', tags: ['vehicle', 'colorful', 'street'] }, + { id: 'bangkok_03', name: '大象', category: 'nature', rarity: 'core', image: 'images/elements/bangkok/bangkok_03.png', tags: ['animal', 'grey', 'gentle'] }, + { id: 'bangkok_04', name: '泰拳手套', category: 'item', rarity: 'core', image: 'images/elements/bangkok/bangkok_04.png', tags: ['sport', 'red', 'fighting'] }, + { id: 'bangkok_05', name: '芒果糯米饭', category: 'food', rarity: 'core', image: 'images/elements/bangkok/bangkok_05.png', tags: ['sweet', 'mango', 'sticky'] }, + { id: 'bangkok_06', name: '金佛', category: 'landmark', rarity: 'core', image: 'images/elements/bangkok/bangkok_06.png', tags: ['buddha', 'golden', 'temple'] }, + { id: 'bangkok_07', name: '莲花', category: 'nature', rarity: 'core', image: 'images/elements/bangkok/bangkok_07.png', tags: ['flower', 'pink', 'water'] }, + { id: 'bangkok_08', name: '榴莲', category: 'food', rarity: 'core', image: 'images/elements/bangkok/bangkok_08.png', tags: ['fruit', 'spiky', 'yellow'] }, + { id: 'bangkok_09', name: '泰丝', category: 'culture', rarity: 'core', image: 'images/elements/bangkok/bangkok_09.png', tags: ['fabric', 'shimmer', 'craft'] }, + { id: 'bangkok_10', name: '船面', category: 'food', rarity: 'core', image: 'images/elements/bangkok/bangkok_10.png', tags: ['noodle', 'boat', 'dark'] }, + { id: 'bangkok_11', name: '佛塔', category: 'landmark', rarity: 'core', image: 'images/elements/bangkok/bangkok_11.png', tags: ['stupa', 'golden', 'tall'] }, + { id: 'bangkok_12', name: '椰子', category: 'nature', rarity: 'accent', image: 'images/elements/bangkok/bangkok_12.png', tags: ['fruit', 'tropical', 'green'] }, + { id: 'bangkok_13', name: '夜市灯笼', category: 'culture', rarity: 'accent', image: 'images/elements/bangkok/bangkok_13.png', tags: ['lantern', 'glow', 'night'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 13001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 13002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 13003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 13004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 13005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 13006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了曼谷猫!', + titlePassport: '曼谷护照盖章完成', + }, +} diff --git a/js/content/cities/beijing.js b/js/content/cities/beijing.js new file mode 100644 index 0000000..42b1c2d --- /dev/null +++ b/js/content/cities/beijing.js @@ -0,0 +1,65 @@ +module.exports = { + id: 'beijing', + contentVersion: 1, + continentId: 'asia', + sortOrder: 1, + unlockAfterCityId: null, + bundle: { + packId: 'main', + preload: 'startup', + }, + display: { + name: '北京', + nameEn: 'Beijing', + bgColor: '#CC2936', + tagline: '天安门前看猫猫', + funFact: '北京是世界上拥有最多宫殿的城市', + }, + cover: { + catImage: 'images/cats/cat_beijing.png', + catThumb: 'images/cats/cat_beijing_thumb.png', + shareAccent: '#F6D365', + }, + cat: { + id: 'cat_beijing', + name: '京京', + intro: '戴虎头帽的橘猫,最爱在胡同里晒太阳', + baseColor: '#F28C28', + pattern: 'tabby', + patternColor: '#D4761A', + accessory: '虎头帽', + expression: 'smile', + }, + passport: { + stampId: 'stamp_beijing', + stampLabel: '北京', + }, + elements: [ + { id: 'beijing_01', name: '糖葫芦', category: 'food', rarity: 'core', image: 'images/elements/beijing/beijing_01.png', tags: ['sweet', 'street-food', 'red'] }, + { id: 'beijing_02', name: '京剧脸谱', category: 'culture', rarity: 'core', image: 'images/elements/beijing/beijing_02.png', tags: ['opera', 'mask', 'colorful'] }, + { id: 'beijing_03', name: '天安门', category: 'landmark', rarity: 'core', image: 'images/elements/beijing/beijing_03.png', tags: ['building', 'red', 'iconic'] }, + { id: 'beijing_04', name: '烤鸭', category: 'food', rarity: 'core', image: 'images/elements/beijing/beijing_04.png', tags: ['meat', 'famous', 'golden'] }, + { id: 'beijing_05', name: '兔儿爷', category: 'culture', rarity: 'core', image: 'images/elements/beijing/beijing_05.png', tags: ['toy', 'festival', 'rabbit'] }, + { id: 'beijing_06', name: '长城砖', category: 'landmark', rarity: 'core', image: 'images/elements/beijing/beijing_06.png', tags: ['wall', 'historic', 'grey'] }, + { id: 'beijing_07', name: '故宫角楼', category: 'landmark', rarity: 'core', image: 'images/elements/beijing/beijing_07.png', tags: ['palace', 'golden', 'ancient'] }, + { id: 'beijing_08', name: '豆汁', category: 'food', rarity: 'core', image: 'images/elements/beijing/beijing_08.png', tags: ['drink', 'green', 'local'] }, + { id: 'beijing_09', name: '铜锣', category: 'item', rarity: 'core', image: 'images/elements/beijing/beijing_09.png', tags: ['instrument', 'golden', 'round'] }, + { id: 'beijing_10', name: '毛笔', category: 'item', rarity: 'core', image: 'images/elements/beijing/beijing_10.png', tags: ['writing', 'black', 'thin'] }, + { id: 'beijing_11', name: '鸟巢', category: 'landmark', rarity: 'core', image: 'images/elements/beijing/beijing_11.png', tags: ['stadium', 'modern', 'steel'] }, + { id: 'beijing_12', name: '冰糖葫芦', category: 'food', rarity: 'core', image: 'images/elements/beijing/beijing_12.png', tags: ['candy', 'stick', 'red'] }, + { id: 'beijing_13', name: '四合院门', category: 'culture', rarity: 'accent', image: 'images/elements/beijing/beijing_13.png', tags: ['door', 'red', 'traditional'] }, + { id: 'beijing_14', name: '景泰蓝', category: 'item', rarity: 'accent', image: 'images/elements/beijing/beijing_14.png', tags: ['craft', 'blue', 'enamel'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 11001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 11002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 11003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 11004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 11005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 11006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了北京猫!', + titlePassport: '北京护照盖章完成', + }, +} diff --git a/js/content/cities/istanbul.js b/js/content/cities/istanbul.js new file mode 100644 index 0000000..2d4a4e2 --- /dev/null +++ b/js/content/cities/istanbul.js @@ -0,0 +1,65 @@ +module.exports = { + id: 'istanbul', + contentVersion: 1, + continentId: 'asia', + sortOrder: 6, + unlockAfterCityId: 'singapore', + bundle: { + packId: 'asia-rest', + preload: 'on-city-page', + }, + display: { + name: '伊斯坦布尔', + nameEn: 'Istanbul', + bgColor: '#1A5276', + tagline: '博斯普鲁斯的猫', + funFact: '伊斯坦布尔是唯一横跨两大洲的城市', + }, + cover: { + catImage: 'images/cats/cat_istanbul.png', + catThumb: 'images/cats/cat_istanbul_thumb.png', + shareAccent: '#E8D5B7', + }, + cat: { + id: 'cat_istanbul', + name: '安安', + intro: '佩戴恶魔之眼项圈的安哥拉猫,神秘优雅', + baseColor: '#F5F5F5', + pattern: 'solid', + patternColor: '#E8E8E8', + accessory: '恶魔之眼项圈', + expression: 'mysterious', + }, + passport: { + stampId: 'stamp_istanbul', + stampLabel: '伊斯坦布尔', + }, + elements: [ + { id: 'istanbul_01', name: '蓝色清真寺', category: 'landmark', rarity: 'core', image: 'images/elements/istanbul/istanbul_01.png', tags: ['mosque', 'blue', 'dome'] }, + { id: 'istanbul_02', name: '土耳其红茶', category: 'food', rarity: 'core', image: 'images/elements/istanbul/istanbul_02.png', tags: ['tea', 'glass', 'red'] }, + { id: 'istanbul_03', name: '热气球', category: 'item', rarity: 'core', image: 'images/elements/istanbul/istanbul_03.png', tags: ['balloon', 'sky', 'colorful'] }, + { id: 'istanbul_04', name: '烤肉串', category: 'food', rarity: 'core', image: 'images/elements/istanbul/istanbul_04.png', tags: ['kebab', 'grill', 'meat'] }, + { id: 'istanbul_05', name: '郁金香', category: 'nature', rarity: 'core', image: 'images/elements/istanbul/istanbul_05.png', tags: ['flower', 'red', 'spring'] }, + { id: 'istanbul_06', name: '恶魔之眼', category: 'culture', rarity: 'core', image: 'images/elements/istanbul/istanbul_06.png', tags: ['amulet', 'blue', 'protection'] }, + { id: 'istanbul_07', name: '土耳其冰淇淋', category: 'food', rarity: 'core', image: 'images/elements/istanbul/istanbul_07.png', tags: ['dessert', 'stretchy', 'fun'] }, + { id: 'istanbul_08', name: '地毯', category: 'item', rarity: 'core', image: 'images/elements/istanbul/istanbul_08.png', tags: ['textile', 'pattern', 'woven'] }, + { id: 'istanbul_09', name: '石榴', category: 'nature', rarity: 'core', image: 'images/elements/istanbul/istanbul_09.png', tags: ['fruit', 'red', 'juicy'] }, + { id: 'istanbul_10', name: '旋转舞裙', category: 'culture', rarity: 'core', image: 'images/elements/istanbul/istanbul_10.png', tags: ['dance', 'whirl', 'white'] }, + { id: 'istanbul_11', name: '圣索菲亚', category: 'landmark', rarity: 'core', image: 'images/elements/istanbul/istanbul_11.png', tags: ['cathedral', 'dome', 'historic'] }, + { id: 'istanbul_12', name: '土耳其软糖', category: 'food', rarity: 'core', image: 'images/elements/istanbul/istanbul_12.png', tags: ['candy', 'sweet', 'colorful'] }, + { id: 'istanbul_13', name: '波斯猫雕像', category: 'culture', rarity: 'core', image: 'images/elements/istanbul/istanbul_13.png', tags: ['statue', 'cat', 'elegant'] }, + { id: 'istanbul_14', name: '香料', category: 'item', rarity: 'core', image: 'images/elements/istanbul/istanbul_14.png', tags: ['spice', 'bazaar', 'aromatic'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 16001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 16002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 16003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 16004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 16005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 16006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了伊斯坦布尔猫!', + titlePassport: '伊斯坦布尔护照盖章完成', + }, +} diff --git a/js/content/cities/seoul.js b/js/content/cities/seoul.js new file mode 100644 index 0000000..643bc2e --- /dev/null +++ b/js/content/cities/seoul.js @@ -0,0 +1,64 @@ +module.exports = { + id: 'seoul', + contentVersion: 1, + continentId: 'asia', + sortOrder: 4, + unlockAfterCityId: 'bangkok', + bundle: { + packId: 'asia-rest', + preload: 'on-city-page', + }, + display: { + name: '首尔', + nameEn: 'Seoul', + bgColor: '#4A90D9', + tagline: '景福宫前遇猫猫', + funFact: '首尔的地铁系统覆盖率全球第一', + }, + cover: { + catImage: 'images/cats/cat_seoul.png', + catThumb: 'images/cats/cat_seoul_thumb.png', + shareAccent: '#F5E6CA', + }, + cat: { + id: 'cat_seoul', + name: '韩韩', + intro: '戴韩服小帽的韩国短尾猫,爱在景福宫前眨眼卖萌', + baseColor: '#F5E6CA', + pattern: 'bicolor', + patternColor: '#D4A574', + accessory: '韩服小帽', + expression: 'wink', + }, + passport: { + stampId: 'stamp_seoul', + stampLabel: '首尔', + }, + elements: [ + { id: 'seoul_01', name: '泡菜坛', category: 'food', rarity: 'core', image: 'images/elements/seoul/seoul_01.png', tags: ['fermented', 'jar', 'red'] }, + { id: 'seoul_02', name: '石锅拌饭', category: 'food', rarity: 'core', image: 'images/elements/seoul/seoul_02.png', tags: ['rice', 'hot', 'colorful'] }, + { id: 'seoul_03', name: '韩服', category: 'culture', rarity: 'core', image: 'images/elements/seoul/seoul_03.png', tags: ['clothing', 'traditional', 'colorful'] }, + { id: 'seoul_04', name: '景福宫', category: 'landmark', rarity: 'core', image: 'images/elements/seoul/seoul_04.png', tags: ['palace', 'historic', 'grand'] }, + { id: 'seoul_05', name: '烧酒瓶', category: 'item', rarity: 'core', image: 'images/elements/seoul/seoul_05.png', tags: ['drink', 'green', 'bottle'] }, + { id: 'seoul_06', name: '年糕', category: 'food', rarity: 'core', image: 'images/elements/seoul/seoul_06.png', tags: ['sweet', 'chewy', 'white'] }, + { id: 'seoul_07', name: 'K-pop话筒', category: 'item', rarity: 'core', image: 'images/elements/seoul/seoul_07.png', tags: ['music', 'pop', 'shiny'] }, + { id: 'seoul_08', name: '太极旗扇', category: 'culture', rarity: 'core', image: 'images/elements/seoul/seoul_08.png', tags: ['flag', 'fan', 'national'] }, + { id: 'seoul_09', name: '韩式炸鸡', category: 'food', rarity: 'core', image: 'images/elements/seoul/seoul_09.png', tags: ['fried', 'crispy', 'golden'] }, + { id: 'seoul_10', name: '柿子', category: 'nature', rarity: 'core', image: 'images/elements/seoul/seoul_10.png', tags: ['fruit', 'orange', 'autumn'] }, + { id: 'seoul_11', name: '海苔卷', category: 'food', rarity: 'core', image: 'images/elements/seoul/seoul_11.png', tags: ['seaweed', 'roll', 'green'] }, + { id: 'seoul_12', name: '南山塔', category: 'landmark', rarity: 'core', image: 'images/elements/seoul/seoul_12.png', tags: ['tower', 'romantic', 'iconic'] }, + { id: 'seoul_13', name: '木槿花', category: 'nature', rarity: 'core', image: 'images/elements/seoul/seoul_13.png', tags: ['flower', 'pink', 'national'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 14001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 14002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 14003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 14004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 14005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 14006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了首尔猫!', + titlePassport: '首尔护照盖章完成', + }, +} diff --git a/js/content/cities/singapore.js b/js/content/cities/singapore.js new file mode 100644 index 0000000..f0b8972 --- /dev/null +++ b/js/content/cities/singapore.js @@ -0,0 +1,64 @@ +module.exports = { + id: 'singapore', + contentVersion: 1, + continentId: 'asia', + sortOrder: 5, + unlockAfterCityId: 'seoul', + bundle: { + packId: 'asia-rest', + preload: 'on-city-page', + }, + display: { + name: '新加坡', + nameEn: 'Singapore', + bgColor: '#2ECC71', + tagline: '鱼尾狮旁撸猫猫', + funFact: '新加坡是全球绿化覆盖率最高的城市之一', + }, + cover: { + catImage: 'images/cats/cat_singapore.png', + catThumb: 'images/cats/cat_singapore_thumb.png', + shareAccent: '#F28C28', + }, + cat: { + id: 'cat_singapore', + name: '狮狮', + intro: '戴小狮子鬃毛的花猫,骄傲地守护鱼尾狮', + baseColor: '#FFFFFF', + pattern: 'calico', + patternColor: '#F28C28', + accessory: '小狮子鬃毛', + expression: 'proud', + }, + passport: { + stampId: 'stamp_singapore', + stampLabel: '新加坡', + }, + elements: [ + { id: 'singapore_01', name: '鱼尾狮', category: 'landmark', rarity: 'core', image: 'images/elements/singapore/singapore_01.png', tags: ['statue', 'water', 'iconic'] }, + { id: 'singapore_02', name: '辣椒螃蟹', category: 'food', rarity: 'core', image: 'images/elements/singapore/singapore_02.png', tags: ['seafood', 'spicy', 'red'] }, + { id: 'singapore_03', name: '榴莲建筑', category: 'landmark', rarity: 'core', image: 'images/elements/singapore/singapore_03.png', tags: ['theater', 'spiky', 'modern'] }, + { id: 'singapore_04', name: '叻沙', category: 'food', rarity: 'core', image: 'images/elements/singapore/singapore_04.png', tags: ['noodle', 'coconut', 'spicy'] }, + { id: 'singapore_05', name: '金沙酒店', category: 'landmark', rarity: 'core', image: 'images/elements/singapore/singapore_05.png', tags: ['hotel', 'pool', 'skyline'] }, + { id: 'singapore_06', name: '兰花', category: 'nature', rarity: 'core', image: 'images/elements/singapore/singapore_06.png', tags: ['flower', 'purple', 'national'] }, + { id: 'singapore_07', name: '肉骨茶', category: 'food', rarity: 'core', image: 'images/elements/singapore/singapore_07.png', tags: ['soup', 'herbal', 'pork'] }, + { id: 'singapore_08', name: '冰激凌三明治', category: 'food', rarity: 'core', image: 'images/elements/singapore/singapore_08.png', tags: ['dessert', 'cold', 'colorful'] }, + { id: 'singapore_09', name: '组屋', category: 'culture', rarity: 'core', image: 'images/elements/singapore/singapore_09.png', tags: ['housing', 'colorful', 'block'] }, + { id: 'singapore_10', name: '咖椰吐司', category: 'food', rarity: 'core', image: 'images/elements/singapore/singapore_10.png', tags: ['toast', 'jam', 'breakfast'] }, + { id: 'singapore_11', name: '摩天轮', category: 'item', rarity: 'core', image: 'images/elements/singapore/singapore_11.png', tags: ['wheel', 'night', 'view'] }, + { id: 'singapore_12', name: '热带雨林', category: 'nature', rarity: 'core', image: 'images/elements/singapore/singapore_12.png', tags: ['forest', 'green', 'tropical'] }, + { id: 'singapore_13', name: '娘惹瓷砖', category: 'culture', rarity: 'core', image: 'images/elements/singapore/singapore_13.png', tags: ['tile', 'pattern', 'peranakan'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 15001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 15002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 15003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 15004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 15005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 15006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了新加坡猫!', + titlePassport: '新加坡护照盖章完成', + }, +} diff --git a/js/content/cities/tokyo.js b/js/content/cities/tokyo.js new file mode 100644 index 0000000..f6b7ac3 --- /dev/null +++ b/js/content/cities/tokyo.js @@ -0,0 +1,65 @@ +module.exports = { + id: 'tokyo', + contentVersion: 1, + continentId: 'asia', + sortOrder: 2, + unlockAfterCityId: 'beijing', + bundle: { + packId: 'asia-rest', + preload: 'on-city-page', + }, + display: { + name: '东京', + nameEn: 'Tokyo', + bgColor: '#E84057', + tagline: '东京タワー下的喵', + funFact: '东京的铁路网是世界上最密集的', + }, + cover: { + catImage: 'images/cats/cat_tokyo.png', + catThumb: 'images/cats/cat_tokyo_thumb.png', + shareAccent: '#FFB7C5', + }, + cat: { + id: 'cat_tokyo', + name: '樱樱', + intro: '戴着招财猫铃铛的白猫,喜欢在樱花树下打盹', + baseColor: '#FFFFFF', + pattern: 'solid', + patternColor: '#F5F5F5', + accessory: '招财猫铃铛', + expression: 'wink', + }, + passport: { + stampId: 'stamp_tokyo', + stampLabel: '东京', + }, + elements: [ + { id: 'tokyo_01', name: '寿司', category: 'food', rarity: 'core', image: 'images/elements/tokyo/tokyo_01.png', tags: ['fish', 'rice', 'fresh'] }, + { id: 'tokyo_02', name: '招财猫', category: 'culture', rarity: 'core', image: 'images/elements/tokyo/tokyo_02.png', tags: ['lucky', 'cat', 'golden'] }, + { id: 'tokyo_03', name: '富士山', category: 'landmark', rarity: 'core', image: 'images/elements/tokyo/tokyo_03.png', tags: ['mountain', 'snow', 'iconic'] }, + { id: 'tokyo_04', name: '樱花', category: 'nature', rarity: 'core', image: 'images/elements/tokyo/tokyo_04.png', tags: ['flower', 'pink', 'spring'] }, + { id: 'tokyo_05', name: '鸟居', category: 'landmark', rarity: 'core', image: 'images/elements/tokyo/tokyo_05.png', tags: ['gate', 'red', 'shrine'] }, + { id: 'tokyo_06', name: '拉面', category: 'food', rarity: 'core', image: 'images/elements/tokyo/tokyo_06.png', tags: ['noodle', 'hot', 'bowl'] }, + { id: 'tokyo_07', name: '抹茶', category: 'food', rarity: 'core', image: 'images/elements/tokyo/tokyo_07.png', tags: ['tea', 'green', 'powder'] }, + { id: 'tokyo_08', name: '浮世绘', category: 'culture', rarity: 'core', image: 'images/elements/tokyo/tokyo_08.png', tags: ['art', 'wave', 'woodblock'] }, + { id: 'tokyo_09', name: '新干线', category: 'item', rarity: 'core', image: 'images/elements/tokyo/tokyo_09.png', tags: ['train', 'fast', 'white'] }, + { id: 'tokyo_10', name: '达摩', category: 'culture', rarity: 'core', image: 'images/elements/tokyo/tokyo_10.png', tags: ['doll', 'red', 'wish'] }, + { id: 'tokyo_11', name: '和服扇', category: 'item', rarity: 'core', image: 'images/elements/tokyo/tokyo_11.png', tags: ['fan', 'elegant', 'pattern'] }, + { id: 'tokyo_12', name: '章鱼烧', category: 'food', rarity: 'core', image: 'images/elements/tokyo/tokyo_12.png', tags: ['octopus', 'ball', 'street-food'] }, + { id: 'tokyo_13', name: '东京塔', category: 'landmark', rarity: 'accent', image: 'images/elements/tokyo/tokyo_13.png', tags: ['tower', 'orange', 'night'] }, + { id: 'tokyo_14', name: '柴犬', category: 'nature', rarity: 'accent', image: 'images/elements/tokyo/tokyo_14.png', tags: ['dog', 'cute', 'fluffy'] }, + ], + levelPresets: [ + { id: 1, difficultyTier: 'intro', seedBase: 12001, elementCount: 6, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.95, targetDurationSec: [60, 120] }, + { id: 2, difficultyTier: 'easy', seedBase: 12002, elementCount: 7, piecesPerElement: 3, layers: 2, density: 'low', targetPassRate: 0.90, targetDurationSec: [60, 120] }, + { id: 3, difficultyTier: 'normal', seedBase: 12003, elementCount: 8, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.80, targetDurationSec: [90, 150] }, + { id: 4, difficultyTier: 'normal', seedBase: 12004, elementCount: 9, piecesPerElement: 3, layers: 3, density: 'medium', targetPassRate: 0.70, targetDurationSec: [90, 150] }, + { id: 5, difficultyTier: 'hard', seedBase: 12005, elementCount: 10, piecesPerElement: 3, layers: 4, density: 'medium_high', targetPassRate: 0.60, targetDurationSec: [120, 180] }, + { id: 6, difficultyTier: 'boss', seedBase: 12006, elementCount: 10, piecesPerElement: [3,3,3,3,3,6,3,3,3,3], layers: 4, density: 'high', targetPassRate: 0.55, targetDurationSec: [120, 240] }, + ], + shareCard: { + titleUnlocked: '我解锁了东京猫!', + titlePassport: '东京护照盖章完成', + }, +}