courseToGameDefinition.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import {
  2. type GameDefinition,
  3. type GameControl,
  4. type GameControlDisplayContent,
  5. type GameControlDisplayContentOverride,
  6. type PunchPolicyType,
  7. } from '../core/gameDefinition'
  8. import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
  9. function sortBySequence<T extends { sequence: number | null }>(items: T[]): T[] {
  10. return [...items].sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
  11. }
  12. function buildDisplayBody(label: string, sequence: number | null): string {
  13. if (typeof sequence === 'number') {
  14. return `检查点 ${sequence} · ${label || String(sequence)}`
  15. }
  16. return label
  17. }
  18. function applyDisplayContentOverride(
  19. baseContent: GameControlDisplayContent,
  20. override: GameControlDisplayContentOverride | undefined,
  21. ): GameControlDisplayContent {
  22. if (!override) {
  23. return baseContent
  24. }
  25. return {
  26. title: override.title || baseContent.title,
  27. body: override.body || baseContent.body,
  28. autoPopup: override.autoPopup !== undefined ? override.autoPopup : baseContent.autoPopup,
  29. once: override.once !== undefined ? override.once : baseContent.once,
  30. priority: override.priority !== undefined ? override.priority : baseContent.priority,
  31. }
  32. }
  33. export function buildGameDefinitionFromCourse(
  34. course: OrienteeringCourseData,
  35. controlRadiusMeters: number,
  36. mode: GameDefinition['mode'] = 'classic-sequential',
  37. autoFinishOnLastControl = true,
  38. punchPolicy: PunchPolicyType = 'enter-confirm',
  39. punchRadiusMeters = 5,
  40. requiresFocusSelection = false,
  41. skipEnabled = false,
  42. skipRadiusMeters = 30,
  43. skipRequiresConfirm = true,
  44. controlScoreOverrides: Record<string, number> = {},
  45. controlContentOverrides: Record<string, GameControlDisplayContentOverride> = {},
  46. defaultControlScore: number | null = null,
  47. ): GameDefinition {
  48. const controls: GameControl[] = []
  49. for (let startIndex = 0; startIndex < course.layers.starts.length; startIndex += 1) {
  50. const start = course.layers.starts[startIndex]
  51. const startId = `start-${startIndex + 1}`
  52. controls.push({
  53. id: startId,
  54. code: start.label || 'S',
  55. label: start.label || 'Start',
  56. kind: 'start',
  57. point: start.point,
  58. sequence: null,
  59. score: null,
  60. displayContent: applyDisplayContentOverride({
  61. title: '比赛开始',
  62. body: `${start.label || '开始点'}已激活,按提示前往下一个目标点。`,
  63. autoPopup: true,
  64. once: false,
  65. priority: 1,
  66. }, controlContentOverrides[startId]),
  67. })
  68. }
  69. for (const control of sortBySequence(course.layers.controls)) {
  70. const label = control.label || String(control.sequence)
  71. const controlId = `control-${control.sequence}`
  72. const score = controlId in controlScoreOverrides
  73. ? controlScoreOverrides[controlId]
  74. : defaultControlScore
  75. controls.push({
  76. id: controlId,
  77. code: label,
  78. label,
  79. kind: 'control',
  80. point: control.point,
  81. sequence: control.sequence,
  82. score,
  83. displayContent: applyDisplayContentOverride({
  84. title: score !== null ? `收集 ${label} (+${score}分)` : `收集 ${label}`,
  85. body: score !== null ? `${buildDisplayBody(label, control.sequence)} · ${score}分` : buildDisplayBody(label, control.sequence),
  86. autoPopup: true,
  87. once: false,
  88. priority: 1,
  89. }, controlContentOverrides[controlId]),
  90. })
  91. }
  92. for (let finishIndex = 0; finishIndex < course.layers.finishes.length; finishIndex += 1) {
  93. const finish = course.layers.finishes[finishIndex]
  94. const finishId = `finish-${finishIndex + 1}`
  95. const legacyFinishId = `finish-${controls.length + 1}`
  96. controls.push({
  97. id: finishId,
  98. code: finish.label || 'F',
  99. label: finish.label || 'Finish',
  100. kind: 'finish',
  101. point: finish.point,
  102. sequence: null,
  103. score: null,
  104. displayContent: applyDisplayContentOverride({
  105. title: '完成路线',
  106. body: `${finish.label || '结束点'}已完成,准备查看本局结果。`,
  107. autoPopup: true,
  108. once: false,
  109. priority: 2,
  110. }, controlContentOverrides[finishId] || controlContentOverrides[legacyFinishId]),
  111. })
  112. }
  113. return {
  114. id: `course-${course.title || 'default'}`,
  115. mode,
  116. title: course.title || (mode === 'score-o' ? 'Score-O' : 'Classic Sequential'),
  117. controlRadiusMeters,
  118. punchRadiusMeters,
  119. punchPolicy,
  120. requiresFocusSelection,
  121. skipEnabled,
  122. skipRadiusMeters,
  123. skipRequiresConfirm,
  124. controls,
  125. autoFinishOnLastControl,
  126. }
  127. }