switch to typescript
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
/config.js
|
/config.json
|
||||||
/data
|
/data
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
export default {
|
|
||||||
http: {
|
|
||||||
hostname: '127.0.0.1',
|
|
||||||
port: 1337,
|
|
||||||
},
|
|
||||||
ws: {
|
|
||||||
hostname: '127.0.0.1',
|
|
||||||
port: 1338,
|
|
||||||
connectstring: `ws://localhost:1338/ws`,
|
|
||||||
},
|
|
||||||
persistence: {
|
|
||||||
interval: 30000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
14
config.example.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"hostname": "127.0.0.1",
|
||||||
|
"port": 1337
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"hostname": "127.0.0.1",
|
||||||
|
"port": 1338,
|
||||||
|
"connectstring": "ws://localhost:1338/ws"
|
||||||
|
},
|
||||||
|
"persistence": {
|
||||||
|
"interval": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
7358
package-lock.json
generated
21
package.json
|
|
@ -7,9 +7,28 @@
|
||||||
"image-size": "^0.9.3",
|
"image-size": "^0.9.3",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"sharp": "^0.28.1",
|
"sharp": "^0.28.1",
|
||||||
|
"vue": "^3.0.11",
|
||||||
|
"vue-router": "^4.0.8",
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^26.6.3"
|
"@types/exif": "^0.6.2",
|
||||||
|
"@types/express": "^4.17.11",
|
||||||
|
"@types/multer": "^1.4.5",
|
||||||
|
"@types/sharp": "^0.28.1",
|
||||||
|
"@types/ws": "^7.4.4",
|
||||||
|
"@vitejs/plugin-vue": "^1.2.2",
|
||||||
|
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"rollup": "^2.48.0",
|
||||||
|
"rollup-plugin-typescript2": "^0.30.0",
|
||||||
|
"rollup-plugin-vue": "^6.0.0-beta.10",
|
||||||
|
"ts-node": "^9.1.1",
|
||||||
|
"typescript": "^4.2.4",
|
||||||
|
"vite": "^2.3.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"rollup": "rollup",
|
||||||
|
"vite": "vite"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'image-teaser',
|
|
||||||
props: {
|
|
||||||
image: Object
|
|
||||||
},
|
|
||||||
template: `<div class="imageteaser" :style="style" @click="onClick"></div>`,
|
|
||||||
computed: {
|
|
||||||
style() {
|
|
||||||
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
|
||||||
return {
|
|
||||||
'background-image': `url("${url}")`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.$emit('click')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
// ingame component
|
|
||||||
// shows the preview image
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'preview-overlay',
|
|
||||||
template: `
|
|
||||||
<div class="overlay" @click="$emit('bgclick')">
|
|
||||||
<div class="preview">
|
|
||||||
<div class="img" :style="previewStyle"></div>
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
props: {
|
|
||||||
img: String,
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
bgclick: null,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
previewStyle () {
|
|
||||||
return {
|
|
||||||
backgroundImage: `url('${this.img}')`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
import Time from './../../common/Time.js'
|
|
||||||
|
|
||||||
// ingame component
|
|
||||||
// shows timer, tiles left, etc..
|
|
||||||
// maybe split it up later
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'puzzle-status',
|
|
||||||
template: `
|
|
||||||
<div class="timer">
|
|
||||||
<div>
|
|
||||||
🧩 {{piecesDone}}/{{piecesTotal}}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{icon}} {{durationStr}}
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
finished: Boolean,
|
|
||||||
duration: Number,
|
|
||||||
piecesDone: Number,
|
|
||||||
piecesTotal: Number,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
icon () {
|
|
||||||
return this.finished ? '🏁' : '⏳'
|
|
||||||
},
|
|
||||||
durationStr () {
|
|
||||||
return Time.durationStr(this.duration)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
// ingame component
|
|
||||||
// allows to change (player) settings
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'settings-overlay',
|
|
||||||
template: `
|
|
||||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
|
||||||
<table class="overlay-content settings" @click.stop="">
|
|
||||||
<tr>
|
|
||||||
<td><label>Background: </label></td>
|
|
||||||
<td><input type="color" v-model="modelValue.background" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Color: </label></td>
|
|
||||||
<td><input type="color" v-model="modelValue.color" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Name: </label></td>
|
|
||||||
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
emits: {
|
|
||||||
bgclick: null,
|
|
||||||
'update:modelValue': null,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$watch('modelValue', val => {
|
|
||||||
this.$emit('update:modelValue', val)
|
|
||||||
}, { deep: true })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="/style.css" />
|
|
||||||
<script src="https://unpkg.com/vue@3.0.11"></script>
|
|
||||||
<script src="https://unpkg.com/vue-router@4.0.8"></script>
|
|
||||||
<title>🧩 jigsaw.hyottoko.club</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module">
|
|
||||||
import App from "/App.vue.js"
|
|
||||||
import Index from "/views/Index.vue.js"
|
|
||||||
import NewGame from "/views/NewGame.vue.js"
|
|
||||||
import Game from "/views/Game.vue.js"
|
|
||||||
import Replay from "/views/Replay.vue.js"
|
|
||||||
import Util from './../common/Util.js'
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const res = await fetch(`/api/conf`)
|
|
||||||
const conf = await res.json()
|
|
||||||
|
|
||||||
function initme() {
|
|
||||||
let ID = localStorage.getItem('ID')
|
|
||||||
if (!ID) {
|
|
||||||
ID = Util.uniqId()
|
|
||||||
localStorage.setItem('ID', ID)
|
|
||||||
}
|
|
||||||
return ID
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = VueRouter.createRouter({
|
|
||||||
history: VueRouter.createWebHashHistory(),
|
|
||||||
routes: [
|
|
||||||
{ name: 'index', path: '/', component: Index },
|
|
||||||
{ name: 'new-game', path: '/new-game', component: NewGame },
|
|
||||||
{ name: 'game', path: '/g/:id', component: Game },
|
|
||||||
{ name: 'replay', path: '/replay/:id', component: Replay },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
if (from.name !== undefined) {
|
|
||||||
document.documentElement.classList.remove(`view-${from.name}`)
|
|
||||||
}
|
|
||||||
document.documentElement.classList.add(`view-${to.name}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = Vue.createApp(App)
|
|
||||||
app.config.globalProperties.$config = conf
|
|
||||||
app.config.globalProperties.$clientId = initme()
|
|
||||||
app.use(router)
|
|
||||||
app.mount('#app')
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
import Time from './../../common/Time.js'
|
|
||||||
import GameTeaser from './../components/GameTeaser.vue.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
GameTeaser,
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<h1>Running games</h1>
|
|
||||||
<div class="game-teaser-wrap" v-for="g in gamesRunning">
|
|
||||||
<game-teaser :game="g" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>Finished games</h1>
|
|
||||||
<div class="game-teaser-wrap" v-for="g in gamesFinished">
|
|
||||||
<game-teaser :game="g" />
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
gamesRunning: [],
|
|
||||||
gamesFinished: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
const res = await fetch('/api/index-data')
|
|
||||||
const json = await res.json()
|
|
||||||
this.gamesRunning = json.gamesRunning
|
|
||||||
this.gamesFinished = json.gamesFinished
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
time(start, end) {
|
|
||||||
const icon = end ? '🏁' : '⏳'
|
|
||||||
const from = start;
|
|
||||||
const to = end || Time.timestamp()
|
|
||||||
const timeDiffStr = Time.timeDiffStr(from, to)
|
|
||||||
return `${icon} ${timeDiffStr}`
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
rollup.server.config.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// rollup.config.js
|
||||||
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/server/main.ts',
|
||||||
|
output: {
|
||||||
|
dir: 'build/server',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [typescript({
|
||||||
|
"tsconfig": "tsconfig.server.json"
|
||||||
|
})],
|
||||||
|
};
|
||||||
7
scripts/build
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh -ex
|
||||||
|
|
||||||
|
# server build
|
||||||
|
npm run rollup -- -c rollup.server.config.js
|
||||||
|
|
||||||
|
# frontend build
|
||||||
|
npm run vite -- build
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
cd "$RUN_DIR/server"
|
cd "$RUN_DIR/build/server"
|
||||||
|
|
||||||
nodemon --max-old-space-size=64 index.js -e js
|
nodemon --max-old-space-size=64 main.js -c ../../config.json
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,111 @@
|
||||||
import Geometry from './Geometry.js'
|
import Geometry from './Geometry'
|
||||||
import Protocol from './Protocol.js'
|
import Protocol from './Protocol'
|
||||||
import Time from './Time.js'
|
import { Rng } from './Rng'
|
||||||
import Util from './Util.js'
|
import Time from './Time'
|
||||||
|
import Util from './Util'
|
||||||
|
|
||||||
|
interface EncodedPlayer extends Array<any> {}
|
||||||
|
interface EncodedPiece extends Array<any> {}
|
||||||
|
|
||||||
|
interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameRng {
|
||||||
|
obj: Rng
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Game {
|
||||||
|
id: string
|
||||||
|
players: Array<EncodedPlayer>
|
||||||
|
puzzle: Puzzle
|
||||||
|
evtInfos: Record<string, EvtInfo>
|
||||||
|
scoreMode?: number
|
||||||
|
rng: GameRng
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Puzzle {
|
||||||
|
tiles: Array<EncodedPiece>
|
||||||
|
data: PuzzleData
|
||||||
|
info: PuzzleInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PuzzleData {
|
||||||
|
started: number
|
||||||
|
finished: number
|
||||||
|
maxGroup: number
|
||||||
|
maxZ: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PuzzleTable {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PieceShape {
|
||||||
|
top: 0|1|-1
|
||||||
|
bottom: 0|1|-1
|
||||||
|
left: 0|1|-1
|
||||||
|
right: 0|1|-1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Piece {
|
||||||
|
owner: string|number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PuzzleInfo {
|
||||||
|
table: PuzzleTable
|
||||||
|
targetTiles: number,
|
||||||
|
imageUrl: string
|
||||||
|
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
tileSize: number
|
||||||
|
tileDrawSize: number
|
||||||
|
tileMarginWidth: number
|
||||||
|
tileDrawOffset: number
|
||||||
|
snapDistance: number
|
||||||
|
|
||||||
|
tiles: number
|
||||||
|
tilesX: number
|
||||||
|
tilesY: number
|
||||||
|
|
||||||
|
// TODO: ts type Array<PieceShape>
|
||||||
|
shapes: Array<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
d: 0|1
|
||||||
|
name: string|null
|
||||||
|
color: string|null
|
||||||
|
bgcolor: string|null
|
||||||
|
points: number
|
||||||
|
ts: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvtInfo {
|
||||||
|
_last_mouse: Point|null
|
||||||
|
_last_mouse_down: Point|null
|
||||||
|
}
|
||||||
|
|
||||||
const SCORE_MODE_FINAL = 0
|
const SCORE_MODE_FINAL = 0
|
||||||
const SCORE_MODE_ANY = 1
|
const SCORE_MODE_ANY = 1
|
||||||
|
|
||||||
const IDLE_TIMEOUT_SEC = 30
|
const IDLE_TIMEOUT_SEC = 30
|
||||||
|
|
||||||
// Map<gameId, GameObject>
|
// Map<gameId, Game>
|
||||||
const GAMES = {}
|
const GAMES: Record<string, Game> = {}
|
||||||
|
|
||||||
function exists(gameId) {
|
function exists(gameId: string) {
|
||||||
return (!!GAMES[gameId]) || false
|
return (!!GAMES[gameId]) || false
|
||||||
}
|
}
|
||||||
|
|
||||||
function __createPlayerObject(id, ts) {
|
function __createPlayerObject(id: string, ts: number): Player {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|
@ -29,11 +119,11 @@ function __createPlayerObject(id, ts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGame(gameId, game) {
|
function setGame(gameId: string, game: Game) {
|
||||||
GAMES[gameId] = game
|
GAMES[gameId] = game
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayerIndexById(gameId, playerId) {
|
function getPlayerIndexById(gameId: string, playerId: string): number {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let player of GAMES[gameId].players) {
|
for (let player of GAMES[gameId].players) {
|
||||||
if (Util.decodePlayer(player).id === playerId) {
|
if (Util.decodePlayer(player).id === playerId) {
|
||||||
|
|
@ -44,19 +134,19 @@ function getPlayerIndexById(gameId, playerId) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayerIdByIndex(gameId, playerIndex) {
|
function getPlayerIdByIndex(gameId: string, playerIndex: number) {
|
||||||
if (GAMES[gameId].players.length > playerIndex) {
|
if (GAMES[gameId].players.length > playerIndex) {
|
||||||
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id
|
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayer(gameId, playerId) {
|
function getPlayer(gameId: string, playerId: string) {
|
||||||
let idx = getPlayerIndexById(gameId, playerId)
|
let idx = getPlayerIndexById(gameId, playerId)
|
||||||
return Util.decodePlayer(GAMES[gameId].players[idx])
|
return Util.decodePlayer(GAMES[gameId].players[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPlayer(gameId, playerId, player) {
|
function setPlayer(gameId: string, playerId: string, player: Player) {
|
||||||
let idx = getPlayerIndexById(gameId, playerId)
|
let idx = getPlayerIndexById(gameId, playerId)
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
GAMES[gameId].players.push(Util.encodePlayer(player))
|
GAMES[gameId].players.push(Util.encodePlayer(player))
|
||||||
|
|
@ -65,30 +155,30 @@ function setPlayer(gameId, playerId, player) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTile(gameId, tileIdx, tile) {
|
function setTile(gameId: string, tileIdx: number, tile: Piece) {
|
||||||
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPuzzleData(gameId, data) {
|
function setPuzzleData(gameId: string, data: PuzzleData) {
|
||||||
GAMES[gameId].puzzle.data = data
|
GAMES[gameId].puzzle.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
function playerExists(gameId, playerId) {
|
function playerExists(gameId: string, playerId: string) {
|
||||||
const idx = getPlayerIndexById(gameId, playerId)
|
const idx = getPlayerIndexById(gameId, playerId)
|
||||||
return idx !== -1
|
return idx !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActivePlayers(gameId, ts) {
|
function getActivePlayers(gameId: string, ts: number) {
|
||||||
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
||||||
return getAllPlayers(gameId).filter(p => p.ts >= minTs)
|
return getAllPlayers(gameId).filter((p: Player) => p.ts >= minTs)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIdlePlayers(gameId, ts) {
|
function getIdlePlayers(gameId: string, ts: number) {
|
||||||
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
||||||
return getAllPlayers(gameId).filter(p => p.ts < minTs && p.points > 0)
|
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPlayer(gameId, playerId, ts) {
|
function addPlayer(gameId: string, playerId: string, ts: number) {
|
||||||
if (!playerExists(gameId, playerId)) {
|
if (!playerExists(gameId, playerId)) {
|
||||||
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -96,7 +186,7 @@ function addPlayer(gameId, playerId, ts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEvtInfo(gameId, playerId) {
|
function getEvtInfo(gameId: string, playerId: string) {
|
||||||
if (playerId in GAMES[gameId].evtInfos) {
|
if (playerId in GAMES[gameId].evtInfos) {
|
||||||
return GAMES[gameId].evtInfos[playerId]
|
return GAMES[gameId].evtInfos[playerId]
|
||||||
}
|
}
|
||||||
|
|
@ -106,12 +196,12 @@ function getEvtInfo(gameId, playerId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEvtInfo(gameId, playerId, evtInfo) {
|
function setEvtInfo(gameId: string, playerId: string, evtInfo: EvtInfo) {
|
||||||
GAMES[gameId].evtInfos[playerId] = evtInfo
|
GAMES[gameId].evtInfos[playerId] = evtInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllGames() {
|
function getAllGames(): Array<Game> {
|
||||||
return Object.values(GAMES).sort((a, b) => {
|
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
||||||
// when both have same finished state, sort by started
|
// when both have same finished state, sort by started
|
||||||
if (isFinished(a.id) === isFinished(b.id)) {
|
if (isFinished(a.id) === isFinished(b.id)) {
|
||||||
return b.puzzle.data.started - a.puzzle.data.started
|
return b.puzzle.data.started - a.puzzle.data.started
|
||||||
|
|
@ -121,37 +211,37 @@ function getAllGames() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllPlayers(gameId) {
|
function getAllPlayers(gameId: string) {
|
||||||
return GAMES[gameId]
|
return GAMES[gameId]
|
||||||
? GAMES[gameId].players.map(Util.decodePlayer)
|
? GAMES[gameId].players.map(Util.decodePlayer)
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(gameId) {
|
function get(gameId: string) {
|
||||||
return GAMES[gameId]
|
return GAMES[gameId]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTileCount(gameId) {
|
function getTileCount(gameId: string) {
|
||||||
return GAMES[gameId].puzzle.tiles.length
|
return GAMES[gameId].puzzle.tiles.length
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageUrl(gameId) {
|
function getImageUrl(gameId: string) {
|
||||||
return GAMES[gameId].puzzle.info.imageUrl
|
return GAMES[gameId].puzzle.info.imageUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
function setImageUrl(gameId, imageUrl) {
|
function setImageUrl(gameId: string, imageUrl: string) {
|
||||||
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScoreMode(gameId) {
|
function getScoreMode(gameId: string) {
|
||||||
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
|
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFinished(gameId) {
|
function isFinished(gameId: string) {
|
||||||
return getFinishedTileCount(gameId) === getTileCount(gameId)
|
return getFinishedTileCount(gameId) === getTileCount(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFinishedTileCount(gameId) {
|
function getFinishedTileCount(gameId: string) {
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||||
if (Util.decodeTile(t).owner === -1) {
|
if (Util.decodeTile(t).owner === -1) {
|
||||||
|
|
@ -161,12 +251,12 @@ function getFinishedTileCount(gameId) {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTilesSortedByZIndex(gameId) {
|
function getTilesSortedByZIndex(gameId: string) {
|
||||||
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
|
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
|
||||||
return tiles.sort((t1, t2) => t1.z - t2.z)
|
return tiles.sort((t1, t2) => t1.z - t2.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePlayer(gameId, playerId, change) {
|
function changePlayer(gameId: string, playerId: string, change: any) {
|
||||||
const player = getPlayer(gameId, playerId)
|
const player = getPlayer(gameId, playerId)
|
||||||
for (let k of Object.keys(change)) {
|
for (let k of Object.keys(change)) {
|
||||||
player[k] = change[k]
|
player[k] = change[k]
|
||||||
|
|
@ -174,13 +264,14 @@ function changePlayer(gameId, playerId, change) {
|
||||||
setPlayer(gameId, playerId, player)
|
setPlayer(gameId, playerId, player)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeData(gameId, change) {
|
function changeData(gameId: string, change: any) {
|
||||||
for (let k of Object.keys(change)) {
|
for (let k of Object.keys(change)) {
|
||||||
|
// @ts-ignore
|
||||||
GAMES[gameId].puzzle.data[k] = change[k]
|
GAMES[gameId].puzzle.data[k] = change[k]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTile(gameId, tileIdx, change) {
|
function changeTile(gameId: string, tileIdx: number, change: any) {
|
||||||
for (let k of Object.keys(change)) {
|
for (let k of Object.keys(change)) {
|
||||||
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||||
tile[k] = change[k]
|
tile[k] = change[k]
|
||||||
|
|
@ -188,16 +279,16 @@ function changeTile(gameId, tileIdx, change) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTile = (gameId, tileIdx) => {
|
const getTile = (gameId: string, tileIdx: number) => {
|
||||||
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileGroup = (gameId, tileIdx) => {
|
const getTileGroup = (gameId: string, tileIdx: number) => {
|
||||||
const tile = getTile(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return tile.group
|
return tile.group
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFinalTilePos = (gameId, tileIdx) => {
|
const getFinalTilePos = (gameId: string, tileIdx: number) => {
|
||||||
const info = GAMES[gameId].puzzle.info
|
const info = GAMES[gameId].puzzle.info
|
||||||
const boardPos = {
|
const boardPos = {
|
||||||
x: (info.table.width - info.width) / 2,
|
x: (info.table.width - info.width) / 2,
|
||||||
|
|
@ -207,13 +298,13 @@ const getFinalTilePos = (gameId, tileIdx) => {
|
||||||
return Geometry.pointAdd(boardPos, srcPos)
|
return Geometry.pointAdd(boardPos, srcPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTilePos = (gameId, tileIdx) => {
|
const getTilePos = (gameId: string, tileIdx: number) => {
|
||||||
const tile = getTile(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return tile.pos
|
return tile.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: instead, just make the table bigger and use that :)
|
// todo: instead, just make the table bigger and use that :)
|
||||||
const getBounds = (gameId) => {
|
const getBounds = (gameId: string) => {
|
||||||
const tw = getTableWidth(gameId)
|
const tw = getTableWidth(gameId)
|
||||||
const th = getTableHeight(gameId)
|
const th = getTableHeight(gameId)
|
||||||
|
|
||||||
|
|
@ -227,7 +318,7 @@ const getBounds = (gameId) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileBounds = (gameId, tileIdx) => {
|
const getTileBounds = (gameId: string, tileIdx: number) => {
|
||||||
const s = getTileSize(gameId)
|
const s = getTileSize(gameId)
|
||||||
const tile = getTile(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return {
|
return {
|
||||||
|
|
@ -238,55 +329,55 @@ const getTileBounds = (gameId, tileIdx) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileZIndex = (gameId, tileIdx) => {
|
const getTileZIndex = (gameId: string, tileIdx: number) => {
|
||||||
const tile = getTile(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return tile.z
|
return tile.z
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstOwnedTileIdx = (gameId, userId) => {
|
const getFirstOwnedTileIdx = (gameId: string, playerId: string) => {
|
||||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||||
const tile = Util.decodeTile(t)
|
const tile = Util.decodeTile(t)
|
||||||
if (tile.owner === userId) {
|
if (tile.owner === playerId) {
|
||||||
return tile.idx
|
return tile.idx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstOwnedTile = (gameId, userId) => {
|
const getFirstOwnedTile = (gameId: string, playerId: string) => {
|
||||||
const idx = getFirstOwnedTileIdx(gameId, userId)
|
const idx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileDrawOffset = (gameId) => {
|
const getTileDrawOffset = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.tileDrawOffset
|
return GAMES[gameId].puzzle.info.tileDrawOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileDrawSize = (gameId) => {
|
const getTileDrawSize = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.tileDrawSize
|
return GAMES[gameId].puzzle.info.tileDrawSize
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTileSize = (gameId) => {
|
const getTileSize = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.tileSize
|
return GAMES[gameId].puzzle.info.tileSize
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStartTs = (gameId) => {
|
const getStartTs = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.data.started
|
return GAMES[gameId].puzzle.data.started
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFinishTs = (gameId) => {
|
const getFinishTs = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.data.finished
|
return GAMES[gameId].puzzle.data.finished
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMaxGroup = (gameId) => {
|
const getMaxGroup = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.data.maxGroup
|
return GAMES[gameId].puzzle.data.maxGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMaxZIndex = (gameId) => {
|
const getMaxZIndex = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.data.maxZ
|
return GAMES[gameId].puzzle.data.maxZ
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
|
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>) => {
|
||||||
let maxZ = 0
|
let maxZ = 0
|
||||||
for (let tileIdx of tileIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
||||||
|
|
@ -297,7 +388,7 @@ const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
|
||||||
return maxZ
|
return maxZ
|
||||||
}
|
}
|
||||||
|
|
||||||
function srcPosByTileIdx(gameId, tileIdx) {
|
function srcPosByTileIdx(gameId: string, tileIdx: number) {
|
||||||
const info = GAMES[gameId].puzzle.info
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
const c = Util.coordByTileIdx(info, tileIdx)
|
const c = Util.coordByTileIdx(info, tileIdx)
|
||||||
|
|
@ -307,7 +398,7 @@ function srcPosByTileIdx(gameId, tileIdx) {
|
||||||
return { x: cx, y: cy }
|
return { x: cx, y: cy }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSurroundingTilesByIdx(gameId, tileIdx) {
|
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
||||||
const info = GAMES[gameId].puzzle.info
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
const c = Util.coordByTileIdx(info, tileIdx)
|
const c = Util.coordByTileIdx(info, tileIdx)
|
||||||
|
|
@ -324,19 +415,19 @@ function getSurroundingTilesByIdx(gameId, tileIdx) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTilesZIndex = (gameId, tileIdxs, zIndex) => {
|
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number) => {
|
||||||
for (let tilesIdx of tileIdxs) {
|
for (let tilesIdx of tileIdxs) {
|
||||||
changeTile(gameId, tilesIdx, { z: zIndex })
|
changeTile(gameId, tilesIdx, { z: zIndex })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveTileDiff = (gameId, tileIdx, diff) => {
|
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point) => {
|
||||||
const oldPos = getTilePos(gameId, tileIdx)
|
const oldPos = getTilePos(gameId, tileIdx)
|
||||||
const pos = Geometry.pointAdd(oldPos, diff)
|
const pos = Geometry.pointAdd(oldPos, diff)
|
||||||
changeTile(gameId, tileIdx, { pos })
|
changeTile(gameId, tileIdx, { pos })
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveTilesDiff = (gameId, tileIdxs, diff) => {
|
const moveTilesDiff = (gameId: string, tileIdxs: Array<number>, diff: Point) => {
|
||||||
const tileDrawSize = getTileDrawSize(gameId)
|
const tileDrawSize = getTileDrawSize(gameId)
|
||||||
const bounds = getBounds(gameId)
|
const bounds = getBounds(gameId)
|
||||||
const cappedDiff = diff
|
const cappedDiff = diff
|
||||||
|
|
@ -360,20 +451,24 @@ const moveTilesDiff = (gameId, tileIdxs, diff) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishTiles = (gameId, tileIdxs) => {
|
const finishTiles = (gameId: string, tileIdxs: Array<number>) => {
|
||||||
for (let tileIdx of tileIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
|
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTilesOwner = (gameId, tileIdxs, owner) => {
|
const setTilesOwner = (
|
||||||
|
gameId: string,
|
||||||
|
tileIdxs: Array<number>,
|
||||||
|
owner: string|number
|
||||||
|
) => {
|
||||||
for (let tileIdx of tileIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
changeTile(gameId, tileIdx, { owner })
|
changeTile(gameId, tileIdx, { owner })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all grouped tiles for a tile
|
// get all grouped tiles for a tile
|
||||||
function getGroupedTileIdxs(gameId, tileIdx) {
|
function getGroupedTileIdxs(gameId: string, tileIdx: number) {
|
||||||
const tiles = GAMES[gameId].puzzle.tiles
|
const tiles = GAMES[gameId].puzzle.tiles
|
||||||
const tile = Util.decodeTile(tiles[tileIdx])
|
const tile = Util.decodeTile(tiles[tileIdx])
|
||||||
|
|
||||||
|
|
@ -393,7 +488,7 @@ function getGroupedTileIdxs(gameId, tileIdx) {
|
||||||
|
|
||||||
// Returns the index of the puzzle tile with the highest z index
|
// Returns the index of the puzzle tile with the highest z index
|
||||||
// that is not finished yet and that matches the position
|
// that is not finished yet and that matches the position
|
||||||
const freeTileIdxByPos = (gameId, pos) => {
|
const freeTileIdxByPos = (gameId: string, pos: Point) => {
|
||||||
let info = GAMES[gameId].puzzle.info
|
let info = GAMES[gameId].puzzle.info
|
||||||
let tiles = GAMES[gameId].puzzle.tiles
|
let tiles = GAMES[gameId].puzzle.tiles
|
||||||
|
|
||||||
|
|
@ -421,75 +516,75 @@ const freeTileIdxByPos = (gameId, pos) => {
|
||||||
return tileIdx
|
return tileIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlayerBgColor = (gameId, playerId) => {
|
const getPlayerBgColor = (gameId: string, playerId: string) => {
|
||||||
const p = getPlayer(gameId, playerId)
|
const p = getPlayer(gameId, playerId)
|
||||||
return p ? p.bgcolor : null
|
return p ? p.bgcolor : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlayerColor = (gameId, playerId) => {
|
const getPlayerColor = (gameId: string, playerId: string) => {
|
||||||
const p = getPlayer(gameId, playerId)
|
const p = getPlayer(gameId, playerId)
|
||||||
return p ? p.color : null
|
return p ? p.color : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlayerName = (gameId, playerId) => {
|
const getPlayerName = (gameId: string, playerId: string) => {
|
||||||
const p = getPlayer(gameId, playerId)
|
const p = getPlayer(gameId, playerId)
|
||||||
return p ? p.name : null
|
return p ? p.name : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlayerPoints = (gameId, playerId) => {
|
const getPlayerPoints = (gameId: string, playerId: string) => {
|
||||||
const p = getPlayer(gameId, playerId)
|
const p = getPlayer(gameId, playerId)
|
||||||
return p ? p.points : null
|
return p ? p.points : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine if two tiles are grouped together
|
// determine if two tiles are grouped together
|
||||||
const areGrouped = (gameId, tileIdx1, tileIdx2) => {
|
const areGrouped = (gameId: string, tileIdx1: number, tileIdx2: number) => {
|
||||||
const g1 = getTileGroup(gameId, tileIdx1)
|
const g1 = getTileGroup(gameId, tileIdx1)
|
||||||
const g2 = getTileGroup(gameId, tileIdx2)
|
const g2 = getTileGroup(gameId, tileIdx2)
|
||||||
return g1 && g1 === g2
|
return g1 && g1 === g2
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTableWidth = (gameId) => {
|
const getTableWidth = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.table.width
|
return GAMES[gameId].puzzle.info.table.width
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTableHeight = (gameId) => {
|
const getTableHeight = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.table.height
|
return GAMES[gameId].puzzle.info.table.height
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPuzzle = (gameId) => {
|
const getPuzzle = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle
|
return GAMES[gameId].puzzle
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRng = (gameId) => {
|
const getRng = (gameId: string): Rng => {
|
||||||
return GAMES[gameId].rng.obj
|
return GAMES[gameId].rng.obj
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPuzzleWidth = (gameId) => {
|
const getPuzzleWidth = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.width
|
return GAMES[gameId].puzzle.info.width
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPuzzleHeight = (gameId) => {
|
const getPuzzleHeight = (gameId: string) => {
|
||||||
return GAMES[gameId].puzzle.info.height
|
return GAMES[gameId].puzzle.info.height
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(gameId, playerId, input, ts) {
|
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
|
||||||
const puzzle = GAMES[gameId].puzzle
|
const puzzle = GAMES[gameId].puzzle
|
||||||
const evtInfo = getEvtInfo(gameId, playerId)
|
const evtInfo = getEvtInfo(gameId, playerId)
|
||||||
|
|
||||||
const changes = []
|
const changes = [] as Array<Array<any>>
|
||||||
|
|
||||||
const _dataChange = () => {
|
const _dataChange = () => {
|
||||||
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
||||||
}
|
}
|
||||||
|
|
||||||
const _tileChange = (tileIdx) => {
|
const _tileChange = (tileIdx: number) => {
|
||||||
changes.push([
|
changes.push([
|
||||||
Protocol.CHANGE_TILE,
|
Protocol.CHANGE_TILE,
|
||||||
Util.encodeTile(getTile(gameId, tileIdx)),
|
Util.encodeTile(getTile(gameId, tileIdx)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const _tileChanges = (tileIdxs) => {
|
const _tileChanges = (tileIdxs: Array<number>) => {
|
||||||
for (const tileIdx of tileIdxs) {
|
for (const tileIdx of tileIdxs) {
|
||||||
_tileChange(tileIdx)
|
_tileChange(tileIdx)
|
||||||
}
|
}
|
||||||
|
|
@ -503,7 +598,7 @@ function handleInput(gameId, playerId, input, ts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// put both tiles (and their grouped tiles) in the same group
|
// put both tiles (and their grouped tiles) in the same group
|
||||||
const groupTiles = (gameId, tileIdx1, tileIdx2) => {
|
const groupTiles = (gameId: string, tileIdx1: number, tileIdx2: number) => {
|
||||||
const tiles = GAMES[gameId].puzzle.tiles
|
const tiles = GAMES[gameId].puzzle.tiles
|
||||||
const group1 = getTileGroup(gameId, tileIdx1)
|
const group1 = getTileGroup(gameId, tileIdx1)
|
||||||
const group2 = getTileGroup(gameId, tileIdx2)
|
const group2 = getTileGroup(gameId, tileIdx2)
|
||||||
|
|
@ -669,7 +764,12 @@ function handleInput(gameId, playerId, input, ts) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Snap to other tiles
|
// Snap to other tiles
|
||||||
const check = (gameId, tileIdx, otherTileIdx, off) => {
|
const check = (
|
||||||
|
gameId: string,
|
||||||
|
tileIdx: number,
|
||||||
|
otherTileIdx: number,
|
||||||
|
off: Array<number>
|
||||||
|
) => {
|
||||||
let info = GAMES[gameId].puzzle.info
|
let info = GAMES[gameId].puzzle.info
|
||||||
if (otherTileIdx < 0) {
|
if (otherTileIdx < 0) {
|
||||||
return false
|
return false
|
||||||
|
|
@ -1,25 +1,37 @@
|
||||||
function pointSub(a, b) {
|
interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rect {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointSub(a: Point, b: Point): Point {
|
||||||
return { x: a.x - b.x, y: a.y - b.y }
|
return { x: a.x - b.x, y: a.y - b.y }
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointAdd(a, b) {
|
function pointAdd(a: Point, b: Point): Point {
|
||||||
return { x: a.x + b.x, y: a.y + b.y }
|
return { x: a.x + b.x, y: a.y + b.y }
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointDistance(a, b) {
|
function pointDistance(a: Point, b: Point): number {
|
||||||
const diffX = a.x - b.x
|
const diffX = a.x - b.x
|
||||||
const diffY = a.y - b.y
|
const diffY = a.y - b.y
|
||||||
return Math.sqrt(diffX * diffX + diffY * diffY)
|
return Math.sqrt(diffX * diffX + diffY * diffY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointInBounds(pt, rect) {
|
function pointInBounds(pt: Point, rect: Rect): boolean {
|
||||||
return pt.x >= rect.x
|
return pt.x >= rect.x
|
||||||
&& pt.x <= rect.x + rect.w
|
&& pt.x <= rect.x + rect.w
|
||||||
&& pt.y >= rect.y
|
&& pt.y >= rect.y
|
||||||
&& pt.y <= rect.y + rect.h
|
&& pt.y <= rect.y + rect.h
|
||||||
}
|
}
|
||||||
|
|
||||||
function rectCenter(rect) {
|
function rectCenter(rect: Rect): Point {
|
||||||
return {
|
return {
|
||||||
x: rect.x + (rect.w / 2),
|
x: rect.x + (rect.w / 2),
|
||||||
y: rect.y + (rect.h / 2),
|
y: rect.y + (rect.h / 2),
|
||||||
|
|
@ -35,7 +47,7 @@ function rectCenter(rect) {
|
||||||
* @param number y
|
* @param number y
|
||||||
* @returns {x, y, w, h}
|
* @returns {x, y, w, h}
|
||||||
*/
|
*/
|
||||||
function rectMoved(rect, x, y) {
|
function rectMoved(rect: Rect, x: number, y: number): Rect {
|
||||||
return {
|
return {
|
||||||
x: rect.x + x,
|
x: rect.x + x,
|
||||||
y: rect.y + y,
|
y: rect.y + y,
|
||||||
|
|
@ -51,7 +63,7 @@ function rectMoved(rect, x, y) {
|
||||||
* @param {x, y, w, h} rectB
|
* @param {x, y, w, h} rectB
|
||||||
* @returns bool
|
* @returns bool
|
||||||
*/
|
*/
|
||||||
function rectsOverlap(rectA, rectB) {
|
function rectsOverlap(rectA: Rect, rectB: Rect): boolean {
|
||||||
return !(
|
return !(
|
||||||
rectB.x > (rectA.x + rectA.w)
|
rectB.x > (rectA.x + rectA.w)
|
||||||
|| rectA.x > (rectB.x + rectB.w)
|
|| rectA.x > (rectB.x + rectB.w)
|
||||||
|
|
@ -60,7 +72,7 @@ function rectsOverlap(rectA, rectB) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function rectCenterDistance(rectA, rectB) {
|
function rectCenterDistance(rectA: Rect, rectB: Rect): number {
|
||||||
return pointDistance(rectCenter(rectA), rectCenter(rectB))
|
return pointDistance(rectCenter(rectA), rectCenter(rectB))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,24 +1,32 @@
|
||||||
|
interface RngSerialized {
|
||||||
|
rand_high: number,
|
||||||
|
rand_low: number,
|
||||||
|
}
|
||||||
|
|
||||||
export class Rng {
|
export class Rng {
|
||||||
constructor(seed) {
|
rand_high: number
|
||||||
|
rand_low: number
|
||||||
|
|
||||||
|
constructor(seed: number) {
|
||||||
this.rand_high = seed || 0xDEADC0DE
|
this.rand_high = seed || 0xDEADC0DE
|
||||||
this.rand_low = seed ^ 0x49616E42
|
this.rand_low = seed ^ 0x49616E42
|
||||||
}
|
}
|
||||||
|
|
||||||
random (min, max) {
|
random (min: number, max: number) {
|
||||||
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
|
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
|
||||||
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
|
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
|
||||||
var n = (this.rand_high >>> 0) / 0xffffffff;
|
var n = (this.rand_high >>> 0) / 0xffffffff;
|
||||||
return (min + n * (max-min+1))|0;
|
return (min + n * (max-min+1))|0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static serialize (rng) {
|
static serialize (rng: Rng): RngSerialized {
|
||||||
return {
|
return {
|
||||||
rand_high: rng.rand_high,
|
rand_high: rng.rand_high,
|
||||||
rand_low: rng.rand_low
|
rand_low: rng.rand_low
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static unserialize (rngSerialized) {
|
static unserialize (rngSerialized: RngSerialized): Rng {
|
||||||
const rng = new Rng(0)
|
const rng = new Rng(0)
|
||||||
rng.rand_high = rngSerialized.rand_high
|
rng.rand_high = rngSerialized.rand_high
|
||||||
rng.rand_low = rngSerialized.rand_low
|
rng.rand_low = rngSerialized.rand_low
|
||||||
|
|
@ -17,7 +17,7 @@ export const timestamp = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const durationStr = (duration) => {
|
export const durationStr = (duration: number) => {
|
||||||
const d = Math.floor(duration / DAY)
|
const d = Math.floor(duration / DAY)
|
||||||
duration = duration % DAY
|
duration = duration % DAY
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const durationStr = (duration) => {
|
||||||
return `${d}d ${h}h ${m}m ${s}s`
|
return `${d}d ${h}h ${m}m ${s}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const timeDiffStr = (from, to) => durationStr(to - from)
|
export const timeDiffStr = (from: number, to: number) => durationStr(to - from)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
MS,
|
MS,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Rng } from './Rng.js'
|
import { Rng } from './Rng'
|
||||||
|
|
||||||
|
|
||||||
const pad = (x, pad) => {
|
const pad = (x: any, pad: string) => {
|
||||||
const str = `${x}`
|
const str = `${x}`
|
||||||
if (str.length >= pad.length) {
|
if (str.length >= pad.length) {
|
||||||
return str
|
return str
|
||||||
|
|
@ -9,8 +9,8 @@ const pad = (x, pad) => {
|
||||||
return pad.substr(0, pad.length - str.length) + str
|
return pad.substr(0, pad.length - str.length) + str
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logger = (...pre) => {
|
export const logger = (...pre: Array<any>) => {
|
||||||
const log = (m) => (...args) => {
|
const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
const hh = pad(d.getHours(), '00')
|
const hh = pad(d.getHours(), '00')
|
||||||
const mm = pad(d.getMinutes(), '00')
|
const mm = pad(d.getMinutes(), '00')
|
||||||
|
|
@ -29,34 +29,21 @@ export const uniqId = () => Date.now().toString(36) + Math.random().toString(36)
|
||||||
|
|
||||||
// get a random int between min and max (inclusive)
|
// get a random int between min and max (inclusive)
|
||||||
export const randomInt = (
|
export const randomInt = (
|
||||||
/** @type Rng */ rng,
|
rng: Rng,
|
||||||
min,
|
min: number,
|
||||||
max
|
max: number,
|
||||||
) => rng.random(min, max)
|
) => rng.random(min, max)
|
||||||
|
|
||||||
// get one random item from the given array
|
// get one random item from the given array
|
||||||
export const choice = (
|
export const choice = (
|
||||||
/** @type Rng */ rng,
|
rng: Rng,
|
||||||
array
|
array: Array<any>
|
||||||
) => array[randomInt(rng, 0, array.length - 1)]
|
) => array[randomInt(rng, 0, array.length - 1)]
|
||||||
|
|
||||||
export const throttle = (fn, delay) => {
|
|
||||||
let canCall = true
|
|
||||||
return (...args) => {
|
|
||||||
if (canCall) {
|
|
||||||
fn.apply(null, args)
|
|
||||||
canCall = false
|
|
||||||
setTimeout(() => {
|
|
||||||
canCall = true
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a shuffled (shallow) copy of the given array
|
// return a shuffled (shallow) copy of the given array
|
||||||
export const shuffle = (
|
export const shuffle = (
|
||||||
/** @type Rng */ rng,
|
rng: Rng,
|
||||||
array
|
array: Array<any>
|
||||||
) => {
|
) => {
|
||||||
const arr = array.slice()
|
const arr = array.slice()
|
||||||
for (let i = 0; i <= arr.length - 2; i++)
|
for (let i = 0; i <= arr.length - 2; i++)
|
||||||
|
|
@ -69,7 +56,7 @@ export const shuffle = (
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeShape(data) {
|
function encodeShape(data: any): number {
|
||||||
if (typeof data === 'number') {
|
if (typeof data === 'number') {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +73,7 @@ function encodeShape(data) {
|
||||||
| ((data.left + 1) << 6)
|
| ((data.left + 1) << 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeShape(data) {
|
function decodeShape(data: any) {
|
||||||
if (typeof data !== 'number') {
|
if (typeof data !== 'number') {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -98,14 +85,14 @@ function decodeShape(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeTile(data) {
|
function encodeTile(data: any): Array<any> {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
|
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeTile(data) {
|
function decodeTile(data: any) {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +108,7 @@ function decodeTile(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodePlayer(data) {
|
function encodePlayer(data: any): Array<any> {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +125,7 @@ function encodePlayer(data) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodePlayer(data) {
|
function decodePlayer(data: any) {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +142,7 @@ function decodePlayer(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeGame(data) {
|
function encodeGame(data: any): Array<any> {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +157,7 @@ function encodeGame(data) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeGame(data) {
|
function decodeGame(data: any) {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +174,7 @@ function decodeGame(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function coordByTileIdx(info, tileIdx) {
|
function coordByTileIdx(info: any, tileIdx: number) {
|
||||||
const wTiles = info.width / info.tileSize
|
const wTiles = info.width / info.tileSize
|
||||||
return {
|
return {
|
||||||
x: tileIdx % wTiles,
|
x: tileIdx % wTiles,
|
||||||
|
|
@ -195,7 +182,7 @@ function coordByTileIdx(info, tileIdx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = (str) => {
|
const hash = (str: string): number => {
|
||||||
let hash = 0
|
let hash = 0
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
|
@ -211,7 +198,6 @@ export default {
|
||||||
uniqId,
|
uniqId,
|
||||||
randomInt,
|
randomInt,
|
||||||
choice,
|
choice,
|
||||||
throttle,
|
|
||||||
shuffle,
|
shuffle,
|
||||||
|
|
||||||
encodeShape,
|
encodeShape,
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
export default {
|
<template>
|
||||||
name: 'app',
|
|
||||||
template: `
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<ul class="nav" v-if="showNav">
|
<ul class="nav" v-if="showNav">
|
||||||
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
|
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
|
||||||
|
|
@ -8,11 +6,18 @@ export default {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'app',
|
||||||
computed: {
|
computed: {
|
||||||
showNav () {
|
showNav () {
|
||||||
// TODO: add info wether to show nav to route props
|
// TODO: add info wether to show nav to route props
|
||||||
return !['game', 'replay'].includes(this.$route.name)
|
return !['game', 'replay'].includes(String(this.$route.name))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,31 +1,39 @@
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const MIN_ZOOM = .1
|
const MIN_ZOOM = .1
|
||||||
const MAX_ZOOM = 6
|
const MAX_ZOOM = 6
|
||||||
const ZOOM_STEP = .05
|
const ZOOM_STEP = .05
|
||||||
|
|
||||||
|
type ZOOM_DIR = 'in'|'out'
|
||||||
|
interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
interface Dim {
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
}
|
||||||
|
|
||||||
export default function Camera () {
|
export default function Camera () {
|
||||||
let x = 0
|
let x = 0
|
||||||
let y = 0
|
let y = 0
|
||||||
let curZoom = 1
|
let curZoom = 1
|
||||||
|
|
||||||
const move = (byX, byY) => {
|
const move = (byX: number, byY: number) => {
|
||||||
x += byX / curZoom
|
x += byX / curZoom
|
||||||
y += byY / curZoom
|
y += byY / curZoom
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateNewZoom = (inout) => {
|
const calculateNewZoom = (inout: ZOOM_DIR): number => {
|
||||||
const factor = inout === 'in' ? 1 : -1
|
const factor = inout === 'in' ? 1 : -1
|
||||||
const newzoom = curZoom + ZOOM_STEP * curZoom * factor
|
const newzoom = curZoom + ZOOM_STEP * curZoom * factor
|
||||||
const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM)
|
const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM)
|
||||||
return capped
|
return capped
|
||||||
}
|
}
|
||||||
|
|
||||||
const canZoom = (inout) => {
|
const canZoom = (inout: ZOOM_DIR): boolean => {
|
||||||
return curZoom != calculateNewZoom(inout)
|
return curZoom != calculateNewZoom(inout)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setZoom = (newzoom, viewportCoordCenter) => {
|
const setZoom = (newzoom: number, viewportCoordCenter: Point): boolean => {
|
||||||
if (curZoom == newzoom) {
|
if (curZoom == newzoom) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +51,7 @@ export default function Camera () {
|
||||||
* Zooms towards/away from the provided coordinate, if possible.
|
* Zooms towards/away from the provided coordinate, if possible.
|
||||||
* If at max or min zoom respectively, no zooming is performed.
|
* If at max or min zoom respectively, no zooming is performed.
|
||||||
*/
|
*/
|
||||||
const zoom = (inout, viewportCoordCenter) => {
|
const zoom = (inout: ZOOM_DIR, viewportCoordCenter: Point): boolean => {
|
||||||
return setZoom(calculateNewZoom(inout), viewportCoordCenter)
|
return setZoom(calculateNewZoom(inout), viewportCoordCenter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +60,7 @@ export default function Camera () {
|
||||||
* coordinate in the world, rounded
|
* coordinate in the world, rounded
|
||||||
* @param {x, y} viewportCoord
|
* @param {x, y} viewportCoord
|
||||||
*/
|
*/
|
||||||
const viewportToWorld = (viewportCoord) => {
|
const viewportToWorld = (viewportCoord: Point): Point => {
|
||||||
const { x, y } = viewportToWorldRaw(viewportCoord)
|
const { x, y } = viewportToWorldRaw(viewportCoord)
|
||||||
return { x: Math.round(x), y: Math.round(y) }
|
return { x: Math.round(x), y: Math.round(y) }
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +70,7 @@ export default function Camera () {
|
||||||
* coordinate in the world, not rounded
|
* coordinate in the world, not rounded
|
||||||
* @param {x, y} viewportCoord
|
* @param {x, y} viewportCoord
|
||||||
*/
|
*/
|
||||||
const viewportToWorldRaw = (viewportCoord) => {
|
const viewportToWorldRaw = (viewportCoord: Point): Point => {
|
||||||
return {
|
return {
|
||||||
x: (viewportCoord.x / curZoom) - x,
|
x: (viewportCoord.x / curZoom) - x,
|
||||||
y: (viewportCoord.y / curZoom) - y,
|
y: (viewportCoord.y / curZoom) - y,
|
||||||
|
|
@ -74,7 +82,7 @@ export default function Camera () {
|
||||||
* coordinate in the viewport, rounded
|
* coordinate in the viewport, rounded
|
||||||
* @param {x, y} worldCoord
|
* @param {x, y} worldCoord
|
||||||
*/
|
*/
|
||||||
const worldToViewport = (worldCoord) => {
|
const worldToViewport = (worldCoord: Point): Point => {
|
||||||
const { x, y } = worldToViewportRaw(worldCoord)
|
const { x, y } = worldToViewportRaw(worldCoord)
|
||||||
return { x: Math.round(x), y: Math.round(y) }
|
return { x: Math.round(x), y: Math.round(y) }
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +92,7 @@ export default function Camera () {
|
||||||
* coordinate in the viewport, not rounded
|
* coordinate in the viewport, not rounded
|
||||||
* @param {x, y} worldCoord
|
* @param {x, y} worldCoord
|
||||||
*/
|
*/
|
||||||
const worldToViewportRaw = (worldCoord) => {
|
const worldToViewportRaw = (worldCoord: Point): Point => {
|
||||||
return {
|
return {
|
||||||
x: (worldCoord.x + x) * curZoom,
|
x: (worldCoord.x + x) * curZoom,
|
||||||
y: (worldCoord.y + y) * curZoom,
|
y: (worldCoord.y + y) * curZoom,
|
||||||
|
|
@ -96,7 +104,7 @@ export default function Camera () {
|
||||||
* one in the viewport, rounded
|
* one in the viewport, rounded
|
||||||
* @param {w, h} worldDim
|
* @param {w, h} worldDim
|
||||||
*/
|
*/
|
||||||
const worldDimToViewport = (worldDim) => {
|
const worldDimToViewport = (worldDim: Dim): Dim => {
|
||||||
const { w, h } = worldDimToViewportRaw(worldDim)
|
const { w, h } = worldDimToViewportRaw(worldDim)
|
||||||
return { w: Math.round(w), h: Math.round(h) }
|
return { w: Math.round(w), h: Math.round(h) }
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +115,7 @@ export default function Camera () {
|
||||||
* one in the viewport, not rounded
|
* one in the viewport, not rounded
|
||||||
* @param {w, h} worldDim
|
* @param {w, h} worldDim
|
||||||
*/
|
*/
|
||||||
const worldDimToViewportRaw = (worldDim) => {
|
const worldDimToViewportRaw = (worldDim: Dim): Dim => {
|
||||||
return {
|
return {
|
||||||
w: worldDim.w * curZoom,
|
w: worldDim.w * curZoom,
|
||||||
h: worldDim.h * curZoom,
|
h: worldDim.h * curZoom,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { logger } from '../common/Util.js'
|
import { logger } from '../common/Util'
|
||||||
import Protocol from './../common/Protocol.js'
|
import Protocol from './../common/Protocol'
|
||||||
|
|
||||||
const log = logger('Communication.js')
|
const log = logger('Communication.js')
|
||||||
|
|
||||||
|
|
@ -14,27 +14,26 @@ const CONN_STATE_CONNECTED = 2 // connected
|
||||||
const CONN_STATE_CONNECTING = 3 // connecting
|
const CONN_STATE_CONNECTING = 3 // connecting
|
||||||
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
|
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
|
||||||
|
|
||||||
/** @type WebSocket */
|
let ws: WebSocket
|
||||||
let ws
|
let changesCallback = (msg: Array<any>) => {}
|
||||||
let changesCallback = () => {}
|
let connectionStateChangeCallback = (state: number) => {}
|
||||||
let connectionStateChangeCallback = () => {}
|
|
||||||
|
|
||||||
// TODO: change these to something like on(EVT, cb)
|
// TODO: change these to something like on(EVT, cb)
|
||||||
function onServerChange(callback) {
|
function onServerChange(callback: (msg: Array<any>) => void) {
|
||||||
changesCallback = callback
|
changesCallback = callback
|
||||||
}
|
}
|
||||||
function onConnectionStateChange(callback) {
|
function onConnectionStateChange(callback: (state: number) => void) {
|
||||||
connectionStateChangeCallback = callback
|
connectionStateChangeCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectionState = CONN_STATE_NOT_CONNECTED
|
let connectionState = CONN_STATE_NOT_CONNECTED
|
||||||
const setConnectionState = (v) => {
|
const setConnectionState = (state: number) => {
|
||||||
if (connectionState !== v) {
|
if (connectionState !== state) {
|
||||||
connectionState = v
|
connectionState = state
|
||||||
connectionStateChangeCallback(v)
|
connectionStateChangeCallback(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function send(message) {
|
function send(message: Array<any>): void {
|
||||||
if (connectionState === CONN_STATE_CONNECTED) {
|
if (connectionState === CONN_STATE_CONNECTED) {
|
||||||
try {
|
try {
|
||||||
ws.send(JSON.stringify(message))
|
ws.send(JSON.stringify(message))
|
||||||
|
|
@ -45,9 +44,14 @@ function send(message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let clientSeq
|
let clientSeq: number
|
||||||
let events
|
let events: Record<number, any>
|
||||||
function connect(address, gameId, clientId) {
|
|
||||||
|
function connect(
|
||||||
|
address: string,
|
||||||
|
gameId: string,
|
||||||
|
clientId: string
|
||||||
|
): Promise<any> {
|
||||||
clientSeq = 0
|
clientSeq = 0
|
||||||
events = {}
|
events = {}
|
||||||
setConnectionState(CONN_STATE_CONNECTING)
|
setConnectionState(CONN_STATE_CONNECTING)
|
||||||
|
|
@ -55,7 +59,6 @@ function connect(address, gameId, clientId) {
|
||||||
ws = new WebSocket(address, clientId + '|' + gameId)
|
ws = new WebSocket(address, clientId + '|' + gameId)
|
||||||
ws.onopen = (e) => {
|
ws.onopen = (e) => {
|
||||||
setConnectionState(CONN_STATE_CONNECTED)
|
setConnectionState(CONN_STATE_CONNECTED)
|
||||||
connectionStateChangeCallback()
|
|
||||||
send([Protocol.EV_CLIENT_INIT])
|
send([Protocol.EV_CLIENT_INIT])
|
||||||
}
|
}
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
|
|
@ -94,7 +97,11 @@ function connect(address, gameId, clientId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOOD: change replay stuff
|
// TOOD: change replay stuff
|
||||||
function connectReplay(address, gameId, clientId) {
|
function connectReplay(
|
||||||
|
address: string,
|
||||||
|
gameId: string,
|
||||||
|
clientId: string
|
||||||
|
): Promise<{ game: any, log: Array<any> }> {
|
||||||
clientSeq = 0
|
clientSeq = 0
|
||||||
events = {}
|
events = {}
|
||||||
setConnectionState(CONN_STATE_CONNECTING)
|
setConnectionState(CONN_STATE_CONNECTING)
|
||||||
|
|
@ -110,7 +117,8 @@ function connectReplay(address, gameId, clientId) {
|
||||||
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
|
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
|
||||||
const game = msg[1]
|
const game = msg[1]
|
||||||
const log = msg[2]
|
const log = msg[2]
|
||||||
resolve({game, log})
|
const replay: { game: any, log: Array<any> } = { game, log }
|
||||||
|
resolve(replay)
|
||||||
} else {
|
} else {
|
||||||
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
|
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +139,7 @@ function connectReplay(address, gameId, clientId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect() {
|
function disconnect(): void {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close(CODE_CUSTOM_DISCONNECT)
|
ws.close(CODE_CUSTOM_DISCONNECT)
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +147,7 @@ function disconnect() {
|
||||||
events = {}
|
events = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendClientEvent(evt) {
|
function sendClientEvent(evt: any): void {
|
||||||
// when sending event, increase number of sent events
|
// when sending event, increase number of sent events
|
||||||
// and add the event locally
|
// and add the event locally
|
||||||
clientSeq++;
|
clientSeq++;
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { logger } from '../common/Util.js'
|
import { logger } from '../common/Util'
|
||||||
|
|
||||||
const log = logger('Debug.js')
|
const log = logger('Debug.js')
|
||||||
|
|
||||||
let _pt = 0
|
let _pt = 0
|
||||||
let _mindiff = 0
|
let _mindiff = 0
|
||||||
|
|
||||||
const checkpoint_start = (mindiff) => {
|
const checkpoint_start = (mindiff: number) => {
|
||||||
_pt = performance.now()
|
_pt = performance.now()
|
||||||
_mindiff = mindiff
|
_mindiff = mindiff
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkpoint = (label) => {
|
const checkpoint = (label: string) => {
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
const diff = now - _pt
|
const diff = now - _pt
|
||||||
if (diff > _mindiff) {
|
if (diff > _mindiff) {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { Rng } from '../common/Rng.js'
|
import { Rng } from '../common/Rng'
|
||||||
import Util from '../common/Util.js'
|
import Util from '../common/Util'
|
||||||
|
|
||||||
let minVx = -10
|
let minVx = -10
|
||||||
let deltaVx = 20
|
let deltaVx = 20
|
||||||
|
|
@ -20,7 +20,7 @@ const explosionDividerFactor = 10
|
||||||
const nBombs = 1
|
const nBombs = 1
|
||||||
const percentChanceNewBomb = 5
|
const percentChanceNewBomb = 5
|
||||||
|
|
||||||
function color(/** @type Rng */ rng) {
|
function color(rng: Rng) {
|
||||||
const r = Util.randomInt(rng, 0, 255)
|
const r = Util.randomInt(rng, 0, 255)
|
||||||
const g = Util.randomInt(rng, 0, 255)
|
const g = Util.randomInt(rng, 0, 255)
|
||||||
const b = Util.randomInt(rng, 0, 255)
|
const b = Util.randomInt(rng, 0, 255)
|
||||||
|
|
@ -29,7 +29,19 @@ function color(/** @type Rng */ rng) {
|
||||||
|
|
||||||
// A Bomb. Or firework.
|
// A Bomb. Or firework.
|
||||||
class Bomb {
|
class Bomb {
|
||||||
constructor(/** @type Rng */ rng) {
|
radius: number
|
||||||
|
previousRadius: number
|
||||||
|
explodingDuration: number
|
||||||
|
hasExploded: boolean
|
||||||
|
alive: boolean
|
||||||
|
color: string
|
||||||
|
px: number
|
||||||
|
py: number
|
||||||
|
vx: number
|
||||||
|
vy: number
|
||||||
|
duration: number
|
||||||
|
|
||||||
|
constructor(rng: Rng) {
|
||||||
this.radius = bombRadius
|
this.radius = bombRadius
|
||||||
this.previousRadius = bombRadius
|
this.previousRadius = bombRadius
|
||||||
this.explodingDuration = explodingDuration
|
this.explodingDuration = explodingDuration
|
||||||
|
|
@ -46,7 +58,7 @@ class Bomb {
|
||||||
this.duration = 0
|
this.duration = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
update(particlesVector) {
|
update(particlesVector?: Array<Particle>) {
|
||||||
if (this.hasExploded) {
|
if (this.hasExploded) {
|
||||||
const deltaRadius = explosionRadius - this.radius
|
const deltaRadius = explosionRadius - this.radius
|
||||||
this.previousRadius = this.radius
|
this.previousRadius = this.radius
|
||||||
|
|
@ -60,7 +72,9 @@ class Bomb {
|
||||||
this.vx += 0
|
this.vx += 0
|
||||||
this.vy += gravity
|
this.vy += gravity
|
||||||
if (this.vy >= 0) { // invertion point
|
if (this.vy >= 0) { // invertion point
|
||||||
this.explode(particlesVector)
|
if (particlesVector) {
|
||||||
|
this.explode(particlesVector)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.px += this.vx
|
this.px += this.vx
|
||||||
|
|
@ -68,7 +82,7 @@ class Bomb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx: CanvasRenderingContext2D) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false)
|
ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false)
|
||||||
if (!this.hasExploded) {
|
if (!this.hasExploded) {
|
||||||
|
|
@ -78,7 +92,7 @@ class Bomb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
explode(particlesVector) {
|
explode(particlesVector: Array<Particle>) {
|
||||||
this.hasExploded = true
|
this.hasExploded = true
|
||||||
const e = 3 + Math.floor(Math.random() * 3)
|
const e = 3 + Math.floor(Math.random() * 3)
|
||||||
for (let j = 0; j < e; j++) {
|
for (let j = 0; j < e; j++) {
|
||||||
|
|
@ -94,7 +108,15 @@ class Bomb {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Particle {
|
class Particle {
|
||||||
constructor(parent, angle, speed) {
|
px: any
|
||||||
|
py: any
|
||||||
|
vx: number
|
||||||
|
vy: number
|
||||||
|
color: any
|
||||||
|
duration: number
|
||||||
|
alive: boolean
|
||||||
|
radius: number
|
||||||
|
constructor(parent: Bomb, angle: number, speed: number) {
|
||||||
this.px = parent.px
|
this.px = parent.px
|
||||||
this.py = parent.py
|
this.py = parent.py
|
||||||
this.vx = Math.cos(angle) * speed
|
this.vx = Math.cos(angle) * speed
|
||||||
|
|
@ -102,6 +124,7 @@ class Particle {
|
||||||
this.color = parent.color
|
this.color = parent.color
|
||||||
this.duration = 40 + Math.floor(Math.random() * 20)
|
this.duration = 40 + Math.floor(Math.random() * 20)
|
||||||
this.alive = true
|
this.alive = true
|
||||||
|
this.radius = 0
|
||||||
}
|
}
|
||||||
update() {
|
update() {
|
||||||
this.vx += 0
|
this.vx += 0
|
||||||
|
|
@ -117,7 +140,7 @@ class Particle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx: CanvasRenderingContext2D) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false)
|
ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false)
|
||||||
ctx.fillStyle = this.color
|
ctx.fillStyle = this.color
|
||||||
|
|
@ -127,12 +150,22 @@ class Particle {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Controller {
|
class Controller {
|
||||||
constructor(canvas, /** @type Rng */ rng) {
|
canvas: HTMLCanvasElement
|
||||||
|
rng: Rng
|
||||||
|
ctx: CanvasRenderingContext2D
|
||||||
|
readyBombs: Array<Bomb>
|
||||||
|
explodedBombs: Array<Bomb>
|
||||||
|
particles: Array<Particle>
|
||||||
|
constructor(canvas: HTMLCanvasElement, rng: Rng) {
|
||||||
this.canvas = canvas
|
this.canvas = canvas
|
||||||
this.rng = rng
|
this.rng = rng
|
||||||
this.ctx = this.canvas.getContext('2d')
|
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
|
||||||
this.resize()
|
this.resize()
|
||||||
|
|
||||||
|
this.readyBombs = []
|
||||||
|
this.explodedBombs = []
|
||||||
|
this.particles = []
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.resize()
|
this.resize()
|
||||||
})
|
})
|
||||||
|
|
@ -177,6 +210,9 @@ class Controller {
|
||||||
const aliveBombs = []
|
const aliveBombs = []
|
||||||
while (this.explodedBombs.length > 0) {
|
while (this.explodedBombs.length > 0) {
|
||||||
const bomb = this.explodedBombs.shift()
|
const bomb = this.explodedBombs.shift()
|
||||||
|
if (!bomb) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
bomb.update()
|
bomb.update()
|
||||||
if (bomb.alive) {
|
if (bomb.alive) {
|
||||||
aliveBombs.push(bomb)
|
aliveBombs.push(bomb)
|
||||||
|
|
@ -187,6 +223,9 @@ class Controller {
|
||||||
const notExplodedBombs = []
|
const notExplodedBombs = []
|
||||||
while (this.readyBombs.length > 0) {
|
while (this.readyBombs.length > 0) {
|
||||||
const bomb = this.readyBombs.shift()
|
const bomb = this.readyBombs.shift()
|
||||||
|
if (!bomb) {
|
||||||
|
break
|
||||||
|
}
|
||||||
bomb.update(this.particles)
|
bomb.update(this.particles)
|
||||||
if (bomb.hasExploded) {
|
if (bomb.hasExploded) {
|
||||||
this.explodedBombs.push(bomb)
|
this.explodedBombs.push(bomb)
|
||||||
|
|
@ -200,6 +239,9 @@ class Controller {
|
||||||
const aliveParticles = []
|
const aliveParticles = []
|
||||||
while (this.particles.length > 0) {
|
while (this.particles.length > 0) {
|
||||||
const particle = this.particles.shift()
|
const particle = this.particles.shift()
|
||||||
|
if (!particle) {
|
||||||
|
break
|
||||||
|
}
|
||||||
particle.update()
|
particle.update()
|
||||||
if (particle.alive) {
|
if (particle.alive) {
|
||||||
aliveParticles.push(particle)
|
aliveParticles.push(particle)
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
function createCanvas(width = 0, height = 0) {
|
function createCanvas(width:number = 0, height:number = 0): HTMLCanvasElement {
|
||||||
const c = document.createElement('canvas')
|
const c = document.createElement('canvas')
|
||||||
c.width = width
|
c.width = width
|
||||||
c.height = height
|
c.height = height
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadImageToBitmap(imagePath) {
|
async function loadImageToBitmap(imagePath: string): Promise<ImageBitmap> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
|
@ -17,24 +17,16 @@ async function loadImageToBitmap(imagePath) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resizeBitmap (bitmap, width, height) {
|
async function resizeBitmap (bitmap: ImageBitmap, width: number, height: number): Promise<ImageBitmap> {
|
||||||
const c = createCanvas(width, height)
|
const c = createCanvas(width, height)
|
||||||
const ctx = c.getContext('2d')
|
const ctx = c.getContext('2d') as CanvasRenderingContext2D
|
||||||
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height)
|
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height)
|
||||||
return await createImageBitmap(c)
|
return await createImageBitmap(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBitmap(width, height, color) {
|
async function colorize(image: ImageBitmap, mask: ImageBitmap, color: string): Promise<ImageBitmap> {
|
||||||
const c = createCanvas(width, height)
|
|
||||||
const ctx = c.getContext('2d')
|
|
||||||
ctx.fillStyle = color
|
|
||||||
ctx.fillRect(0, 0, width, height)
|
|
||||||
return await createImageBitmap(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function colorize(image, mask, color) {
|
|
||||||
const c = createCanvas(image.width, image.height)
|
const c = createCanvas(image.width, image.height)
|
||||||
const ctx = c.getContext('2d')
|
const ctx = c.getContext('2d') as CanvasRenderingContext2D
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.drawImage(mask, 0, 0)
|
ctx.drawImage(mask, 0, 0)
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color
|
||||||
|
|
@ -49,7 +41,6 @@ async function colorize(image, mask, color) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createBitmap,
|
|
||||||
createCanvas,
|
createCanvas,
|
||||||
loadImageToBitmap,
|
loadImageToBitmap,
|
||||||
resizeBitmap,
|
resizeBitmap,
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import Geometry from '../common/Geometry.js'
|
import Geometry from '../common/Geometry'
|
||||||
import Graphics from './Graphics.js'
|
import Graphics from './Graphics'
|
||||||
import Util, { logger } from './../common/Util.js'
|
import Util, { logger } from './../common/Util'
|
||||||
|
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
|
||||||
|
|
||||||
const log = logger('PuzzleGraphics.js')
|
const log = logger('PuzzleGraphics.js')
|
||||||
|
|
||||||
async function createPuzzleTileBitmaps(img, tiles, info) {
|
async function createPuzzleTileBitmaps(img: ImageBitmap, tiles: Array<any>, info: PuzzleInfo) {
|
||||||
log.log('start createPuzzleTileBitmaps')
|
log.log('start createPuzzleTileBitmaps')
|
||||||
var tileSize = info.tileSize
|
var tileSize = info.tileSize
|
||||||
var tileMarginWidth = info.tileMarginWidth
|
var tileMarginWidth = info.tileMarginWidth
|
||||||
|
|
@ -24,8 +25,8 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
||||||
|
|
||||||
const bitmaps = new Array(tiles.length)
|
const bitmaps = new Array(tiles.length)
|
||||||
|
|
||||||
const paths = {}
|
const paths: Record<string, Path2D> = {}
|
||||||
function pathForShape(shape) {
|
function pathForShape(shape: PieceShape) {
|
||||||
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
|
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
|
||||||
if (paths[key]) {
|
if (paths[key]) {
|
||||||
return paths[key]
|
return paths[key]
|
||||||
|
|
@ -83,10 +84,10 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
||||||
const ctx = c.getContext('2d')
|
const ctx = c.getContext('2d') as CanvasRenderingContext2D
|
||||||
|
|
||||||
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
||||||
const ctx2 = c2.getContext('2d')
|
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
|
||||||
|
|
||||||
for (let t of tiles) {
|
for (let t of tiles) {
|
||||||
const tile = Util.decodeTile(t)
|
const tile = Util.decodeTile(t)
|
||||||
|
|
@ -197,7 +198,7 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
||||||
return bitmaps
|
return bitmaps
|
||||||
}
|
}
|
||||||
|
|
||||||
function srcRectByIdx(puzzleInfo, idx) {
|
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number) {
|
||||||
const c = Util.coordByTileIdx(puzzleInfo, idx)
|
const c = Util.coordByTileIdx(puzzleInfo, idx)
|
||||||
return {
|
return {
|
||||||
x: c.x * puzzleInfo.tileSize,
|
x: c.x * puzzleInfo.tileSize,
|
||||||
|
|
@ -207,7 +208,7 @@ function srcRectByIdx(puzzleInfo, idx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPuzzleBitmaps(puzzle) {
|
async function loadPuzzleBitmaps(puzzle: Puzzle) {
|
||||||
// load bitmap, to determine the original size of the image
|
// load bitmap, to determine the original size of the image
|
||||||
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
|
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
|
||||||
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
|
||||||
import Communication from './../Communication.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'connection-overlay',
|
|
||||||
template: `
|
|
||||||
<div class="overlay connection-lost" v-if="show">
|
<div class="overlay connection-lost" v-if="show">
|
||||||
<div class="overlay-content" v-if="lostConnection">
|
<div class="overlay-content" v-if="lostConnection">
|
||||||
<div>⁉️ LOST CONNECTION ⁉️</div>
|
<div>⁉️ LOST CONNECTION ⁉️</div>
|
||||||
|
|
@ -13,7 +7,15 @@ export default {
|
||||||
<div class="overlay-content" v-if="connecting">
|
<div class="overlay-content" v-if="connecting">
|
||||||
<div>Connecting...</div>
|
<div>Connecting...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
import Communication from './../Communication'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'connection-overlay',
|
||||||
emits: {
|
emits: {
|
||||||
reconnect: null,
|
reconnect: null,
|
||||||
},
|
},
|
||||||
|
|
@ -21,14 +23,15 @@ export default {
|
||||||
connectionState: Number,
|
connectionState: Number,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lostConnection () {
|
lostConnection (): boolean {
|
||||||
return this.connectionState === Communication.CONN_STATE_DISCONNECTED
|
return this.connectionState === Communication.CONN_STATE_DISCONNECTED
|
||||||
},
|
},
|
||||||
connecting () {
|
connecting (): boolean {
|
||||||
return this.connectionState === Communication.CONN_STATE_CONNECTING
|
return this.connectionState === Communication.CONN_STATE_CONNECTING
|
||||||
},
|
},
|
||||||
show () {
|
show (): boolean {
|
||||||
return this.lostConnection || this.connecting
|
return !!(this.lostConnection || this.connecting)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
|
||||||
import Time from './../../common/Time.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'game-teaser',
|
|
||||||
props: {
|
|
||||||
game: Object,
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div class="game-teaser" :style="style">
|
<div class="game-teaser" :style="style">
|
||||||
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
|
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
|
||||||
<span class="game-info-text">
|
<span class="game-info-text">
|
||||||
|
|
@ -19,9 +10,22 @@ export default {
|
||||||
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
|
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
|
||||||
↪️ Watch replay
|
↪️ Watch replay
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import Time from './../../common/Time'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'game-teaser',
|
||||||
|
props: {
|
||||||
|
game: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style() {
|
style (): object {
|
||||||
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
|
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
|
||||||
return {
|
return {
|
||||||
'background-image': `url("${url}")`,
|
'background-image': `url("${url}")`,
|
||||||
|
|
@ -29,7 +33,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
time(start, end) {
|
time(start: number, end: number) {
|
||||||
const icon = end ? '🏁' : '⏳'
|
const icon = end ? '🏁' : '⏳'
|
||||||
const from = start;
|
const from = start;
|
||||||
const to = end || Time.timestamp()
|
const to = end || Time.timestamp()
|
||||||
|
|
@ -37,4 +41,5 @@ export default {
|
||||||
return `${icon} ${timeDiffStr}`
|
return `${icon} ${timeDiffStr}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||||
// ingame component
|
|
||||||
// shows the help (key bindings)
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'help-overlay',
|
|
||||||
template: `<div class="overlay transparent" @click="$emit('bgclick')">
|
|
||||||
<table class="overlay-content help" @click.stop="">
|
<table class="overlay-content help" @click.stop="">
|
||||||
<tr><td>⬆️ Move up:</td><td><div><kbd>W</kbd>/<kbd>↑</kbd>/🖱️</div></td></tr>
|
<tr><td>⬆️ Move up:</td><td><div><kbd>W</kbd>/<kbd>↑</kbd>/🖱️</div></td></tr>
|
||||||
<tr><td>⬇️ Move down:</td><td><div><kbd>S</kbd>/<kbd>↓</kbd>/🖱️</div></td></tr>
|
<tr><td>⬇️ Move down:</td><td><div><kbd>S</kbd>/<kbd>↓</kbd>/🖱️</div></td></tr>
|
||||||
|
|
@ -19,8 +13,15 @@ export default {
|
||||||
<tr><td>🧩✔️ Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
|
<tr><td>🧩✔️ Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
|
||||||
<tr><td>🧩❓ Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
|
<tr><td>🧩❓ Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'help-overlay',
|
||||||
emits: {
|
emits: {
|
||||||
bgclick: null,
|
bgclick: null,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
29
src/frontend/components/ImageTeaser.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="imageteaser" :style="style" @click="onClick"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'image-teaser',
|
||||||
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style (): object {
|
||||||
|
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
||||||
|
return {
|
||||||
|
'backgroundImage': `url("${url}")`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.$emit('click')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,16 +1,4 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
|
||||||
import GameCommon from './../../common/GameCommon.js'
|
|
||||||
import Upload from './../components/Upload.vue.js'
|
|
||||||
import ImageTeaser from './../components/ImageTeaser.vue.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'new-game-dialog',
|
|
||||||
components: {
|
|
||||||
Upload,
|
|
||||||
ImageTeaser,
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div>
|
<div>
|
||||||
<h1>New game</h1>
|
<h1>New game</h1>
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -49,9 +37,23 @@ export default {
|
||||||
|
|
||||||
<h1>Image lib</h1>
|
<h1>Image lib</h1>
|
||||||
<div>
|
<div>
|
||||||
<image-teaser v-for="i in images" :image="i" @click="image = i" />
|
<image-teaser v-for="(i,idx) in images" :image="i" @click="image = i" :key="idx" />
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
import GameCommon from './../../common/GameCommon'
|
||||||
|
import Upload from './../components/Upload.vue'
|
||||||
|
import ImageTeaser from './../components/ImageTeaser.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'new-game-dialog',
|
||||||
|
components: {
|
||||||
|
Upload,
|
||||||
|
ImageTeaser,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
images: Array,
|
images: Array,
|
||||||
},
|
},
|
||||||
|
|
@ -66,8 +68,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mediaImgUploaded(j) {
|
// TODO: ts type UploadedImage
|
||||||
this.image = j.image
|
mediaImgUploaded(data: any) {
|
||||||
|
this.image = data.image
|
||||||
},
|
},
|
||||||
canStartNewGame () {
|
canStartNewGame () {
|
||||||
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
|
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
|
||||||
|
|
@ -84,11 +87,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
scoreModeInt () {
|
scoreModeInt (): number {
|
||||||
return parseInt(this.scoreMode, 10)
|
return parseInt(`${this.scoreMode}`, 10)
|
||||||
},
|
},
|
||||||
tilesInt () {
|
tilesInt (): number {
|
||||||
return parseInt(this.tiles, 10)
|
return parseInt(`${this.tiles}`, 10)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
27
src/frontend/components/PreviewOverlay.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="overlay" @click="$emit('bgclick')">
|
||||||
|
<div class="preview">
|
||||||
|
<div class="img" :style="previewStyle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'preview-overlay',
|
||||||
|
props: {
|
||||||
|
img: String,
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
bgclick: null,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
previewStyle (): object {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url('${this.img}')`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
45
src/frontend/components/PuzzleStatus.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="timer">
|
||||||
|
<div>
|
||||||
|
🧩 {{piecesDone}}/{{piecesTotal}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{icon}} {{durationStr}}
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import Time from './../../common/Time'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'puzzle-status',
|
||||||
|
props: {
|
||||||
|
finished: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
piecesDone: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
piecesTotal: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
icon (): string {
|
||||||
|
return this.finished ? '🏁' : '⏳'
|
||||||
|
},
|
||||||
|
durationStr (): string {
|
||||||
|
return Time.durationStr(this.duration)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
|
||||||
// ingame component
|
|
||||||
// shows player scores
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "scores",
|
|
||||||
template: `
|
|
||||||
<div class="scores">
|
<div class="scores">
|
||||||
<div>Scores</div>
|
<div>Scores</div>
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -21,21 +14,33 @@ export default {
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`,
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "scores",
|
||||||
|
props: {
|
||||||
|
activePlayers: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
idlePlayers: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
actives () {
|
actives (): Array<any> {
|
||||||
// TODO: dont sort in place
|
// TODO: dont sort in place
|
||||||
this.activePlayers.sort((a, b) => b.points - a.points)
|
this.activePlayers.sort((a: any, b: any) => b.points - a.points)
|
||||||
return this.activePlayers
|
return this.activePlayers
|
||||||
},
|
},
|
||||||
idles () {
|
idles (): Array<any> {
|
||||||
// TODO: dont sort in place
|
// TODO: dont sort in place
|
||||||
this.idlePlayers.sort((a, b) => b.points - a.points)
|
this.idlePlayers.sort((a: any, b: any) => b.points - a.points)
|
||||||
return this.idlePlayers
|
return this.idlePlayers
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: {
|
})
|
||||||
activePlayers: Array,
|
</script>
|
||||||
idlePlayers: Array,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
38
src/frontend/components/SettingsOverlay.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||||
|
<table class="overlay-content settings" @click.stop="">
|
||||||
|
<tr>
|
||||||
|
<td><label>Background: </label></td>
|
||||||
|
<td><input type="color" v-model="modelValue.background" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label>Color: </label></td>
|
||||||
|
<td><input type="color" v-model="modelValue.color" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label>Name: </label></td>
|
||||||
|
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'settings-overlay',
|
||||||
|
emits: {
|
||||||
|
bgclick: null,
|
||||||
|
'update:modelValue': null,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
// TODO: ts type PlayerSettings
|
||||||
|
this.$watch('modelValue', (val: any) => {
|
||||||
|
this.$emit('update:modelValue', val)
|
||||||
|
}, { deep: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
<label>
|
||||||
|
<input type="file" style="display: none" @change="upload" :accept="accept" />
|
||||||
|
<span class="btn">{{label || 'Upload File'}}</span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
name: 'upload',
|
name: 'upload',
|
||||||
props: {
|
props: {
|
||||||
accept: String,
|
accept: String,
|
||||||
label: String,
|
label: String,
|
||||||
},
|
},
|
||||||
template: `
|
|
||||||
<label>
|
|
||||||
<input type="file" style="display: none" @change="upload" :accept="accept" />
|
|
||||||
<span class="btn">{{label || 'Upload File'}}</span>
|
|
||||||
</label>
|
|
||||||
`,
|
|
||||||
methods: {
|
methods: {
|
||||||
async upload(evt) {
|
async upload(evt: Event) {
|
||||||
const file = evt.target.files[0]
|
const target = (evt.target as HTMLInputElement)
|
||||||
|
if (!target.files) return;
|
||||||
|
const file = target.files[0]
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file, file.name);
|
formData.append('file', file, file.name);
|
||||||
|
|
@ -26,4 +29,5 @@ export default {
|
||||||
this.$emit('uploaded', j)
|
this.$emit('uploaded', j)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,18 +1,32 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import {run} from './gameloop.js'
|
import {run} from './gameloop'
|
||||||
import Camera from './Camera.js'
|
import Camera from './Camera'
|
||||||
import Graphics from './Graphics.js'
|
import Graphics from './Graphics'
|
||||||
import Debug from './Debug.js'
|
import Debug from './Debug'
|
||||||
import Communication from './Communication.js'
|
import Communication from './Communication'
|
||||||
import Util, { logger } from './../common/Util.js'
|
import Util from './../common/Util'
|
||||||
import PuzzleGraphics from './PuzzleGraphics.js'
|
import PuzzleGraphics from './PuzzleGraphics'
|
||||||
import Game from './../common/GameCommon.js'
|
import Game, { Player, Piece } from './../common/GameCommon'
|
||||||
import fireworksController from './Fireworks.js'
|
import fireworksController from './Fireworks'
|
||||||
import Protocol from '../common/Protocol.js'
|
import Protocol from '../common/Protocol'
|
||||||
import Time from '../common/Time.js'
|
import Time from '../common/Time'
|
||||||
|
|
||||||
// const log = logger('game.js')
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
DEBUG?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://stackoverflow.com/a/59906630/392905
|
||||||
|
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
|
||||||
|
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
|
||||||
|
type FixedLengthArray<T extends any[]> =
|
||||||
|
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
|
||||||
|
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const images = import.meta.globEager('./*.png')
|
||||||
|
|
||||||
export const MODE_PLAY = 'play'
|
export const MODE_PLAY = 'play'
|
||||||
export const MODE_REPLAY = 'replay'
|
export const MODE_REPLAY = 'replay'
|
||||||
|
|
@ -20,7 +34,34 @@ export const MODE_REPLAY = 'replay'
|
||||||
let PIECE_VIEW_FIXED = true
|
let PIECE_VIEW_FIXED = true
|
||||||
let PIECE_VIEW_LOOSE = true
|
let PIECE_VIEW_LOOSE = true
|
||||||
|
|
||||||
const shouldDrawPiece = (piece) => {
|
interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
interface Hud {
|
||||||
|
setActivePlayers: (v: Array<any>) => void
|
||||||
|
setIdlePlayers: (v: Array<any>) => void
|
||||||
|
setFinished: (v: boolean) => void
|
||||||
|
setDuration: (v: number) => void
|
||||||
|
setPiecesDone: (v: number) => void
|
||||||
|
setPiecesTotal: (v: number) => void
|
||||||
|
setConnectionState: (v: number) => void
|
||||||
|
togglePreview: () => void
|
||||||
|
setReplaySpeed?: (v: number) => void
|
||||||
|
setReplayPaused?: (v: boolean) => void
|
||||||
|
}
|
||||||
|
interface Replay {
|
||||||
|
log: Array<any>
|
||||||
|
logIdx: number
|
||||||
|
speeds: Array<number>
|
||||||
|
speedIdx: number
|
||||||
|
paused: boolean
|
||||||
|
lastRealTs: number
|
||||||
|
lastGameTs: number
|
||||||
|
gameStartTs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDrawPiece = (piece: Piece) => {
|
||||||
if (piece.owner === -1) {
|
if (piece.owner === -1) {
|
||||||
return PIECE_VIEW_FIXED
|
return PIECE_VIEW_FIXED
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +70,7 @@ const shouldDrawPiece = (piece) => {
|
||||||
|
|
||||||
let RERENDER = true
|
let RERENDER = true
|
||||||
|
|
||||||
function addCanvasToDom(TARGET_EL, canvas) {
|
function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
|
||||||
canvas.width = window.innerWidth
|
canvas.width = window.innerWidth
|
||||||
canvas.height = window.innerHeight
|
canvas.height = window.innerHeight
|
||||||
TARGET_EL.appendChild(canvas)
|
TARGET_EL.appendChild(canvas)
|
||||||
|
|
@ -41,8 +82,8 @@ function addCanvasToDom(TARGET_EL, canvas) {
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventAdapter (canvas, window, viewport) {
|
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
|
||||||
let events = []
|
let events: Array<Array<any>> = []
|
||||||
|
|
||||||
let KEYS_ON = true
|
let KEYS_ON = true
|
||||||
|
|
||||||
|
|
@ -54,15 +95,15 @@ function EventAdapter (canvas, window, viewport) {
|
||||||
let ZOOM_OUT = false
|
let ZOOM_OUT = false
|
||||||
let SHIFT = false
|
let SHIFT = false
|
||||||
|
|
||||||
const toWorldPoint = (x, y) => {
|
const toWorldPoint = (x: number, y: number) => {
|
||||||
const pos = viewport.viewportToWorld({x, y})
|
const pos = viewport.viewportToWorld({x, y})
|
||||||
return [pos.x, pos.y]
|
return [pos.x, pos.y]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mousePos = (ev) => toWorldPoint(ev.offsetX, ev.offsetY)
|
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
|
||||||
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
||||||
|
|
||||||
const key = (state, ev) => {
|
const key = (state: boolean, ev: KeyboardEvent) => {
|
||||||
if (!KEYS_ON) {
|
if (!KEYS_ON) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -108,10 +149,10 @@ function EventAdapter (canvas, window, viewport) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('keydown', (ev) => key(true, ev))
|
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
||||||
window.addEventListener('keyup', (ev) => key(false, ev))
|
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
||||||
|
|
||||||
window.addEventListener('keypress', (ev) => {
|
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
||||||
if (!KEYS_ON) {
|
if (!KEYS_ON) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +169,7 @@ function EventAdapter (canvas, window, viewport) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const addEvent = (event) => {
|
const addEvent = (event: Array<any>) => {
|
||||||
events.push(event)
|
events.push(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +203,7 @@ function EventAdapter (canvas, window, viewport) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setHotkeys = (state) => {
|
const setHotkeys = (state: boolean) => {
|
||||||
KEYS_ON = state
|
KEYS_ON = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,23 +216,23 @@ function EventAdapter (canvas, window, viewport) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(
|
export async function main(
|
||||||
gameId,
|
gameId: string,
|
||||||
clientId,
|
clientId: string,
|
||||||
wsAddress,
|
wsAddress: string,
|
||||||
MODE,
|
MODE: string,
|
||||||
TARGET_EL,
|
TARGET_EL: HTMLElement,
|
||||||
HUD
|
HUD: Hud
|
||||||
) {
|
) {
|
||||||
if (typeof DEBUG === 'undefined') window.DEBUG = false
|
if (typeof window.DEBUG === 'undefined') window.DEBUG = false
|
||||||
|
|
||||||
const shouldDrawPlayerText = (player) => {
|
const shouldDrawPlayerText = (player: Player) => {
|
||||||
return MODE === MODE_REPLAY || player.id !== clientId
|
return MODE === MODE_REPLAY || player.id !== clientId
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
|
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
|
||||||
const cursorHand = await Graphics.loadImageToBitmap('/hand.png')
|
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
|
||||||
const cursorGrabMask = await Graphics.loadImageToBitmap('/grab_mask.png')
|
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
|
||||||
const cursorHandMask = await Graphics.loadImageToBitmap('/hand_mask.png')
|
const cursorHandMask = await Graphics.loadImageToBitmap(images['./hand_mask.png'].default)
|
||||||
|
|
||||||
// all cursors must be of the same dimensions
|
// all cursors must be of the same dimensions
|
||||||
const CURSOR_W = cursorGrab.width
|
const CURSOR_W = cursorGrab.width
|
||||||
|
|
@ -199,13 +240,17 @@ export async function main(
|
||||||
const CURSOR_H = cursorGrab.height
|
const CURSOR_H = cursorGrab.height
|
||||||
const CURSOR_H_2 = Math.round(CURSOR_H / 2)
|
const CURSOR_H_2 = Math.round(CURSOR_H / 2)
|
||||||
|
|
||||||
const cursors = {}
|
const cursors: Record<string, ImageBitmap> = {}
|
||||||
const getPlayerCursor = async (p) => {
|
const getPlayerCursor = async (p: Player) => {
|
||||||
const key = p.color + ' ' + p.d
|
const key = p.color + ' ' + p.d
|
||||||
if (!cursors[key]) {
|
if (!cursors[key]) {
|
||||||
const cursor = p.d ? cursorGrab : cursorHand
|
const cursor = p.d ? cursorGrab : cursorHand
|
||||||
const mask = p.d ? cursorGrabMask : cursorHandMask
|
if (p.color) {
|
||||||
cursors[key] = await Graphics.colorize(cursor, mask, p.color)
|
const mask = p.d ? cursorGrabMask : cursorHandMask
|
||||||
|
cursors[key] = await Graphics.colorize(cursor, mask, p.color)
|
||||||
|
} else {
|
||||||
|
cursors[key] = cursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cursors[key]
|
return cursors[key]
|
||||||
}
|
}
|
||||||
|
|
@ -215,22 +260,22 @@ export async function main(
|
||||||
|
|
||||||
// stuff only available in replay mode...
|
// stuff only available in replay mode...
|
||||||
// TODO: refactor
|
// TODO: refactor
|
||||||
const REPLAY = {
|
const REPLAY: Replay = {
|
||||||
log: null,
|
log: [],
|
||||||
logIdx: 0,
|
logIdx: 0,
|
||||||
speeds: [0.5, 1, 2, 5, 10, 20, 50],
|
speeds: [0.5, 1, 2, 5, 10, 20, 50],
|
||||||
speedIdx: 1,
|
speedIdx: 1,
|
||||||
paused: false,
|
paused: false,
|
||||||
lastRealTs: null,
|
lastRealTs: 0,
|
||||||
lastGameTs: null,
|
lastGameTs: 0,
|
||||||
gameStartTs: null,
|
gameStartTs: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
Communication.onConnectionStateChange((state) => {
|
Communication.onConnectionStateChange((state) => {
|
||||||
HUD.setConnectionState(state)
|
HUD.setConnectionState(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
let TIME
|
let TIME: () => number = () => 0
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
if (MODE === MODE_PLAY) {
|
if (MODE === MODE_PLAY) {
|
||||||
const game = await Communication.connect(wsAddress, gameId, clientId)
|
const game = await Communication.connect(wsAddress, gameId, clientId)
|
||||||
|
|
@ -239,12 +284,12 @@ export async function main(
|
||||||
TIME = () => Time.timestamp()
|
TIME = () => Time.timestamp()
|
||||||
} else if (MODE === MODE_REPLAY) {
|
} else if (MODE === MODE_REPLAY) {
|
||||||
// TODO: change how replay connect is done...
|
// TODO: change how replay connect is done...
|
||||||
const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId)
|
const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
|
||||||
const gameObject = Util.decodeGame(game)
|
const gameObject = Util.decodeGame(replay.game)
|
||||||
Game.setGame(gameObject.id, gameObject)
|
Game.setGame(gameObject.id, gameObject)
|
||||||
REPLAY.log = log
|
REPLAY.log = replay.log
|
||||||
REPLAY.lastRealTs = Time.timestamp()
|
REPLAY.lastRealTs = Time.timestamp()
|
||||||
REPLAY.gameStartTs = REPLAY.log[0][REPLAY.log[0].length - 2]
|
REPLAY.gameStartTs = parseInt(REPLAY.log[0][REPLAY.log[0].length - 2], 10)
|
||||||
REPLAY.lastGameTs = REPLAY.gameStartTs
|
REPLAY.lastGameTs = REPLAY.gameStartTs
|
||||||
TIME = () => REPLAY.lastGameTs
|
TIME = () => REPLAY.lastGameTs
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -280,21 +325,21 @@ export async function main(
|
||||||
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
|
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
|
||||||
|
|
||||||
const fireworks = new fireworksController(canvas, Game.getRng(gameId))
|
const fireworks = new fireworksController(canvas, Game.getRng(gameId))
|
||||||
fireworks.init(canvas)
|
fireworks.init()
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||||
canvas.classList.add('loaded')
|
canvas.classList.add('loaded')
|
||||||
|
|
||||||
// initialize some view data
|
// initialize some view data
|
||||||
// this global data will change according to input events
|
// this global data will change according to input events
|
||||||
const viewport = new Camera()
|
const viewport = Camera()
|
||||||
// center viewport
|
// center viewport
|
||||||
viewport.move(
|
viewport.move(
|
||||||
-(TABLE_WIDTH - canvas.width) /2,
|
-(TABLE_WIDTH - canvas.width) /2,
|
||||||
-(TABLE_HEIGHT - canvas.height) /2
|
-(TABLE_HEIGHT - canvas.height) /2
|
||||||
)
|
)
|
||||||
|
|
||||||
const evts = new EventAdapter(canvas, window, viewport)
|
const evts = EventAdapter(canvas, window, viewport)
|
||||||
|
|
||||||
const previewImageUrl = Game.getImageUrl(gameId)
|
const previewImageUrl = Game.getImageUrl(gameId)
|
||||||
|
|
||||||
|
|
@ -335,8 +380,12 @@ export async function main(
|
||||||
}
|
}
|
||||||
|
|
||||||
const doSetSpeedStatus = () => {
|
const doSetSpeedStatus = () => {
|
||||||
HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx])
|
if (HUD.setReplaySpeed) {
|
||||||
HUD.setReplayPaused(REPLAY.paused)
|
HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx])
|
||||||
|
}
|
||||||
|
if (HUD.setReplayPaused) {
|
||||||
|
HUD.setReplayPaused(REPLAY.paused)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replayOnSpeedUp = () => {
|
const replayOnSpeedUp = () => {
|
||||||
|
|
@ -419,17 +468,18 @@ export async function main(
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryWithTs = logEntry.slice()
|
const entryWithTs = logEntry.slice()
|
||||||
entryWithTs[entryWithTs.length - 1] = nextTs
|
|
||||||
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
|
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
|
||||||
Game.addPlayer(gameId, ...entryWithTs.slice(1))
|
const playerId = entryWithTs[1]
|
||||||
|
Game.addPlayer(gameId, playerId, nextTs)
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
|
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
|
||||||
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
||||||
Game.addPlayer(gameId, playerId, ...entryWithTs.slice(2))
|
Game.addPlayer(gameId, playerId, nextTs)
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
|
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||||
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
||||||
Game.handleInput(gameId, playerId, ...entryWithTs.slice(2))
|
const input = entryWithTs[2]
|
||||||
|
Game.handleInput(gameId, playerId, input, nextTs)
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
}
|
}
|
||||||
REPLAY.logIdx = nextIdx
|
REPLAY.logIdx = nextIdx
|
||||||
|
|
@ -440,7 +490,7 @@ export async function main(
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _last_mouse_down = null
|
let _last_mouse_down: Point|null = null
|
||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
// handle key downs once per onUpdate
|
// handle key downs once per onUpdate
|
||||||
// this will create Protocol.INPUT_EV_MOVE events if something
|
// this will create Protocol.INPUT_EV_MOVE events if something
|
||||||
|
|
@ -552,13 +602,13 @@ export async function main(
|
||||||
let dim
|
let dim
|
||||||
let bmp
|
let bmp
|
||||||
|
|
||||||
if (DEBUG) Debug.checkpoint_start(0)
|
if (window.DEBUG) Debug.checkpoint_start(0)
|
||||||
|
|
||||||
// CLEAR CTX
|
// CLEAR CTX
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
ctx.fillStyle = playerBgColor()
|
ctx.fillStyle = playerBgColor()
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
if (DEBUG) Debug.checkpoint('clear done')
|
if (window.DEBUG) Debug.checkpoint('clear done')
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -568,14 +618,14 @@ export async function main(
|
||||||
dim = viewport.worldDimToViewportRaw(BOARD_DIM)
|
dim = viewport.worldDimToViewportRaw(BOARD_DIM)
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, .3)'
|
ctx.fillStyle = 'rgba(255, 255, 255, .3)'
|
||||||
ctx.fillRect(pos.x, pos.y, dim.w, dim.h)
|
ctx.fillRect(pos.x, pos.y, dim.w, dim.h)
|
||||||
if (DEBUG) Debug.checkpoint('board done')
|
if (window.DEBUG) Debug.checkpoint('board done')
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// DRAW TILES
|
// DRAW TILES
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
const tiles = Game.getTilesSortedByZIndex(gameId)
|
const tiles = Game.getTilesSortedByZIndex(gameId)
|
||||||
if (DEBUG) Debug.checkpoint('get tiles done')
|
if (window.DEBUG) Debug.checkpoint('get tiles done')
|
||||||
|
|
||||||
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
|
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
|
||||||
for (const tile of tiles) {
|
for (const tile of tiles) {
|
||||||
|
|
@ -592,13 +642,13 @@ export async function main(
|
||||||
pos.x, pos.y, dim.w, dim.h
|
pos.x, pos.y, dim.w, dim.h
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (DEBUG) Debug.checkpoint('tiles done')
|
if (window.DEBUG) Debug.checkpoint('tiles done')
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// DRAW PLAYERS
|
// DRAW PLAYERS
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
const texts = []
|
const texts: Array<FixedLengthArray<[string, number, number]>> = []
|
||||||
// Cursors
|
// Cursors
|
||||||
for (const p of Game.getActivePlayers(gameId, ts)) {
|
for (const p of Game.getActivePlayers(gameId, ts)) {
|
||||||
bmp = await getPlayerCursor(p)
|
bmp = await getPlayerCursor(p)
|
||||||
|
|
@ -619,14 +669,14 @@ export async function main(
|
||||||
ctx.fillText(txt, x, y)
|
ctx.fillText(txt, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) Debug.checkpoint('players done')
|
if (window.DEBUG) Debug.checkpoint('players done')
|
||||||
|
|
||||||
// propagate HUD changes
|
// propagate HUD changes
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
|
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
|
||||||
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
|
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
|
||||||
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
|
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
|
||||||
if (DEBUG) Debug.checkpoint('HUD done')
|
if (window.DEBUG) Debug.checkpoint('HUD done')
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
if (justFinished()) {
|
if (justFinished()) {
|
||||||
|
|
@ -642,18 +692,18 @@ export async function main(
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setHotkeys: (state) => {
|
setHotkeys: (state: boolean) => {
|
||||||
evts.setHotkeys(state)
|
evts.setHotkeys(state)
|
||||||
},
|
},
|
||||||
onBgChange: (value) => {
|
onBgChange: (value: string) => {
|
||||||
localStorage.setItem('bg_color', value)
|
localStorage.setItem('bg_color', value)
|
||||||
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
||||||
},
|
},
|
||||||
onColorChange: (value) => {
|
onColorChange: (value: string) => {
|
||||||
localStorage.setItem('player_color', value)
|
localStorage.setItem('player_color', value)
|
||||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
|
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
|
||||||
},
|
},
|
||||||
onNameChange: (value) => {
|
onNameChange: (value: string) => {
|
||||||
localStorage.setItem('player_name', value)
|
localStorage.setItem('player_name', value)
|
||||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
||||||
},
|
},
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
export const run = options => {
|
interface GameLoopOptions {
|
||||||
|
fps?: number
|
||||||
|
slow?: number
|
||||||
|
update: (step: number) => any
|
||||||
|
render: (passed: number) => any
|
||||||
|
}
|
||||||
|
export const run = (options: GameLoopOptions) => {
|
||||||
const fps = options.fps || 60
|
const fps = options.fps || 60
|
||||||
const slow = options.slow || 1
|
const slow = options.slow || 1
|
||||||
const update = options.update
|
const update = options.update
|
||||||
|
Before Width: | Height: | Size: 148 B After Width: | Height: | Size: 148 B |
|
Before Width: | Height: | Size: 125 B After Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 170 B After Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 140 B |
10
src/frontend/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<title>🧩 jigsaw.hyottoko.club</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
src/frontend/main.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as VueRouter from 'vue-router'
|
||||||
|
import * as Vue from 'vue'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import Index from './views/Index.vue'
|
||||||
|
import NewGame from './views/NewGame.vue'
|
||||||
|
import Game from './views/Game.vue'
|
||||||
|
import Replay from './views/Replay.vue'
|
||||||
|
import Util from './../common/Util'
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const res = await fetch(`/api/conf`)
|
||||||
|
const conf = await res.json()
|
||||||
|
|
||||||
|
function initme() {
|
||||||
|
let ID = localStorage.getItem('ID')
|
||||||
|
if (!ID) {
|
||||||
|
ID = Util.uniqId()
|
||||||
|
localStorage.setItem('ID', ID)
|
||||||
|
}
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = VueRouter.createRouter({
|
||||||
|
history: VueRouter.createWebHashHistory(),
|
||||||
|
routes: [
|
||||||
|
{ name: 'index', path: '/', component: Index },
|
||||||
|
{ name: 'new-game', path: '/new-game', component: NewGame },
|
||||||
|
{ name: 'game', path: '/g/:id', component: Game },
|
||||||
|
{ name: 'replay', path: '/replay/:id', component: Replay },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
if (from.name) {
|
||||||
|
document.documentElement.classList.remove(`view-${String(from.name)}`)
|
||||||
|
}
|
||||||
|
document.documentElement.classList.add(`view-${String(to.name)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = Vue.createApp(App)
|
||||||
|
app.config.globalProperties.$config = conf
|
||||||
|
app.config.globalProperties.$clientId = initme()
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
})()
|
||||||
5
src/frontend/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,5 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
<div id="game">
|
||||||
import Scores from './../components/Scores.vue.js'
|
|
||||||
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
|
|
||||||
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
|
|
||||||
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
|
|
||||||
import ConnectionOverlay from './../components/ConnectionOverlay.vue.js'
|
|
||||||
import HelpOverlay from './../components/HelpOverlay.vue.js'
|
|
||||||
|
|
||||||
import { main, MODE_PLAY } from './../game.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'game',
|
|
||||||
components: {
|
|
||||||
PuzzleStatus,
|
|
||||||
Scores,
|
|
||||||
SettingsOverlay,
|
|
||||||
PreviewOverlay,
|
|
||||||
ConnectionOverlay,
|
|
||||||
HelpOverlay,
|
|
||||||
},
|
|
||||||
template: `<div id="game">
|
|
||||||
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
||||||
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
||||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||||
|
|
@ -46,18 +26,42 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
|
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue'
|
||||||
|
|
||||||
|
import Scores from './../components/Scores.vue'
|
||||||
|
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||||
|
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||||
|
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||||
|
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
|
||||||
|
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||||
|
|
||||||
|
import { main, MODE_PLAY } from './../game'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'game',
|
||||||
|
components: {
|
||||||
|
PuzzleStatus,
|
||||||
|
Scores,
|
||||||
|
SettingsOverlay,
|
||||||
|
PreviewOverlay,
|
||||||
|
ConnectionOverlay,
|
||||||
|
HelpOverlay,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activePlayers: [],
|
// TODO: ts Array<Player> type
|
||||||
idlePlayers: [],
|
activePlayers: [] as PropType<Array<any>>,
|
||||||
|
idlePlayers: [] as PropType<Array<any>>,
|
||||||
|
|
||||||
finished: false,
|
finished: false,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
piecesDone: 0,
|
piecesDone: 0,
|
||||||
piecesTotal: 0,
|
piecesTotal: 0,
|
||||||
|
|
||||||
overlay: null,
|
overlay: '',
|
||||||
|
|
||||||
connectionState: 0,
|
connectionState: 0,
|
||||||
|
|
||||||
|
|
@ -68,10 +72,10 @@ export default {
|
||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
previewImageUrl: '',
|
previewImageUrl: '',
|
||||||
setHotkeys: () => {},
|
setHotkeys: (v: boolean) => {},
|
||||||
onBgChange: () => {},
|
onBgChange: (v: string) => {},
|
||||||
onColorChange: () => {},
|
onColorChange: (v: string) => {},
|
||||||
onNameChange: () => {},
|
onNameChange: (v: string) => {},
|
||||||
disconnect: () => {},
|
disconnect: () => {},
|
||||||
connect: () => {},
|
connect: () => {},
|
||||||
},
|
},
|
||||||
|
|
@ -81,29 +85,31 @@ export default {
|
||||||
if (!this.$route.params.id) {
|
if (!this.$route.params.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$watch(() => this.g.player.background, (value) => {
|
this.$watch(() => this.g.player.background, (value: string) => {
|
||||||
this.g.onBgChange(value)
|
this.g.onBgChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.color, (value) => {
|
this.$watch(() => this.g.player.color, (value: string) => {
|
||||||
this.g.onColorChange(value)
|
this.g.onColorChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.name, (value) => {
|
this.$watch(() => this.g.player.name, (value: string) => {
|
||||||
this.g.onNameChange(value)
|
this.g.onNameChange(value)
|
||||||
})
|
})
|
||||||
this.g = await main(
|
this.g = await main(
|
||||||
this.$route.params.id,
|
`${this.$route.params.id}`,
|
||||||
|
// @ts-ignore
|
||||||
this.$clientId,
|
this.$clientId,
|
||||||
|
// @ts-ignore
|
||||||
this.$config.WS_ADDRESS,
|
this.$config.WS_ADDRESS,
|
||||||
MODE_PLAY,
|
MODE_PLAY,
|
||||||
this.$el,
|
this.$el,
|
||||||
{
|
{
|
||||||
setActivePlayers: (v) => { this.activePlayers = v },
|
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||||
setIdlePlayers: (v) => { this.idlePlayers = v },
|
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||||
setFinished: (v) => { this.finished = v },
|
setFinished: (v: boolean) => { this.finished = v },
|
||||||
setDuration: (v) => { this.duration = v },
|
setDuration: (v: number) => { this.duration = v },
|
||||||
setPiecesDone: (v) => { this.piecesDone = v },
|
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||||
setPiecesTotal: (v) => { this.piecesTotal = v },
|
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||||
setConnectionState: (v) => { this.connectionState = v },
|
setConnectionState: (v: number) => { this.connectionState = v },
|
||||||
togglePreview: () => { this.toggle('preview', false) },
|
togglePreview: () => { this.toggle('preview', false) },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -112,22 +118,23 @@ export default {
|
||||||
this.g.disconnect()
|
this.g.disconnect()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reconnect() {
|
reconnect(): void {
|
||||||
this.g.connect()
|
this.g.connect()
|
||||||
},
|
},
|
||||||
toggle(overlay, affectsHotkeys) {
|
toggle(overlay: string, affectsHotkeys: boolean): void {
|
||||||
if (this.overlay === null) {
|
if (this.overlay === '') {
|
||||||
this.overlay = overlay
|
this.overlay = overlay
|
||||||
if (affectsHotkeys) {
|
if (affectsHotkeys) {
|
||||||
this.g.setHotkeys(false)
|
this.g.setHotkeys(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// could check if overlay was the provided one
|
// could check if overlay was the provided one
|
||||||
this.overlay = null
|
this.overlay = ''
|
||||||
if (affectsHotkeys) {
|
if (affectsHotkeys) {
|
||||||
this.g.setHotkeys(true)
|
this.g.setHotkeys(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
36
src/frontend/views/Index.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Running games</h1>
|
||||||
|
<div class="game-teaser-wrap" v-for="(g, idx) in gamesRunning" :key="idx">
|
||||||
|
<game-teaser :game="g" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Finished games</h1>
|
||||||
|
<div class="game-teaser-wrap" v-for="(g, idx) in gamesFinished" :key="idx">
|
||||||
|
<game-teaser :game="g" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
import GameTeaser from './../components/GameTeaser.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
GameTeaser,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
gamesRunning: [],
|
||||||
|
gamesFinished: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
const res = await fetch('/api/index-data')
|
||||||
|
const json = await res.json()
|
||||||
|
this.gamesRunning = json.gamesRunning
|
||||||
|
this.gamesFinished = json.gamesFinished
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
<div>
|
||||||
|
<new-game-dialog :images="images" @newGame="onNewGame" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
import NewGameDialog from './../components/NewGameDialog.vue.js'
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
export default {
|
// TODO: maybe move dialog back, now that this is a view on its own
|
||||||
|
import NewGameDialog from './../components/NewGameDialog.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
NewGameDialog,
|
NewGameDialog,
|
||||||
},
|
},
|
||||||
// TODO: maybe move dialog back, now that this is a view on its own
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<new-game-dialog :images="images" @newGame="onNewGame" />
|
|
||||||
</div>`,
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
images: [],
|
images: [],
|
||||||
|
|
@ -22,7 +25,8 @@ export default {
|
||||||
this.images = json.images
|
this.images = json.images
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onNewGame(gameSettings) {
|
// TODO: ts GameSettings type
|
||||||
|
async onNewGame(gameSettings: any) {
|
||||||
const res = await fetch('/newgame', {
|
const res = await fetch('/newgame', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -37,4 +41,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,23 +1,5 @@
|
||||||
"use strict"
|
<template>
|
||||||
|
<div id="replay">
|
||||||
import Scores from './../components/Scores.vue.js'
|
|
||||||
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
|
|
||||||
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
|
|
||||||
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
|
|
||||||
import HelpOverlay from './../components/HelpOverlay.vue.js'
|
|
||||||
|
|
||||||
import { main, MODE_REPLAY } from './../game.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'replay',
|
|
||||||
components: {
|
|
||||||
PuzzleStatus,
|
|
||||||
Scores,
|
|
||||||
SettingsOverlay,
|
|
||||||
PreviewOverlay,
|
|
||||||
HelpOverlay,
|
|
||||||
},
|
|
||||||
template: `<div id="replay">
|
|
||||||
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
||||||
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
||||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||||
|
|
@ -46,18 +28,41 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
|
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
|
||||||
</div>`,
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
import Scores from './../components/Scores.vue'
|
||||||
|
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||||
|
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||||
|
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||||
|
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||||
|
|
||||||
|
import { main, MODE_REPLAY } from './../game'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'replay',
|
||||||
|
components: {
|
||||||
|
PuzzleStatus,
|
||||||
|
Scores,
|
||||||
|
SettingsOverlay,
|
||||||
|
PreviewOverlay,
|
||||||
|
HelpOverlay,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activePlayers: [],
|
activePlayers: [] as Array<any>,
|
||||||
idlePlayers: [],
|
idlePlayers: [] as Array<any>,
|
||||||
|
|
||||||
finished: false,
|
finished: false,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
piecesDone: 0,
|
piecesDone: 0,
|
||||||
piecesTotal: 0,
|
piecesTotal: 0,
|
||||||
|
|
||||||
overlay: null,
|
overlay: '',
|
||||||
|
|
||||||
|
connectionState: 0,
|
||||||
|
|
||||||
g: {
|
g: {
|
||||||
player: {
|
player: {
|
||||||
|
|
@ -66,10 +71,10 @@ export default {
|
||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
previewImageUrl: '',
|
previewImageUrl: '',
|
||||||
setHotkeys: () => {},
|
setHotkeys: (v: boolean) => {},
|
||||||
onBgChange: () => {},
|
onBgChange: (v: string) => {},
|
||||||
onColorChange: () => {},
|
onColorChange: (v: string) => {},
|
||||||
onNameChange: () => {},
|
onNameChange: (v: string) => {},
|
||||||
replayOnSpeedUp: () => {},
|
replayOnSpeedUp: () => {},
|
||||||
replayOnSpeedDown: () => {},
|
replayOnSpeedDown: () => {},
|
||||||
replayOnPauseToggle: () => {},
|
replayOnPauseToggle: () => {},
|
||||||
|
|
@ -86,59 +91,62 @@ export default {
|
||||||
if (!this.$route.params.id) {
|
if (!this.$route.params.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$watch(() => this.g.player.background, (value) => {
|
this.$watch(() => this.g.player.background, (value: string) => {
|
||||||
this.g.onBgChange(value)
|
this.g.onBgChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.color, (value) => {
|
this.$watch(() => this.g.player.color, (value: string) => {
|
||||||
this.g.onColorChange(value)
|
this.g.onColorChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.name, (value) => {
|
this.$watch(() => this.g.player.name, (value: string) => {
|
||||||
this.g.onNameChange(value)
|
this.g.onNameChange(value)
|
||||||
})
|
})
|
||||||
this.g = await main(
|
this.g = await main(
|
||||||
this.$route.params.id,
|
`${this.$route.params.id}`,
|
||||||
|
// @ts-ignore
|
||||||
this.$clientId,
|
this.$clientId,
|
||||||
|
// @ts-ignore
|
||||||
this.$config.WS_ADDRESS,
|
this.$config.WS_ADDRESS,
|
||||||
MODE_REPLAY,
|
MODE_REPLAY,
|
||||||
this.$el,
|
this.$el,
|
||||||
{
|
{
|
||||||
setActivePlayers: (v) => { this.activePlayers = v },
|
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||||
setIdlePlayers: (v) => { this.idlePlayers = v },
|
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||||
setFinished: (v) => { this.finished = v },
|
setFinished: (v: boolean) => { this.finished = v },
|
||||||
setDuration: (v) => { this.duration = v },
|
setDuration: (v: number) => { this.duration = v },
|
||||||
setPiecesDone: (v) => { this.piecesDone = v },
|
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||||
setPiecesTotal: (v) => { this.piecesTotal = v },
|
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||||
togglePreview: () => { this.toggle('preview', false) },
|
togglePreview: () => { this.toggle('preview', false) },
|
||||||
setConnectionState: (v) => { this.connectionState = v },
|
setConnectionState: (v: number) => { this.connectionState = v },
|
||||||
setReplaySpeed: (v) => { this.replay.speed = v },
|
setReplaySpeed: (v: number) => { this.replay.speed = v },
|
||||||
setReplayPaused: (v) => { this.replay.paused = v },
|
setReplayPaused: (v: boolean) => { this.replay.paused = v },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
this.g.disconnect()
|
this.g.disconnect()
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
replayText () {
|
|
||||||
return 'Replay-Speed: ' +
|
|
||||||
(this.replay.speed + 'x') +
|
|
||||||
(this.replay.paused ? ' Paused' : '')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
toggle(overlay, affectsHotkeys) {
|
toggle(overlay: string, affectsHotkeys: boolean): void {
|
||||||
if (this.overlay === null) {
|
if (this.overlay === '') {
|
||||||
this.overlay = overlay
|
this.overlay = overlay
|
||||||
if (affectsHotkeys) {
|
if (affectsHotkeys) {
|
||||||
this.g.setHotkeys(false)
|
this.g.setHotkeys(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// could check if overlay was the provided one
|
// could check if overlay was the provided one
|
||||||
this.overlay = null
|
this.overlay = ''
|
||||||
if (affectsHotkeys) {
|
if (affectsHotkeys) {
|
||||||
this.g.setHotkeys(true)
|
this.g.setHotkeys(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
computed: {
|
||||||
|
replayText (): string {
|
||||||
|
return 'Replay-Speed: ' +
|
||||||
|
(this.replay.speed + 'x') +
|
||||||
|
(this.replay.paused ? ' Paused' : '')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -4,10 +4,9 @@ import { dirname } from 'path'
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
const BASE_DIR = `${__dirname}/..`
|
const BASE_DIR = `${__dirname}/../..`
|
||||||
|
|
||||||
export const DATA_DIR = `${BASE_DIR}/data`
|
export const DATA_DIR = `${BASE_DIR}/data`
|
||||||
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
|
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
|
||||||
export const UPLOAD_URL = `/uploads`
|
export const UPLOAD_URL = `/uploads`
|
||||||
export const COMMON_DIR = `${BASE_DIR}/common/`
|
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
|
||||||
export const PUBLIC_DIR = `${BASE_DIR}/public/`
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import GameCommon from './../common/GameCommon.js'
|
import GameCommon from './../common/GameCommon'
|
||||||
import Util from './../common/Util.js'
|
import Util from './../common/Util'
|
||||||
import { Rng } from '../common/Rng.js'
|
import { Rng } from '../common/Rng'
|
||||||
import GameLog from './GameLog.js'
|
import GameLog from './GameLog'
|
||||||
import { createPuzzle } from './Puzzle.js'
|
import { createPuzzle } from './Puzzle'
|
||||||
import Protocol from '../common/Protocol.js'
|
import Protocol from '../common/Protocol'
|
||||||
import GameStorage from './GameStorage.js'
|
import GameStorage from './GameStorage'
|
||||||
|
|
||||||
async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
|
async function createGameObject(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
|
||||||
const seed = Util.hash(gameId + ' ' + ts)
|
const seed = Util.hash(gameId + ' ' + ts)
|
||||||
const rng = new Rng(seed)
|
const rng = new Rng(seed)
|
||||||
return {
|
return {
|
||||||
|
|
@ -19,7 +19,7 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createGame(gameId, targetTiles, image, ts, scoreMode) {
|
async function createGame(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
|
||||||
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
|
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
|
||||||
|
|
||||||
GameLog.create(gameId)
|
GameLog.create(gameId)
|
||||||
|
|
@ -29,7 +29,7 @@ async function createGame(gameId, targetTiles, image, ts, scoreMode) {
|
||||||
GameStorage.setDirty(gameId)
|
GameStorage.setDirty(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPlayer(gameId, playerId, ts) {
|
function addPlayer(gameId: string, playerId: string, ts: number) {
|
||||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||||
const diff = ts - GameCommon.getStartTs(gameId)
|
const diff = ts - GameCommon.getStartTs(gameId)
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
|
|
@ -42,7 +42,7 @@ function addPlayer(gameId, playerId, ts) {
|
||||||
GameStorage.setDirty(gameId)
|
GameStorage.setDirty(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(gameId, playerId, input, ts) {
|
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
|
||||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||||
const diff = ts - GameCommon.getStartTs(gameId)
|
const diff = ts - GameCommon.getStartTs(gameId)
|
||||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
|
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
|
||||||
|
|
@ -4,21 +4,21 @@ import { DATA_DIR } from '../server/Dirs.js'
|
||||||
|
|
||||||
const log = logger('GameLog.js')
|
const log = logger('GameLog.js')
|
||||||
|
|
||||||
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`
|
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
|
||||||
|
|
||||||
const create = (gameId) => {
|
const create = (gameId: string) => {
|
||||||
const file = filename(gameId)
|
const file = filename(gameId)
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
fs.appendFileSync(file, '')
|
fs.appendFileSync(file, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = (gameId) => {
|
const exists = (gameId: string) => {
|
||||||
const file = filename(gameId)
|
const file = filename(gameId)
|
||||||
return fs.existsSync(file)
|
return fs.existsSync(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _log = (gameId, ...args) => {
|
const _log = (gameId: string, ...args: Array<any>) => {
|
||||||
const file = filename(gameId)
|
const file = filename(gameId)
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
return
|
return
|
||||||
|
|
@ -27,13 +27,13 @@ const _log = (gameId, ...args) => {
|
||||||
fs.appendFileSync(file, str + "\n")
|
fs.appendFileSync(file, str + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = (gameId) => {
|
const get = (gameId: string) => {
|
||||||
const file = filename(gameId)
|
const file = filename(gameId)
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
||||||
return lines.filter(line => !!line).map((line) => {
|
return lines.filter((line: string) => !!line).map((line: string) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(line)
|
return JSON.parse(line)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
import { logger } from '../common/Util.js'
|
import { logger } from '../common/Util.js'
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
|
||||||
const log = logger('GameSocket.js')
|
const log = logger('GameSocket.js')
|
||||||
|
|
||||||
// Map<gameId, Socket[]>
|
// Map<gameId, Socket[]>
|
||||||
const SOCKETS = {}
|
const SOCKETS = {} as Record<string, Array<WebSocket>>
|
||||||
|
|
||||||
function socketExists(gameId, socket) {
|
function socketExists(gameId: string, socket: WebSocket) {
|
||||||
if (!(gameId in SOCKETS)) {
|
if (!(gameId in SOCKETS)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return SOCKETS[gameId].includes(socket)
|
return SOCKETS[gameId].includes(socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSocket(gameId, socket) {
|
function removeSocket(gameId: string, socket: WebSocket) {
|
||||||
if (!(gameId in SOCKETS)) {
|
if (!(gameId in SOCKETS)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SOCKETS[gameId] = SOCKETS[gameId].filter(s => s !== socket)
|
SOCKETS[gameId] = SOCKETS[gameId].filter((s: WebSocket) => s !== socket)
|
||||||
log.log('removed socket: ', gameId, socket.protocol)
|
log.log('removed socket: ', gameId, socket.protocol)
|
||||||
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
|
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSocket(gameId, socket) {
|
function addSocket(gameId: string, socket: WebSocket) {
|
||||||
if (!(gameId in SOCKETS)) {
|
if (!(gameId in SOCKETS)) {
|
||||||
SOCKETS[gameId] = []
|
SOCKETS[gameId] = []
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +33,7 @@ function addSocket(gameId, socket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSockets(gameId) {
|
function getSockets(gameId: string) {
|
||||||
if (!(gameId in SOCKETS)) {
|
if (!(gameId in SOCKETS)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import GameCommon from './../common/GameCommon.js'
|
import GameCommon from './../common/GameCommon'
|
||||||
import Util, { logger } from './../common/Util.js'
|
import Util, { logger } from './../common/Util'
|
||||||
import { Rng } from '../common/Rng.js'
|
import { Rng } from '../common/Rng'
|
||||||
import { DATA_DIR } from './Dirs.js'
|
import { DATA_DIR } from './Dirs'
|
||||||
import Time from '../common/Time.js'
|
import Time from './../common/Time'
|
||||||
|
|
||||||
const log = logger('GameStorage.js')
|
const log = logger('GameStorage.js')
|
||||||
|
|
||||||
const DIRTY_GAMES = {}
|
const DIRTY_GAMES = {} as any
|
||||||
function setDirty(gameId) {
|
function setDirty(gameId: string): void {
|
||||||
DIRTY_GAMES[gameId] = true
|
DIRTY_GAMES[gameId] = true
|
||||||
}
|
}
|
||||||
function setClean(gameId) {
|
function setClean(gameId: string): void {
|
||||||
delete DIRTY_GAMES[gameId]
|
delete DIRTY_GAMES[gameId]
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGames() {
|
function loadGames(): void {
|
||||||
const files = fs.readdirSync(DATA_DIR)
|
const files = fs.readdirSync(DATA_DIR)
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const m = f.match(/^([a-z0-9]+)\.json$/)
|
const m = f.match(/^([a-z0-9]+)\.json$/)
|
||||||
|
|
@ -27,7 +27,7 @@ function loadGames() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGame(gameId) {
|
function loadGame(gameId: string): void {
|
||||||
const file = `${DATA_DIR}/${gameId}.json`
|
const file = `${DATA_DIR}/${gameId}.json`
|
||||||
const contents = fs.readFileSync(file, 'utf-8')
|
const contents = fs.readFileSync(file, 'utf-8')
|
||||||
let game
|
let game
|
||||||
|
|
@ -40,7 +40,7 @@ function loadGame(gameId) {
|
||||||
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
|
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
|
||||||
}
|
}
|
||||||
if (typeof game.puzzle.data.finished === 'undefined') {
|
if (typeof game.puzzle.data.finished === 'undefined') {
|
||||||
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find(t => t.owner !== -1)
|
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find((t: any) => t.owner !== -1)
|
||||||
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
|
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
|
||||||
}
|
}
|
||||||
if (!Array.isArray(game.players)) {
|
if (!Array.isArray(game.players)) {
|
||||||
|
|
@ -50,7 +50,7 @@ function loadGame(gameId) {
|
||||||
id: game.id,
|
id: game.id,
|
||||||
rng: {
|
rng: {
|
||||||
type: game.rng ? game.rng.type : '_fake_',
|
type: game.rng ? game.rng.type : '_fake_',
|
||||||
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(),
|
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
|
||||||
},
|
},
|
||||||
puzzle: game.puzzle,
|
puzzle: game.puzzle,
|
||||||
players: game.players,
|
players: game.players,
|
||||||
|
|
@ -66,7 +66,7 @@ function persistGames() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistGame(gameId) {
|
function persistGame(gameId: string) {
|
||||||
const game = GameCommon.get(gameId)
|
const game = GameCommon.get(gameId)
|
||||||
if (game.id in DIRTY_GAMES) {
|
if (game.id in DIRTY_GAMES) {
|
||||||
setClean(game.id)
|
setClean(game.id)
|
||||||
|
|
@ -2,9 +2,10 @@ import sizeOf from 'image-size'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import exif from 'exif'
|
import exif from 'exif'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
|
||||||
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js'
|
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js'
|
||||||
|
|
||||||
const resizeImage = async (filename) => {
|
const resizeImage = async (filename: string) => {
|
||||||
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +34,7 @@ const resizeImage = async (filename) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExifOrientation(imagePath) {
|
async function getExifOrientation(imagePath: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
|
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -60,7 +61,7 @@ const allImages = () => {
|
||||||
return images
|
return images
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDimensions(imagePath) {
|
async function getDimensions(imagePath: string) {
|
||||||
let dimensions = sizeOf(imagePath)
|
let dimensions = sizeOf(imagePath)
|
||||||
const orientation = await getExifOrientation(imagePath)
|
const orientation = await getExifOrientation(imagePath)
|
||||||
// when image is rotated to the left or right, switch width/height
|
// when image is rotated to the left or right, switch width/height
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
import Util from '../common/Util.js'
|
import Util from '../common/Util'
|
||||||
import { Rng } from '../common/Rng.js'
|
import { Rng } from '../common/Rng'
|
||||||
import Images from './Images.js'
|
import Images from './Images.js'
|
||||||
|
|
||||||
|
interface PuzzleInfo {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
tileSize: number
|
||||||
|
tileMarginWidth: number
|
||||||
|
tileDrawSize: number
|
||||||
|
tiles: number
|
||||||
|
tilesX: number
|
||||||
|
tilesY: number
|
||||||
|
}
|
||||||
|
|
||||||
// cut size of each puzzle tile in the
|
// cut size of each puzzle tile in the
|
||||||
// final resized version of the puzzle image
|
// final resized version of the puzzle image
|
||||||
const TILE_SIZE = 64
|
const TILE_SIZE = 64
|
||||||
|
|
||||||
async function createPuzzle(
|
async function createPuzzle(
|
||||||
/** @type Rng */ rng,
|
rng: Rng,
|
||||||
targetTiles,
|
targetTiles: number,
|
||||||
image,
|
image: { file: string, url: string },
|
||||||
ts
|
ts: number
|
||||||
) {
|
) {
|
||||||
const imagePath = image.file
|
const imagePath = image.file
|
||||||
const imageUrl = image.url
|
const imageUrl = image.url
|
||||||
|
|
||||||
// determine puzzle information from the image dimensions
|
// determine puzzle information from the image dimensions
|
||||||
const dim = await Images.getDimensions(imagePath)
|
const dim = await Images.getDimensions(imagePath)
|
||||||
|
if (!dim || !dim.width || !dim.height) {
|
||||||
|
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
|
||||||
|
}
|
||||||
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
|
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
|
||||||
|
|
||||||
let tiles = new Array(info.tiles)
|
let tiles = new Array(info.tiles)
|
||||||
|
|
@ -143,8 +157,8 @@ async function createPuzzle(
|
||||||
}
|
}
|
||||||
|
|
||||||
function determinePuzzleTileShapes(
|
function determinePuzzleTileShapes(
|
||||||
/** @type Rng */ rng,
|
rng: Rng,
|
||||||
info
|
info: PuzzleInfo
|
||||||
) {
|
) {
|
||||||
const tabs = [-1, 1]
|
const tabs = [-1, 1]
|
||||||
|
|
||||||
|
|
@ -161,7 +175,7 @@ function determinePuzzleTileShapes(
|
||||||
return shapes.map(Util.encodeShape)
|
return shapes.map(Util.encodeShape)
|
||||||
}
|
}
|
||||||
|
|
||||||
const determineTilesXY = (w, h, targetTiles) => {
|
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
|
||||||
const w_ = w < h ? (w * h) : (w * w)
|
const w_ = w < h ? (w * h) : (w * w)
|
||||||
const h_ = w < h ? (h * h) : (w * h)
|
const h_ = w < h ? (h * h) : (w * h)
|
||||||
let size = 0
|
let size = 0
|
||||||
|
|
@ -177,7 +191,7 @@ const determineTilesXY = (w, h, targetTiles) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const determinePuzzleInfo = (w, h, targetTiles) => {
|
const determinePuzzleInfo = (w: number, h: number, targetTiles: number) => {
|
||||||
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
|
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
|
||||||
const tiles = tilesX * tilesY
|
const tiles = tilesX * tilesY
|
||||||
const tileSize = TILE_SIZE
|
const tileSize = TILE_SIZE
|
||||||
|
|
@ -14,44 +14,49 @@ config = {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class EvtBus {
|
class EvtBus {
|
||||||
|
private _on: any
|
||||||
constructor() {
|
constructor() {
|
||||||
this._on = {}
|
this._on = {} as any
|
||||||
}
|
}
|
||||||
|
|
||||||
on(type, callback) {
|
on(type: string, callback: Function) {
|
||||||
this._on[type] = this._on[type] || []
|
this._on[type] = this._on[type] || []
|
||||||
this._on[type].push(callback)
|
this._on[type].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(type, ...args) {
|
dispatch(type: string, ...args: Array<any>) {
|
||||||
(this._on[type] || []).forEach(cb => {
|
(this._on[type] || []).forEach((cb: Function) => {
|
||||||
cb(...args)
|
cb(...args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketServer {
|
class WebSocketServer {
|
||||||
constructor(config) {
|
evt: EvtBus
|
||||||
|
private _websocketserver: WebSocket.Server|null
|
||||||
|
config: any
|
||||||
|
|
||||||
|
constructor(config: any) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this._websocketserver = null
|
this._websocketserver = null
|
||||||
|
|
||||||
this.evt = new EvtBus()
|
this.evt = new EvtBus()
|
||||||
}
|
}
|
||||||
|
|
||||||
on(type, callback) {
|
on(type: string, callback: Function) {
|
||||||
this.evt.on(type, callback)
|
this.evt.on(type, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
this._websocketserver = new WebSocket.Server(this.config)
|
this._websocketserver = new WebSocket.Server(this.config)
|
||||||
this._websocketserver.on('connection', (socket, request, client) => {
|
this._websocketserver.on('connection', (socket: WebSocket, request: Request) => {
|
||||||
const pathname = new URL(this.config.connectstring).pathname
|
const pathname = new URL(this.config.connectstring).pathname
|
||||||
if (request.url.indexOf(pathname) !== 0) {
|
if (request.url.indexOf(pathname) !== 0) {
|
||||||
log.log('bad request url: ', request.url)
|
log.log('bad request url: ', request.url)
|
||||||
socket.close()
|
socket.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.on('message', (data) => {
|
socket.on('message', (data: any) => {
|
||||||
log.log(`ws`, socket.protocol, data)
|
log.log(`ws`, socket.protocol, data)
|
||||||
this.evt.dispatch('message', {socket, data})
|
this.evt.dispatch('message', {socket, data})
|
||||||
})
|
})
|
||||||
|
|
@ -62,10 +67,12 @@ class WebSocketServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this._websocketserver.close()
|
if (this._websocketserver) {
|
||||||
|
this._websocketserver.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOne(data, socket) {
|
notifyOne(data: any, socket: WebSocket) {
|
||||||
socket.send(JSON.stringify(data))
|
socket.send(JSON.stringify(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,41 @@
|
||||||
import WebSocketServer from './WebSocketServer.js'
|
import WebSocketServer from './WebSocketServer'
|
||||||
|
import WebSocket from 'ws'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import config from './../config.js'
|
import Protocol from './../common/Protocol'
|
||||||
import Protocol from './../common/Protocol.js'
|
import Util, { logger } from './../common/Util'
|
||||||
import Util, { logger } from './../common/Util.js'
|
import Game from './Game'
|
||||||
import Game from './Game.js'
|
|
||||||
import bodyParser from 'body-parser'
|
import bodyParser from 'body-parser'
|
||||||
import v8 from 'v8'
|
import v8 from 'v8'
|
||||||
import GameLog from './GameLog.js'
|
import fs from 'fs'
|
||||||
import GameSockets from './GameSockets.js'
|
import GameLog from './GameLog'
|
||||||
import Time from '../common/Time.js'
|
import GameSockets from './GameSockets'
|
||||||
import Images from './Images.js'
|
import Time from '../common/Time'
|
||||||
|
import Images from './Images'
|
||||||
import {
|
import {
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
UPLOAD_URL,
|
UPLOAD_URL,
|
||||||
COMMON_DIR,
|
|
||||||
PUBLIC_DIR,
|
PUBLIC_DIR,
|
||||||
} from './Dirs.js'
|
} from './Dirs'
|
||||||
import GameCommon from '../common/GameCommon.js'
|
import GameCommon from '../common/GameCommon'
|
||||||
import GameStorage from './GameStorage.js'
|
import GameStorage from './GameStorage'
|
||||||
|
|
||||||
const log = logger('index.js')
|
let configFile = ''
|
||||||
|
let last = ''
|
||||||
|
for (const val of process.argv) {
|
||||||
|
if (last === '-c') {
|
||||||
|
configFile = val
|
||||||
|
}
|
||||||
|
last = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configFile === '') {
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(String(fs.readFileSync(configFile)))
|
||||||
|
|
||||||
|
const log = logger('main.js')
|
||||||
|
|
||||||
const port = config.http.port
|
const port = config.http.port
|
||||||
const hostname = config.http.hostname
|
const hostname = config.http.hostname
|
||||||
|
|
@ -50,7 +64,7 @@ app.get('/api/newgame-data', (req, res) => {
|
||||||
app.get('/api/index-data', (req, res) => {
|
app.get('/api/index-data', (req, res) => {
|
||||||
const ts = Time.timestamp()
|
const ts = Time.timestamp()
|
||||||
const games = [
|
const games = [
|
||||||
...Game.getAllGames().map(game => ({
|
...Game.getAllGames().map((game: any) => ({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
hasReplay: GameLog.exists(game.id),
|
hasReplay: GameLog.exists(game.id),
|
||||||
started: Game.getStartTs(game.id),
|
started: Game.getStartTs(game.id),
|
||||||
|
|
@ -69,7 +83,7 @@ app.get('/api/index-data', (req, res) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/upload', (req, res) => {
|
app.post('/upload', (req, res) => {
|
||||||
upload(req, res, async (err) => {
|
upload(req, res, async (err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.log(err)
|
log.log(err)
|
||||||
res.status(400).send("Something went wrong!");
|
res.status(400).send("Something went wrong!");
|
||||||
|
|
@ -107,20 +121,19 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
|
||||||
res.send({ id: gameId })
|
res.send({ id: gameId })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/common/', express.static(COMMON_DIR))
|
|
||||||
app.use('/uploads/', express.static(UPLOAD_DIR))
|
app.use('/uploads/', express.static(UPLOAD_DIR))
|
||||||
app.use('/', express.static(PUBLIC_DIR))
|
app.use('/', express.static(PUBLIC_DIR))
|
||||||
|
|
||||||
const wss = new WebSocketServer(config.ws);
|
const wss = new WebSocketServer(config.ws);
|
||||||
|
|
||||||
const notify = (data, sockets) => {
|
const notify = (data: any, sockets: Array<WebSocket>) => {
|
||||||
// TODO: throttle?
|
// TODO: throttle?
|
||||||
for (let socket of sockets) {
|
for (let socket of sockets) {
|
||||||
wss.notifyOne(data, socket)
|
wss.notifyOne(data, socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.on('close', async ({socket}) => {
|
wss.on('close', async ({socket} : {socket: WebSocket}) => {
|
||||||
try {
|
try {
|
||||||
const proto = socket.protocol.split('|')
|
const proto = socket.protocol.split('|')
|
||||||
const clientId = proto[0]
|
const clientId = proto[0]
|
||||||
|
|
@ -131,7 +144,7 @@ wss.on('close', async ({socket}) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wss.on('message', async ({socket, data}) => {
|
wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
|
||||||
try {
|
try {
|
||||||
const proto = socket.protocol.split('|')
|
const proto = socket.protocol.split('|')
|
||||||
const clientId = proto[0]
|
const clientId = proto[0]
|
||||||
|
|
@ -236,7 +249,7 @@ const persistInterval = setInterval(() => {
|
||||||
memoryUsageHuman()
|
memoryUsageHuman()
|
||||||
}, config.persistence.interval)
|
}, config.persistence.interval)
|
||||||
|
|
||||||
const gracefulShutdown = (signal) => {
|
const gracefulShutdown = (signal: any) => {
|
||||||
log.log(`${signal} received...`)
|
log.log(`${signal} received...`)
|
||||||
|
|
||||||
log.log('clearing persist interval...')
|
log.log('clearing persist interval...')
|
||||||
14
tsconfig.server.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "es2020",
|
||||||
|
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts", "src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
vite.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import vite from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default vite.defineConfig({
|
||||||
|
plugins: [ vue() ],
|
||||||
|
root: './src/frontend',
|
||||||
|
build: {
|
||||||
|
outDir: '../../build/public',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
})
|
||||||