完善样式系统与调试链路底座
This commit is contained in:
@@ -7,6 +7,9 @@ import {
|
||||
type GameControlDisplayContentOverride,
|
||||
type PunchPolicyType,
|
||||
} from '../core/gameDefinition'
|
||||
import {
|
||||
resolveContentCardCtaConfig,
|
||||
} from '../experience/contentCard'
|
||||
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
||||
|
||||
function sortBySequence<T extends { sequence: number | null }>(items: T[]): T[] {
|
||||
@@ -69,6 +72,11 @@ function applyDisplayContentOverride(
|
||||
priority: override.priority !== undefined ? override.priority : baseContent.priority,
|
||||
clickTitle: override.clickTitle !== undefined ? override.clickTitle : baseContent.clickTitle,
|
||||
clickBody: override.clickBody !== undefined ? override.clickBody : baseContent.clickBody,
|
||||
ctas: override.ctas && override.ctas.length
|
||||
? override.ctas
|
||||
.map((item) => resolveContentCardCtaConfig(item))
|
||||
.filter((item): item is NonNullable<typeof item> => !!item)
|
||||
: baseContent.ctas,
|
||||
contentExperience: applyExperienceOverride(baseContent.contentExperience, override.contentExperience),
|
||||
clickExperience: applyExperienceOverride(baseContent.clickExperience, override.clickExperience),
|
||||
}
|
||||
@@ -111,6 +119,7 @@ export function buildGameDefinitionFromCourse(
|
||||
priority: 1,
|
||||
clickTitle: '比赛开始',
|
||||
clickBody: `${start.label || '开始点'}已激活,按提示前往下一个目标点。`,
|
||||
ctas: [],
|
||||
contentExperience: null,
|
||||
clickExperience: null,
|
||||
}, controlContentOverrides[startId]),
|
||||
@@ -140,6 +149,7 @@ export function buildGameDefinitionFromCourse(
|
||||
priority: 1,
|
||||
clickTitle: score !== null ? `收集 ${label} (+${score}分)` : `收集 ${label}`,
|
||||
clickBody: score !== null ? `${buildDisplayBody(label, control.sequence)} · ${score}分` : buildDisplayBody(label, control.sequence),
|
||||
ctas: [],
|
||||
contentExperience: null,
|
||||
clickExperience: null,
|
||||
}, controlContentOverrides[controlId]),
|
||||
@@ -167,6 +177,7 @@ export function buildGameDefinitionFromCourse(
|
||||
priority: 2,
|
||||
clickTitle: '完成路线',
|
||||
clickBody: `${finish.label || '结束点'}已完成,准备查看本局结果。`,
|
||||
ctas: [],
|
||||
contentExperience: null,
|
||||
clickExperience: null,
|
||||
}, controlContentOverrides[finishId] || controlContentOverrides[legacyFinishId]),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { type LonLatPoint } from '../../utils/projection'
|
||||
import { type GameAudioConfig } from '../audio/audioConfig'
|
||||
import {
|
||||
type ContentCardCtaConfig,
|
||||
type ContentCardCtaConfigOverride,
|
||||
type ContentCardTemplate,
|
||||
} from '../experience/contentCard'
|
||||
import { type H5ExperiencePresentation } from '../experience/h5Experience'
|
||||
|
||||
export type GameMode = 'classic-sequential' | 'score-o'
|
||||
@@ -23,7 +28,7 @@ export interface GameContentExperienceConfigOverride {
|
||||
}
|
||||
|
||||
export interface GameControlDisplayContent {
|
||||
template: 'minimal' | 'story' | 'focus'
|
||||
template: ContentCardTemplate
|
||||
title: string
|
||||
body: string
|
||||
autoPopup: boolean
|
||||
@@ -31,12 +36,13 @@ export interface GameControlDisplayContent {
|
||||
priority: number
|
||||
clickTitle: string | null
|
||||
clickBody: string | null
|
||||
ctas: ContentCardCtaConfig[]
|
||||
contentExperience: GameContentExperienceConfig | null
|
||||
clickExperience: GameContentExperienceConfig | null
|
||||
}
|
||||
|
||||
export interface GameControlDisplayContentOverride {
|
||||
template?: 'minimal' | 'story' | 'focus'
|
||||
template?: ContentCardTemplate
|
||||
title?: string
|
||||
body?: string
|
||||
autoPopup?: boolean
|
||||
@@ -44,6 +50,7 @@ export interface GameControlDisplayContentOverride {
|
||||
priority?: number
|
||||
clickTitle?: string
|
||||
clickBody?: string
|
||||
ctas?: ContentCardCtaConfigOverride[]
|
||||
contentExperience?: GameContentExperienceConfigOverride
|
||||
clickExperience?: GameContentExperienceConfigOverride
|
||||
}
|
||||
|
||||
95
miniprogram/game/experience/contentCard.ts
Normal file
95
miniprogram/game/experience/contentCard.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export type ContentCardTemplate = 'minimal' | 'story' | 'focus'
|
||||
export type ContentCardCtaType = 'detail' | 'photo' | 'audio' | 'quiz'
|
||||
|
||||
export interface ContentCardQuizConfig {
|
||||
bonusScore: number
|
||||
countdownSeconds: number
|
||||
minValue: number
|
||||
maxValue: number
|
||||
allowSubtraction: boolean
|
||||
}
|
||||
|
||||
export interface ContentCardCtaConfig {
|
||||
type: ContentCardCtaType
|
||||
label: string
|
||||
quiz: ContentCardQuizConfig | null
|
||||
}
|
||||
|
||||
export interface ContentCardCtaConfigOverride {
|
||||
type?: ContentCardCtaType
|
||||
label?: string
|
||||
bonusScore?: number
|
||||
countdownSeconds?: number
|
||||
minValue?: number
|
||||
maxValue?: number
|
||||
allowSubtraction?: boolean
|
||||
}
|
||||
|
||||
export interface ContentCardActionViewModel {
|
||||
key: string
|
||||
type: ContentCardCtaType
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DEFAULT_CONTENT_CARD_QUIZ_CONFIG: ContentCardQuizConfig = {
|
||||
bonusScore: 1,
|
||||
countdownSeconds: 12,
|
||||
minValue: 10,
|
||||
maxValue: 999,
|
||||
allowSubtraction: true,
|
||||
}
|
||||
|
||||
export function buildDefaultContentCardCtaLabel(type: ContentCardCtaType): string {
|
||||
if (type === 'detail') {
|
||||
return '查看详情'
|
||||
}
|
||||
if (type === 'photo') {
|
||||
return '拍照打卡'
|
||||
}
|
||||
if (type === 'audio') {
|
||||
return '语音留言'
|
||||
}
|
||||
return '答题加分'
|
||||
}
|
||||
|
||||
export function buildDefaultContentCardQuizConfig(
|
||||
override?: ContentCardCtaConfigOverride | null,
|
||||
): ContentCardQuizConfig {
|
||||
const minValue = Number(override && override.minValue)
|
||||
const maxValue = Number(override && override.maxValue)
|
||||
return {
|
||||
bonusScore: Number.isFinite(Number(override && override.bonusScore))
|
||||
? Math.max(1, Math.round(Number(override && override.bonusScore)))
|
||||
: DEFAULT_CONTENT_CARD_QUIZ_CONFIG.bonusScore,
|
||||
countdownSeconds: Number.isFinite(Number(override && override.countdownSeconds))
|
||||
? Math.max(5, Math.round(Number(override && override.countdownSeconds)))
|
||||
: DEFAULT_CONTENT_CARD_QUIZ_CONFIG.countdownSeconds,
|
||||
minValue: Number.isFinite(minValue)
|
||||
? Math.max(10, Math.round(minValue))
|
||||
: DEFAULT_CONTENT_CARD_QUIZ_CONFIG.minValue,
|
||||
maxValue: Number.isFinite(maxValue)
|
||||
? Math.max(
|
||||
Number.isFinite(minValue) ? Math.max(10, Math.round(minValue)) : DEFAULT_CONTENT_CARD_QUIZ_CONFIG.minValue,
|
||||
Math.round(maxValue),
|
||||
)
|
||||
: DEFAULT_CONTENT_CARD_QUIZ_CONFIG.maxValue,
|
||||
allowSubtraction: override && typeof override.allowSubtraction === 'boolean'
|
||||
? override.allowSubtraction
|
||||
: DEFAULT_CONTENT_CARD_QUIZ_CONFIG.allowSubtraction,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveContentCardCtaConfig(
|
||||
override: ContentCardCtaConfigOverride | null | undefined,
|
||||
): ContentCardCtaConfig | null {
|
||||
const type = override && override.type
|
||||
if (type !== 'detail' && type !== 'photo' && type !== 'audio' && type !== 'quiz') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
label: override && override.label ? override.label : buildDefaultContentCardCtaLabel(type),
|
||||
quiz: type === 'quiz' ? buildDefaultContentCardQuizConfig(override) : null,
|
||||
}
|
||||
}
|
||||
87
miniprogram/game/presentation/courseStyleConfig.ts
Normal file
87
miniprogram/game/presentation/courseStyleConfig.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
export type ControlPointStyleId = 'classic-ring' | 'solid-dot' | 'double-ring' | 'badge' | 'pulse-core'
|
||||
|
||||
export type CourseLegStyleId = 'classic-leg' | 'dashed-leg' | 'glow-leg' | 'progress-leg'
|
||||
|
||||
export interface ControlPointStyleEntry {
|
||||
style: ControlPointStyleId
|
||||
colorHex: string
|
||||
sizeScale?: number
|
||||
accentRingScale?: number
|
||||
glowStrength?: number
|
||||
labelScale?: number
|
||||
labelColorHex?: string
|
||||
}
|
||||
|
||||
export interface CourseLegStyleEntry {
|
||||
style: CourseLegStyleId
|
||||
colorHex: string
|
||||
widthScale?: number
|
||||
glowStrength?: number
|
||||
}
|
||||
|
||||
export interface ScoreBandStyleEntry extends ControlPointStyleEntry {
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface SequentialCourseStyleConfig {
|
||||
controls: {
|
||||
default: ControlPointStyleEntry
|
||||
current: ControlPointStyleEntry
|
||||
completed: ControlPointStyleEntry
|
||||
skipped: ControlPointStyleEntry
|
||||
start: ControlPointStyleEntry
|
||||
finish: ControlPointStyleEntry
|
||||
}
|
||||
legs: {
|
||||
default: CourseLegStyleEntry
|
||||
completed: CourseLegStyleEntry
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScoreOCourseStyleConfig {
|
||||
controls: {
|
||||
default: ControlPointStyleEntry
|
||||
focused: ControlPointStyleEntry
|
||||
collected: ControlPointStyleEntry
|
||||
start: ControlPointStyleEntry
|
||||
finish: ControlPointStyleEntry
|
||||
scoreBands: ScoreBandStyleEntry[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CourseStyleConfig {
|
||||
sequential: SequentialCourseStyleConfig
|
||||
scoreO: ScoreOCourseStyleConfig
|
||||
}
|
||||
|
||||
export const DEFAULT_COURSE_STYLE_CONFIG: CourseStyleConfig = {
|
||||
sequential: {
|
||||
controls: {
|
||||
default: { style: 'classic-ring', colorHex: '#cc006b', sizeScale: 1, labelScale: 1 },
|
||||
current: { style: 'pulse-core', colorHex: '#38fff2', sizeScale: 1.08, accentRingScale: 1.28, glowStrength: 0.9, labelScale: 1.08, labelColorHex: '#fff4fb' },
|
||||
completed: { style: 'solid-dot', colorHex: '#7e838a', sizeScale: 0.88, labelScale: 0.96 },
|
||||
skipped: { style: 'badge', colorHex: '#8a9198', sizeScale: 0.9, accentRingScale: 1.12, labelScale: 0.94 },
|
||||
start: { style: 'double-ring', colorHex: '#cc006b', sizeScale: 1.04, accentRingScale: 1.3, labelScale: 1.02 },
|
||||
finish: { style: 'double-ring', colorHex: '#cc006b', sizeScale: 1.08, accentRingScale: 1.34, glowStrength: 0.32, labelScale: 1.06, labelColorHex: '#fff4de' },
|
||||
},
|
||||
legs: {
|
||||
default: { style: 'classic-leg', colorHex: '#cc006b', widthScale: 1 },
|
||||
completed: { style: 'progress-leg', colorHex: '#7a8088', widthScale: 0.92, glowStrength: 0.24 },
|
||||
},
|
||||
},
|
||||
scoreO: {
|
||||
controls: {
|
||||
default: { style: 'badge', colorHex: '#cc006b', sizeScale: 0.96, accentRingScale: 1.1, labelScale: 1.02 },
|
||||
focused: { style: 'pulse-core', colorHex: '#fff0fa', sizeScale: 1.12, accentRingScale: 1.36, glowStrength: 1, labelScale: 1.12, labelColorHex: '#fffafc' },
|
||||
collected: { style: 'solid-dot', colorHex: '#d6dae0', sizeScale: 0.82, labelScale: 0.92 },
|
||||
start: { style: 'double-ring', colorHex: '#cc006b', sizeScale: 1.02, accentRingScale: 1.24, labelScale: 1.02 },
|
||||
finish: { style: 'double-ring', colorHex: '#cc006b', sizeScale: 1.06, accentRingScale: 1.28, glowStrength: 0.26, labelScale: 1.04, labelColorHex: '#fff4de' },
|
||||
scoreBands: [
|
||||
{ min: 0, max: 19, style: 'badge', colorHex: '#56ccf2', sizeScale: 0.88, accentRingScale: 1.06, labelScale: 0.94 },
|
||||
{ min: 20, max: 49, style: 'badge', colorHex: '#f2c94c', sizeScale: 1.02, accentRingScale: 1.18, labelScale: 1.02 },
|
||||
{ min: 50, max: 999999, style: 'badge', colorHex: '#eb5757', sizeScale: 1.14, accentRingScale: 1.32, glowStrength: 0.72, labelScale: 1.1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
109
miniprogram/game/presentation/gpsMarkerStyleConfig.ts
Normal file
109
miniprogram/game/presentation/gpsMarkerStyleConfig.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
export type GpsMarkerStyleId = 'dot' | 'beacon' | 'disc' | 'badge'
|
||||
export type GpsMarkerSizePreset = 'small' | 'medium' | 'large'
|
||||
export type GpsMarkerAnimationProfile = 'minimal' | 'dynamic-runner' | 'warning-reactive'
|
||||
export type GpsMarkerMotionState = 'idle' | 'moving' | 'fast-moving' | 'warning'
|
||||
export type GpsMarkerColorPreset =
|
||||
| 'mint'
|
||||
| 'cyan'
|
||||
| 'sky'
|
||||
| 'blue'
|
||||
| 'violet'
|
||||
| 'pink'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
export type GpsMarkerLogoMode = 'center-badge'
|
||||
|
||||
export interface GpsMarkerColorPresetEntry {
|
||||
colorHex: string
|
||||
ringColorHex: string
|
||||
indicatorColorHex: string
|
||||
}
|
||||
|
||||
export const GPS_MARKER_COLOR_PRESET_MAP: Record<GpsMarkerColorPreset, GpsMarkerColorPresetEntry> = {
|
||||
mint: {
|
||||
colorHex: '#18b39a',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#9bfff0',
|
||||
},
|
||||
cyan: {
|
||||
colorHex: '#1db7cf',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#b2f7ff',
|
||||
},
|
||||
sky: {
|
||||
colorHex: '#54a3ff',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#d6efff',
|
||||
},
|
||||
blue: {
|
||||
colorHex: '#4568ff',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#bec9ff',
|
||||
},
|
||||
violet: {
|
||||
colorHex: '#8658ff',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#dbcaff',
|
||||
},
|
||||
pink: {
|
||||
colorHex: '#ff5cb5',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#ffd0ea',
|
||||
},
|
||||
orange: {
|
||||
colorHex: '#ff9238',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#ffd7b0',
|
||||
},
|
||||
yellow: {
|
||||
colorHex: '#f3c72b',
|
||||
ringColorHex: '#ffffff',
|
||||
indicatorColorHex: '#fff1ae',
|
||||
},
|
||||
}
|
||||
|
||||
export interface GpsMarkerStyleConfig {
|
||||
visible: boolean
|
||||
style: GpsMarkerStyleId
|
||||
size: GpsMarkerSizePreset
|
||||
colorPreset: GpsMarkerColorPreset
|
||||
colorHex: string
|
||||
ringColorHex: string
|
||||
indicatorColorHex: string
|
||||
showHeadingIndicator: boolean
|
||||
animationProfile: GpsMarkerAnimationProfile
|
||||
motionState: GpsMarkerMotionState
|
||||
motionIntensity: number
|
||||
pulseStrength: number
|
||||
headingAlpha: number
|
||||
effectScale: number
|
||||
wakeStrength: number
|
||||
warningGlowStrength: number
|
||||
indicatorScale: number
|
||||
logoScale: number
|
||||
logoUrl: string
|
||||
logoMode: GpsMarkerLogoMode
|
||||
}
|
||||
|
||||
export const DEFAULT_GPS_MARKER_STYLE_CONFIG: GpsMarkerStyleConfig = {
|
||||
visible: true,
|
||||
style: 'beacon',
|
||||
size: 'medium',
|
||||
colorPreset: 'cyan',
|
||||
colorHex: GPS_MARKER_COLOR_PRESET_MAP.cyan.colorHex,
|
||||
ringColorHex: GPS_MARKER_COLOR_PRESET_MAP.cyan.ringColorHex,
|
||||
indicatorColorHex: GPS_MARKER_COLOR_PRESET_MAP.cyan.indicatorColorHex,
|
||||
showHeadingIndicator: true,
|
||||
animationProfile: 'dynamic-runner',
|
||||
motionState: 'idle',
|
||||
motionIntensity: 0,
|
||||
pulseStrength: 1,
|
||||
headingAlpha: 1,
|
||||
effectScale: 1,
|
||||
wakeStrength: 0,
|
||||
warningGlowStrength: 0,
|
||||
indicatorScale: 1,
|
||||
logoScale: 1,
|
||||
logoUrl: '',
|
||||
logoMode: 'center-badge',
|
||||
}
|
||||
92
miniprogram/game/presentation/trackStyleConfig.ts
Normal file
92
miniprogram/game/presentation/trackStyleConfig.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export type TrackDisplayMode = 'none' | 'full' | 'tail'
|
||||
export type TrackStyleProfile = 'classic' | 'neon'
|
||||
export type TrackTailLengthPreset = 'short' | 'medium' | 'long'
|
||||
export type TrackColorPreset =
|
||||
| 'mint'
|
||||
| 'cyan'
|
||||
| 'sky'
|
||||
| 'blue'
|
||||
| 'violet'
|
||||
| 'pink'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
|
||||
export interface TrackColorPresetEntry {
|
||||
colorHex: string
|
||||
headColorHex: string
|
||||
}
|
||||
|
||||
export const TRACK_TAIL_LENGTH_METERS: Record<TrackTailLengthPreset, number> = {
|
||||
short: 32,
|
||||
medium: 52,
|
||||
long: 78,
|
||||
}
|
||||
|
||||
export const TRACK_COLOR_PRESET_MAP: Record<TrackColorPreset, TrackColorPresetEntry> = {
|
||||
mint: {
|
||||
colorHex: '#15a38d',
|
||||
headColorHex: '#63fff0',
|
||||
},
|
||||
cyan: {
|
||||
colorHex: '#18b8c9',
|
||||
headColorHex: '#7cf4ff',
|
||||
},
|
||||
sky: {
|
||||
colorHex: '#4a9cff',
|
||||
headColorHex: '#c9eeff',
|
||||
},
|
||||
blue: {
|
||||
colorHex: '#3a63ff',
|
||||
headColorHex: '#9fb4ff',
|
||||
},
|
||||
violet: {
|
||||
colorHex: '#7c4dff',
|
||||
headColorHex: '#d0b8ff',
|
||||
},
|
||||
pink: {
|
||||
colorHex: '#ff4fb3',
|
||||
headColorHex: '#ffc0ec',
|
||||
},
|
||||
orange: {
|
||||
colorHex: '#ff8a2b',
|
||||
headColorHex: '#ffd0a3',
|
||||
},
|
||||
yellow: {
|
||||
colorHex: '#f0c419',
|
||||
headColorHex: '#fff0a8',
|
||||
},
|
||||
}
|
||||
|
||||
export interface TrackVisualizationConfig {
|
||||
mode: TrackDisplayMode
|
||||
style: TrackStyleProfile
|
||||
tailLength: TrackTailLengthPreset
|
||||
colorPreset: TrackColorPreset
|
||||
tailMeters: number
|
||||
tailMaxSeconds: number
|
||||
fadeOutWhenStill: boolean
|
||||
stillSpeedKmh: number
|
||||
fadeOutDurationMs: number
|
||||
colorHex: string
|
||||
headColorHex: string
|
||||
widthPx: number
|
||||
headWidthPx: number
|
||||
glowStrength: number
|
||||
}
|
||||
|
||||
export const DEFAULT_TRACK_VISUALIZATION_CONFIG: TrackVisualizationConfig = {
|
||||
mode: 'full',
|
||||
style: 'neon',
|
||||
tailLength: 'medium',
|
||||
colorPreset: 'mint',
|
||||
tailMeters: TRACK_TAIL_LENGTH_METERS.medium,
|
||||
tailMaxSeconds: 30,
|
||||
fadeOutWhenStill: true,
|
||||
stillSpeedKmh: 0.6,
|
||||
fadeOutDurationMs: 3000,
|
||||
colorHex: TRACK_COLOR_PRESET_MAP.mint.colorHex,
|
||||
headColorHex: TRACK_COLOR_PRESET_MAP.mint.headColorHex,
|
||||
widthPx: 4.2,
|
||||
headWidthPx: 6.8,
|
||||
glowStrength: 0.2,
|
||||
}
|
||||
Reference in New Issue
Block a user