import { type MapScene } from './mapRenderer' import { type ControlPointStyleEntry, type CourseLegStyleEntry, type ScoreBandStyleEntry } from '../../game/presentation/courseStyleConfig' export type RgbaColor = [number, number, number, number] export interface ResolvedControlStyle { entry: ControlPointStyleEntry color: RgbaColor } export interface ResolvedLegStyle { entry: CourseLegStyleEntry color: RgbaColor } function resolveCompletedBoundaryEntry(scene: MapScene, baseEntry: ControlPointStyleEntry): ControlPointStyleEntry { const completedPalette = scene.gameMode === 'score-o' ? scene.courseStyleConfig.scoreO.controls.collected : scene.courseStyleConfig.sequential.controls.completed return { ...baseEntry, colorHex: completedPalette.colorHex, labelColorHex: completedPalette.labelColorHex || baseEntry.labelColorHex, glowStrength: 0, } } function mergeControlStyleEntries( baseEntry: ControlPointStyleEntry, overrideEntry?: ControlPointStyleEntry | null, ): ControlPointStyleEntry { if (!overrideEntry) { return baseEntry } return { ...baseEntry, ...overrideEntry, } } function mergeLegStyleEntries( baseEntry: CourseLegStyleEntry, overrideEntry?: CourseLegStyleEntry | null, ): CourseLegStyleEntry { if (!overrideEntry) { return baseEntry } return { ...baseEntry, ...overrideEntry, } } export function hexToRgbaColor(hex: string, alphaOverride?: number): RgbaColor { const fallback: RgbaColor = [1, 1, 1, alphaOverride !== undefined ? alphaOverride : 1] if (typeof hex !== 'string' || !hex || hex.charAt(0) !== '#') { return fallback } const normalized = hex.slice(1) if (normalized.length !== 6 && normalized.length !== 8) { return fallback } const red = parseInt(normalized.slice(0, 2), 16) const green = parseInt(normalized.slice(2, 4), 16) const blue = parseInt(normalized.slice(4, 6), 16) const alpha = normalized.length === 8 ? parseInt(normalized.slice(6, 8), 16) : 255 if (!Number.isFinite(red) || !Number.isFinite(green) || !Number.isFinite(blue) || !Number.isFinite(alpha)) { return fallback } return [ red / 255, green / 255, blue / 255, alphaOverride !== undefined ? alphaOverride : alpha / 255, ] } function resolveScoreBandStyle(scene: MapScene, sequence: number): ScoreBandStyleEntry | null { const score = scene.controlScoresBySequence[sequence] if (score === undefined) { return null } const bands = scene.courseStyleConfig.scoreO.controls.scoreBands for (let index = 0; index < bands.length; index += 1) { const band = bands[index] if (score >= band.min && score <= band.max) { return band } } return null } export function resolveControlStyle(scene: MapScene, kind: 'start' | 'control' | 'finish', sequence: number | null, index?: number): ResolvedControlStyle { if (kind === 'start') { const baseEntry = index !== undefined && scene.startStyleOverrides[index] ? scene.startStyleOverrides[index] : scene.gameMode === 'score-o' ? scene.courseStyleConfig.scoreO.controls.start : scene.courseStyleConfig.sequential.controls.start const entry = scene.completedStart ? resolveCompletedBoundaryEntry(scene, baseEntry) : baseEntry return { entry, color: hexToRgbaColor(entry.colorHex) } } if (kind === 'finish') { const baseEntry = index !== undefined && scene.finishStyleOverrides[index] ? scene.finishStyleOverrides[index] : scene.gameMode === 'score-o' ? scene.courseStyleConfig.scoreO.controls.finish : scene.courseStyleConfig.sequential.controls.finish const entry = scene.completedFinish ? resolveCompletedBoundaryEntry(scene, baseEntry) : baseEntry return { entry, color: hexToRgbaColor(entry.colorHex) } } if (sequence === null) { const entry = scene.courseStyleConfig.sequential.controls.default return { entry, color: hexToRgbaColor(entry.colorHex) } } const sequenceOverride = scene.controlStyleOverridesBySequence[sequence] const defaultOverride = scene.defaultControlStyleOverride if (scene.gameMode === 'score-o') { if (scene.completedControlSequences.includes(sequence)) { const entry = mergeControlStyleEntries( scene.courseStyleConfig.scoreO.controls.collected, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } if (scene.focusedControlSequences.includes(sequence)) { const bandEntry = resolveScoreBandStyle(scene, sequence) const baseEntry = bandEntry || scene.courseStyleConfig.scoreO.controls.default const focusedEntry = scene.courseStyleConfig.scoreO.controls.focused const focusedMergedEntry: ControlPointStyleEntry = { ...baseEntry, ...focusedEntry, colorHex: baseEntry.colorHex, } const entry = mergeControlStyleEntries(focusedMergedEntry, sequenceOverride || defaultOverride) return { entry, color: hexToRgbaColor(entry.colorHex) } } const bandEntry = resolveScoreBandStyle(scene, sequence) const entry = mergeControlStyleEntries( bandEntry || scene.courseStyleConfig.scoreO.controls.default, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } if (scene.readyControlSequences.includes(sequence) || scene.activeControlSequences.includes(sequence)) { const entry = mergeControlStyleEntries( scene.courseStyleConfig.sequential.controls.current, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } if (scene.completedControlSequences.includes(sequence)) { const entry = mergeControlStyleEntries( scene.courseStyleConfig.sequential.controls.completed, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } if (scene.skippedControlSequences.includes(sequence)) { const entry = mergeControlStyleEntries( scene.courseStyleConfig.sequential.controls.skipped, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } const entry = mergeControlStyleEntries( scene.courseStyleConfig.sequential.controls.default, sequenceOverride || defaultOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } export function resolveLegStyle(scene: MapScene, index: number): ResolvedLegStyle { if (scene.gameMode === 'score-o') { const entry = mergeLegStyleEntries( scene.courseStyleConfig.sequential.legs.default, scene.legStyleOverridesByIndex[index] || scene.defaultLegStyleOverride, ) return { entry, color: hexToRgbaColor(entry.colorHex) } } const completed = scene.completedLegIndices.includes(index) const baseEntry = completed ? scene.courseStyleConfig.sequential.legs.completed : scene.courseStyleConfig.sequential.legs.default const entry = mergeLegStyleEntries(baseEntry, scene.legStyleOverridesByIndex[index] || scene.defaultLegStyleOverride) return { entry, color: hexToRgbaColor(entry.colorHex) } }