|
@@ -13,7 +13,8 @@ import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
|
|
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
|
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
|
|
import { formatAnimationLevelText, resolveAnimationLevel, type AnimationLevel } from '../../utils/animationLevel'
|
|
import { formatAnimationLevelText, resolveAnimationLevel, type AnimationLevel } from '../../utils/animationLevel'
|
|
|
import { GameRuntime } from '../../game/core/gameRuntime'
|
|
import { GameRuntime } from '../../game/core/gameRuntime'
|
|
|
-import { type GameControlDisplayContentOverride } from '../../game/core/gameDefinition'
|
|
|
|
|
|
|
+import { type GameControl, type GameControlDisplayContentOverride } from '../../game/core/gameDefinition'
|
|
|
|
|
+import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
|
|
|
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
|
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
|
|
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
|
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
|
|
import { FeedbackDirector } from '../../game/feedback/feedbackDirector'
|
|
import { FeedbackDirector } from '../../game/feedback/feedbackDirector'
|
|
@@ -228,6 +229,8 @@ export interface MapEngineViewState {
|
|
|
contentCardVisible: boolean
|
|
contentCardVisible: boolean
|
|
|
contentCardTitle: string
|
|
contentCardTitle: string
|
|
|
contentCardBody: string
|
|
contentCardBody: string
|
|
|
|
|
+ pendingContentEntryVisible: boolean
|
|
|
|
|
+ pendingContentEntryText: string
|
|
|
punchButtonFxClass: string
|
|
punchButtonFxClass: string
|
|
|
panelProgressFxClass: string
|
|
panelProgressFxClass: string
|
|
|
panelDistanceFxClass: string
|
|
panelDistanceFxClass: string
|
|
@@ -245,6 +248,18 @@ export interface MapEngineViewState {
|
|
|
|
|
|
|
|
export interface MapEngineCallbacks {
|
|
export interface MapEngineCallbacks {
|
|
|
onData: (patch: Partial<MapEngineViewState>) => void
|
|
onData: (patch: Partial<MapEngineViewState>) => void
|
|
|
|
|
+ onOpenH5Experience?: (request: H5ExperienceRequest) => void
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ContentCardEntry {
|
|
|
|
|
+ title: string
|
|
|
|
|
+ body: string
|
|
|
|
|
+ motionClass: string
|
|
|
|
|
+ contentKey: string
|
|
|
|
|
+ once: boolean
|
|
|
|
|
+ priority: number
|
|
|
|
|
+ autoPopup: boolean
|
|
|
|
|
+ h5Request: H5ExperienceRequest | null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export interface MapEngineGameInfoRow {
|
|
export interface MapEngineGameInfoRow {
|
|
@@ -368,6 +383,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|
|
'contentCardVisible',
|
|
'contentCardVisible',
|
|
|
'contentCardTitle',
|
|
'contentCardTitle',
|
|
|
'contentCardBody',
|
|
'contentCardBody',
|
|
|
|
|
+ 'pendingContentEntryVisible',
|
|
|
|
|
+ 'pendingContentEntryText',
|
|
|
'punchButtonFxClass',
|
|
'punchButtonFxClass',
|
|
|
'panelProgressFxClass',
|
|
'panelProgressFxClass',
|
|
|
'panelDistanceFxClass',
|
|
'panelDistanceFxClass',
|
|
@@ -889,17 +906,22 @@ export class MapEngine {
|
|
|
contentCardTimer: number
|
|
contentCardTimer: number
|
|
|
currentContentCardPriority: number
|
|
currentContentCardPriority: number
|
|
|
shownContentCardKeys: Record<string, true>
|
|
shownContentCardKeys: Record<string, true>
|
|
|
|
|
+ currentContentCard: ContentCardEntry | null
|
|
|
|
|
+ pendingContentCards: ContentCardEntry[]
|
|
|
|
|
+ currentH5ExperienceOpen: boolean
|
|
|
mapPulseTimer: number
|
|
mapPulseTimer: number
|
|
|
stageFxTimer: number
|
|
stageFxTimer: number
|
|
|
sessionTimerInterval: number
|
|
sessionTimerInterval: number
|
|
|
hasGpsCenteredOnce: boolean
|
|
hasGpsCenteredOnce: boolean
|
|
|
gpsLockEnabled: boolean
|
|
gpsLockEnabled: boolean
|
|
|
|
|
+ onOpenH5Experience?: (request: H5ExperienceRequest) => void
|
|
|
|
|
|
|
|
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
|
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
|
|
this.buildVersion = buildVersion
|
|
this.buildVersion = buildVersion
|
|
|
this.animationLevel = resolveAnimationLevel(wx.getSystemInfoSync())
|
|
this.animationLevel = resolveAnimationLevel(wx.getSystemInfoSync())
|
|
|
this.compassTuningProfile = 'balanced'
|
|
this.compassTuningProfile = 'balanced'
|
|
|
this.onData = callbacks.onData
|
|
this.onData = callbacks.onData
|
|
|
|
|
+ this.onOpenH5Experience = callbacks.onOpenH5Experience
|
|
|
this.accelerometerErrorText = null
|
|
this.accelerometerErrorText = null
|
|
|
this.renderer = new WebGLMapRenderer(
|
|
this.renderer = new WebGLMapRenderer(
|
|
|
(stats) => {
|
|
(stats) => {
|
|
@@ -1144,6 +1166,9 @@ export class MapEngine {
|
|
|
this.contentCardTimer = 0
|
|
this.contentCardTimer = 0
|
|
|
this.currentContentCardPriority = 0
|
|
this.currentContentCardPriority = 0
|
|
|
this.shownContentCardKeys = {}
|
|
this.shownContentCardKeys = {}
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.pendingContentCards = []
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
this.mapPulseTimer = 0
|
|
this.mapPulseTimer = 0
|
|
|
this.stageFxTimer = 0
|
|
this.stageFxTimer = 0
|
|
|
this.sessionTimerInterval = 0
|
|
this.sessionTimerInterval = 0
|
|
@@ -1258,6 +1283,8 @@ export class MapEngine {
|
|
|
contentCardVisible: false,
|
|
contentCardVisible: false,
|
|
|
contentCardTitle: '',
|
|
contentCardTitle: '',
|
|
|
contentCardBody: '',
|
|
contentCardBody: '',
|
|
|
|
|
+ pendingContentEntryVisible: false,
|
|
|
|
|
+ pendingContentEntryText: '',
|
|
|
punchButtonFxClass: '',
|
|
punchButtonFxClass: '',
|
|
|
panelProgressFxClass: '',
|
|
panelProgressFxClass: '',
|
|
|
panelDistanceFxClass: '',
|
|
panelDistanceFxClass: '',
|
|
@@ -1707,6 +1734,196 @@ export class MapEngine {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ getPendingManualContentCount(): number {
|
|
|
|
|
+ return this.pendingContentCards.filter((item) => !item.autoPopup).length
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ buildPendingContentEntryText(): string {
|
|
|
|
|
+ const count = this.getPendingManualContentCount()
|
|
|
|
|
+ if (count <= 1) {
|
|
|
|
|
+ return count === 1 ? '查看内容' : ''
|
|
|
|
|
+ }
|
|
|
|
|
+ return `查看内容(${count})`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ syncPendingContentEntryState(immediate = true): void {
|
|
|
|
|
+ const count = this.getPendingManualContentCount()
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ pendingContentEntryVisible: count > 0,
|
|
|
|
|
+ pendingContentEntryText: this.buildPendingContentEntryText(),
|
|
|
|
|
+ }, immediate)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ resolveContentControlByKey(contentKey: string): { control: GameControl; displayMode: 'auto' | 'click' } | null {
|
|
|
|
|
+ if (!contentKey || !this.gameRuntime.definition) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const isClickContent = contentKey.indexOf(':click') >= 0
|
|
|
|
|
+ const controlId = isClickContent ? contentKey.replace(/:click$/, '') : contentKey
|
|
|
|
|
+ const control = this.gameRuntime.definition.controls.find((item) => item.id === controlId)
|
|
|
|
|
+ if (!control || !control.displayContent) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ control,
|
|
|
|
|
+ displayMode: isClickContent ? 'click' : 'auto',
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ buildContentH5Request(
|
|
|
|
|
+ contentKey: string,
|
|
|
|
|
+ title: string,
|
|
|
|
|
+ body: string,
|
|
|
|
|
+ motionClass: string,
|
|
|
|
|
+ once: boolean,
|
|
|
|
|
+ priority: number,
|
|
|
|
|
+ autoPopup: boolean,
|
|
|
|
|
+ ): H5ExperienceRequest | null {
|
|
|
|
|
+ const resolved = this.resolveContentControlByKey(contentKey)
|
|
|
|
|
+ if (!resolved) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const displayContent = resolved.control.displayContent
|
|
|
|
|
+ if (!displayContent) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const experienceConfig = resolved.displayMode === 'click'
|
|
|
|
|
+ ? displayContent.clickExperience
|
|
|
|
|
+ : displayContent.contentExperience
|
|
|
|
|
+ if (!experienceConfig || experienceConfig.type !== 'h5' || !experienceConfig.url) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ kind: 'content',
|
|
|
|
|
+ title: title || resolved.control.label || '内容体验',
|
|
|
|
|
+ url: experienceConfig.url,
|
|
|
|
|
+ bridgeVersion: experienceConfig.bridge || 'content-v1',
|
|
|
|
|
+ context: {
|
|
|
|
|
+ eventId: this.configAppId || '',
|
|
|
|
|
+ configTitle: this.state.mapName || '',
|
|
|
|
|
+ configVersion: this.configVersion || '',
|
|
|
|
|
+ mode: this.gameMode,
|
|
|
|
|
+ sessionStatus: this.gameRuntime.state ? this.gameRuntime.state.status : 'idle',
|
|
|
|
|
+ controlId: resolved.control.id,
|
|
|
|
|
+ controlKind: resolved.control.kind,
|
|
|
|
|
+ controlCode: resolved.control.code,
|
|
|
|
|
+ controlLabel: resolved.control.label,
|
|
|
|
|
+ controlSequence: resolved.control.sequence,
|
|
|
|
|
+ displayMode: resolved.displayMode,
|
|
|
|
|
+ title,
|
|
|
|
|
+ body,
|
|
|
|
|
+ },
|
|
|
|
|
+ fallback: {
|
|
|
|
|
+ title,
|
|
|
|
|
+ body,
|
|
|
|
|
+ motionClass,
|
|
|
|
|
+ contentKey,
|
|
|
|
|
+ once,
|
|
|
|
|
+ priority,
|
|
|
|
|
+ autoPopup,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hasActiveContentExperience(): boolean {
|
|
|
|
|
+ return this.state.contentCardVisible || this.currentH5ExperienceOpen
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ enqueueContentCard(item: ContentCardEntry): void {
|
|
|
|
|
+ if (item.once && item.contentKey && this.shownContentCardKeys[item.contentKey]) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (item.contentKey && this.pendingContentCards.some((pending) => pending.contentKey === item.contentKey && pending.autoPopup === item.autoPopup)) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.pendingContentCards.push(item)
|
|
|
|
|
+ this.syncPendingContentEntryState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ openContentCardEntry(item: ContentCardEntry): void {
|
|
|
|
|
+ this.clearContentCardTimer()
|
|
|
|
|
+ if (item.h5Request && this.onOpenH5Experience) {
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ contentCardVisible: false,
|
|
|
|
|
+ contentCardFxClass: '',
|
|
|
|
|
+ pendingContentEntryVisible: false,
|
|
|
|
|
+ pendingContentEntryText: '',
|
|
|
|
|
+ }, true)
|
|
|
|
|
+ this.currentContentCardPriority = item.priority
|
|
|
|
|
+ this.currentContentCard = item
|
|
|
|
|
+ this.currentH5ExperienceOpen = true
|
|
|
|
|
+ if (item.once && item.contentKey) {
|
|
|
|
|
+ this.shownContentCardKeys[item.contentKey] = true
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.onOpenH5Experience(item.h5Request)
|
|
|
|
|
+ return
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
|
|
+ this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ contentCardVisible: true,
|
|
|
|
|
+ contentCardTitle: item.title,
|
|
|
|
|
+ contentCardBody: item.body,
|
|
|
|
|
+ contentCardFxClass: item.motionClass,
|
|
|
|
|
+ pendingContentEntryVisible: false,
|
|
|
|
|
+ pendingContentEntryText: '',
|
|
|
|
|
+ }, true)
|
|
|
|
|
+ this.currentContentCardPriority = item.priority
|
|
|
|
|
+ this.currentContentCard = item
|
|
|
|
|
+ if (item.once && item.contentKey) {
|
|
|
|
|
+ this.shownContentCardKeys[item.contentKey] = true
|
|
|
|
|
+ }
|
|
|
|
|
+ this.contentCardTimer = setTimeout(() => {
|
|
|
|
|
+ this.contentCardTimer = 0
|
|
|
|
|
+ this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ contentCardVisible: false,
|
|
|
|
|
+ contentCardFxClass: '',
|
|
|
|
|
+ }, true)
|
|
|
|
|
+ this.flushQueuedContentCards()
|
|
|
|
|
+ }, 2600) as unknown as number
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ flushQueuedContentCards(): void {
|
|
|
|
|
+ if (this.state.contentCardVisible || !this.pendingContentCards.length) {
|
|
|
|
|
+ this.syncPendingContentEntryState()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let candidateIndex = -1
|
|
|
|
|
+ let candidatePriority = Number.NEGATIVE_INFINITY
|
|
|
|
|
+
|
|
|
|
|
+ for (let index = 0; index < this.pendingContentCards.length; index += 1) {
|
|
|
|
|
+ const item = this.pendingContentCards[index]
|
|
|
|
|
+ if (!item.autoPopup) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if (item.priority > candidatePriority) {
|
|
|
|
|
+ candidatePriority = item.priority
|
|
|
|
|
+ candidateIndex = index
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (candidateIndex < 0) {
|
|
|
|
|
+ this.syncPendingContentEntryState()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextItem = this.pendingContentCards.splice(candidateIndex, 1)[0]
|
|
|
|
|
+ this.openContentCardEntry(nextItem)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
clearMapPulseTimer(): void {
|
|
clearMapPulseTimer(): void {
|
|
|
if (this.mapPulseTimer) {
|
|
if (this.mapPulseTimer) {
|
|
|
clearTimeout(this.mapPulseTimer)
|
|
clearTimeout(this.mapPulseTimer)
|
|
@@ -1734,6 +1951,8 @@ export class MapEngine {
|
|
|
contentCardVisible: false,
|
|
contentCardVisible: false,
|
|
|
contentCardTitle: '',
|
|
contentCardTitle: '',
|
|
|
contentCardBody: '',
|
|
contentCardBody: '',
|
|
|
|
|
+ pendingContentEntryVisible: this.getPendingManualContentCount() > 0,
|
|
|
|
|
+ pendingContentEntryText: this.buildPendingContentEntryText(),
|
|
|
contentCardFxClass: '',
|
|
contentCardFxClass: '',
|
|
|
mapPulseVisible: false,
|
|
mapPulseVisible: false,
|
|
|
mapPulseFxClass: '',
|
|
mapPulseFxClass: '',
|
|
@@ -1744,11 +1963,20 @@ export class MapEngine {
|
|
|
panelDistanceFxClass: '',
|
|
panelDistanceFxClass: '',
|
|
|
}, true)
|
|
}, true)
|
|
|
this.currentContentCardPriority = 0
|
|
this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
resetSessionContentExperienceState(): void {
|
|
resetSessionContentExperienceState(): void {
|
|
|
this.shownContentCardKeys = {}
|
|
this.shownContentCardKeys = {}
|
|
|
this.currentContentCardPriority = 0
|
|
this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.pendingContentCards = []
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ pendingContentEntryVisible: false,
|
|
|
|
|
+ pendingContentEntryText: '',
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
clearSessionTimerInterval(): void {
|
|
clearSessionTimerInterval(): void {
|
|
@@ -1909,45 +2137,100 @@ export class MapEngine {
|
|
|
const once = !!(options && options.once)
|
|
const once = !!(options && options.once)
|
|
|
const priority = options && typeof options.priority === 'number' ? options.priority : 0
|
|
const priority = options && typeof options.priority === 'number' ? options.priority : 0
|
|
|
const contentKey = options && options.contentKey ? options.contentKey : ''
|
|
const contentKey = options && options.contentKey ? options.contentKey : ''
|
|
|
|
|
+ const entry = {
|
|
|
|
|
+ title,
|
|
|
|
|
+ body,
|
|
|
|
|
+ motionClass,
|
|
|
|
|
+ contentKey,
|
|
|
|
|
+ once,
|
|
|
|
|
+ priority,
|
|
|
|
|
+ autoPopup,
|
|
|
|
|
+ h5Request: this.buildContentH5Request(contentKey, title, body, motionClass, once, priority, autoPopup),
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (!autoPopup) {
|
|
|
|
|
|
|
+ if (once && contentKey && this.shownContentCardKeys[contentKey]) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (once && contentKey && this.shownContentCardKeys[contentKey]) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (!autoPopup) {
|
|
|
|
|
+ this.enqueueContentCard(entry)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (this.state.contentCardVisible && priority < this.currentContentCardPriority) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (this.currentH5ExperienceOpen) {
|
|
|
|
|
+ this.enqueueContentCard(entry)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.clearContentCardTimer()
|
|
|
|
|
- this.setState({
|
|
|
|
|
- contentCardVisible: true,
|
|
|
|
|
- contentCardTitle: title,
|
|
|
|
|
- contentCardBody: body,
|
|
|
|
|
- contentCardFxClass: motionClass,
|
|
|
|
|
- }, true)
|
|
|
|
|
- this.currentContentCardPriority = priority
|
|
|
|
|
- if (once && contentKey) {
|
|
|
|
|
- this.shownContentCardKeys[contentKey] = true
|
|
|
|
|
|
|
+ if (this.state.contentCardVisible) {
|
|
|
|
|
+ if (priority > this.currentContentCardPriority) {
|
|
|
|
|
+ this.openContentCardEntry(entry)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.enqueueContentCard(entry)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- this.contentCardTimer = setTimeout(() => {
|
|
|
|
|
- this.contentCardTimer = 0
|
|
|
|
|
- this.currentContentCardPriority = 0
|
|
|
|
|
- this.setState({
|
|
|
|
|
- contentCardVisible: false,
|
|
|
|
|
- contentCardFxClass: '',
|
|
|
|
|
- }, true)
|
|
|
|
|
- }, 2600) as unknown as number
|
|
|
|
|
|
|
+
|
|
|
|
|
+ this.openContentCardEntry(entry)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
closeContentCard(): void {
|
|
closeContentCard(): void {
|
|
|
this.clearContentCardTimer()
|
|
this.clearContentCardTimer()
|
|
|
this.currentContentCardPriority = 0
|
|
this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
this.setState({
|
|
this.setState({
|
|
|
contentCardVisible: false,
|
|
contentCardVisible: false,
|
|
|
contentCardFxClass: '',
|
|
contentCardFxClass: '',
|
|
|
}, true)
|
|
}, true)
|
|
|
|
|
+ this.flushQueuedContentCards()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ openPendingContentCard(): void {
|
|
|
|
|
+ if (!this.pendingContentCards.length) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let candidateIndex = -1
|
|
|
|
|
+ let candidatePriority = Number.NEGATIVE_INFINITY
|
|
|
|
|
+ for (let index = 0; index < this.pendingContentCards.length; index += 1) {
|
|
|
|
|
+ const item = this.pendingContentCards[index]
|
|
|
|
|
+ if (item.autoPopup) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if (item.priority > candidatePriority) {
|
|
|
|
|
+ candidatePriority = item.priority
|
|
|
|
|
+ candidateIndex = index
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (candidateIndex < 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pending = this.pendingContentCards.splice(candidateIndex, 1)[0]
|
|
|
|
|
+ this.openContentCardEntry({
|
|
|
|
|
+ ...pending,
|
|
|
|
|
+ autoPopup: true,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ handleH5ExperienceClosed(): void {
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
|
|
+ this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.flushQueuedContentCards()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ handleH5ExperienceFallback(fallback: H5ExperienceFallbackPayload): void {
|
|
|
|
|
+ this.currentH5ExperienceOpen = false
|
|
|
|
|
+ this.currentContentCardPriority = 0
|
|
|
|
|
+ this.currentContentCard = null
|
|
|
|
|
+ this.openContentCardEntry({
|
|
|
|
|
+ ...fallback,
|
|
|
|
|
+ h5Request: null,
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
applyGameEffects(effects: GameEffect[]): string | null {
|
|
applyGameEffects(effects: GameEffect[]): string | null {
|
|
@@ -2693,24 +2976,29 @@ export class MapEngine {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
handleMapTap(stageX: number, stageY: number): void {
|
|
handleMapTap(stageX: number, stageY: number): void {
|
|
|
- if (!this.gameRuntime.definition || !this.gameRuntime.state || this.gameRuntime.definition.mode !== 'score-o') {
|
|
|
|
|
|
|
+ if (!this.gameRuntime.definition || !this.gameRuntime.state) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const focusedControlId = this.findFocusableControlAt(stageX, stageY)
|
|
|
|
|
- if (focusedControlId === undefined) {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if (this.gameRuntime.definition.mode === 'score-o') {
|
|
|
|
|
+ const focusedControlId = this.findFocusableControlAt(stageX, stageY)
|
|
|
|
|
+ if (focusedControlId !== undefined) {
|
|
|
|
|
+ const gameResult = this.gameRuntime.dispatch({
|
|
|
|
|
+ type: 'control_focused',
|
|
|
|
|
+ at: Date.now(),
|
|
|
|
|
+ controlId: focusedControlId,
|
|
|
|
|
+ })
|
|
|
|
|
+ this.commitGameResult(
|
|
|
|
|
+ gameResult,
|
|
|
|
|
+ focusedControlId ? `已选择目标点 (${this.buildVersion})` : `已取消目标点选择 (${this.buildVersion})`,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const gameResult = this.gameRuntime.dispatch({
|
|
|
|
|
- type: 'control_focused',
|
|
|
|
|
- at: Date.now(),
|
|
|
|
|
- controlId: focusedControlId,
|
|
|
|
|
- })
|
|
|
|
|
- this.commitGameResult(
|
|
|
|
|
- gameResult,
|
|
|
|
|
- focusedControlId ? `已选择目标点 (${this.buildVersion})` : `已取消目标点选择 (${this.buildVersion})`,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const contentControlId = this.findContentControlAt(stageX, stageY)
|
|
|
|
|
+ if (contentControlId) {
|
|
|
|
|
+ this.openControlClickContent(contentControlId)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
findFocusableControlAt(stageX: number, stageY: number): string | null | undefined {
|
|
findFocusableControlAt(stageX: number, stageY: number): string | null | undefined {
|
|
@@ -2749,6 +3037,134 @@ export class MapEngine {
|
|
|
return matchedControlId === this.gamePresentation.map.focusedControlId ? null : matchedControlId
|
|
return matchedControlId === this.gamePresentation.map.focusedControlId ? null : matchedControlId
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ findContentControlAt(stageX: number, stageY: number): string | undefined {
|
|
|
|
|
+ if (!this.gameRuntime.definition || !this.courseData || !this.state.stageWidth || !this.state.stageHeight) {
|
|
|
|
|
+ return undefined
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let matchedControlId: string | undefined
|
|
|
|
|
+ let matchedDistance = Number.POSITIVE_INFINITY
|
|
|
|
|
+ let matchedPriority = Number.NEGATIVE_INFINITY
|
|
|
|
|
+ const hitRadiusPx = Math.max(28, this.getControlHitRadiusPx())
|
|
|
|
|
+
|
|
|
|
|
+ for (const control of this.gameRuntime.definition.controls) {
|
|
|
|
|
+ if (
|
|
|
|
|
+ !control.displayContent
|
|
|
|
|
+ || (
|
|
|
|
|
+ !control.displayContent.clickTitle
|
|
|
|
|
+ && !control.displayContent.clickBody
|
|
|
|
|
+ && !(control.displayContent.clickExperience && control.displayContent.clickExperience.type === 'h5')
|
|
|
|
|
+ && !(control.displayContent.contentExperience && control.displayContent.contentExperience.type === 'h5')
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.isControlTapContentVisible(control)) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const screenPoint = this.getControlScreenPoint(control.id)
|
|
|
|
|
+ if (!screenPoint) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const distancePx = Math.sqrt(
|
|
|
|
|
+ Math.pow(screenPoint.x - stageX, 2)
|
|
|
|
|
+ + Math.pow(screenPoint.y - stageY, 2),
|
|
|
|
|
+ )
|
|
|
|
|
+ if (distancePx > hitRadiusPx) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const controlPriority = this.getControlTapContentPriority(control)
|
|
|
|
|
+ const sameDistance = Math.abs(distancePx - matchedDistance) <= 2
|
|
|
|
|
+ if (
|
|
|
|
|
+ distancePx < matchedDistance
|
|
|
|
|
+ || (sameDistance && controlPriority > matchedPriority)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ matchedDistance = distancePx
|
|
|
|
|
+ matchedPriority = controlPriority
|
|
|
|
|
+ matchedControlId = control.id
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return matchedControlId
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getControlTapContentPriority(control: { kind: 'start' | 'control' | 'finish'; id: string }): number {
|
|
|
|
|
+ if (!this.gameRuntime.state || !this.gamePresentation.map) {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const currentTargetControlId = this.gameRuntime.state.currentTargetControlId
|
|
|
|
|
+ const completedControlIds = this.gameRuntime.state.completedControlIds
|
|
|
|
|
+
|
|
|
|
|
+ if (currentTargetControlId === control.id) {
|
|
|
|
|
+ return 100
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (control.kind === 'start') {
|
|
|
|
|
+ return completedControlIds.includes(control.id) ? 10 : 90
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (control.kind === 'finish') {
|
|
|
|
|
+ return completedControlIds.includes(control.id)
|
|
|
|
|
+ ? 80
|
|
|
|
|
+ : (this.gamePresentation.map.completedStart ? 85 : 5)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return completedControlIds.includes(control.id) ? 40 : 60
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isControlTapContentVisible(control: { kind: 'start' | 'control' | 'finish'; sequence: number | null; id: string }): boolean {
|
|
|
|
|
+ if (this.gamePresentation.map.revealFullCourse) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (control.kind === 'start') {
|
|
|
|
|
+ return this.gamePresentation.map.activeStart || this.gamePresentation.map.completedStart
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (control.kind === 'finish') {
|
|
|
|
|
+ return this.gamePresentation.map.activeFinish || this.gamePresentation.map.focusedFinish || this.gamePresentation.map.completedFinish
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (control.sequence === null) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const readyControlSequences = this.resolveReadyControlSequences()
|
|
|
|
|
+ return this.gamePresentation.map.activeControlSequences.includes(control.sequence)
|
|
|
|
|
+ || this.gamePresentation.map.completedControlSequences.includes(control.sequence)
|
|
|
|
|
+ || this.gamePresentation.map.skippedControlSequences.includes(control.sequence)
|
|
|
|
|
+ || this.gamePresentation.map.focusedControlSequences.includes(control.sequence)
|
|
|
|
|
+ || readyControlSequences.includes(control.sequence)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ openControlClickContent(controlId: string): void {
|
|
|
|
|
+ if (!this.gameRuntime.definition) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const control = this.gameRuntime.definition.controls.find((item) => item.id === controlId)
|
|
|
|
|
+ if (!control || !control.displayContent) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const title = control.displayContent.clickTitle || control.displayContent.title || control.label || '内容体验'
|
|
|
|
|
+ const body = control.displayContent.clickBody || control.displayContent.body || ''
|
|
|
|
|
+ if (!title && !body) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.showContentCard(title, body, 'game-content-card--fx-pop', {
|
|
|
|
|
+ contentKey: `${control.id}:click`,
|
|
|
|
|
+ autoPopup: true,
|
|
|
|
|
+ once: false,
|
|
|
|
|
+ priority: control.displayContent.priority,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
getControlHitRadiusPx(): number {
|
|
getControlHitRadiusPx(): number {
|
|
|
if (!this.state.tileSizePx) {
|
|
if (!this.state.tileSizePx) {
|
|
|
return 28
|
|
return 28
|