courseStyleResolver.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import { type MapScene } from './mapRenderer'
  2. import { type ControlPointStyleEntry, type CourseLegStyleEntry, type ScoreBandStyleEntry } from '../../game/presentation/courseStyleConfig'
  3. export type RgbaColor = [number, number, number, number]
  4. export interface ResolvedControlStyle {
  5. entry: ControlPointStyleEntry
  6. color: RgbaColor
  7. }
  8. export interface ResolvedLegStyle {
  9. entry: CourseLegStyleEntry
  10. color: RgbaColor
  11. }
  12. function resolveCompletedBoundaryEntry(scene: MapScene, baseEntry: ControlPointStyleEntry): ControlPointStyleEntry {
  13. const completedPalette = scene.gameMode === 'score-o'
  14. ? scene.courseStyleConfig.scoreO.controls.collected
  15. : scene.courseStyleConfig.sequential.controls.completed
  16. return {
  17. ...baseEntry,
  18. colorHex: completedPalette.colorHex,
  19. labelColorHex: completedPalette.labelColorHex || baseEntry.labelColorHex,
  20. glowStrength: 0,
  21. }
  22. }
  23. function mergeControlStyleEntries(
  24. baseEntry: ControlPointStyleEntry,
  25. overrideEntry?: ControlPointStyleEntry | null,
  26. ): ControlPointStyleEntry {
  27. if (!overrideEntry) {
  28. return baseEntry
  29. }
  30. return {
  31. ...baseEntry,
  32. ...overrideEntry,
  33. }
  34. }
  35. function mergeLegStyleEntries(
  36. baseEntry: CourseLegStyleEntry,
  37. overrideEntry?: CourseLegStyleEntry | null,
  38. ): CourseLegStyleEntry {
  39. if (!overrideEntry) {
  40. return baseEntry
  41. }
  42. return {
  43. ...baseEntry,
  44. ...overrideEntry,
  45. }
  46. }
  47. export function hexToRgbaColor(hex: string, alphaOverride?: number): RgbaColor {
  48. const fallback: RgbaColor = [1, 1, 1, alphaOverride !== undefined ? alphaOverride : 1]
  49. if (typeof hex !== 'string' || !hex || hex.charAt(0) !== '#') {
  50. return fallback
  51. }
  52. const normalized = hex.slice(1)
  53. if (normalized.length !== 6 && normalized.length !== 8) {
  54. return fallback
  55. }
  56. const red = parseInt(normalized.slice(0, 2), 16)
  57. const green = parseInt(normalized.slice(2, 4), 16)
  58. const blue = parseInt(normalized.slice(4, 6), 16)
  59. const alpha = normalized.length === 8 ? parseInt(normalized.slice(6, 8), 16) : 255
  60. if (!Number.isFinite(red) || !Number.isFinite(green) || !Number.isFinite(blue) || !Number.isFinite(alpha)) {
  61. return fallback
  62. }
  63. return [
  64. red / 255,
  65. green / 255,
  66. blue / 255,
  67. alphaOverride !== undefined ? alphaOverride : alpha / 255,
  68. ]
  69. }
  70. function resolveScoreBandStyle(scene: MapScene, sequence: number): ScoreBandStyleEntry | null {
  71. const score = scene.controlScoresBySequence[sequence]
  72. if (score === undefined) {
  73. return null
  74. }
  75. const bands = scene.courseStyleConfig.scoreO.controls.scoreBands
  76. for (let index = 0; index < bands.length; index += 1) {
  77. const band = bands[index]
  78. if (score >= band.min && score <= band.max) {
  79. return band
  80. }
  81. }
  82. return null
  83. }
  84. export function resolveControlStyle(scene: MapScene, kind: 'start' | 'control' | 'finish', sequence: number | null, index?: number): ResolvedControlStyle {
  85. if (kind === 'start') {
  86. const baseEntry = index !== undefined && scene.startStyleOverrides[index]
  87. ? scene.startStyleOverrides[index]
  88. : scene.gameMode === 'score-o'
  89. ? scene.courseStyleConfig.scoreO.controls.start
  90. : scene.courseStyleConfig.sequential.controls.start
  91. const entry = scene.completedStart
  92. ? resolveCompletedBoundaryEntry(scene, baseEntry)
  93. : baseEntry
  94. return { entry, color: hexToRgbaColor(entry.colorHex) }
  95. }
  96. if (kind === 'finish') {
  97. const baseEntry = index !== undefined && scene.finishStyleOverrides[index]
  98. ? scene.finishStyleOverrides[index]
  99. : scene.gameMode === 'score-o'
  100. ? scene.courseStyleConfig.scoreO.controls.finish
  101. : scene.courseStyleConfig.sequential.controls.finish
  102. const entry = scene.completedFinish
  103. ? resolveCompletedBoundaryEntry(scene, baseEntry)
  104. : baseEntry
  105. return { entry, color: hexToRgbaColor(entry.colorHex) }
  106. }
  107. if (sequence === null) {
  108. const entry = scene.courseStyleConfig.sequential.controls.default
  109. return { entry, color: hexToRgbaColor(entry.colorHex) }
  110. }
  111. const sequenceOverride = scene.controlStyleOverridesBySequence[sequence]
  112. const defaultOverride = scene.defaultControlStyleOverride
  113. if (scene.gameMode === 'score-o') {
  114. if (scene.completedControlSequences.includes(sequence)) {
  115. const entry = mergeControlStyleEntries(
  116. scene.courseStyleConfig.scoreO.controls.collected,
  117. sequenceOverride || defaultOverride,
  118. )
  119. return { entry, color: hexToRgbaColor(entry.colorHex) }
  120. }
  121. if (scene.focusedControlSequences.includes(sequence)) {
  122. const bandEntry = resolveScoreBandStyle(scene, sequence)
  123. const baseEntry = bandEntry || scene.courseStyleConfig.scoreO.controls.default
  124. const focusedEntry = scene.courseStyleConfig.scoreO.controls.focused
  125. const focusedMergedEntry: ControlPointStyleEntry = {
  126. ...baseEntry,
  127. ...focusedEntry,
  128. colorHex: baseEntry.colorHex,
  129. }
  130. const entry = mergeControlStyleEntries(focusedMergedEntry, sequenceOverride || defaultOverride)
  131. return { entry, color: hexToRgbaColor(entry.colorHex) }
  132. }
  133. const bandEntry = resolveScoreBandStyle(scene, sequence)
  134. const entry = mergeControlStyleEntries(
  135. bandEntry || scene.courseStyleConfig.scoreO.controls.default,
  136. sequenceOverride || defaultOverride,
  137. )
  138. return { entry, color: hexToRgbaColor(entry.colorHex) }
  139. }
  140. if (scene.readyControlSequences.includes(sequence) || scene.activeControlSequences.includes(sequence)) {
  141. const entry = mergeControlStyleEntries(
  142. scene.courseStyleConfig.sequential.controls.current,
  143. sequenceOverride || defaultOverride,
  144. )
  145. return { entry, color: hexToRgbaColor(entry.colorHex) }
  146. }
  147. if (scene.completedControlSequences.includes(sequence)) {
  148. const entry = mergeControlStyleEntries(
  149. scene.courseStyleConfig.sequential.controls.completed,
  150. sequenceOverride || defaultOverride,
  151. )
  152. return { entry, color: hexToRgbaColor(entry.colorHex) }
  153. }
  154. if (scene.skippedControlSequences.includes(sequence)) {
  155. const entry = mergeControlStyleEntries(
  156. scene.courseStyleConfig.sequential.controls.skipped,
  157. sequenceOverride || defaultOverride,
  158. )
  159. return { entry, color: hexToRgbaColor(entry.colorHex) }
  160. }
  161. const entry = mergeControlStyleEntries(
  162. scene.courseStyleConfig.sequential.controls.default,
  163. sequenceOverride || defaultOverride,
  164. )
  165. return { entry, color: hexToRgbaColor(entry.colorHex) }
  166. }
  167. export function resolveLegStyle(scene: MapScene, index: number): ResolvedLegStyle {
  168. if (scene.gameMode === 'score-o') {
  169. const entry = mergeLegStyleEntries(
  170. scene.courseStyleConfig.sequential.legs.default,
  171. scene.legStyleOverridesByIndex[index] || scene.defaultLegStyleOverride,
  172. )
  173. return { entry, color: hexToRgbaColor(entry.colorHex) }
  174. }
  175. const completed = scene.completedLegIndices.includes(index)
  176. const baseEntry = completed ? scene.courseStyleConfig.sequential.legs.completed : scene.courseStyleConfig.sequential.legs.default
  177. const entry = mergeLegStyleEntries(baseEntry, scene.legStyleOverridesByIndex[index] || scene.defaultLegStyleOverride)
  178. return { entry, color: hexToRgbaColor(entry.colorHex) }
  179. }