courseToGameDefinition.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { type GameDefinition, type GameControl, type PunchPolicyType } from '../core/gameDefinition'
  2. import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
  3. function sortBySequence<T extends { sequence: number | null }>(items: T[]): T[] {
  4. return [...items].sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
  5. }
  6. function buildDisplayBody(label: string, sequence: number | null): string {
  7. if (typeof sequence === 'number') {
  8. return `检查点 ${sequence} · ${label || String(sequence)}`
  9. }
  10. return label
  11. }
  12. export function buildGameDefinitionFromCourse(
  13. course: OrienteeringCourseData,
  14. controlRadiusMeters: number,
  15. mode: GameDefinition['mode'] = 'classic-sequential',
  16. autoFinishOnLastControl = true,
  17. punchPolicy: PunchPolicyType = 'enter-confirm',
  18. punchRadiusMeters = 5,
  19. requiresFocusSelection = false,
  20. skipEnabled = false,
  21. skipRadiusMeters = 30,
  22. skipRequiresConfirm = true,
  23. controlScoreOverrides: Record<string, number> = {},
  24. defaultControlScore: number | null = null,
  25. ): GameDefinition {
  26. const controls: GameControl[] = []
  27. for (const start of course.layers.starts) {
  28. controls.push({
  29. id: `start-${controls.length + 1}`,
  30. code: start.label || 'S',
  31. label: start.label || 'Start',
  32. kind: 'start',
  33. point: start.point,
  34. sequence: null,
  35. score: null,
  36. displayContent: null,
  37. })
  38. }
  39. for (const control of sortBySequence(course.layers.controls)) {
  40. const label = control.label || String(control.sequence)
  41. const controlId = `control-${control.sequence}`
  42. const score = controlId in controlScoreOverrides
  43. ? controlScoreOverrides[controlId]
  44. : defaultControlScore
  45. controls.push({
  46. id: controlId,
  47. code: label,
  48. label,
  49. kind: 'control',
  50. point: control.point,
  51. sequence: control.sequence,
  52. score,
  53. displayContent: {
  54. title: score !== null ? `收集 ${label} (+${score}分)` : `收集 ${label}`,
  55. body: score !== null ? `${buildDisplayBody(label, control.sequence)} · ${score}分` : buildDisplayBody(label, control.sequence),
  56. },
  57. })
  58. }
  59. for (const finish of course.layers.finishes) {
  60. controls.push({
  61. id: `finish-${controls.length + 1}`,
  62. code: finish.label || 'F',
  63. label: finish.label || 'Finish',
  64. kind: 'finish',
  65. point: finish.point,
  66. sequence: null,
  67. score: null,
  68. displayContent: null,
  69. })
  70. }
  71. return {
  72. id: `course-${course.title || 'default'}`,
  73. mode,
  74. title: course.title || (mode === 'score-o' ? 'Score-O' : 'Classic Sequential'),
  75. controlRadiusMeters,
  76. punchRadiusMeters,
  77. punchPolicy,
  78. requiresFocusSelection,
  79. skipEnabled,
  80. skipRadiusMeters,
  81. skipRequiresConfirm,
  82. controls,
  83. autoFinishOnLastControl,
  84. }
  85. }