courseToGameDefinition.ts 6.2 KB

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