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 GameLaunchEnvelope { config: GameConfigLaunchRequest business: BusinessLaunchContext | null } export interface MapPageLaunchOptions { launchId?: 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 } 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 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', }, } } 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 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), } } const preset = resolveDemoPreset(normalizeOptionalString(options ? options.preset : undefined)) return getDemoGameLaunchEnvelope(preset) }