feat(app): show leaderboard & enable sending result on game done
This commit is contained in:
parent
8ba28261f8
commit
c09181f380
7 changed files with 1090 additions and 19 deletions
|
|
@ -16,8 +16,6 @@
|
||||||
<div id="body-wrapper">
|
<div id="body-wrapper">
|
||||||
<div id="app">
|
<div id="app">
|
||||||
Please use Chrome or Firefox.
|
Please use Chrome or Firefox.
|
||||||
<br />
|
|
||||||
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="./main.js"></script>
|
<script type="module" src="./main.js"></script>
|
||||||
|
|
|
||||||
39
app/main.js
39
app/main.js
|
|
@ -2,22 +2,41 @@ import mnswpr from '@ayo-run/mnswpr/mnswpr.js'
|
||||||
import '@ayo-run/mnswpr/mnswpr.css'
|
import '@ayo-run/mnswpr/mnswpr.css'
|
||||||
import * as pkg from '@ayo-run/mnswpr/package.json'
|
import * as pkg from '@ayo-run/mnswpr/package.json'
|
||||||
import { LoadingService } from '../utils/'
|
import { LoadingService } from '../utils/'
|
||||||
|
import { LeaderBoardService } from './modules/leader-board/leader-board.js'
|
||||||
|
|
||||||
|
const leaderBoardService = new LeaderBoardService()
|
||||||
|
|
||||||
const version = import.meta.env.MODE === 'development'
|
const version = import.meta.env.MODE === 'development'
|
||||||
? 'dev'
|
? 'dev'
|
||||||
: pkg.version
|
: pkg.version
|
||||||
|
|
||||||
|
const initializeGameBoard = async (level) => {
|
||||||
|
const prevousLeaderBoard = document.getElementById('leaderboard')
|
||||||
|
const loadingService = new LoadingService()
|
||||||
|
const loadingWrapper = document.createElement('div')
|
||||||
|
loadingWrapper.id = 'loading-wrapper'
|
||||||
|
loadingService.addLoading(loadingWrapper)
|
||||||
|
|
||||||
|
const appElement = document.getElementById('app')
|
||||||
|
if (prevousLeaderBoard){
|
||||||
|
const parent = prevousLeaderBoard.parentNode
|
||||||
|
parent.replaceChild(loadingWrapper, prevousLeaderBoard)
|
||||||
|
}else{
|
||||||
|
appElement.append(loadingWrapper)
|
||||||
|
}
|
||||||
|
const leaderBoardWrapper = await leaderBoardService.update(level.id, `Best Times (${level.name})`)
|
||||||
|
leaderBoardWrapper.id = 'leaderboard'
|
||||||
|
|
||||||
|
appElement.replaceChild(leaderBoardWrapper, loadingWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendGameResult = (game) => {
|
||||||
|
leaderBoardService.send(game, 'time')
|
||||||
|
}
|
||||||
|
|
||||||
const game = new mnswpr('app', version, {
|
const game = new mnswpr('app', version, {
|
||||||
levelChanged: () => console.log('[hook]: level reset'),
|
levelChanged: (level) => initializeGameBoard(level),
|
||||||
gameDone: (game) => console.log('[hook]: game done', game)
|
gameDone: (game) => sendGameResult(game)
|
||||||
})
|
})
|
||||||
game.initialize()
|
game.initialize()
|
||||||
|
|
||||||
const loadingService = new LoadingService()
|
|
||||||
const loadingWrapper = document.createElement('div')
|
|
||||||
loadingWrapper.id = 'loading-wrapper'
|
|
||||||
loadingService.addLoading(loadingWrapper)
|
|
||||||
|
|
||||||
const appElement = document.getElementById('app')
|
|
||||||
appElement.append(loadingWrapper)
|
|
||||||
|
|
||||||
|
|
|
||||||
176
app/modules/leader-board/leader-board.js
Normal file
176
app/modules/leader-board/leader-board.js
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { TimerService } from '../../../utils/timer/timer'
|
||||||
|
import { LoggerService } from '../../../utils/logger/logger'
|
||||||
|
import { UserService } from '../user/user'
|
||||||
|
|
||||||
|
import { initializeApp } from 'firebase/app'
|
||||||
|
import {
|
||||||
|
getFirestore, doc, getDocs, getDoc, setDoc, collection, query, orderBy, limit
|
||||||
|
} from 'firebase/firestore/lite'
|
||||||
|
|
||||||
|
|
||||||
|
export class LeaderBoardService {
|
||||||
|
|
||||||
|
timerService = new TimerService()
|
||||||
|
loggerService = new LoggerService()
|
||||||
|
user = new UserService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Create the Leader Board service
|
||||||
|
* @param {String} leaders
|
||||||
|
* @param {String} all
|
||||||
|
* @param {String} configuration
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
// necessary keys to interact with firebase
|
||||||
|
// not a secret
|
||||||
|
// https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public/37484053#37484053
|
||||||
|
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||||
|
const config = {
|
||||||
|
apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ',
|
||||||
|
authDomain: 'moment-188701.firebaseapp.com',
|
||||||
|
databaseURL: 'https://moment-188701.firebaseio.com',
|
||||||
|
projectId: 'secure-moment-188701',
|
||||||
|
storageBucket: 'secure-moment-188701.firebasestorage.app',
|
||||||
|
messagingSenderId: '113827947104',
|
||||||
|
appId: '1:113827947104:web:b176f746d8358302c51905',
|
||||||
|
measurementId: 'G-LZRDY0TG46'
|
||||||
|
}
|
||||||
|
const app = initializeApp(config)
|
||||||
|
this._store = getFirestore(app)
|
||||||
|
|
||||||
|
const configRef = doc(this.store, 'mw-config', 'configuration')
|
||||||
|
getDoc(configRef)
|
||||||
|
.then(res => {
|
||||||
|
this.configuration = res.data()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get store() {
|
||||||
|
return this._store
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the leader board
|
||||||
|
* @param {String} level - the id of the game level
|
||||||
|
* @param {String} title - the displayed name of the game level
|
||||||
|
* @returns {HTMLDivElement} - element with the rendered leader board
|
||||||
|
*/
|
||||||
|
async update(level, title) {
|
||||||
|
const displayElement = document.createElement('div')
|
||||||
|
|
||||||
|
this.lastPlace = Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
|
const q = query(
|
||||||
|
collection(this.store, 'mw-leaders', level, 'games'),
|
||||||
|
orderBy('time'),
|
||||||
|
limit(10)
|
||||||
|
)
|
||||||
|
this.topListSnapshot = await getDocs(q)
|
||||||
|
this.renderList(displayElement, title, this.topListSnapshot.docs)
|
||||||
|
|
||||||
|
return displayElement
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList(displayElement, title, docs) {
|
||||||
|
if (!displayElement) return
|
||||||
|
|
||||||
|
displayElement.innerHTML = ''
|
||||||
|
const leaderHeading = document.createElement('h3')
|
||||||
|
leaderHeading.innerText = title
|
||||||
|
leaderHeading.style.borderBottom = '1px solid #c0c0c0'
|
||||||
|
leaderHeading.style.paddingBottom = '10px'
|
||||||
|
|
||||||
|
|
||||||
|
displayElement.style.maxWidth = '270px'
|
||||||
|
displayElement.style.margin = '0 auto'
|
||||||
|
|
||||||
|
const leaderList = document.createElement('div')
|
||||||
|
|
||||||
|
leaderList.innerHTML = ''
|
||||||
|
leaderList.style.listStyle = 'none'
|
||||||
|
leaderList.style.textAlign = 'left'
|
||||||
|
leaderList.style.marginTop = '-15px'
|
||||||
|
|
||||||
|
if (docs && docs.length) {
|
||||||
|
let i = 1
|
||||||
|
docs.forEach(game => {
|
||||||
|
if (game) {
|
||||||
|
const prettyTime = this.timerService.pretty(game.data().time)
|
||||||
|
const name = game.data().name || 'Anonymous'
|
||||||
|
const item = document.createElement('div')
|
||||||
|
item.style.display = 'flex'
|
||||||
|
const nameElement =document.createElement('div')
|
||||||
|
nameElement.innerHTML = name
|
||||||
|
nameElement.setAttribute('title', name)
|
||||||
|
nameElement.style.textOverflow = 'ellipsis'
|
||||||
|
nameElement.style.whiteSpace = 'nowrap'
|
||||||
|
nameElement.style.overflow = 'hidden'
|
||||||
|
nameElement.style.padding = '0 5px'
|
||||||
|
nameElement.style.cursor = 'pointer'
|
||||||
|
nameElement.style.fontWeight = 'bold'
|
||||||
|
nameElement.style.fontStyle = 'italic'
|
||||||
|
// nameElement.onmousedown = () => console.log(game.data());
|
||||||
|
|
||||||
|
const indexElement = document.createElement('div')
|
||||||
|
indexElement.innerText = `#${i++}`
|
||||||
|
|
||||||
|
const timeElement = document.createElement('div')
|
||||||
|
timeElement.innerText = prettyTime
|
||||||
|
|
||||||
|
item.append(indexElement, nameElement, timeElement)
|
||||||
|
leaderList.append(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (docs.length >= 10) {
|
||||||
|
this.lastPlace = docs[9].data().time
|
||||||
|
}
|
||||||
|
|
||||||
|
displayElement.append(leaderHeading, leaderList)
|
||||||
|
} else {
|
||||||
|
const message = document.createElement('em')
|
||||||
|
message.innerText = 'Be the first to the top!'
|
||||||
|
displayElement.append(leaderHeading, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async send(game, key) {
|
||||||
|
const sessionId = new Date().toDateString().replace(/\s/g, '_')
|
||||||
|
const gameId = new Date().toTimeString().replace(/\s/g, '_')
|
||||||
|
const data = { }
|
||||||
|
data[gameId] = game
|
||||||
|
|
||||||
|
const sessionRef = doc(this.store, 'mw-all', this.user.browserId, 'games', sessionId)
|
||||||
|
await setDoc(sessionRef, data, { merge: true })
|
||||||
|
|
||||||
|
const winningCondigion = (
|
||||||
|
this.configuration
|
||||||
|
&& game.status === this.configuration.passingStatus
|
||||||
|
&& game[key] < this.lastPlace
|
||||||
|
)
|
||||||
|
|
||||||
|
if (winningCondigion) {
|
||||||
|
let name = window.prompt(this.configuration.message)
|
||||||
|
if (!name) {
|
||||||
|
name = 'Anonymous'
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGame = {
|
||||||
|
name,
|
||||||
|
browserId: this.user.browserId,
|
||||||
|
...game
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameScoreRef = doc(collection(this.store, 'mw-leaders', game.level, 'games'))
|
||||||
|
await setDoc(gameScoreRef, newGame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationPromt() {
|
||||||
|
if (!this.configuration) {
|
||||||
|
this.loggerService.debug('Failed to fetch server configuration. Please contact your developer.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/modules/user/user.js
Normal file
21
app/modules/user/user.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export class UserService {
|
||||||
|
constructor() {
|
||||||
|
if (!this.id) {
|
||||||
|
this.browserId = this.generateId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId() {
|
||||||
|
|
||||||
|
var nav = window.navigator
|
||||||
|
var screen = window.screen
|
||||||
|
var guid = nav.mimeTypes.length
|
||||||
|
guid += nav.userAgent.replace(/\D+/g, '')
|
||||||
|
guid += nav.plugins.length
|
||||||
|
guid += screen.height || ''
|
||||||
|
guid += screen.width || ''
|
||||||
|
guid += screen.pixelDepth || ''
|
||||||
|
|
||||||
|
return guid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ayo-run/mnswpr": "workspace:*"
|
"@ayo-run/mnswpr": "workspace:*",
|
||||||
|
"firebase": "^12.11.0"
|
||||||
},
|
},
|
||||||
"author": "Ayo Ayco"
|
"author": "Ayo Ayco"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const PC_BUSY_DELAY = 500
|
||||||
* @param {String} appId
|
* @param {String} appId
|
||||||
* @param {String} version
|
* @param {String} version
|
||||||
* @param {{
|
* @param {{
|
||||||
* levelChanged: () => void,
|
* levelChanged: (setting: any) => void,
|
||||||
* gameDone: (game: any) => void
|
* gameDone: (game: any) => void
|
||||||
* } | undefined } hooks
|
* } | undefined } hooks
|
||||||
*/
|
*/
|
||||||
|
|
@ -224,7 +224,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
* - for initializing the leaderboard
|
* - for initializing the leaderboard
|
||||||
*/
|
*/
|
||||||
if (initial)
|
if (initial)
|
||||||
hooks.levelChanged()
|
hooks.levelChanged(setting)
|
||||||
|
|
||||||
timerService.initialize(timerDisplay)
|
timerService.initialize(timerDisplay)
|
||||||
updateFlagsCountDisplay()
|
updateFlagsCountDisplay()
|
||||||
|
|
|
||||||
864
pnpm-lock.yaml
864
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue