switch to typescript
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/node_modules
|
||||
/config.js
|
||||
/config.json
|
||||
/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
|
||||
}
|
||||
}
|
||||
7342
package-lock.json
generated
21
package.json
|
|
@ -7,9 +7,28 @@
|
|||
"image-size": "^0.9.3",
|
||||
"multer": "^1.4.2",
|
||||
"sharp": "^0.28.1",
|
||||
"vue": "^3.0.11",
|
||||
"vue-router": "^4.0.8",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"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
|
||||
|
||||
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 Protocol from './Protocol.js'
|
||||
import Time from './Time.js'
|
||||
import Util from './Util.js'
|
||||
import Geometry from './Geometry'
|
||||
import Protocol from './Protocol'
|
||||
import { Rng } from './Rng'
|
||||
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_ANY = 1
|
||||
|
||||
const IDLE_TIMEOUT_SEC = 30
|
||||
|
||||
// Map<gameId, GameObject>
|
||||
const GAMES = {}
|
||||
// Map<gameId, Game>
|
||||
const GAMES: Record<string, Game> = {}
|
||||
|
||||
function exists(gameId) {
|
||||
function exists(gameId: string) {
|
||||
return (!!GAMES[gameId]) || false
|
||||
}
|
||||
|
||||
function __createPlayerObject(id, ts) {
|
||||
function __createPlayerObject(id: string, ts: number): Player {
|
||||
return {
|
||||
id: id,
|
||||
x: 0,
|
||||
|
|
@ -29,11 +119,11 @@ function __createPlayerObject(id, ts) {
|
|||
}
|
||||
}
|
||||
|
||||
function setGame(gameId, game) {
|
||||
function setGame(gameId: string, game: Game) {
|
||||
GAMES[gameId] = game
|
||||
}
|
||||
|
||||
function getPlayerIndexById(gameId, playerId) {
|
||||
function getPlayerIndexById(gameId: string, playerId: string): number {
|
||||
let i = 0;
|
||||
for (let player of GAMES[gameId].players) {
|
||||
if (Util.decodePlayer(player).id === playerId) {
|
||||
|
|
@ -44,19 +134,19 @@ function getPlayerIndexById(gameId, playerId) {
|
|||
return -1
|
||||
}
|
||||
|
||||
function getPlayerIdByIndex(gameId, playerIndex) {
|
||||
function getPlayerIdByIndex(gameId: string, playerIndex: number) {
|
||||
if (GAMES[gameId].players.length > playerIndex) {
|
||||
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getPlayer(gameId, playerId) {
|
||||
function getPlayer(gameId: string, playerId: string) {
|
||||
let idx = getPlayerIndexById(gameId, playerId)
|
||||
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)
|
||||
if (idx === -1) {
|
||||
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)
|
||||
}
|
||||
|
||||
function setPuzzleData(gameId, data) {
|
||||
function setPuzzleData(gameId: string, data: PuzzleData) {
|
||||
GAMES[gameId].puzzle.data = data
|
||||
}
|
||||
|
||||
function playerExists(gameId, playerId) {
|
||||
function playerExists(gameId: string, playerId: string) {
|
||||
const idx = getPlayerIndexById(gameId, playerId)
|
||||
return idx !== -1
|
||||
}
|
||||
|
||||
function getActivePlayers(gameId, ts) {
|
||||
function getActivePlayers(gameId: string, ts: number) {
|
||||
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
|
||||
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)) {
|
||||
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
||||
} 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) {
|
||||
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
|
||||
}
|
||||
|
||||
function getAllGames() {
|
||||
return Object.values(GAMES).sort((a, b) => {
|
||||
function getAllGames(): Array<Game> {
|
||||
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
||||
// when both have same finished state, sort by started
|
||||
if (isFinished(a.id) === isFinished(b.id)) {
|
||||
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]
|
||||
? GAMES[gameId].players.map(Util.decodePlayer)
|
||||
: []
|
||||
}
|
||||
|
||||
function get(gameId) {
|
||||
function get(gameId: string) {
|
||||
return GAMES[gameId]
|
||||
}
|
||||
|
||||
function getTileCount(gameId) {
|
||||
function getTileCount(gameId: string) {
|
||||
return GAMES[gameId].puzzle.tiles.length
|
||||
}
|
||||
|
||||
function getImageUrl(gameId) {
|
||||
function getImageUrl(gameId: string) {
|
||||
return GAMES[gameId].puzzle.info.imageUrl
|
||||
}
|
||||
|
||||
function setImageUrl(gameId, imageUrl) {
|
||||
function setImageUrl(gameId: string, imageUrl: string) {
|
||||
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
||||
}
|
||||
|
||||
function getScoreMode(gameId) {
|
||||
function getScoreMode(gameId: string) {
|
||||
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
|
||||
}
|
||||
|
||||
function isFinished(gameId) {
|
||||
function isFinished(gameId: string) {
|
||||
return getFinishedTileCount(gameId) === getTileCount(gameId)
|
||||
}
|
||||
|
||||
function getFinishedTileCount(gameId) {
|
||||
function getFinishedTileCount(gameId: string) {
|
||||
let count = 0
|
||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||
if (Util.decodeTile(t).owner === -1) {
|
||||
|
|
@ -161,12 +251,12 @@ function getFinishedTileCount(gameId) {
|
|||
return count
|
||||
}
|
||||
|
||||
function getTilesSortedByZIndex(gameId) {
|
||||
function getTilesSortedByZIndex(gameId: string) {
|
||||
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
|
||||
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)
|
||||
for (let k of Object.keys(change)) {
|
||||
player[k] = change[k]
|
||||
|
|
@ -174,13 +264,14 @@ function changePlayer(gameId, playerId, change) {
|
|||
setPlayer(gameId, playerId, player)
|
||||
}
|
||||
|
||||
function changeData(gameId, change) {
|
||||
function changeData(gameId: string, change: any) {
|
||||
for (let k of Object.keys(change)) {
|
||||
// @ts-ignore
|
||||
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)) {
|
||||
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||
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])
|
||||
}
|
||||
|
||||
const getTileGroup = (gameId, tileIdx) => {
|
||||
const getTileGroup = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.group
|
||||
}
|
||||
|
||||
const getFinalTilePos = (gameId, tileIdx) => {
|
||||
const getFinalTilePos = (gameId: string, tileIdx: number) => {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
const boardPos = {
|
||||
x: (info.table.width - info.width) / 2,
|
||||
|
|
@ -207,13 +298,13 @@ const getFinalTilePos = (gameId, tileIdx) => {
|
|||
return Geometry.pointAdd(boardPos, srcPos)
|
||||
}
|
||||
|
||||
const getTilePos = (gameId, tileIdx) => {
|
||||
const getTilePos = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.pos
|
||||
}
|
||||
|
||||
// todo: instead, just make the table bigger and use that :)
|
||||
const getBounds = (gameId) => {
|
||||
const getBounds = (gameId: string) => {
|
||||
const tw = getTableWidth(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 tile = getTile(gameId, tileIdx)
|
||||
return {
|
||||
|
|
@ -238,55 +329,55 @@ const getTileBounds = (gameId, tileIdx) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getTileZIndex = (gameId, tileIdx) => {
|
||||
const getTileZIndex = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.z
|
||||
}
|
||||
|
||||
const getFirstOwnedTileIdx = (gameId, userId) => {
|
||||
const getFirstOwnedTileIdx = (gameId: string, playerId: string) => {
|
||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||
const tile = Util.decodeTile(t)
|
||||
if (tile.owner === userId) {
|
||||
if (tile.owner === playerId) {
|
||||
return tile.idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const getFirstOwnedTile = (gameId, userId) => {
|
||||
const idx = getFirstOwnedTileIdx(gameId, userId)
|
||||
const getFirstOwnedTile = (gameId: string, playerId: string) => {
|
||||
const idx = getFirstOwnedTileIdx(gameId, playerId)
|
||||
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
||||
}
|
||||
|
||||
const getTileDrawOffset = (gameId) => {
|
||||
const getTileDrawOffset = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileDrawOffset
|
||||
}
|
||||
|
||||
const getTileDrawSize = (gameId) => {
|
||||
const getTileDrawSize = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileDrawSize
|
||||
}
|
||||
|
||||
const getTileSize = (gameId) => {
|
||||
const getTileSize = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileSize
|
||||
}
|
||||
|
||||
const getStartTs = (gameId) => {
|
||||
const getStartTs = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.started
|
||||
}
|
||||
|
||||
const getFinishTs = (gameId) => {
|
||||
const getFinishTs = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.finished
|
||||
}
|
||||
|
||||
const getMaxGroup = (gameId) => {
|
||||
const getMaxGroup = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.maxGroup
|
||||
}
|
||||
|
||||
const getMaxZIndex = (gameId) => {
|
||||
const getMaxZIndex = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.maxZ
|
||||
}
|
||||
|
||||
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
|
||||
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>) => {
|
||||
let maxZ = 0
|
||||
for (let tileIdx of tileIdxs) {
|
||||
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
||||
|
|
@ -297,7 +388,7 @@ const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
|
|||
return maxZ
|
||||
}
|
||||
|
||||
function srcPosByTileIdx(gameId, tileIdx) {
|
||||
function srcPosByTileIdx(gameId: string, tileIdx: number) {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
|
||||
const c = Util.coordByTileIdx(info, tileIdx)
|
||||
|
|
@ -307,7 +398,7 @@ function srcPosByTileIdx(gameId, tileIdx) {
|
|||
return { x: cx, y: cy }
|
||||
}
|
||||
|
||||
function getSurroundingTilesByIdx(gameId, tileIdx) {
|
||||
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
|
||||
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) {
|
||||
changeTile(gameId, tilesIdx, { z: zIndex })
|
||||
}
|
||||
}
|
||||
|
||||
const moveTileDiff = (gameId, tileIdx, diff) => {
|
||||
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point) => {
|
||||
const oldPos = getTilePos(gameId, tileIdx)
|
||||
const pos = Geometry.pointAdd(oldPos, diff)
|
||||
changeTile(gameId, tileIdx, { pos })
|
||||
}
|
||||
|
||||
const moveTilesDiff = (gameId, tileIdxs, diff) => {
|
||||
const moveTilesDiff = (gameId: string, tileIdxs: Array<number>, diff: Point) => {
|
||||
const tileDrawSize = getTileDrawSize(gameId)
|
||||
const bounds = getBounds(gameId)
|
||||
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) {
|
||||
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) {
|
||||
changeTile(gameId, tileIdx, { owner })
|
||||
}
|
||||
}
|
||||
|
||||
// get all grouped tiles for a tile
|
||||
function getGroupedTileIdxs(gameId, tileIdx) {
|
||||
function getGroupedTileIdxs(gameId: string, tileIdx: number) {
|
||||
const tiles = GAMES[gameId].puzzle.tiles
|
||||
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
|
||||
// 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 tiles = GAMES[gameId].puzzle.tiles
|
||||
|
||||
|
|
@ -421,75 +516,75 @@ const freeTileIdxByPos = (gameId, pos) => {
|
|||
return tileIdx
|
||||
}
|
||||
|
||||
const getPlayerBgColor = (gameId, playerId) => {
|
||||
const getPlayerBgColor = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.bgcolor : null
|
||||
}
|
||||
|
||||
const getPlayerColor = (gameId, playerId) => {
|
||||
const getPlayerColor = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.color : null
|
||||
}
|
||||
|
||||
const getPlayerName = (gameId, playerId) => {
|
||||
const getPlayerName = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.name : null
|
||||
}
|
||||
|
||||
const getPlayerPoints = (gameId, playerId) => {
|
||||
const getPlayerPoints = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.points : null
|
||||
}
|
||||
|
||||
// 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 g2 = getTileGroup(gameId, tileIdx2)
|
||||
return g1 && g1 === g2
|
||||
}
|
||||
|
||||
const getTableWidth = (gameId) => {
|
||||
const getTableWidth = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.table.width
|
||||
}
|
||||
|
||||
const getTableHeight = (gameId) => {
|
||||
const getTableHeight = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.table.height
|
||||
}
|
||||
|
||||
const getPuzzle = (gameId) => {
|
||||
const getPuzzle = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle
|
||||
}
|
||||
|
||||
const getRng = (gameId) => {
|
||||
const getRng = (gameId: string): Rng => {
|
||||
return GAMES[gameId].rng.obj
|
||||
}
|
||||
|
||||
const getPuzzleWidth = (gameId) => {
|
||||
const getPuzzleWidth = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.width
|
||||
}
|
||||
|
||||
const getPuzzleHeight = (gameId) => {
|
||||
const getPuzzleHeight = (gameId: string) => {
|
||||
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 evtInfo = getEvtInfo(gameId, playerId)
|
||||
|
||||
const changes = []
|
||||
const changes = [] as Array<Array<any>>
|
||||
|
||||
const _dataChange = () => {
|
||||
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
||||
}
|
||||
|
||||
const _tileChange = (tileIdx) => {
|
||||
const _tileChange = (tileIdx: number) => {
|
||||
changes.push([
|
||||
Protocol.CHANGE_TILE,
|
||||
Util.encodeTile(getTile(gameId, tileIdx)),
|
||||
])
|
||||
}
|
||||
|
||||
const _tileChanges = (tileIdxs) => {
|
||||
const _tileChanges = (tileIdxs: Array<number>) => {
|
||||
for (const tileIdx of tileIdxs) {
|
||||
_tileChange(tileIdx)
|
||||
}
|
||||
|
|
@ -503,7 +598,7 @@ function handleInput(gameId, playerId, input, ts) {
|
|||
}
|
||||
|
||||
// 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 group1 = getTileGroup(gameId, tileIdx1)
|
||||
const group2 = getTileGroup(gameId, tileIdx2)
|
||||
|
|
@ -669,7 +764,12 @@ function handleInput(gameId, playerId, input, ts) {
|
|||
}
|
||||
} else {
|
||||
// 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
|
||||
if (otherTileIdx < 0) {
|
||||
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 }
|
||||
}
|
||||
|
||||
function pointAdd(a, b) {
|
||||
function pointAdd(a: Point, b: Point): Point {
|
||||
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 diffY = a.y - b.y
|
||||
return Math.sqrt(diffX * diffX + diffY * diffY)
|
||||
}
|
||||
|
||||
function pointInBounds(pt, rect) {
|
||||
function pointInBounds(pt: Point, rect: Rect): boolean {
|
||||
return pt.x >= rect.x
|
||||
&& pt.x <= rect.x + rect.w
|
||||
&& pt.y >= rect.y
|
||||
&& pt.y <= rect.y + rect.h
|
||||
}
|
||||
|
||||
function rectCenter(rect) {
|
||||
function rectCenter(rect: Rect): Point {
|
||||
return {
|
||||
x: rect.x + (rect.w / 2),
|
||||
y: rect.y + (rect.h / 2),
|
||||
|
|
@ -35,7 +47,7 @@ function rectCenter(rect) {
|
|||
* @param number y
|
||||
* @returns {x, y, w, h}
|
||||
*/
|
||||
function rectMoved(rect, x, y) {
|
||||
function rectMoved(rect: Rect, x: number, y: number): Rect {
|
||||
return {
|
||||
x: rect.x + x,
|
||||
y: rect.y + y,
|
||||
|
|
@ -51,7 +63,7 @@ function rectMoved(rect, x, y) {
|
|||
* @param {x, y, w, h} rectB
|
||||
* @returns bool
|
||||
*/
|
||||
function rectsOverlap(rectA, rectB) {
|
||||
function rectsOverlap(rectA: Rect, rectB: Rect): boolean {
|
||||
return !(
|
||||
rectB.x > (rectA.x + rectA.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))
|
||||
}
|
||||
|
||||
|
|
@ -1,24 +1,32 @@
|
|||
interface RngSerialized {
|
||||
rand_high: number,
|
||||
rand_low: number,
|
||||
}
|
||||
|
||||
export class Rng {
|
||||
constructor(seed) {
|
||||
rand_high: number
|
||||
rand_low: number
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rand_high = seed || 0xDEADC0DE
|
||||
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_low = (this.rand_low + this.rand_high) & 0xffffffff;
|
||||
var n = (this.rand_high >>> 0) / 0xffffffff;
|
||||
return (min + n * (max-min+1))|0;
|
||||
}
|
||||
|
||||
static serialize (rng) {
|
||||
static serialize (rng: Rng): RngSerialized {
|
||||
return {
|
||||
rand_high: rng.rand_high,
|
||||
rand_low: rng.rand_low
|
||||
}
|
||||
}
|
||||
|
||||
static unserialize (rngSerialized) {
|
||||
static unserialize (rngSerialized: RngSerialized): Rng {
|
||||
const rng = new Rng(0)
|
||||
rng.rand_high = rngSerialized.rand_high
|
||||
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)
|
||||
duration = duration % DAY
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export const durationStr = (duration) => {
|
|||
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 {
|
||||
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}`
|
||||
if (str.length >= pad.length) {
|
||||
return str
|
||||
|
|
@ -9,8 +9,8 @@ const pad = (x, pad) => {
|
|||
return pad.substr(0, pad.length - str.length) + str
|
||||
}
|
||||
|
||||
export const logger = (...pre) => {
|
||||
const log = (m) => (...args) => {
|
||||
export const logger = (...pre: Array<any>) => {
|
||||
const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
|
||||
const d = new Date()
|
||||
const hh = pad(d.getHours(), '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)
|
||||
export const randomInt = (
|
||||
/** @type Rng */ rng,
|
||||
min,
|
||||
max
|
||||
rng: Rng,
|
||||
min: number,
|
||||
max: number,
|
||||
) => rng.random(min, max)
|
||||
|
||||
// get one random item from the given array
|
||||
export const choice = (
|
||||
/** @type Rng */ rng,
|
||||
array
|
||||
rng: Rng,
|
||||
array: Array<any>
|
||||
) => 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
|
||||
export const shuffle = (
|
||||
/** @type Rng */ rng,
|
||||
array
|
||||
rng: Rng,
|
||||
array: Array<any>
|
||||
) => {
|
||||
const arr = array.slice()
|
||||
for (let i = 0; i <= arr.length - 2; i++)
|
||||
|
|
@ -69,7 +56,7 @@ export const shuffle = (
|
|||
return arr
|
||||
}
|
||||
|
||||
function encodeShape(data) {
|
||||
function encodeShape(data: any): number {
|
||||
if (typeof data === 'number') {
|
||||
return data
|
||||
}
|
||||
|
|
@ -86,7 +73,7 @@ function encodeShape(data) {
|
|||
| ((data.left + 1) << 6)
|
||||
}
|
||||
|
||||
function decodeShape(data) {
|
||||
function decodeShape(data: any) {
|
||||
if (typeof data !== 'number') {
|
||||
return data
|
||||
}
|
||||
|
|
@ -98,14 +85,14 @@ function decodeShape(data) {
|
|||
}
|
||||
}
|
||||
|
||||
function encodeTile(data) {
|
||||
function encodeTile(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
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)) {
|
||||
return data
|
||||
}
|
||||
|
|
@ -121,7 +108,7 @@ function decodeTile(data) {
|
|||
}
|
||||
}
|
||||
|
||||
function encodePlayer(data) {
|
||||
function encodePlayer(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
|
@ -138,7 +125,7 @@ function encodePlayer(data) {
|
|||
]
|
||||
}
|
||||
|
||||
function decodePlayer(data) {
|
||||
function decodePlayer(data: any) {
|
||||
if (!Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
|
@ -155,7 +142,7 @@ function decodePlayer(data) {
|
|||
}
|
||||
}
|
||||
|
||||
function encodeGame(data) {
|
||||
function encodeGame(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
|
@ -170,7 +157,7 @@ function encodeGame(data) {
|
|||
]
|
||||
}
|
||||
|
||||
function decodeGame(data) {
|
||||
function decodeGame(data: any) {
|
||||
if (!Array.isArray(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
|
||||
return {
|
||||
x: tileIdx % wTiles,
|
||||
|
|
@ -195,7 +182,7 @@ function coordByTileIdx(info, tileIdx) {
|
|||
}
|
||||
}
|
||||
|
||||
const hash = (str) => {
|
||||
const hash = (str: string): number => {
|
||||
let hash = 0
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
|
|
@ -211,7 +198,6 @@ export default {
|
|||
uniqId,
|
||||
randomInt,
|
||||
choice,
|
||||
throttle,
|
||||
shuffle,
|
||||
|
||||
encodeShape,
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
export default {
|
||||
name: 'app',
|
||||
template: `
|
||||
<template>
|
||||
<div id="app">
|
||||
<ul class="nav" v-if="showNav">
|
||||
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
|
||||
|
|
@ -8,11 +6,18 @@ export default {
|
|||
</ul>
|
||||
|
||||
<router-view />
|
||||
</div>`,
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
computed: {
|
||||
showNav () {
|
||||
// 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 MAX_ZOOM = 6
|
||||
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 () {
|
||||
let x = 0
|
||||
let y = 0
|
||||
let curZoom = 1
|
||||
|
||||
const move = (byX, byY) => {
|
||||
const move = (byX: number, byY: number) => {
|
||||
x += byX / curZoom
|
||||
y += byY / curZoom
|
||||
}
|
||||
|
||||
const calculateNewZoom = (inout) => {
|
||||
const calculateNewZoom = (inout: ZOOM_DIR): number => {
|
||||
const factor = inout === 'in' ? 1 : -1
|
||||
const newzoom = curZoom + ZOOM_STEP * curZoom * factor
|
||||
const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM)
|
||||
return capped
|
||||
}
|
||||
|
||||
const canZoom = (inout) => {
|
||||
const canZoom = (inout: ZOOM_DIR): boolean => {
|
||||
return curZoom != calculateNewZoom(inout)
|
||||
}
|
||||
|
||||
const setZoom = (newzoom, viewportCoordCenter) => {
|
||||
const setZoom = (newzoom: number, viewportCoordCenter: Point): boolean => {
|
||||
if (curZoom == newzoom) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -43,7 +51,7 @@ export default function Camera () {
|
|||
* Zooms towards/away from the provided coordinate, if possible.
|
||||
* 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)
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +60,7 @@ export default function Camera () {
|
|||
* coordinate in the world, rounded
|
||||
* @param {x, y} viewportCoord
|
||||
*/
|
||||
const viewportToWorld = (viewportCoord) => {
|
||||
const viewportToWorld = (viewportCoord: Point): Point => {
|
||||
const { x, y } = viewportToWorldRaw(viewportCoord)
|
||||
return { x: Math.round(x), y: Math.round(y) }
|
||||
}
|
||||
|
|
@ -62,7 +70,7 @@ export default function Camera () {
|
|||
* coordinate in the world, not rounded
|
||||
* @param {x, y} viewportCoord
|
||||
*/
|
||||
const viewportToWorldRaw = (viewportCoord) => {
|
||||
const viewportToWorldRaw = (viewportCoord: Point): Point => {
|
||||
return {
|
||||
x: (viewportCoord.x / curZoom) - x,
|
||||
y: (viewportCoord.y / curZoom) - y,
|
||||
|
|
@ -74,7 +82,7 @@ export default function Camera () {
|
|||
* coordinate in the viewport, rounded
|
||||
* @param {x, y} worldCoord
|
||||
*/
|
||||
const worldToViewport = (worldCoord) => {
|
||||
const worldToViewport = (worldCoord: Point): Point => {
|
||||
const { x, y } = worldToViewportRaw(worldCoord)
|
||||
return { x: Math.round(x), y: Math.round(y) }
|
||||
}
|
||||
|
|
@ -84,7 +92,7 @@ export default function Camera () {
|
|||
* coordinate in the viewport, not rounded
|
||||
* @param {x, y} worldCoord
|
||||
*/
|
||||
const worldToViewportRaw = (worldCoord) => {
|
||||
const worldToViewportRaw = (worldCoord: Point): Point => {
|
||||
return {
|
||||
x: (worldCoord.x + x) * curZoom,
|
||||
y: (worldCoord.y + y) * curZoom,
|
||||
|
|
@ -96,7 +104,7 @@ export default function Camera () {
|
|||
* one in the viewport, rounded
|
||||
* @param {w, h} worldDim
|
||||
*/
|
||||
const worldDimToViewport = (worldDim) => {
|
||||
const worldDimToViewport = (worldDim: Dim): Dim => {
|
||||
const { w, h } = worldDimToViewportRaw(worldDim)
|
||||
return { w: Math.round(w), h: Math.round(h) }
|
||||
}
|
||||
|
|
@ -107,7 +115,7 @@ export default function Camera () {
|
|||
* one in the viewport, not rounded
|
||||
* @param {w, h} worldDim
|
||||
*/
|
||||
const worldDimToViewportRaw = (worldDim) => {
|
||||
const worldDimToViewportRaw = (worldDim: Dim): Dim => {
|
||||
return {
|
||||
w: worldDim.w * curZoom,
|
||||
h: worldDim.h * curZoom,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict"
|
||||
|
||||
import { logger } from '../common/Util.js'
|
||||
import Protocol from './../common/Protocol.js'
|
||||
import { logger } from '../common/Util'
|
||||
import Protocol from './../common/Protocol'
|
||||
|
||||
const log = logger('Communication.js')
|
||||
|
||||
|
|
@ -14,27 +14,26 @@ const CONN_STATE_CONNECTED = 2 // connected
|
|||
const CONN_STATE_CONNECTING = 3 // connecting
|
||||
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
|
||||
|
||||
/** @type WebSocket */
|
||||
let ws
|
||||
let changesCallback = () => {}
|
||||
let connectionStateChangeCallback = () => {}
|
||||
let ws: WebSocket
|
||||
let changesCallback = (msg: Array<any>) => {}
|
||||
let connectionStateChangeCallback = (state: number) => {}
|
||||
|
||||
// TODO: change these to something like on(EVT, cb)
|
||||
function onServerChange(callback) {
|
||||
function onServerChange(callback: (msg: Array<any>) => void) {
|
||||
changesCallback = callback
|
||||
}
|
||||
function onConnectionStateChange(callback) {
|
||||
function onConnectionStateChange(callback: (state: number) => void) {
|
||||
connectionStateChangeCallback = callback
|
||||
}
|
||||
|
||||
let connectionState = CONN_STATE_NOT_CONNECTED
|
||||
const setConnectionState = (v) => {
|
||||
if (connectionState !== v) {
|
||||
connectionState = v
|
||||
connectionStateChangeCallback(v)
|
||||
const setConnectionState = (state: number) => {
|
||||
if (connectionState !== state) {
|
||||
connectionState = state
|
||||
connectionStateChangeCallback(state)
|
||||
}
|
||||
}
|
||||
function send(message) {
|
||||
function send(message: Array<any>): void {
|
||||
if (connectionState === CONN_STATE_CONNECTED) {
|
||||
try {
|
||||
ws.send(JSON.stringify(message))
|
||||
|
|
@ -45,9 +44,14 @@ function send(message) {
|
|||
}
|
||||
|
||||
|
||||
let clientSeq
|
||||
let events
|
||||
function connect(address, gameId, clientId) {
|
||||
let clientSeq: number
|
||||
let events: Record<number, any>
|
||||
|
||||
function connect(
|
||||
address: string,
|
||||
gameId: string,
|
||||
clientId: string
|
||||
): Promise<any> {
|
||||
clientSeq = 0
|
||||
events = {}
|
||||
setConnectionState(CONN_STATE_CONNECTING)
|
||||
|
|
@ -55,7 +59,6 @@ function connect(address, gameId, clientId) {
|
|||
ws = new WebSocket(address, clientId + '|' + gameId)
|
||||
ws.onopen = (e) => {
|
||||
setConnectionState(CONN_STATE_CONNECTED)
|
||||
connectionStateChangeCallback()
|
||||
send([Protocol.EV_CLIENT_INIT])
|
||||
}
|
||||
ws.onmessage = (e) => {
|
||||
|
|
@ -94,7 +97,11 @@ function connect(address, gameId, clientId) {
|
|||
}
|
||||
|
||||
// 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
|
||||
events = {}
|
||||
setConnectionState(CONN_STATE_CONNECTING)
|
||||
|
|
@ -110,7 +117,8 @@ function connectReplay(address, gameId, clientId) {
|
|||
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
|
||||
const game = msg[1]
|
||||
const log = msg[2]
|
||||
resolve({game, log})
|
||||
const replay: { game: any, log: Array<any> } = { game, log }
|
||||
resolve(replay)
|
||||
} else {
|
||||
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
|
||||
}
|
||||
|
|
@ -131,7 +139,7 @@ function connectReplay(address, gameId, clientId) {
|
|||
})
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
function disconnect(): void {
|
||||
if (ws) {
|
||||
ws.close(CODE_CUSTOM_DISCONNECT)
|
||||
}
|
||||
|
|
@ -139,7 +147,7 @@ function disconnect() {
|
|||
events = {}
|
||||
}
|
||||
|
||||
function sendClientEvent(evt) {
|
||||
function sendClientEvent(evt: any): void {
|
||||
// when sending event, increase number of sent events
|
||||
// and add the event locally
|
||||
clientSeq++;
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
"use strict"
|
||||
|
||||
import { logger } from '../common/Util.js'
|
||||
import { logger } from '../common/Util'
|
||||
|
||||
const log = logger('Debug.js')
|
||||
|
||||
let _pt = 0
|
||||
let _mindiff = 0
|
||||
|
||||
const checkpoint_start = (mindiff) => {
|
||||
const checkpoint_start = (mindiff: number) => {
|
||||
_pt = performance.now()
|
||||
_mindiff = mindiff
|
||||
}
|
||||
|
||||
const checkpoint = (label) => {
|
||||
const checkpoint = (label: string) => {
|
||||
const now = performance.now()
|
||||
const diff = now - _pt
|
||||
if (diff > _mindiff) {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict"
|
||||
|
||||
import { Rng } from '../common/Rng.js'
|
||||
import Util from '../common/Util.js'
|
||||
import { Rng } from '../common/Rng'
|
||||
import Util from '../common/Util'
|
||||
|
||||
let minVx = -10
|
||||
let deltaVx = 20
|
||||
|
|
@ -20,7 +20,7 @@ const explosionDividerFactor = 10
|
|||
const nBombs = 1
|
||||
const percentChanceNewBomb = 5
|
||||
|
||||
function color(/** @type Rng */ rng) {
|
||||
function color(rng: Rng) {
|
||||
const r = Util.randomInt(rng, 0, 255)
|
||||
const g = 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.
|
||||
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.previousRadius = bombRadius
|
||||
this.explodingDuration = explodingDuration
|
||||
|
|
@ -46,7 +58,7 @@ class Bomb {
|
|||
this.duration = 0
|
||||
}
|
||||
|
||||
update(particlesVector) {
|
||||
update(particlesVector?: Array<Particle>) {
|
||||
if (this.hasExploded) {
|
||||
const deltaRadius = explosionRadius - this.radius
|
||||
this.previousRadius = this.radius
|
||||
|
|
@ -60,15 +72,17 @@ class Bomb {
|
|||
this.vx += 0
|
||||
this.vy += gravity
|
||||
if (this.vy >= 0) { // invertion point
|
||||
if (particlesVector) {
|
||||
this.explode(particlesVector)
|
||||
}
|
||||
}
|
||||
|
||||
this.px += this.vx
|
||||
this.py += this.vy
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false)
|
||||
if (!this.hasExploded) {
|
||||
|
|
@ -78,7 +92,7 @@ class Bomb {
|
|||
}
|
||||
}
|
||||
|
||||
explode(particlesVector) {
|
||||
explode(particlesVector: Array<Particle>) {
|
||||
this.hasExploded = true
|
||||
const e = 3 + Math.floor(Math.random() * 3)
|
||||
for (let j = 0; j < e; j++) {
|
||||
|
|
@ -94,7 +108,15 @@ class Bomb {
|
|||
}
|
||||
|
||||
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.py = parent.py
|
||||
this.vx = Math.cos(angle) * speed
|
||||
|
|
@ -102,6 +124,7 @@ class Particle {
|
|||
this.color = parent.color
|
||||
this.duration = 40 + Math.floor(Math.random() * 20)
|
||||
this.alive = true
|
||||
this.radius = 0
|
||||
}
|
||||
update() {
|
||||
this.vx += 0
|
||||
|
|
@ -117,7 +140,7 @@ class Particle {
|
|||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false)
|
||||
ctx.fillStyle = this.color
|
||||
|
|
@ -127,12 +150,22 @@ class Particle {
|
|||
}
|
||||
|
||||
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.rng = rng
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
this.resize()
|
||||
|
||||
this.readyBombs = []
|
||||
this.explodedBombs = []
|
||||
this.particles = []
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.resize()
|
||||
})
|
||||
|
|
@ -177,6 +210,9 @@ class Controller {
|
|||
const aliveBombs = []
|
||||
while (this.explodedBombs.length > 0) {
|
||||
const bomb = this.explodedBombs.shift()
|
||||
if (!bomb) {
|
||||
break;
|
||||
}
|
||||
bomb.update()
|
||||
if (bomb.alive) {
|
||||
aliveBombs.push(bomb)
|
||||
|
|
@ -187,6 +223,9 @@ class Controller {
|
|||
const notExplodedBombs = []
|
||||
while (this.readyBombs.length > 0) {
|
||||
const bomb = this.readyBombs.shift()
|
||||
if (!bomb) {
|
||||
break
|
||||
}
|
||||
bomb.update(this.particles)
|
||||
if (bomb.hasExploded) {
|
||||
this.explodedBombs.push(bomb)
|
||||
|
|
@ -200,6 +239,9 @@ class Controller {
|
|||
const aliveParticles = []
|
||||
while (this.particles.length > 0) {
|
||||
const particle = this.particles.shift()
|
||||
if (!particle) {
|
||||
break
|
||||
}
|
||||
particle.update()
|
||||
if (particle.alive) {
|
||||
aliveParticles.push(particle)
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
"use strict"
|
||||
|
||||
function createCanvas(width = 0, height = 0) {
|
||||
function createCanvas(width:number = 0, height:number = 0): HTMLCanvasElement {
|
||||
const c = document.createElement('canvas')
|
||||
c.width = width
|
||||
c.height = height
|
||||
return c
|
||||
}
|
||||
|
||||
async function loadImageToBitmap(imagePath) {
|
||||
async function loadImageToBitmap(imagePath: string): Promise<ImageBitmap> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
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 ctx = c.getContext('2d')
|
||||
const ctx = c.getContext('2d') as CanvasRenderingContext2D
|
||||
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height)
|
||||
return await createImageBitmap(c)
|
||||
}
|
||||
|
||||
async function createBitmap(width, height, color) {
|
||||
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) {
|
||||
async function colorize(image: ImageBitmap, mask: ImageBitmap, color: string): Promise<ImageBitmap> {
|
||||
const c = createCanvas(image.width, image.height)
|
||||
const ctx = c.getContext('2d')
|
||||
const ctx = c.getContext('2d') as CanvasRenderingContext2D
|
||||
ctx.save()
|
||||
ctx.drawImage(mask, 0, 0)
|
||||
ctx.fillStyle = color
|
||||
|
|
@ -49,7 +41,6 @@ async function colorize(image, mask, color) {
|
|||
}
|
||||
|
||||
export default {
|
||||
createBitmap,
|
||||
createCanvas,
|
||||
loadImageToBitmap,
|
||||
resizeBitmap,
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
"use strict"
|
||||
|
||||
import Geometry from '../common/Geometry.js'
|
||||
import Graphics from './Graphics.js'
|
||||
import Util, { logger } from './../common/Util.js'
|
||||
import Geometry from '../common/Geometry'
|
||||
import Graphics from './Graphics'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
|
||||
|
||||
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')
|
||||
var tileSize = info.tileSize
|
||||
var tileMarginWidth = info.tileMarginWidth
|
||||
|
|
@ -24,8 +25,8 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
|||
|
||||
const bitmaps = new Array(tiles.length)
|
||||
|
||||
const paths = {}
|
||||
function pathForShape(shape) {
|
||||
const paths: Record<string, Path2D> = {}
|
||||
function pathForShape(shape: PieceShape) {
|
||||
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
|
||||
if (paths[key]) {
|
||||
return paths[key]
|
||||
|
|
@ -83,10 +84,10 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
|||
}
|
||||
|
||||
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 ctx2 = c2.getContext('2d')
|
||||
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
|
||||
|
||||
for (let t of tiles) {
|
||||
const tile = Util.decodeTile(t)
|
||||
|
|
@ -197,7 +198,7 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
|
|||
return bitmaps
|
||||
}
|
||||
|
||||
function srcRectByIdx(puzzleInfo, idx) {
|
||||
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number) {
|
||||
const c = Util.coordByTileIdx(puzzleInfo, idx)
|
||||
return {
|
||||
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
|
||||
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
|
||||
|
||||
|
|
@ -1,10 +1,4 @@
|
|||
"use strict"
|
||||
|
||||
import Communication from './../Communication.js'
|
||||
|
||||
export default {
|
||||
name: 'connection-overlay',
|
||||
template: `
|
||||
<template>
|
||||
<div class="overlay connection-lost" v-if="show">
|
||||
<div class="overlay-content" v-if="lostConnection">
|
||||
<div>⁉️ LOST CONNECTION ⁉️</div>
|
||||
|
|
@ -13,7 +7,15 @@ export default {
|
|||
<div class="overlay-content" v-if="connecting">
|
||||
<div>Connecting...</div>
|
||||
</div>
|
||||
</div>`,
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import Communication from './../Communication'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'connection-overlay',
|
||||
emits: {
|
||||
reconnect: null,
|
||||
},
|
||||
|
|
@ -21,14 +23,15 @@ export default {
|
|||
connectionState: Number,
|
||||
},
|
||||
computed: {
|
||||
lostConnection () {
|
||||
lostConnection (): boolean {
|
||||
return this.connectionState === Communication.CONN_STATE_DISCONNECTED
|
||||
},
|
||||
connecting () {
|
||||
connecting (): boolean {
|
||||
return this.connectionState === Communication.CONN_STATE_CONNECTING
|
||||
},
|
||||
show () {
|
||||
return this.lostConnection || this.connecting
|
||||
show (): boolean {
|
||||
return !!(this.lostConnection || this.connecting)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,13 +1,4 @@
|
|||
"use strict"
|
||||
|
||||
import Time from './../../common/Time.js'
|
||||
|
||||
export default {
|
||||
name: 'game-teaser',
|
||||
props: {
|
||||
game: Object,
|
||||
},
|
||||
template: `
|
||||
<template>
|
||||
<div class="game-teaser" :style="style">
|
||||
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
|
||||
<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 } }">
|
||||
↪️ Watch replay
|
||||
</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: {
|
||||
style() {
|
||||
style (): object {
|
||||
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
|
||||
return {
|
||||
'background-image': `url("${url}")`,
|
||||
|
|
@ -29,7 +33,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
time(start, end) {
|
||||
time(start: number, end: number) {
|
||||
const icon = end ? '🏁' : '⏳'
|
||||
const from = start;
|
||||
const to = end || Time.timestamp()
|
||||
|
|
@ -37,4 +41,5 @@ export default {
|
|||
return `${icon} ${timeDiffStr}`
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
"use strict"
|
||||
|
||||
// ingame component
|
||||
// shows the help (key bindings)
|
||||
|
||||
export default {
|
||||
name: 'help-overlay',
|
||||
template: `<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<template>
|
||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<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 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 loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
|
||||
</table>
|
||||
</div>`,
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'help-overlay',
|
||||
emits: {
|
||||
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"
|
||||
|
||||
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: `
|
||||
<template>
|
||||
<div>
|
||||
<h1>New game</h1>
|
||||
<table>
|
||||
|
|
@ -49,9 +37,23 @@ export default {
|
|||
|
||||
<h1>Image lib</h1>
|
||||
<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>
|
||||
</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: {
|
||||
images: Array,
|
||||
},
|
||||
|
|
@ -66,8 +68,9 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
mediaImgUploaded(j) {
|
||||
this.image = j.image
|
||||
// TODO: ts type UploadedImage
|
||||
mediaImgUploaded(data: any) {
|
||||
this.image = data.image
|
||||
},
|
||||
canStartNewGame () {
|
||||
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
|
||||
|
|
@ -84,11 +87,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
scoreModeInt () {
|
||||
return parseInt(this.scoreMode, 10)
|
||||
scoreModeInt (): number {
|
||||
return parseInt(`${this.scoreMode}`, 10)
|
||||
},
|
||||
tilesInt () {
|
||||
return parseInt(this.tiles, 10)
|
||||
tilesInt (): number {
|
||||
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"
|
||||
|
||||
// ingame component
|
||||
// shows player scores
|
||||
|
||||
export default {
|
||||
name: "scores",
|
||||
template: `
|
||||
<template>
|
||||
<div class="scores">
|
||||
<div>Scores</div>
|
||||
<table>
|
||||
|
|
@ -21,21 +14,33 @@ export default {
|
|||
</tr>
|
||||
</table>
|
||||
</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: {
|
||||
actives () {
|
||||
actives (): Array<any> {
|
||||
// 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
|
||||
},
|
||||
idles () {
|
||||
idles (): Array<any> {
|
||||
// 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
|
||||
},
|
||||
},
|
||||
props: {
|
||||
activePlayers: Array,
|
||||
idlePlayers: Array,
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
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',
|
||||
props: {
|
||||
accept: String,
|
||||
label: String,
|
||||
},
|
||||
template: `
|
||||
<label>
|
||||
<input type="file" style="display: none" @change="upload" :accept="accept" />
|
||||
<span class="btn">{{label || 'Upload File'}}</span>
|
||||
</label>
|
||||
`,
|
||||
methods: {
|
||||
async upload(evt) {
|
||||
const file = evt.target.files[0]
|
||||
async upload(evt: Event) {
|
||||
const target = (evt.target as HTMLInputElement)
|
||||
if (!target.files) return;
|
||||
const file = target.files[0]
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
|
|
@ -26,4 +29,5 @@ export default {
|
|||
this.$emit('uploaded', j)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,18 +1,32 @@
|
|||
"use strict"
|
||||
|
||||
import {run} from './gameloop.js'
|
||||
import Camera from './Camera.js'
|
||||
import Graphics from './Graphics.js'
|
||||
import Debug from './Debug.js'
|
||||
import Communication from './Communication.js'
|
||||
import Util, { logger } from './../common/Util.js'
|
||||
import PuzzleGraphics from './PuzzleGraphics.js'
|
||||
import Game from './../common/GameCommon.js'
|
||||
import fireworksController from './Fireworks.js'
|
||||
import Protocol from '../common/Protocol.js'
|
||||
import Time from '../common/Time.js'
|
||||
import {run} from './gameloop'
|
||||
import Camera from './Camera'
|
||||
import Graphics from './Graphics'
|
||||
import Debug from './Debug'
|
||||
import Communication from './Communication'
|
||||
import Util from './../common/Util'
|
||||
import PuzzleGraphics from './PuzzleGraphics'
|
||||
import Game, { Player, Piece } from './../common/GameCommon'
|
||||
import fireworksController from './Fireworks'
|
||||
import Protocol from '../common/Protocol'
|
||||
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_REPLAY = 'replay'
|
||||
|
|
@ -20,7 +34,34 @@ export const MODE_REPLAY = 'replay'
|
|||
let PIECE_VIEW_FIXED = 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) {
|
||||
return PIECE_VIEW_FIXED
|
||||
}
|
||||
|
|
@ -29,7 +70,7 @@ const shouldDrawPiece = (piece) => {
|
|||
|
||||
let RERENDER = true
|
||||
|
||||
function addCanvasToDom(TARGET_EL, canvas) {
|
||||
function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
TARGET_EL.appendChild(canvas)
|
||||
|
|
@ -41,8 +82,8 @@ function addCanvasToDom(TARGET_EL, canvas) {
|
|||
return canvas
|
||||
}
|
||||
|
||||
function EventAdapter (canvas, window, viewport) {
|
||||
let events = []
|
||||
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
|
||||
let events: Array<Array<any>> = []
|
||||
|
||||
let KEYS_ON = true
|
||||
|
||||
|
|
@ -54,15 +95,15 @@ function EventAdapter (canvas, window, viewport) {
|
|||
let ZOOM_OUT = false
|
||||
let SHIFT = false
|
||||
|
||||
const toWorldPoint = (x, y) => {
|
||||
const toWorldPoint = (x: number, y: number) => {
|
||||
const pos = viewport.viewportToWorld({x, 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 key = (state, ev) => {
|
||||
const key = (state: boolean, ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
|
|
@ -108,10 +149,10 @@ function EventAdapter (canvas, window, viewport) {
|
|||
}
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (ev) => key(true, ev))
|
||||
window.addEventListener('keyup', (ev) => key(false, ev))
|
||||
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
||||
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
||||
|
||||
window.addEventListener('keypress', (ev) => {
|
||||
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
|
|
@ -128,7 +169,7 @@ function EventAdapter (canvas, window, viewport) {
|
|||
}
|
||||
})
|
||||
|
||||
const addEvent = (event) => {
|
||||
const addEvent = (event: Array<any>) => {
|
||||
events.push(event)
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +203,7 @@ function EventAdapter (canvas, window, viewport) {
|
|||
}
|
||||
}
|
||||
|
||||
const setHotkeys = (state) => {
|
||||
const setHotkeys = (state: boolean) => {
|
||||
KEYS_ON = state
|
||||
}
|
||||
|
||||
|
|
@ -175,23 +216,23 @@ function EventAdapter (canvas, window, viewport) {
|
|||
}
|
||||
|
||||
export async function main(
|
||||
gameId,
|
||||
clientId,
|
||||
wsAddress,
|
||||
MODE,
|
||||
TARGET_EL,
|
||||
HUD
|
||||
gameId: string,
|
||||
clientId: string,
|
||||
wsAddress: string,
|
||||
MODE: string,
|
||||
TARGET_EL: HTMLElement,
|
||||
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
|
||||
}
|
||||
|
||||
const cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
|
||||
const cursorHand = await Graphics.loadImageToBitmap('/hand.png')
|
||||
const cursorGrabMask = await Graphics.loadImageToBitmap('/grab_mask.png')
|
||||
const cursorHandMask = await Graphics.loadImageToBitmap('/hand_mask.png')
|
||||
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
|
||||
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
|
||||
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
|
||||
const cursorHandMask = await Graphics.loadImageToBitmap(images['./hand_mask.png'].default)
|
||||
|
||||
// all cursors must be of the same dimensions
|
||||
const CURSOR_W = cursorGrab.width
|
||||
|
|
@ -199,13 +240,17 @@ export async function main(
|
|||
const CURSOR_H = cursorGrab.height
|
||||
const CURSOR_H_2 = Math.round(CURSOR_H / 2)
|
||||
|
||||
const cursors = {}
|
||||
const getPlayerCursor = async (p) => {
|
||||
const cursors: Record<string, ImageBitmap> = {}
|
||||
const getPlayerCursor = async (p: Player) => {
|
||||
const key = p.color + ' ' + p.d
|
||||
if (!cursors[key]) {
|
||||
const cursor = p.d ? cursorGrab : cursorHand
|
||||
if (p.color) {
|
||||
const mask = p.d ? cursorGrabMask : cursorHandMask
|
||||
cursors[key] = await Graphics.colorize(cursor, mask, p.color)
|
||||
} else {
|
||||
cursors[key] = cursor
|
||||
}
|
||||
}
|
||||
return cursors[key]
|
||||
}
|
||||
|
|
@ -215,22 +260,22 @@ export async function main(
|
|||
|
||||
// stuff only available in replay mode...
|
||||
// TODO: refactor
|
||||
const REPLAY = {
|
||||
log: null,
|
||||
const REPLAY: Replay = {
|
||||
log: [],
|
||||
logIdx: 0,
|
||||
speeds: [0.5, 1, 2, 5, 10, 20, 50],
|
||||
speedIdx: 1,
|
||||
paused: false,
|
||||
lastRealTs: null,
|
||||
lastGameTs: null,
|
||||
gameStartTs: null,
|
||||
lastRealTs: 0,
|
||||
lastGameTs: 0,
|
||||
gameStartTs: 0,
|
||||
}
|
||||
|
||||
Communication.onConnectionStateChange((state) => {
|
||||
HUD.setConnectionState(state)
|
||||
})
|
||||
|
||||
let TIME
|
||||
let TIME: () => number = () => 0
|
||||
const connect = async () => {
|
||||
if (MODE === MODE_PLAY) {
|
||||
const game = await Communication.connect(wsAddress, gameId, clientId)
|
||||
|
|
@ -239,12 +284,12 @@ export async function main(
|
|||
TIME = () => Time.timestamp()
|
||||
} else if (MODE === MODE_REPLAY) {
|
||||
// TODO: change how replay connect is done...
|
||||
const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId)
|
||||
const gameObject = Util.decodeGame(game)
|
||||
const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
|
||||
const gameObject = Util.decodeGame(replay.game)
|
||||
Game.setGame(gameObject.id, gameObject)
|
||||
REPLAY.log = log
|
||||
REPLAY.log = replay.log
|
||||
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
|
||||
TIME = () => REPLAY.lastGameTs
|
||||
} else {
|
||||
|
|
@ -280,21 +325,21 @@ export async function main(
|
|||
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(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')
|
||||
|
||||
// initialize some view data
|
||||
// this global data will change according to input events
|
||||
const viewport = new Camera()
|
||||
const viewport = Camera()
|
||||
// center viewport
|
||||
viewport.move(
|
||||
-(TABLE_WIDTH - canvas.width) /2,
|
||||
-(TABLE_HEIGHT - canvas.height) /2
|
||||
)
|
||||
|
||||
const evts = new EventAdapter(canvas, window, viewport)
|
||||
const evts = EventAdapter(canvas, window, viewport)
|
||||
|
||||
const previewImageUrl = Game.getImageUrl(gameId)
|
||||
|
||||
|
|
@ -335,9 +380,13 @@ export async function main(
|
|||
}
|
||||
|
||||
const doSetSpeedStatus = () => {
|
||||
if (HUD.setReplaySpeed) {
|
||||
HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx])
|
||||
}
|
||||
if (HUD.setReplayPaused) {
|
||||
HUD.setReplayPaused(REPLAY.paused)
|
||||
}
|
||||
}
|
||||
|
||||
const replayOnSpeedUp = () => {
|
||||
if (REPLAY.speedIdx + 1 < REPLAY.speeds.length) {
|
||||
|
|
@ -419,17 +468,18 @@ export async function main(
|
|||
}
|
||||
|
||||
const entryWithTs = logEntry.slice()
|
||||
entryWithTs[entryWithTs.length - 1] = nextTs
|
||||
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
|
||||
Game.addPlayer(gameId, ...entryWithTs.slice(1))
|
||||
const playerId = entryWithTs[1]
|
||||
Game.addPlayer(gameId, playerId, nextTs)
|
||||
RERENDER = true
|
||||
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
|
||||
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
||||
Game.addPlayer(gameId, playerId, ...entryWithTs.slice(2))
|
||||
Game.addPlayer(gameId, playerId, nextTs)
|
||||
RERENDER = true
|
||||
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||
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
|
||||
}
|
||||
REPLAY.logIdx = nextIdx
|
||||
|
|
@ -440,7 +490,7 @@ export async function main(
|
|||
}, 50)
|
||||
}
|
||||
|
||||
let _last_mouse_down = null
|
||||
let _last_mouse_down: Point|null = null
|
||||
const onUpdate = () => {
|
||||
// handle key downs once per onUpdate
|
||||
// this will create Protocol.INPUT_EV_MOVE events if something
|
||||
|
|
@ -552,13 +602,13 @@ export async function main(
|
|||
let dim
|
||||
let bmp
|
||||
|
||||
if (DEBUG) Debug.checkpoint_start(0)
|
||||
if (window.DEBUG) Debug.checkpoint_start(0)
|
||||
|
||||
// CLEAR CTX
|
||||
// ---------------------------------------------------------------
|
||||
ctx.fillStyle = playerBgColor()
|
||||
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)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, .3)'
|
||||
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
|
||||
// ---------------------------------------------------------------
|
||||
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)
|
||||
for (const tile of tiles) {
|
||||
|
|
@ -592,13 +642,13 @@ export async function main(
|
|||
pos.x, pos.y, dim.w, dim.h
|
||||
)
|
||||
}
|
||||
if (DEBUG) Debug.checkpoint('tiles done')
|
||||
if (window.DEBUG) Debug.checkpoint('tiles done')
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
|
||||
// DRAW PLAYERS
|
||||
// ---------------------------------------------------------------
|
||||
const texts = []
|
||||
const texts: Array<FixedLengthArray<[string, number, number]>> = []
|
||||
// Cursors
|
||||
for (const p of Game.getActivePlayers(gameId, ts)) {
|
||||
bmp = await getPlayerCursor(p)
|
||||
|
|
@ -619,14 +669,14 @@ export async function main(
|
|||
ctx.fillText(txt, x, y)
|
||||
}
|
||||
|
||||
if (DEBUG) Debug.checkpoint('players done')
|
||||
if (window.DEBUG) Debug.checkpoint('players done')
|
||||
|
||||
// propagate HUD changes
|
||||
// ---------------------------------------------------------------
|
||||
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
|
||||
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
|
||||
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
|
||||
if (DEBUG) Debug.checkpoint('HUD done')
|
||||
if (window.DEBUG) Debug.checkpoint('HUD done')
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
if (justFinished()) {
|
||||
|
|
@ -642,18 +692,18 @@ export async function main(
|
|||
})
|
||||
|
||||
return {
|
||||
setHotkeys: (state) => {
|
||||
setHotkeys: (state: boolean) => {
|
||||
evts.setHotkeys(state)
|
||||
},
|
||||
onBgChange: (value) => {
|
||||
onBgChange: (value: string) => {
|
||||
localStorage.setItem('bg_color', value)
|
||||
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
||||
},
|
||||
onColorChange: (value) => {
|
||||
onColorChange: (value: string) => {
|
||||
localStorage.setItem('player_color', value)
|
||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
|
||||
},
|
||||
onNameChange: (value) => {
|
||||
onNameChange: (value: string) => {
|
||||
localStorage.setItem('player_name', value)
|
||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
||||
},
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
"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 slow = options.slow || 1
|
||||
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"
|
||||
|
||||
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">
|
||||
<template>
|
||||
<div id="game">
|
||||
<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" />
|
||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||
|
|
@ -46,18 +26,42 @@ export default {
|
|||
</div>
|
||||
|
||||
<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() {
|
||||
return {
|
||||
activePlayers: [],
|
||||
idlePlayers: [],
|
||||
// TODO: ts Array<Player> type
|
||||
activePlayers: [] as PropType<Array<any>>,
|
||||
idlePlayers: [] as PropType<Array<any>>,
|
||||
|
||||
finished: false,
|
||||
duration: 0,
|
||||
piecesDone: 0,
|
||||
piecesTotal: 0,
|
||||
|
||||
overlay: null,
|
||||
overlay: '',
|
||||
|
||||
connectionState: 0,
|
||||
|
||||
|
|
@ -68,10 +72,10 @@ export default {
|
|||
name: '',
|
||||
},
|
||||
previewImageUrl: '',
|
||||
setHotkeys: () => {},
|
||||
onBgChange: () => {},
|
||||
onColorChange: () => {},
|
||||
onNameChange: () => {},
|
||||
setHotkeys: (v: boolean) => {},
|
||||
onBgChange: (v: string) => {},
|
||||
onColorChange: (v: string) => {},
|
||||
onNameChange: (v: string) => {},
|
||||
disconnect: () => {},
|
||||
connect: () => {},
|
||||
},
|
||||
|
|
@ -81,29 +85,31 @@ export default {
|
|||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
this.$watch(() => this.g.player.background, (value) => {
|
||||
this.$watch(() => this.g.player.background, (value: string) => {
|
||||
this.g.onBgChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.color, (value) => {
|
||||
this.$watch(() => this.g.player.color, (value: string) => {
|
||||
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 = await main(
|
||||
this.$route.params.id,
|
||||
`${this.$route.params.id}`,
|
||||
// @ts-ignore
|
||||
this.$clientId,
|
||||
// @ts-ignore
|
||||
this.$config.WS_ADDRESS,
|
||||
MODE_PLAY,
|
||||
this.$el,
|
||||
{
|
||||
setActivePlayers: (v) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v) => { this.idlePlayers = v },
|
||||
setFinished: (v) => { this.finished = v },
|
||||
setDuration: (v) => { this.duration = v },
|
||||
setPiecesDone: (v) => { this.piecesDone = v },
|
||||
setPiecesTotal: (v) => { this.piecesTotal = v },
|
||||
setConnectionState: (v) => { this.connectionState = v },
|
||||
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||
setFinished: (v: boolean) => { this.finished = v },
|
||||
setDuration: (v: number) => { this.duration = v },
|
||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||
setConnectionState: (v: number) => { this.connectionState = v },
|
||||
togglePreview: () => { this.toggle('preview', false) },
|
||||
}
|
||||
)
|
||||
|
|
@ -112,22 +118,23 @@ export default {
|
|||
this.g.disconnect()
|
||||
},
|
||||
methods: {
|
||||
reconnect() {
|
||||
reconnect(): void {
|
||||
this.g.connect()
|
||||
},
|
||||
toggle(overlay, affectsHotkeys) {
|
||||
if (this.overlay === null) {
|
||||
toggle(overlay: string, affectsHotkeys: boolean): void {
|
||||
if (this.overlay === '') {
|
||||
this.overlay = overlay
|
||||
if (affectsHotkeys) {
|
||||
this.g.setHotkeys(false)
|
||||
}
|
||||
} else {
|
||||
// could check if overlay was the provided one
|
||||
this.overlay = null
|
||||
this.overlay = ''
|
||||
if (affectsHotkeys) {
|
||||
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: {
|
||||
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() {
|
||||
return {
|
||||
images: [],
|
||||
|
|
@ -22,7 +25,8 @@ export default {
|
|||
this.images = json.images
|
||||
},
|
||||
methods: {
|
||||
async onNewGame(gameSettings) {
|
||||
// TODO: ts GameSettings type
|
||||
async onNewGame(gameSettings: any) {
|
||||
const res = await fetch('/newgame', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
|
|
@ -37,4 +41,5 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,23 +1,5 @@
|
|||
"use strict"
|
||||
|
||||
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">
|
||||
<template>
|
||||
<div id="replay">
|
||||
<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" />
|
||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||
|
|
@ -46,18 +28,41 @@ export default {
|
|||
</div>
|
||||
|
||||
<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() {
|
||||
return {
|
||||
activePlayers: [],
|
||||
idlePlayers: [],
|
||||
activePlayers: [] as Array<any>,
|
||||
idlePlayers: [] as Array<any>,
|
||||
|
||||
finished: false,
|
||||
duration: 0,
|
||||
piecesDone: 0,
|
||||
piecesTotal: 0,
|
||||
|
||||
overlay: null,
|
||||
overlay: '',
|
||||
|
||||
connectionState: 0,
|
||||
|
||||
g: {
|
||||
player: {
|
||||
|
|
@ -66,10 +71,10 @@ export default {
|
|||
name: '',
|
||||
},
|
||||
previewImageUrl: '',
|
||||
setHotkeys: () => {},
|
||||
onBgChange: () => {},
|
||||
onColorChange: () => {},
|
||||
onNameChange: () => {},
|
||||
setHotkeys: (v: boolean) => {},
|
||||
onBgChange: (v: string) => {},
|
||||
onColorChange: (v: string) => {},
|
||||
onNameChange: (v: string) => {},
|
||||
replayOnSpeedUp: () => {},
|
||||
replayOnSpeedDown: () => {},
|
||||
replayOnPauseToggle: () => {},
|
||||
|
|
@ -86,59 +91,62 @@ export default {
|
|||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
this.$watch(() => this.g.player.background, (value) => {
|
||||
this.$watch(() => this.g.player.background, (value: string) => {
|
||||
this.g.onBgChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.color, (value) => {
|
||||
this.$watch(() => this.g.player.color, (value: string) => {
|
||||
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 = await main(
|
||||
this.$route.params.id,
|
||||
`${this.$route.params.id}`,
|
||||
// @ts-ignore
|
||||
this.$clientId,
|
||||
// @ts-ignore
|
||||
this.$config.WS_ADDRESS,
|
||||
MODE_REPLAY,
|
||||
this.$el,
|
||||
{
|
||||
setActivePlayers: (v) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v) => { this.idlePlayers = v },
|
||||
setFinished: (v) => { this.finished = v },
|
||||
setDuration: (v) => { this.duration = v },
|
||||
setPiecesDone: (v) => { this.piecesDone = v },
|
||||
setPiecesTotal: (v) => { this.piecesTotal = v },
|
||||
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||
setFinished: (v: boolean) => { this.finished = v },
|
||||
setDuration: (v: number) => { this.duration = v },
|
||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||
togglePreview: () => { this.toggle('preview', false) },
|
||||
setConnectionState: (v) => { this.connectionState = v },
|
||||
setReplaySpeed: (v) => { this.replay.speed = v },
|
||||
setReplayPaused: (v) => { this.replay.paused = v },
|
||||
setConnectionState: (v: number) => { this.connectionState = v },
|
||||
setReplaySpeed: (v: number) => { this.replay.speed = v },
|
||||
setReplayPaused: (v: boolean) => { this.replay.paused = v },
|
||||
}
|
||||
)
|
||||
},
|
||||
unmounted () {
|
||||
this.g.disconnect()
|
||||
},
|
||||
computed: {
|
||||
replayText () {
|
||||
return 'Replay-Speed: ' +
|
||||
(this.replay.speed + 'x') +
|
||||
(this.replay.paused ? ' Paused' : '')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle(overlay, affectsHotkeys) {
|
||||
if (this.overlay === null) {
|
||||
toggle(overlay: string, affectsHotkeys: boolean): void {
|
||||
if (this.overlay === '') {
|
||||
this.overlay = overlay
|
||||
if (affectsHotkeys) {
|
||||
this.g.setHotkeys(false)
|
||||
}
|
||||
} else {
|
||||
// could check if overlay was the provided one
|
||||
this.overlay = null
|
||||
this.overlay = ''
|
||||
if (affectsHotkeys) {
|
||||
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 __dirname = dirname(__filename)
|
||||
|
||||
const BASE_DIR = `${__dirname}/..`
|
||||
const BASE_DIR = `${__dirname}/../..`
|
||||
|
||||
export const DATA_DIR = `${BASE_DIR}/data`
|
||||
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
|
||||
export const UPLOAD_URL = `/uploads`
|
||||
export const COMMON_DIR = `${BASE_DIR}/common/`
|
||||
export const PUBLIC_DIR = `${BASE_DIR}/public/`
|
||||
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import GameCommon from './../common/GameCommon.js'
|
||||
import Util from './../common/Util.js'
|
||||
import { Rng } from '../common/Rng.js'
|
||||
import GameLog from './GameLog.js'
|
||||
import { createPuzzle } from './Puzzle.js'
|
||||
import Protocol from '../common/Protocol.js'
|
||||
import GameStorage from './GameStorage.js'
|
||||
import GameCommon from './../common/GameCommon'
|
||||
import Util from './../common/Util'
|
||||
import { Rng } from '../common/Rng'
|
||||
import GameLog from './GameLog'
|
||||
import { createPuzzle } from './Puzzle'
|
||||
import Protocol from '../common/Protocol'
|
||||
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 rng = new Rng(seed)
|
||||
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)
|
||||
|
||||
GameLog.create(gameId)
|
||||
|
|
@ -29,7 +29,7 @@ async function createGame(gameId, targetTiles, image, ts, scoreMode) {
|
|||
GameStorage.setDirty(gameId)
|
||||
}
|
||||
|
||||
function addPlayer(gameId, playerId, ts) {
|
||||
function addPlayer(gameId: string, playerId: string, ts: number) {
|
||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||
const diff = ts - GameCommon.getStartTs(gameId)
|
||||
if (idx === -1) {
|
||||
|
|
@ -42,7 +42,7 @@ function addPlayer(gameId, playerId, ts) {
|
|||
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 diff = ts - GameCommon.getStartTs(gameId)
|
||||
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 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)
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.appendFileSync(file, '')
|
||||
}
|
||||
}
|
||||
|
||||
const exists = (gameId) => {
|
||||
const exists = (gameId: string) => {
|
||||
const file = filename(gameId)
|
||||
return fs.existsSync(file)
|
||||
}
|
||||
|
||||
const _log = (gameId, ...args) => {
|
||||
const _log = (gameId: string, ...args: Array<any>) => {
|
||||
const file = filename(gameId)
|
||||
if (!fs.existsSync(file)) {
|
||||
return
|
||||
|
|
@ -27,13 +27,13 @@ const _log = (gameId, ...args) => {
|
|||
fs.appendFileSync(file, str + "\n")
|
||||
}
|
||||
|
||||
const get = (gameId) => {
|
||||
const get = (gameId: string) => {
|
||||
const file = filename(gameId)
|
||||
if (!fs.existsSync(file)) {
|
||||
return []
|
||||
}
|
||||
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 {
|
||||
return JSON.parse(line)
|
||||
} catch (e) {
|
||||
|
|
@ -1,27 +1,28 @@
|
|||
import { logger } from '../common/Util.js'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
const log = logger('GameSocket.js')
|
||||
|
||||
// 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)) {
|
||||
return false
|
||||
}
|
||||
return SOCKETS[gameId].includes(socket)
|
||||
}
|
||||
|
||||
function removeSocket(gameId, socket) {
|
||||
function removeSocket(gameId: string, socket: WebSocket) {
|
||||
if (!(gameId in SOCKETS)) {
|
||||
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('socket count: ', Object.keys(SOCKETS[gameId]).length)
|
||||
}
|
||||
|
||||
function addSocket(gameId, socket) {
|
||||
function addSocket(gameId: string, socket: WebSocket) {
|
||||
if (!(gameId in SOCKETS)) {
|
||||
SOCKETS[gameId] = []
|
||||
}
|
||||
|
|
@ -32,7 +33,7 @@ function addSocket(gameId, socket) {
|
|||
}
|
||||
}
|
||||
|
||||
function getSockets(gameId) {
|
||||
function getSockets(gameId: string) {
|
||||
if (!(gameId in SOCKETS)) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
import fs from 'fs'
|
||||
import GameCommon from './../common/GameCommon.js'
|
||||
import Util, { logger } from './../common/Util.js'
|
||||
import { Rng } from '../common/Rng.js'
|
||||
import { DATA_DIR } from './Dirs.js'
|
||||
import Time from '../common/Time.js'
|
||||
import GameCommon from './../common/GameCommon'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import { Rng } from '../common/Rng'
|
||||
import { DATA_DIR } from './Dirs'
|
||||
import Time from './../common/Time'
|
||||
|
||||
const log = logger('GameStorage.js')
|
||||
|
||||
const DIRTY_GAMES = {}
|
||||
function setDirty(gameId) {
|
||||
const DIRTY_GAMES = {} as any
|
||||
function setDirty(gameId: string): void {
|
||||
DIRTY_GAMES[gameId] = true
|
||||
}
|
||||
function setClean(gameId) {
|
||||
function setClean(gameId: string): void {
|
||||
delete DIRTY_GAMES[gameId]
|
||||
}
|
||||
|
||||
function loadGames() {
|
||||
function loadGames(): void {
|
||||
const files = fs.readdirSync(DATA_DIR)
|
||||
for (const f of files) {
|
||||
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 contents = fs.readFileSync(file, 'utf-8')
|
||||
let game
|
||||
|
|
@ -40,7 +40,7 @@ function loadGame(gameId) {
|
|||
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
|
||||
}
|
||||
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()
|
||||
}
|
||||
if (!Array.isArray(game.players)) {
|
||||
|
|
@ -50,7 +50,7 @@ function loadGame(gameId) {
|
|||
id: game.id,
|
||||
rng: {
|
||||
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,
|
||||
players: game.players,
|
||||
|
|
@ -66,7 +66,7 @@ function persistGames() {
|
|||
}
|
||||
}
|
||||
|
||||
function persistGame(gameId) {
|
||||
function persistGame(gameId: string) {
|
||||
const game = GameCommon.get(gameId)
|
||||
if (game.id in DIRTY_GAMES) {
|
||||
setClean(game.id)
|
||||
|
|
@ -2,9 +2,10 @@ import sizeOf from 'image-size'
|
|||
import fs from 'fs'
|
||||
import exif from 'exif'
|
||||
import sharp from 'sharp'
|
||||
|
||||
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)$/)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ const resizeImage = async (filename) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function getExifOrientation(imagePath) {
|
||||
async function getExifOrientation(imagePath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
|
||||
if (error) {
|
||||
|
|
@ -60,7 +61,7 @@ const allImages = () => {
|
|||
return images
|
||||
}
|
||||
|
||||
async function getDimensions(imagePath) {
|
||||
async function getDimensions(imagePath: string) {
|
||||
let dimensions = sizeOf(imagePath)
|
||||
const orientation = await getExifOrientation(imagePath)
|
||||
// when image is rotated to the left or right, switch width/height
|
||||
|
|
@ -1,22 +1,36 @@
|
|||
import Util from '../common/Util.js'
|
||||
import { Rng } from '../common/Rng.js'
|
||||
import Util from '../common/Util'
|
||||
import { Rng } from '../common/Rng'
|
||||
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
|
||||
// final resized version of the puzzle image
|
||||
const TILE_SIZE = 64
|
||||
|
||||
async function createPuzzle(
|
||||
/** @type Rng */ rng,
|
||||
targetTiles,
|
||||
image,
|
||||
ts
|
||||
rng: Rng,
|
||||
targetTiles: number,
|
||||
image: { file: string, url: string },
|
||||
ts: number
|
||||
) {
|
||||
const imagePath = image.file
|
||||
const imageUrl = image.url
|
||||
|
||||
// determine puzzle information from the image dimensions
|
||||
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)
|
||||
|
||||
let tiles = new Array(info.tiles)
|
||||
|
|
@ -143,8 +157,8 @@ async function createPuzzle(
|
|||
}
|
||||
|
||||
function determinePuzzleTileShapes(
|
||||
/** @type Rng */ rng,
|
||||
info
|
||||
rng: Rng,
|
||||
info: PuzzleInfo
|
||||
) {
|
||||
const tabs = [-1, 1]
|
||||
|
||||
|
|
@ -161,7 +175,7 @@ function determinePuzzleTileShapes(
|
|||
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 h_ = w < h ? (h * h) : (w * h)
|
||||
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 tiles = tilesX * tilesY
|
||||
const tileSize = TILE_SIZE
|
||||
|
|
@ -14,44 +14,49 @@ config = {
|
|||
*/
|
||||
|
||||
class EvtBus {
|
||||
private _on: any
|
||||
constructor() {
|
||||
this._on = {}
|
||||
this._on = {} as any
|
||||
}
|
||||
|
||||
on(type, callback) {
|
||||
on(type: string, callback: Function) {
|
||||
this._on[type] = this._on[type] || []
|
||||
this._on[type].push(callback)
|
||||
}
|
||||
|
||||
dispatch(type, ...args) {
|
||||
(this._on[type] || []).forEach(cb => {
|
||||
dispatch(type: string, ...args: Array<any>) {
|
||||
(this._on[type] || []).forEach((cb: Function) => {
|
||||
cb(...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class WebSocketServer {
|
||||
constructor(config) {
|
||||
evt: EvtBus
|
||||
private _websocketserver: WebSocket.Server|null
|
||||
config: any
|
||||
|
||||
constructor(config: any) {
|
||||
this.config = config
|
||||
this._websocketserver = null
|
||||
|
||||
this.evt = new EvtBus()
|
||||
}
|
||||
|
||||
on(type, callback) {
|
||||
on(type: string, callback: Function) {
|
||||
this.evt.on(type, callback)
|
||||
}
|
||||
|
||||
listen() {
|
||||
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
|
||||
if (request.url.indexOf(pathname) !== 0) {
|
||||
log.log('bad request url: ', request.url)
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
socket.on('message', (data) => {
|
||||
socket.on('message', (data: any) => {
|
||||
log.log(`ws`, socket.protocol, data)
|
||||
this.evt.dispatch('message', {socket, data})
|
||||
})
|
||||
|
|
@ -62,10 +67,12 @@ class WebSocketServer {
|
|||
}
|
||||
|
||||
close() {
|
||||
if (this._websocketserver) {
|
||||
this._websocketserver.close()
|
||||
}
|
||||
}
|
||||
|
||||
notifyOne(data, socket) {
|
||||
notifyOne(data: any, socket: WebSocket) {
|
||||
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 multer from 'multer'
|
||||
import config from './../config.js'
|
||||
import Protocol from './../common/Protocol.js'
|
||||
import Util, { logger } from './../common/Util.js'
|
||||
import Game from './Game.js'
|
||||
import Protocol from './../common/Protocol'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import Game from './Game'
|
||||
import bodyParser from 'body-parser'
|
||||
import v8 from 'v8'
|
||||
import GameLog from './GameLog.js'
|
||||
import GameSockets from './GameSockets.js'
|
||||
import Time from '../common/Time.js'
|
||||
import Images from './Images.js'
|
||||
import fs from 'fs'
|
||||
import GameLog from './GameLog'
|
||||
import GameSockets from './GameSockets'
|
||||
import Time from '../common/Time'
|
||||
import Images from './Images'
|
||||
import {
|
||||
UPLOAD_DIR,
|
||||
UPLOAD_URL,
|
||||
COMMON_DIR,
|
||||
PUBLIC_DIR,
|
||||
} from './Dirs.js'
|
||||
import GameCommon from '../common/GameCommon.js'
|
||||
import GameStorage from './GameStorage.js'
|
||||
} from './Dirs'
|
||||
import GameCommon from '../common/GameCommon'
|
||||
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 hostname = config.http.hostname
|
||||
|
|
@ -50,7 +64,7 @@ app.get('/api/newgame-data', (req, res) => {
|
|||
app.get('/api/index-data', (req, res) => {
|
||||
const ts = Time.timestamp()
|
||||
const games = [
|
||||
...Game.getAllGames().map(game => ({
|
||||
...Game.getAllGames().map((game: any) => ({
|
||||
id: game.id,
|
||||
hasReplay: GameLog.exists(game.id),
|
||||
started: Game.getStartTs(game.id),
|
||||
|
|
@ -69,7 +83,7 @@ app.get('/api/index-data', (req, res) => {
|
|||
})
|
||||
|
||||
app.post('/upload', (req, res) => {
|
||||
upload(req, res, async (err) => {
|
||||
upload(req, res, async (err: any) => {
|
||||
if (err) {
|
||||
log.log(err)
|
||||
res.status(400).send("Something went wrong!");
|
||||
|
|
@ -107,20 +121,19 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
|
|||
res.send({ id: gameId })
|
||||
})
|
||||
|
||||
app.use('/common/', express.static(COMMON_DIR))
|
||||
app.use('/uploads/', express.static(UPLOAD_DIR))
|
||||
app.use('/', express.static(PUBLIC_DIR))
|
||||
|
||||
const wss = new WebSocketServer(config.ws);
|
||||
|
||||
const notify = (data, sockets) => {
|
||||
const notify = (data: any, sockets: Array<WebSocket>) => {
|
||||
// TODO: throttle?
|
||||
for (let socket of sockets) {
|
||||
wss.notifyOne(data, socket)
|
||||
}
|
||||
}
|
||||
|
||||
wss.on('close', async ({socket}) => {
|
||||
wss.on('close', async ({socket} : {socket: WebSocket}) => {
|
||||
try {
|
||||
const proto = socket.protocol.split('|')
|
||||
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 {
|
||||
const proto = socket.protocol.split('|')
|
||||
const clientId = proto[0]
|
||||
|
|
@ -236,7 +249,7 @@ const persistInterval = setInterval(() => {
|
|||
memoryUsageHuman()
|
||||
}, config.persistence.interval)
|
||||
|
||||
const gracefulShutdown = (signal) => {
|
||||
const gracefulShutdown = (signal: any) => {
|
||||
log.log(`${signal} received...`)
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||