import { continents } from './continents/index.js' import cities from './cities/index.js' import { createDefaultPlayerState } from './default-player-state.js' import { getCatalogChildren } from './navigation/world-catalog.js' import { getEnabledRoots, getNavNode as getRuntimeNavNode } from './navigation/runtime-nav.js' import { createBundleResolver } from './registry/bundle-resolver.js' import { createCityRegistry } from './registry/city-registry.js' import { createContinentRegistry } from './registry/continent-registry.js' import { projectCityCardView, projectCityProgress } from './registry/progress-projector.js' import { validateAssets } from './validation/validate-assets.js' import { validateCity } from './validation/validate-city.js' import { validateContinent } from './validation/validate-continent.js' export function validateContent(options = {}) { const continentIds = new Set(continents.map((continent) => continent.id)) const cityIds = new Set(cities.map((city) => city.id)) const errors = [] const warnings = [] for (const continent of continents) { const result = validateContinent(continent, cityIds) errors.push(...result.errors) warnings.push(...result.warnings) } for (const city of cities) { const result = validateCity(city, continentIds, cityIds) errors.push(...result.errors) warnings.push(...result.warnings) const assetResult = validateAssets(city, options.assetExists) errors.push(...assetResult.errors) warnings.push(...assetResult.warnings) } return { errors, warnings } } export function createContentSystem() { const continentRegistry = createContinentRegistry(continents) const cityRegistry = createCityRegistry(cities) const bundleResolver = createBundleResolver(cityRegistry) const homeRootNodes = getCatalogChildren(null) const giftAlbumDefinitions = [ { id: 'magnets', name: '冰箱贴册' }, { id: 'stamps', name: '邮票册' }, { id: 'cats', name: '猫猫册' }, ] function getCompletedCityCount(playerState) { return cityRegistry .getAllCities() .filter((city) => projectCityProgress(city, playerState).isCompleted) .length } function projectNavNode(node) { if (!node) { return null } return { ...node, isUnlocked: node.isUnlockedByDefault, } } function projectHomeTile(node, playerState) { const completedCityCount = getCompletedCityCount(playerState) const isMashupTile = node.id === 'mashup' const isComingSoonTile = node.id === 'coming_soon' const isUnlocked = isMashupTile ? completedCityCount >= 2 : node.status === 'active' && node.isUnlockedByDefault const isInteractive = isComingSoonTile || isMashupTile || node.type === 'continent' return { ...node, isUnlocked, isInteractive, isSpecial: isMashupTile || isComingSoonTile, } } function getMagnetEntries(playerState) { const magnets = playerState.collectedMagnets ?? [] return cityRegistry.getAllCities().map((city) => { const collectedCount = magnets.filter((entry) => entry.cityId === city.id).length return { cityId: city.id, cityName: city.display.name, cityNameEn: city.display.nameEn, themeColor: city.display.bgColor, collectedCount, totalCount: city.levelPresets.length, } }) } function getStampEntries(playerState) { const stamps = playerState.collectedStamps ?? [] return cityRegistry.getAllCities().map((city) => { const stampId = city.passport?.stampId ?? `stamp_${city.id}` return { cityId: city.id, cityName: city.display.name, cityNameEn: city.display.nameEn, themeColor: city.display.bgColor, stampId, isCollected: stamps.some((entry) => entry.stampId === stampId), } }) } function getCatEntries(playerState) { const collectedCats = new Set(playerState.collectedCats ?? []) return cityRegistry.getAllCities().map((city) => ({ cityId: city.id, cityName: city.display.name, cityNameEn: city.display.nameEn, themeColor: city.display.bgColor, catId: city.cat.id, catName: city.cat.name, catThumb: city.cover.catThumb, isCollected: collectedCats.has(city.id), })) } function summarizeAlbumEntries(albumId, entries) { if (albumId === 'magnets') { return { collectedCount: entries.reduce((sum, entry) => sum + entry.collectedCount, 0), totalCount: entries.reduce((sum, entry) => sum + entry.totalCount, 0), } } return { collectedCount: entries.filter((entry) => entry.isCollected).length, totalCount: entries.length, } } function getGiftAlbumEntries(albumId, playerState) { if (albumId === 'magnets') { return getMagnetEntries(playerState) } if (albumId === 'stamps') { return getStampEntries(playerState) } if (albumId === 'cats') { return getCatEntries(playerState) } return [] } function getGiftAlbums(playerState) { return giftAlbumDefinitions.map((definition) => { const entries = getGiftAlbumEntries(definition.id, playerState) const summary = summarizeAlbumEntries(definition.id, entries) return { ...definition, collectedCount: summary.collectedCount, totalCount: summary.totalCount, } }) } function projectCityNavEntry(cityId, parentId, playerState) { const city = cityRegistry.getCity(cityId) if (!city) { return null } return { ...projectCityCardView(city, playerState), id: city.id, type: 'city', parentId, themeColor: city.display.bgColor, childType: null, childIds: [], pageSize: 0, status: 'active', isUnlockedByDefault: cityId === 'beijing', sortOrder: city.sortOrder, } } function getNavChildren(nodeId, playerState) { const node = getRuntimeNavNode(nodeId) if (!node || !node.childType) { return [] } if (node.childType === 'city') { return node.childIds .map((cityId) => projectCityNavEntry(cityId, node.id, playerState)) .filter(Boolean) } return node.childIds .map((childId) => projectNavNode(getRuntimeNavNode(childId))) .filter(Boolean) } return { getContinentList() { return continentRegistry.getContinentList() }, getContinent(continentId) { return continentRegistry.getContinent(continentId) }, getCity(cityId) { return cityRegistry.getCity(cityId) }, listCitiesByContinent(continentId) { return cityRegistry.listCitiesByContinent(continentId) }, getLevelPreset(cityId, levelId) { return cityRegistry.getLevelPreset(cityId, levelId) }, getCityProgress(cityId, playerState) { const city = cityRegistry.getCity(cityId) return city ? projectCityProgress(city, playerState) : null }, getCityCardView(cityId, playerState) { const city = cityRegistry.getCity(cityId) return city ? projectCityCardView(city, playerState) : null }, listCityCards(continentId, playerState) { const navChildren = getNavChildren(continentId, playerState) if (navChildren.length > 0) { return navChildren.filter((entry) => entry.type === 'city') } return cityRegistry .listCitiesByContinent(continentId) .map((city) => projectCityCardView(city, playerState)) }, getRootNavNodes() { return getEnabledRoots().map((node) => projectNavNode(node)) }, getHomeTiles(playerState) { return homeRootNodes.map((node) => projectHomeTile(node, playerState)) }, getHomeTile(tileId, playerState) { const node = homeRootNodes.find((entry) => entry.id === tileId) ?? null return node ? projectHomeTile(node, playerState) : null }, getGiftAlbums(playerState) { return getGiftAlbums(playerState) }, getGiftAlbumEntries(albumId, playerState) { return getGiftAlbumEntries(albumId, playerState) }, getNavNode(nodeId) { return projectNavNode(getRuntimeNavNode(nodeId)) }, getNavChildren(nodeId, playerState) { return getNavChildren(nodeId, playerState) }, ensureCityBundle(cityId) { return bundleResolver.ensureCityBundle(cityId) }, validateContent(options) { return validateContent(options) }, } } export { continents, cities, createDefaultPlayerState, } export default createContentSystem