- 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) <noreply@anthropic.com>
10 KiB
10 KiB
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. 数据层级
运行时的数据层级固定为:
ContinentManifest
-> CityManifest
-> ElementSpec[]
-> CatSpec
-> LevelPreset[]
-> PassportSpec
-> ShareCardSpec
4.1 ContinentManifest
洲配置只描述“入口”和“顺序”,不重复城市详情。
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
城市配置是运行时的主实体。
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
元素是关卡和图鉴的最小单位。
{
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/naturerarity先保留core/accent两档,MVP 默认都可用coretags给后续活动筛选、图鉴展示、搜索扩展留接口
4.4 LevelPreset
关卡不直接保存所有物件坐标,只保存生成输入。
{
id: 1,
difficultyTier: 'intro',
seedBase: 11001,
elementCount: 6,
piecesPerElement: 3,
layers: 2,
density: 'low',
targetPassRate: 0.95,
targetDurationSec: [90, 150],
}
设计原则:
- 关卡配置是“生成器输入 + 目标指标”
- 真正布局由
difficulty generator按seed生成 - 同一关卡可复现实例,不手工存静态坐标表
5. 推荐目录结构
系统层推荐的逻辑目录如下:
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 启动阶段
- 读取
continents/index - 初始化
ContinentRegistry - 读取主包内首城市
beijing - 生成城市页卡片数据
- 用
playerState投影出解锁态和通关态
6.2 进入城市页
- 取亚洲
unlockOrder - 读取所有已在本地可用的
CityManifest摘要 - 对未下载但已可见的城市展示灰态卡片
- 对“下一目标城市”预触发分包预下载
6.3 进入单城市
- 检查
bundle.packId - 若分包未加载,则先下载/加载
- 加载
CityManifest - 加载该城市封面猫猫、元素图标、分享卡资源
- 生成 6 个关卡入口及对应种子
6.4 完成城市
levelProgress全部满 6 关- 写入
collectedCats - 写入
passportStamps - 解锁
unlockAfterCityId的后续城市 - 预下载下一个城市内容包
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 元数据
每个城市都带自己的包信息:
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过长、色彩接近、标签过少
推荐接口:
validateContinent(continentConfig)
validateCity(cityConfig, allContinentConfigs)
validateAssetPaths(cityConfig, assetIndex)
10. 内容扩展流程
新增一个城市时,流程固定为:
- 创建
CityManifest - 准备 12-15 个元素图标
- 准备猫猫封面和缩略图
- 补齐护照和分享文案
- 跑内容校验
- 跑难度生成器验证 6 个基础关卡
- 把城市 id 挂到对应洲的
cityIds和unlockOrder - 再跑一次跨文件校验
只有第 7 步会影响洲索引,其余步骤都不该动玩法代码。
11. 版本与兼容策略
11.1 contentVersion
contentVersion用来标记城市配置版本- 仅当字段结构变化或默认行为变化时递增
- 纯文案或美术替换不必改版本
11.2 删除与下线
上线后不建议真正删除城市 id。
若后续城市下线:
- 内容层标为
disabled: true - 入口层不再展示
- 存档里旧
cityId仍然保留,避免脏存档
12. 对工程实现的要求
内容系统落地时需要优先保证这些接口稳定:
getContinentList()
getContinent(continentId)
getCity(cityId)
getCityCardView(cityId, playerState)
getLevelPreset(cityId, levelId)
ensureCityBundle(cityId)
validateContent()
这批接口一旦稳定,UI、地图、图鉴、护照、分享页都能复用同一层内容数据。
13. 本文档不负责的事情
以下内容不在 04 文档内定义:
- 具体关卡布局算法
- overlap graph 结构
- solver / validator 的实现
- 道具状态机细节