Add possibility to guess as a player
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 25s

This commit is contained in:
Johnny322
2026-03-01 22:19:34 +01:00
parent 993b1a2c13
commit f440118fef
3 changed files with 106 additions and 77 deletions

View File

@@ -266,6 +266,9 @@
<button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
Tap To Enable Audio
</button>
<button v-if="canViewerGuess" class="primary viewer-guess" @click="requestGuessStop">
Guess Now
</button>
<div v-if="!currentClipUrl" class="player-empty"></div>
</div>
</div>
@@ -331,13 +334,12 @@ type RealtimeState = {
currentSelectorId: string | null
lastAwardedTeamId: string | null
isAnswerClip: boolean
playbackPosition: number
playbackCapturedAt: number
}
type RealtimeMessage = {
type: 'session:init' | 'state:update'
state: RealtimeState | null
type: 'session:init' | 'state:update' | 'guess:request'
state?: RealtimeState | null
teamId?: string
}
const REALTIME_BASE = '/realtime'
@@ -395,7 +397,6 @@ export default {
isApplyingRemote: false,
queuedRemoteState: null as RealtimeState | null,
syncTimer: 0,
playbackSyncInterval: 0,
audioUnlocked: true,
latestRemoteState: null as RealtimeState | null,
viewerTeamId: ''
@@ -433,6 +434,14 @@ export default {
showEnableAudio() {
return !this.canControlGame && !!this.currentClipUrl && !this.audioUnlocked
},
canViewerGuess() {
return (
!this.canControlGame &&
this.audioUnlocked &&
!!this.viewerTeamId &&
this.getCurrentTileStatus() === 'playing'
)
},
canControlGame() {
return !this.gameId || this.isHost
},
@@ -562,7 +571,6 @@ export default {
this.audioUnlocked = true
this.latestRemoteState = null
this.viewerTeamId = ''
this.stopHostPlaybackSync()
this.setGameInUrl('')
},
async connectSession() {
@@ -585,6 +593,10 @@ export default {
if (!message) return
if (message.type === 'session:init' || message.type === 'state:update') {
if (message.state) await this.applyRemoteState(message.state)
return
}
if (message.type === 'guess:request') {
await this.handleRemoteGuessRequest(message.teamId || '')
}
}
@@ -611,7 +623,6 @@ export default {
}).catch(() => {})
},
buildRealtimeState(): RealtimeState {
const player = this.getPlayer()
return {
step: this.step,
teams: this.teams.map((team) => ({ ...team })),
@@ -621,9 +632,7 @@ export default {
currentClipUrl: this.currentClipUrl,
currentSelectorId: this.currentSelectorId,
lastAwardedTeamId: this.lastAwardedTeamId,
isAnswerClip: this.isAnswerClip,
playbackPosition: player?.currentTime || 0,
playbackCapturedAt: Date.now()
isAnswerClip: this.isAnswerClip
}
},
publishState() {
@@ -673,36 +682,62 @@ export default {
this.isApplyingRemote = false
}
await nextTick()
this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl, state)
this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl)
},
async enableViewerAudio() {
if (this.canControlGame) return
this.audioUnlocked = true
this.ensureAudioContext()
await nextTick()
if (this.latestRemoteState) {
this.syncRemotePlayback(false, this.latestRemoteState)
this.syncRemotePlayback(false)
},
requestGuessStop() {
if (this.canControlGame) return
if (!this.socketConnected || !this.socket) return
if (!this.viewerTeamId) return
if (this.getCurrentTileStatus() !== 'playing') return
this.socket.send(
JSON.stringify({
type: 'guess:request',
teamId: this.viewerTeamId
})
)
},
async handleRemoteGuessRequest(teamId: string) {
if (!this.canControlGame || !this.isHost) return
if (!teamId) return
if (!this.currentTileKey || !this.selectedGame) return
const status = this.getCurrentTileStatus()
if (status !== 'playing') return
const key = this.currentTileKey
const [cIndex, qIndex] = key.split('-').map(Number)
const clue = this.selectedGame.categories[cIndex].clues[qIndex]
this.tiles[key].status = 'guessed'
this.lastAwardedTeamId = null
if (clue.answer) {
this.currentClipUrl = encodeURI(clue.answer)
this.isAnswerClip = true
await nextTick()
const player = this.getPlayer()
if (player) {
this.ensureAudioContext()
player.currentTime = 0
player.load()
player.play().catch(() => {})
}
} else {
const player = this.getPlayer()
player?.pause()
this.currentClipUrl = ''
this.isAnswerClip = false
}
this.queueStateSync()
},
getCurrentTileStatus() {
if (!this.currentTileKey) return 'available'
return this.tiles[this.currentTileKey]?.status || 'available'
},
setPlayerCurrentTime(player: HTMLAudioElement, targetTime: number) {
const seek = () => {
try {
player.currentTime = targetTime
} catch {
// ignore seek failures on unsupported ranges
}
}
if (player.readyState > 0) {
seek()
return
}
player.addEventListener('loadedmetadata', seek, { once: true })
},
syncRemotePlayback(forceReload: boolean, state: RealtimeState) {
syncRemotePlayback(forceReload: boolean) {
if (this.canControlGame) return
const player = this.getPlayer()
if (!player) return
@@ -713,23 +748,11 @@ export default {
return
}
const elapsedSeconds = Math.max(0, (Date.now() - (state.playbackCapturedAt || Date.now())) / 1000)
let targetTime = state.playbackPosition || 0
if (tileStatus === 'playing' || tileStatus === 'guessed') {
targetTime += elapsedSeconds
}
if (Number.isFinite(player.duration) && player.duration > 0) {
targetTime = Math.min(targetTime, Math.max(0, player.duration - 0.05))
}
if (forceReload) {
player.currentTime = 0
player.load()
}
if (Math.abs((player.currentTime || 0) - targetTime) > 0.2) {
this.setPlayerCurrentTime(player, targetTime)
}
if (tileStatus === 'paused') {
player.pause()
return
@@ -794,7 +817,6 @@ export default {
this.tentacleLevels = this.tentacleLevels.map(() => 0)
},
teardownAudio() {
this.stopHostPlaybackSync()
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = 0
@@ -873,25 +895,10 @@ export default {
if (this.rafId) cancelAnimationFrame(this.rafId)
this.rafId = requestAnimationFrame(tick)
},
startHostPlaybackSync() {
if (!this.isHost || !this.socketConnected || !this.socket) return
this.stopHostPlaybackSync()
this.playbackSyncInterval = window.setInterval(() => {
this.queueStateSync()
}, 400)
},
stopHostPlaybackSync() {
if (!this.playbackSyncInterval) return
window.clearInterval(this.playbackSyncInterval)
this.playbackSyncInterval = 0
},
handlePlayerPlay() {
this.isPlaying = true
this.startPulse()
if (this.isHost) {
this.startHostPlaybackSync()
this.queueStateSync()
}
if (this.isHost) this.queueStateSync()
},
handlePlayerPause() {
this.isPlaying = false
@@ -900,10 +907,7 @@ export default {
this.rafId = 0
}
this.pulseLevel = 0
if (this.isHost) {
this.stopHostPlaybackSync()
this.queueStateSync()
}
if (this.isHost) this.queueStateSync()
},
togglePlayback() {
const player = this.getPlayer()
@@ -1125,7 +1129,6 @@ export default {
this.teardownAudio()
this.closeSocket()
if (this.syncTimer) window.clearTimeout(this.syncTimer)
if (this.playbackSyncInterval) window.clearInterval(this.playbackSyncInterval)
}
}
</script>

View File

@@ -560,6 +560,12 @@ audio.hidden-audio {
z-index: 3;
}
.viewer-guess {
position: absolute;
bottom: 20px;
z-index: 3;
}
.end-panel {
text-align: center;
}