export type DemoGamePreset = 'classic' | 'score-o' export type BusinessLaunchSource = 'demo' | 'competition' | 'direct-event' | 'custom' export interface GameConfigLaunchRequest { configUrl: string configLabel: string configChecksumSha256?: string | null releaseId?: string | null routeCode?: string | null } export interface BusinessLaunchContext { source: BusinessLaunchSource competitionId?: string | null eventId?: string | null launchRequestId?: string | null participantId?: string | null sessionId?: string | null sessionToken?: string | null sessionTokenExpiresAt?: string | null realtimeEndpoint?: string | null realtimeToken?: string | null } export interface GameVariantLaunchContext { variantId?: string | null variantName?: string | null routeCode?: string | null assignmentMode?: string | null } export interface GameRuntimeLaunchContext { runtimeBindingId?: string | null placeId?: string | null placeName?: string | null mapId?: string | null mapName?: string | null tileReleaseId?: string | null courseSetId?: string | null courseVariantId?: string | null routeCode?: string | null } export interface GamePresentationLaunchContext { presentationId?: string | null templateKey?: string | null version?: string | null } export interface GameContentBundleLaunchContext { bundleId?: string | null bundleType?: string | null version?: string | null } export interface GameLaunchEnvelope { config: GameConfigLaunchRequest business: BusinessLaunchContext | null variant?: GameVariantLaunchContext | null runtime?: GameRuntimeLaunchContext | null presentation?: GamePresentationLaunchContext | null contentBundle?: GameContentBundleLaunchContext | null } export interface MapPageLaunchOptions { launchId?: string recoverSession?: string preset?: string configUrl?: string configLabel?: string configChecksumSha256?: string releaseId?: string routeCode?: string launchSource?: string competitionId?: string eventId?: string launchRequestId?: string participantId?: string sessionId?: string sessionToken?: string sessionTokenExpiresAt?: string realtimeEndpoint?: string realtimeToken?: string variantId?: string variantName?: string assignmentMode?: string runtimeBindingId?: string placeId?: string placeName?: string mapId?: string mapName?: string tileReleaseId?: string courseSetId?: string courseVariantId?: string presentationId?: string presentationTemplateKey?: string presentationVersion?: string contentBundleId?: string contentBundleType?: string contentBundleVersion?: string } type PendingGameLaunchStore = Record const PENDING_GAME_LAUNCH_STORAGE_KEY = 'cmr.pendingGameLaunch.v1' const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json' const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json' function normalizeOptionalString(value: unknown): string | null { if (typeof value !== 'string') { return null } const normalized = decodeURIComponent(value).trim() return normalized ? normalized : null } function resolveDemoPreset(value: string | null): DemoGamePreset { return value === 'score-o' ? 'score-o' : 'classic' } function resolveBusinessLaunchSource(value: string | null): BusinessLaunchSource { if (value === 'competition' || value === 'direct-event' || value === 'custom') { return value } return 'demo' } function buildDemoConfig(preset: DemoGamePreset): GameConfigLaunchRequest { if (preset === 'score-o') { return { configUrl: SCORE_O_REMOTE_GAME_CONFIG_URL, configLabel: '积分赛配置', } } return { configUrl: CLASSIC_REMOTE_GAME_CONFIG_URL, configLabel: '顺序赛配置', } } function hasBusinessFields(context: Omit): boolean { return Object.values(context).some((value) => typeof value === 'string' && value.length > 0) } function buildBusinessLaunchContext(options?: MapPageLaunchOptions | null): BusinessLaunchContext | null { if (!options) { return null } const context = { competitionId: normalizeOptionalString(options.competitionId), eventId: normalizeOptionalString(options.eventId), launchRequestId: normalizeOptionalString(options.launchRequestId), participantId: normalizeOptionalString(options.participantId), sessionId: normalizeOptionalString(options.sessionId), sessionToken: normalizeOptionalString(options.sessionToken), sessionTokenExpiresAt: normalizeOptionalString(options.sessionTokenExpiresAt), realtimeEndpoint: normalizeOptionalString(options.realtimeEndpoint), realtimeToken: normalizeOptionalString(options.realtimeToken), } const launchSource = normalizeOptionalString(options.launchSource) if (!hasBusinessFields(context) && launchSource === null) { return null } return { source: resolveBusinessLaunchSource(launchSource), ...context, } } function buildVariantLaunchContext(options?: MapPageLaunchOptions | null): GameVariantLaunchContext | null { if (!options) { return null } const variantId = normalizeOptionalString(options.variantId) const variantName = normalizeOptionalString(options.variantName) const routeCode = normalizeOptionalString(options.routeCode) const assignmentMode = normalizeOptionalString(options.assignmentMode) if (!variantId && !variantName && !routeCode && !assignmentMode) { return null } return { variantId, variantName, routeCode, assignmentMode, } } function buildRuntimeLaunchContext(options?: MapPageLaunchOptions | null): GameRuntimeLaunchContext | null { if (!options) { return null } const runtimeBindingId = normalizeOptionalString(options.runtimeBindingId) const placeId = normalizeOptionalString(options.placeId) const placeName = normalizeOptionalString(options.placeName) const mapId = normalizeOptionalString(options.mapId) const mapName = normalizeOptionalString(options.mapName) const tileReleaseId = normalizeOptionalString(options.tileReleaseId) const courseSetId = normalizeOptionalString(options.courseSetId) const courseVariantId = normalizeOptionalString(options.courseVariantId) const routeCode = normalizeOptionalString(options.routeCode) if (!runtimeBindingId && !placeId && !placeName && !mapId && !mapName && !tileReleaseId && !courseSetId && !courseVariantId && !routeCode) { return null } return { runtimeBindingId, placeId, placeName, mapId, mapName, tileReleaseId, courseSetId, courseVariantId, routeCode, } } function buildPresentationLaunchContext(options?: MapPageLaunchOptions | null): GamePresentationLaunchContext | null { if (!options) { return null } const presentationId = normalizeOptionalString(options.presentationId) const templateKey = normalizeOptionalString(options.presentationTemplateKey) const version = normalizeOptionalString(options.presentationVersion) if (!presentationId && !templateKey && !version) { return null } return { presentationId, templateKey, version, } } function buildContentBundleLaunchContext(options?: MapPageLaunchOptions | null): GameContentBundleLaunchContext | null { if (!options) { return null } const bundleId = normalizeOptionalString(options.contentBundleId) const bundleType = normalizeOptionalString(options.contentBundleType) const version = normalizeOptionalString(options.contentBundleVersion) if (!bundleId && !bundleType && !version) { return null } return { bundleId, bundleType, version, } } function loadPendingGameLaunchStore(): PendingGameLaunchStore { try { const stored = wx.getStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY) if (!stored || typeof stored !== 'object') { return {} } return stored as PendingGameLaunchStore } catch { return {} } } function savePendingGameLaunchStore(store: PendingGameLaunchStore): void { try { wx.setStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY, store) } catch {} } export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): GameLaunchEnvelope { return { config: buildDemoConfig(preset), business: { source: 'demo', }, variant: null, runtime: null, presentation: null, contentBundle: null, } } export function stashPendingGameLaunchEnvelope(envelope: GameLaunchEnvelope): string { const launchId = `launch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}` const store = loadPendingGameLaunchStore() store[launchId] = envelope savePendingGameLaunchStore(store) return launchId } export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEnvelope | null { const normalizedLaunchId = normalizeOptionalString(launchId) if (!normalizedLaunchId) { return null } const store = loadPendingGameLaunchStore() const envelope = store[normalizedLaunchId] || null if (!envelope) { return null } delete store[normalizedLaunchId] savePendingGameLaunchStore(store) return envelope } export function buildMapPageUrlWithLaunchId(launchId: string): string { return `/pages/map/map?launchId=${encodeURIComponent(launchId)}` } export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string { return buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope)) } export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string { return `${buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))}&recoverSession=1` } export function getBackendSessionContextFromLaunchEnvelope(envelope: GameLaunchEnvelope | null | undefined): { sessionId: string; sessionToken: string } | null { if (!envelope || !envelope.business || !envelope.business.sessionId || !envelope.business.sessionToken) { return null } return { sessionId: envelope.business.sessionId, sessionToken: envelope.business.sessionToken, } } export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null): GameLaunchEnvelope { const launchId = normalizeOptionalString(options ? options.launchId : undefined) if (launchId) { const pendingEnvelope = consumePendingGameLaunchEnvelope(launchId) if (pendingEnvelope) { return pendingEnvelope } } const configUrl = normalizeOptionalString(options ? options.configUrl : undefined) if (configUrl) { return { config: { configUrl, configLabel: normalizeOptionalString(options ? options.configLabel : undefined) || '线上配置', configChecksumSha256: normalizeOptionalString(options ? options.configChecksumSha256 : undefined), releaseId: normalizeOptionalString(options ? options.releaseId : undefined), routeCode: normalizeOptionalString(options ? options.routeCode : undefined), }, business: buildBusinessLaunchContext(options), variant: buildVariantLaunchContext(options), runtime: buildRuntimeLaunchContext(options), presentation: buildPresentationLaunchContext(options), contentBundle: buildContentBundleLaunchContext(options), } } const preset = resolveDemoPreset(normalizeOptionalString(options ? options.preset : undefined)) return getDemoGameLaunchEnvelope(preset) }