switch to typescript

This commit is contained in:
Zutatensuppe 2021-05-17 00:27:47 +02:00
parent 031ca31c7e
commit 23559b1a3b
63 changed files with 7943 additions and 1397 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/node_modules /node_modules
/config.js /config.json
/data /data

View file

@ -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
View file

@ -0,0 +1,14 @@
{
"http": {
"hostname": "127.0.0.1",
"port": 1337
},
"ws": {
"hostname": "127.0.0.1",
"port": 1338,
"connectstring": "ws://localhost:1338/ws"
},
"persistence": {
"interval": 30000
}
}

7358
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,28 @@
"image-size": "^0.9.3", "image-size": "^0.9.3",
"multer": "^1.4.2", "multer": "^1.4.2",
"sharp": "^0.28.1", "sharp": "^0.28.1",
"vue": "^3.0.11",
"vue-router": "^4.0.8",
"ws": "^7.3.1" "ws": "^7.3.1"
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3" "@types/exif": "^0.6.2",
"@types/express": "^4.17.11",
"@types/multer": "^1.4.5",
"@types/sharp": "^0.28.1",
"@types/ws": "^7.4.4",
"@vitejs/plugin-vue": "^1.2.2",
"@vuedx/typescript-plugin-vue": "^0.6.3",
"jest": "^26.6.3",
"rollup": "^2.48.0",
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-vue": "^6.0.0-beta.10",
"ts-node": "^9.1.1",
"typescript": "^4.2.4",
"vite": "^2.3.2"
},
"scripts": {
"rollup": "rollup",
"vite": "vite"
} }
} }

View file

@ -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')
},
},
}

View file

@ -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}')`,
}
},
},
}

View file

@ -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)
},
}
}

View file

@ -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 })
},
}

View file

@ -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>

View file

@ -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
View 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
View file

@ -0,0 +1,7 @@
#!/bin/sh -ex
# server build
npm run rollup -- -c rollup.server.config.js
# frontend build
npm run vite -- build

View file

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
cd "$RUN_DIR/server" cd "$RUN_DIR/build/server"
nodemon --max-old-space-size=64 index.js -e js nodemon --max-old-space-size=64 main.js -c ../../config.json

View file

@ -1,21 +1,111 @@
import Geometry from './Geometry.js' import Geometry from './Geometry'
import Protocol from './Protocol.js' import Protocol from './Protocol'
import Time from './Time.js' import { Rng } from './Rng'
import Util from './Util.js' import Time from './Time'
import Util from './Util'
interface EncodedPlayer extends Array<any> {}
interface EncodedPiece extends Array<any> {}
interface Point {
x: number
y: number
}
interface GameRng {
obj: Rng
type?: string
}
interface Game {
id: string
players: Array<EncodedPlayer>
puzzle: Puzzle
evtInfos: Record<string, EvtInfo>
scoreMode?: number
rng: GameRng
}
export interface Puzzle {
tiles: Array<EncodedPiece>
data: PuzzleData
info: PuzzleInfo
}
interface PuzzleData {
started: number
finished: number
maxGroup: number
maxZ: number
}
interface PuzzleTable {
width: number
height: number
}
export interface PieceShape {
top: 0|1|-1
bottom: 0|1|-1
left: 0|1|-1
right: 0|1|-1
}
export interface Piece {
owner: string|number
}
export interface PuzzleInfo {
table: PuzzleTable
targetTiles: number,
imageUrl: string
width: number
height: number
tileSize: number
tileDrawSize: number
tileMarginWidth: number
tileDrawOffset: number
snapDistance: number
tiles: number
tilesX: number
tilesY: number
// TODO: ts type Array<PieceShape>
shapes: Array<any>
}
export interface Player {
id: string
x: number
y: number
d: 0|1
name: string|null
color: string|null
bgcolor: string|null
points: number
ts: number
}
interface EvtInfo {
_last_mouse: Point|null
_last_mouse_down: Point|null
}
const SCORE_MODE_FINAL = 0 const SCORE_MODE_FINAL = 0
const SCORE_MODE_ANY = 1 const SCORE_MODE_ANY = 1
const IDLE_TIMEOUT_SEC = 30 const IDLE_TIMEOUT_SEC = 30
// Map<gameId, GameObject> // Map<gameId, Game>
const GAMES = {} const GAMES: Record<string, Game> = {}
function exists(gameId) { function exists(gameId: string) {
return (!!GAMES[gameId]) || false return (!!GAMES[gameId]) || false
} }
function __createPlayerObject(id, ts) { function __createPlayerObject(id: string, ts: number): Player {
return { return {
id: id, id: id,
x: 0, x: 0,
@ -29,11 +119,11 @@ function __createPlayerObject(id, ts) {
} }
} }
function setGame(gameId, game) { function setGame(gameId: string, game: Game) {
GAMES[gameId] = game GAMES[gameId] = game
} }
function getPlayerIndexById(gameId, playerId) { function getPlayerIndexById(gameId: string, playerId: string): number {
let i = 0; let i = 0;
for (let player of GAMES[gameId].players) { for (let player of GAMES[gameId].players) {
if (Util.decodePlayer(player).id === playerId) { if (Util.decodePlayer(player).id === playerId) {
@ -44,19 +134,19 @@ function getPlayerIndexById(gameId, playerId) {
return -1 return -1
} }
function getPlayerIdByIndex(gameId, playerIndex) { function getPlayerIdByIndex(gameId: string, playerIndex: number) {
if (GAMES[gameId].players.length > playerIndex) { if (GAMES[gameId].players.length > playerIndex) {
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id
} }
return null return null
} }
function getPlayer(gameId, playerId) { function getPlayer(gameId: string, playerId: string) {
let idx = getPlayerIndexById(gameId, playerId) let idx = getPlayerIndexById(gameId, playerId)
return Util.decodePlayer(GAMES[gameId].players[idx]) return Util.decodePlayer(GAMES[gameId].players[idx])
} }
function setPlayer(gameId, playerId, player) { function setPlayer(gameId: string, playerId: string, player: Player) {
let idx = getPlayerIndexById(gameId, playerId) let idx = getPlayerIndexById(gameId, playerId)
if (idx === -1) { if (idx === -1) {
GAMES[gameId].players.push(Util.encodePlayer(player)) GAMES[gameId].players.push(Util.encodePlayer(player))
@ -65,30 +155,30 @@ function setPlayer(gameId, playerId, player) {
} }
} }
function setTile(gameId, tileIdx, tile) { function setTile(gameId: string, tileIdx: number, tile: Piece) {
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile) GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
} }
function setPuzzleData(gameId, data) { function setPuzzleData(gameId: string, data: PuzzleData) {
GAMES[gameId].puzzle.data = data GAMES[gameId].puzzle.data = data
} }
function playerExists(gameId, playerId) { function playerExists(gameId: string, playerId: string) {
const idx = getPlayerIndexById(gameId, playerId) const idx = getPlayerIndexById(gameId, playerId)
return idx !== -1 return idx !== -1
} }
function getActivePlayers(gameId, ts) { function getActivePlayers(gameId: string, ts: number) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
return getAllPlayers(gameId).filter(p => p.ts >= minTs) return getAllPlayers(gameId).filter((p: Player) => p.ts >= minTs)
} }
function getIdlePlayers(gameId, ts) { function getIdlePlayers(gameId: string, ts: number) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
return getAllPlayers(gameId).filter(p => p.ts < minTs && p.points > 0) return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
} }
function addPlayer(gameId, playerId, ts) { function addPlayer(gameId: string, playerId: string, ts: number) {
if (!playerExists(gameId, playerId)) { if (!playerExists(gameId, playerId)) {
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts)) setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
} else { } else {
@ -96,7 +186,7 @@ function addPlayer(gameId, playerId, ts) {
} }
} }
function getEvtInfo(gameId, playerId) { function getEvtInfo(gameId: string, playerId: string) {
if (playerId in GAMES[gameId].evtInfos) { if (playerId in GAMES[gameId].evtInfos) {
return GAMES[gameId].evtInfos[playerId] return GAMES[gameId].evtInfos[playerId]
} }
@ -106,12 +196,12 @@ function getEvtInfo(gameId, playerId) {
} }
} }
function setEvtInfo(gameId, playerId, evtInfo) { function setEvtInfo(gameId: string, playerId: string, evtInfo: EvtInfo) {
GAMES[gameId].evtInfos[playerId] = evtInfo GAMES[gameId].evtInfos[playerId] = evtInfo
} }
function getAllGames() { function getAllGames(): Array<Game> {
return Object.values(GAMES).sort((a, b) => { return Object.values(GAMES).sort((a: Game, b: Game) => {
// when both have same finished state, sort by started // when both have same finished state, sort by started
if (isFinished(a.id) === isFinished(b.id)) { if (isFinished(a.id) === isFinished(b.id)) {
return b.puzzle.data.started - a.puzzle.data.started return b.puzzle.data.started - a.puzzle.data.started
@ -121,37 +211,37 @@ function getAllGames() {
}) })
} }
function getAllPlayers(gameId) { function getAllPlayers(gameId: string) {
return GAMES[gameId] return GAMES[gameId]
? GAMES[gameId].players.map(Util.decodePlayer) ? GAMES[gameId].players.map(Util.decodePlayer)
: [] : []
} }
function get(gameId) { function get(gameId: string) {
return GAMES[gameId] return GAMES[gameId]
} }
function getTileCount(gameId) { function getTileCount(gameId: string) {
return GAMES[gameId].puzzle.tiles.length return GAMES[gameId].puzzle.tiles.length
} }
function getImageUrl(gameId) { function getImageUrl(gameId: string) {
return GAMES[gameId].puzzle.info.imageUrl return GAMES[gameId].puzzle.info.imageUrl
} }
function setImageUrl(gameId, imageUrl) { function setImageUrl(gameId: string, imageUrl: string) {
GAMES[gameId].puzzle.info.imageUrl = imageUrl GAMES[gameId].puzzle.info.imageUrl = imageUrl
} }
function getScoreMode(gameId) { function getScoreMode(gameId: string) {
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
} }
function isFinished(gameId) { function isFinished(gameId: string) {
return getFinishedTileCount(gameId) === getTileCount(gameId) return getFinishedTileCount(gameId) === getTileCount(gameId)
} }
function getFinishedTileCount(gameId) { function getFinishedTileCount(gameId: string) {
let count = 0 let count = 0
for (let t of GAMES[gameId].puzzle.tiles) { for (let t of GAMES[gameId].puzzle.tiles) {
if (Util.decodeTile(t).owner === -1) { if (Util.decodeTile(t).owner === -1) {
@ -161,12 +251,12 @@ function getFinishedTileCount(gameId) {
return count return count
} }
function getTilesSortedByZIndex(gameId) { function getTilesSortedByZIndex(gameId: string) {
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile) const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
return tiles.sort((t1, t2) => t1.z - t2.z) return tiles.sort((t1, t2) => t1.z - t2.z)
} }
function changePlayer(gameId, playerId, change) { function changePlayer(gameId: string, playerId: string, change: any) {
const player = getPlayer(gameId, playerId) const player = getPlayer(gameId, playerId)
for (let k of Object.keys(change)) { for (let k of Object.keys(change)) {
player[k] = change[k] player[k] = change[k]
@ -174,13 +264,14 @@ function changePlayer(gameId, playerId, change) {
setPlayer(gameId, playerId, player) setPlayer(gameId, playerId, player)
} }
function changeData(gameId, change) { function changeData(gameId: string, change: any) {
for (let k of Object.keys(change)) { for (let k of Object.keys(change)) {
// @ts-ignore
GAMES[gameId].puzzle.data[k] = change[k] GAMES[gameId].puzzle.data[k] = change[k]
} }
} }
function changeTile(gameId, tileIdx, change) { function changeTile(gameId: string, tileIdx: number, change: any) {
for (let k of Object.keys(change)) { for (let k of Object.keys(change)) {
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
tile[k] = change[k] tile[k] = change[k]
@ -188,16 +279,16 @@ function changeTile(gameId, tileIdx, change) {
} }
} }
const getTile = (gameId, tileIdx) => { const getTile = (gameId: string, tileIdx: number) => {
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
} }
const getTileGroup = (gameId, tileIdx) => { const getTileGroup = (gameId: string, tileIdx: number) => {
const tile = getTile(gameId, tileIdx) const tile = getTile(gameId, tileIdx)
return tile.group return tile.group
} }
const getFinalTilePos = (gameId, tileIdx) => { const getFinalTilePos = (gameId: string, tileIdx: number) => {
const info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
const boardPos = { const boardPos = {
x: (info.table.width - info.width) / 2, x: (info.table.width - info.width) / 2,
@ -207,13 +298,13 @@ const getFinalTilePos = (gameId, tileIdx) => {
return Geometry.pointAdd(boardPos, srcPos) return Geometry.pointAdd(boardPos, srcPos)
} }
const getTilePos = (gameId, tileIdx) => { const getTilePos = (gameId: string, tileIdx: number) => {
const tile = getTile(gameId, tileIdx) const tile = getTile(gameId, tileIdx)
return tile.pos return tile.pos
} }
// todo: instead, just make the table bigger and use that :) // todo: instead, just make the table bigger and use that :)
const getBounds = (gameId) => { const getBounds = (gameId: string) => {
const tw = getTableWidth(gameId) const tw = getTableWidth(gameId)
const th = getTableHeight(gameId) const th = getTableHeight(gameId)
@ -227,7 +318,7 @@ const getBounds = (gameId) => {
} }
} }
const getTileBounds = (gameId, tileIdx) => { const getTileBounds = (gameId: string, tileIdx: number) => {
const s = getTileSize(gameId) const s = getTileSize(gameId)
const tile = getTile(gameId, tileIdx) const tile = getTile(gameId, tileIdx)
return { return {
@ -238,55 +329,55 @@ const getTileBounds = (gameId, tileIdx) => {
} }
} }
const getTileZIndex = (gameId, tileIdx) => { const getTileZIndex = (gameId: string, tileIdx: number) => {
const tile = getTile(gameId, tileIdx) const tile = getTile(gameId, tileIdx)
return tile.z return tile.z
} }
const getFirstOwnedTileIdx = (gameId, userId) => { const getFirstOwnedTileIdx = (gameId: string, playerId: string) => {
for (let t of GAMES[gameId].puzzle.tiles) { for (let t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodeTile(t) const tile = Util.decodeTile(t)
if (tile.owner === userId) { if (tile.owner === playerId) {
return tile.idx return tile.idx
} }
} }
return -1 return -1
} }
const getFirstOwnedTile = (gameId, userId) => { const getFirstOwnedTile = (gameId: string, playerId: string) => {
const idx = getFirstOwnedTileIdx(gameId, userId) const idx = getFirstOwnedTileIdx(gameId, playerId)
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx] return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
} }
const getTileDrawOffset = (gameId) => { const getTileDrawOffset = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileDrawOffset return GAMES[gameId].puzzle.info.tileDrawOffset
} }
const getTileDrawSize = (gameId) => { const getTileDrawSize = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileDrawSize return GAMES[gameId].puzzle.info.tileDrawSize
} }
const getTileSize = (gameId) => { const getTileSize = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileSize return GAMES[gameId].puzzle.info.tileSize
} }
const getStartTs = (gameId) => { const getStartTs = (gameId: string) => {
return GAMES[gameId].puzzle.data.started return GAMES[gameId].puzzle.data.started
} }
const getFinishTs = (gameId) => { const getFinishTs = (gameId: string) => {
return GAMES[gameId].puzzle.data.finished return GAMES[gameId].puzzle.data.finished
} }
const getMaxGroup = (gameId) => { const getMaxGroup = (gameId: string) => {
return GAMES[gameId].puzzle.data.maxGroup return GAMES[gameId].puzzle.data.maxGroup
} }
const getMaxZIndex = (gameId) => { const getMaxZIndex = (gameId: string) => {
return GAMES[gameId].puzzle.data.maxZ return GAMES[gameId].puzzle.data.maxZ
} }
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => { const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>) => {
let maxZ = 0 let maxZ = 0
for (let tileIdx of tileIdxs) { for (let tileIdx of tileIdxs) {
let tileZIndex = getTileZIndex(gameId, tileIdx) let tileZIndex = getTileZIndex(gameId, tileIdx)
@ -297,7 +388,7 @@ const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
return maxZ return maxZ
} }
function srcPosByTileIdx(gameId, tileIdx) { function srcPosByTileIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx) const c = Util.coordByTileIdx(info, tileIdx)
@ -307,7 +398,7 @@ function srcPosByTileIdx(gameId, tileIdx) {
return { x: cx, y: cy } return { x: cx, y: cy }
} }
function getSurroundingTilesByIdx(gameId, tileIdx) { function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx) const c = Util.coordByTileIdx(info, tileIdx)
@ -324,19 +415,19 @@ function getSurroundingTilesByIdx(gameId, tileIdx) {
] ]
} }
const setTilesZIndex = (gameId, tileIdxs, zIndex) => { const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number) => {
for (let tilesIdx of tileIdxs) { for (let tilesIdx of tileIdxs) {
changeTile(gameId, tilesIdx, { z: zIndex }) changeTile(gameId, tilesIdx, { z: zIndex })
} }
} }
const moveTileDiff = (gameId, tileIdx, diff) => { const moveTileDiff = (gameId: string, tileIdx: number, diff: Point) => {
const oldPos = getTilePos(gameId, tileIdx) const oldPos = getTilePos(gameId, tileIdx)
const pos = Geometry.pointAdd(oldPos, diff) const pos = Geometry.pointAdd(oldPos, diff)
changeTile(gameId, tileIdx, { pos }) changeTile(gameId, tileIdx, { pos })
} }
const moveTilesDiff = (gameId, tileIdxs, diff) => { const moveTilesDiff = (gameId: string, tileIdxs: Array<number>, diff: Point) => {
const tileDrawSize = getTileDrawSize(gameId) const tileDrawSize = getTileDrawSize(gameId)
const bounds = getBounds(gameId) const bounds = getBounds(gameId)
const cappedDiff = diff const cappedDiff = diff
@ -360,20 +451,24 @@ const moveTilesDiff = (gameId, tileIdxs, diff) => {
} }
} }
const finishTiles = (gameId, tileIdxs) => { const finishTiles = (gameId: string, tileIdxs: Array<number>) => {
for (let tileIdx of tileIdxs) { for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner: -1, z: 1 }) changeTile(gameId, tileIdx, { owner: -1, z: 1 })
} }
} }
const setTilesOwner = (gameId, tileIdxs, owner) => { const setTilesOwner = (
gameId: string,
tileIdxs: Array<number>,
owner: string|number
) => {
for (let tileIdx of tileIdxs) { for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner }) changeTile(gameId, tileIdx, { owner })
} }
} }
// get all grouped tiles for a tile // get all grouped tiles for a tile
function getGroupedTileIdxs(gameId, tileIdx) { function getGroupedTileIdxs(gameId: string, tileIdx: number) {
const tiles = GAMES[gameId].puzzle.tiles const tiles = GAMES[gameId].puzzle.tiles
const tile = Util.decodeTile(tiles[tileIdx]) const tile = Util.decodeTile(tiles[tileIdx])
@ -393,7 +488,7 @@ function getGroupedTileIdxs(gameId, tileIdx) {
// Returns the index of the puzzle tile with the highest z index // Returns the index of the puzzle tile with the highest z index
// that is not finished yet and that matches the position // that is not finished yet and that matches the position
const freeTileIdxByPos = (gameId, pos) => { const freeTileIdxByPos = (gameId: string, pos: Point) => {
let info = GAMES[gameId].puzzle.info let info = GAMES[gameId].puzzle.info
let tiles = GAMES[gameId].puzzle.tiles let tiles = GAMES[gameId].puzzle.tiles
@ -421,75 +516,75 @@ const freeTileIdxByPos = (gameId, pos) => {
return tileIdx return tileIdx
} }
const getPlayerBgColor = (gameId, playerId) => { const getPlayerBgColor = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId) const p = getPlayer(gameId, playerId)
return p ? p.bgcolor : null return p ? p.bgcolor : null
} }
const getPlayerColor = (gameId, playerId) => { const getPlayerColor = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId) const p = getPlayer(gameId, playerId)
return p ? p.color : null return p ? p.color : null
} }
const getPlayerName = (gameId, playerId) => { const getPlayerName = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId) const p = getPlayer(gameId, playerId)
return p ? p.name : null return p ? p.name : null
} }
const getPlayerPoints = (gameId, playerId) => { const getPlayerPoints = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId) const p = getPlayer(gameId, playerId)
return p ? p.points : null return p ? p.points : null
} }
// determine if two tiles are grouped together // determine if two tiles are grouped together
const areGrouped = (gameId, tileIdx1, tileIdx2) => { const areGrouped = (gameId: string, tileIdx1: number, tileIdx2: number) => {
const g1 = getTileGroup(gameId, tileIdx1) const g1 = getTileGroup(gameId, tileIdx1)
const g2 = getTileGroup(gameId, tileIdx2) const g2 = getTileGroup(gameId, tileIdx2)
return g1 && g1 === g2 return g1 && g1 === g2
} }
const getTableWidth = (gameId) => { const getTableWidth = (gameId: string) => {
return GAMES[gameId].puzzle.info.table.width return GAMES[gameId].puzzle.info.table.width
} }
const getTableHeight = (gameId) => { const getTableHeight = (gameId: string) => {
return GAMES[gameId].puzzle.info.table.height return GAMES[gameId].puzzle.info.table.height
} }
const getPuzzle = (gameId) => { const getPuzzle = (gameId: string) => {
return GAMES[gameId].puzzle return GAMES[gameId].puzzle
} }
const getRng = (gameId) => { const getRng = (gameId: string): Rng => {
return GAMES[gameId].rng.obj return GAMES[gameId].rng.obj
} }
const getPuzzleWidth = (gameId) => { const getPuzzleWidth = (gameId: string) => {
return GAMES[gameId].puzzle.info.width return GAMES[gameId].puzzle.info.width
} }
const getPuzzleHeight = (gameId) => { const getPuzzleHeight = (gameId: string) => {
return GAMES[gameId].puzzle.info.height return GAMES[gameId].puzzle.info.height
} }
function handleInput(gameId, playerId, input, ts) { function handleInput(gameId: string, playerId: string, input: any, ts: number) {
const puzzle = GAMES[gameId].puzzle const puzzle = GAMES[gameId].puzzle
const evtInfo = getEvtInfo(gameId, playerId) const evtInfo = getEvtInfo(gameId, playerId)
const changes = [] const changes = [] as Array<Array<any>>
const _dataChange = () => { const _dataChange = () => {
changes.push([Protocol.CHANGE_DATA, puzzle.data]) changes.push([Protocol.CHANGE_DATA, puzzle.data])
} }
const _tileChange = (tileIdx) => { const _tileChange = (tileIdx: number) => {
changes.push([ changes.push([
Protocol.CHANGE_TILE, Protocol.CHANGE_TILE,
Util.encodeTile(getTile(gameId, tileIdx)), Util.encodeTile(getTile(gameId, tileIdx)),
]) ])
} }
const _tileChanges = (tileIdxs) => { const _tileChanges = (tileIdxs: Array<number>) => {
for (const tileIdx of tileIdxs) { for (const tileIdx of tileIdxs) {
_tileChange(tileIdx) _tileChange(tileIdx)
} }
@ -503,7 +598,7 @@ function handleInput(gameId, playerId, input, ts) {
} }
// put both tiles (and their grouped tiles) in the same group // put both tiles (and their grouped tiles) in the same group
const groupTiles = (gameId, tileIdx1, tileIdx2) => { const groupTiles = (gameId: string, tileIdx1: number, tileIdx2: number) => {
const tiles = GAMES[gameId].puzzle.tiles const tiles = GAMES[gameId].puzzle.tiles
const group1 = getTileGroup(gameId, tileIdx1) const group1 = getTileGroup(gameId, tileIdx1)
const group2 = getTileGroup(gameId, tileIdx2) const group2 = getTileGroup(gameId, tileIdx2)
@ -669,7 +764,12 @@ function handleInput(gameId, playerId, input, ts) {
} }
} else { } else {
// Snap to other tiles // Snap to other tiles
const check = (gameId, tileIdx, otherTileIdx, off) => { const check = (
gameId: string,
tileIdx: number,
otherTileIdx: number,
off: Array<number>
) => {
let info = GAMES[gameId].puzzle.info let info = GAMES[gameId].puzzle.info
if (otherTileIdx < 0) { if (otherTileIdx < 0) {
return false return false

View file

@ -1,25 +1,37 @@
function pointSub(a, b) { interface Point {
x: number
y: number
}
interface Rect {
x: number
y: number
w: number
h: number
}
function pointSub(a: Point, b: Point): Point {
return { x: a.x - b.x, y: a.y - b.y } return { x: a.x - b.x, y: a.y - b.y }
} }
function pointAdd(a, b) { function pointAdd(a: Point, b: Point): Point {
return { x: a.x + b.x, y: a.y + b.y } return { x: a.x + b.x, y: a.y + b.y }
} }
function pointDistance(a, b) { function pointDistance(a: Point, b: Point): number {
const diffX = a.x - b.x const diffX = a.x - b.x
const diffY = a.y - b.y const diffY = a.y - b.y
return Math.sqrt(diffX * diffX + diffY * diffY) return Math.sqrt(diffX * diffX + diffY * diffY)
} }
function pointInBounds(pt, rect) { function pointInBounds(pt: Point, rect: Rect): boolean {
return pt.x >= rect.x return pt.x >= rect.x
&& pt.x <= rect.x + rect.w && pt.x <= rect.x + rect.w
&& pt.y >= rect.y && pt.y >= rect.y
&& pt.y <= rect.y + rect.h && pt.y <= rect.y + rect.h
} }
function rectCenter(rect) { function rectCenter(rect: Rect): Point {
return { return {
x: rect.x + (rect.w / 2), x: rect.x + (rect.w / 2),
y: rect.y + (rect.h / 2), y: rect.y + (rect.h / 2),
@ -35,7 +47,7 @@ function rectCenter(rect) {
* @param number y * @param number y
* @returns {x, y, w, h} * @returns {x, y, w, h}
*/ */
function rectMoved(rect, x, y) { function rectMoved(rect: Rect, x: number, y: number): Rect {
return { return {
x: rect.x + x, x: rect.x + x,
y: rect.y + y, y: rect.y + y,
@ -51,7 +63,7 @@ function rectMoved(rect, x, y) {
* @param {x, y, w, h} rectB * @param {x, y, w, h} rectB
* @returns bool * @returns bool
*/ */
function rectsOverlap(rectA, rectB) { function rectsOverlap(rectA: Rect, rectB: Rect): boolean {
return !( return !(
rectB.x > (rectA.x + rectA.w) rectB.x > (rectA.x + rectA.w)
|| rectA.x > (rectB.x + rectB.w) || rectA.x > (rectB.x + rectB.w)
@ -60,7 +72,7 @@ function rectsOverlap(rectA, rectB) {
) )
} }
function rectCenterDistance(rectA, rectB) { function rectCenterDistance(rectA: Rect, rectB: Rect): number {
return pointDistance(rectCenter(rectA), rectCenter(rectB)) return pointDistance(rectCenter(rectA), rectCenter(rectB))
} }

View file

@ -1,24 +1,32 @@
interface RngSerialized {
rand_high: number,
rand_low: number,
}
export class Rng { export class Rng {
constructor(seed) { rand_high: number
rand_low: number
constructor(seed: number) {
this.rand_high = seed || 0xDEADC0DE this.rand_high = seed || 0xDEADC0DE
this.rand_low = seed ^ 0x49616E42 this.rand_low = seed ^ 0x49616E42
} }
random (min, max) { random (min: number, max: number) {
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff; this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff; this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
var n = (this.rand_high >>> 0) / 0xffffffff; var n = (this.rand_high >>> 0) / 0xffffffff;
return (min + n * (max-min+1))|0; return (min + n * (max-min+1))|0;
} }
static serialize (rng) { static serialize (rng: Rng): RngSerialized {
return { return {
rand_high: rng.rand_high, rand_high: rng.rand_high,
rand_low: rng.rand_low rand_low: rng.rand_low
} }
} }
static unserialize (rngSerialized) { static unserialize (rngSerialized: RngSerialized): Rng {
const rng = new Rng(0) const rng = new Rng(0)
rng.rand_high = rngSerialized.rand_high rng.rand_high = rngSerialized.rand_high
rng.rand_low = rngSerialized.rand_low rng.rand_low = rngSerialized.rand_low

View file

@ -17,7 +17,7 @@ export const timestamp = () => {
) )
} }
export const durationStr = (duration) => { export const durationStr = (duration: number) => {
const d = Math.floor(duration / DAY) const d = Math.floor(duration / DAY)
duration = duration % DAY duration = duration % DAY
@ -32,7 +32,7 @@ export const durationStr = (duration) => {
return `${d}d ${h}h ${m}m ${s}s` return `${d}d ${h}h ${m}m ${s}s`
} }
export const timeDiffStr = (from, to) => durationStr(to - from) export const timeDiffStr = (from: number, to: number) => durationStr(to - from)
export default { export default {
MS, MS,

View file

@ -1,7 +1,7 @@
import { Rng } from './Rng.js' import { Rng } from './Rng'
const pad = (x, pad) => { const pad = (x: any, pad: string) => {
const str = `${x}` const str = `${x}`
if (str.length >= pad.length) { if (str.length >= pad.length) {
return str return str
@ -9,8 +9,8 @@ const pad = (x, pad) => {
return pad.substr(0, pad.length - str.length) + str return pad.substr(0, pad.length - str.length) + str
} }
export const logger = (...pre) => { export const logger = (...pre: Array<any>) => {
const log = (m) => (...args) => { const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
const d = new Date() const d = new Date()
const hh = pad(d.getHours(), '00') const hh = pad(d.getHours(), '00')
const mm = pad(d.getMinutes(), '00') const mm = pad(d.getMinutes(), '00')
@ -29,34 +29,21 @@ export const uniqId = () => Date.now().toString(36) + Math.random().toString(36)
// get a random int between min and max (inclusive) // get a random int between min and max (inclusive)
export const randomInt = ( export const randomInt = (
/** @type Rng */ rng, rng: Rng,
min, min: number,
max max: number,
) => rng.random(min, max) ) => rng.random(min, max)
// get one random item from the given array // get one random item from the given array
export const choice = ( export const choice = (
/** @type Rng */ rng, rng: Rng,
array array: Array<any>
) => array[randomInt(rng, 0, array.length - 1)] ) => array[randomInt(rng, 0, array.length - 1)]
export const throttle = (fn, delay) => {
let canCall = true
return (...args) => {
if (canCall) {
fn.apply(null, args)
canCall = false
setTimeout(() => {
canCall = true
}, delay)
}
}
}
// return a shuffled (shallow) copy of the given array // return a shuffled (shallow) copy of the given array
export const shuffle = ( export const shuffle = (
/** @type Rng */ rng, rng: Rng,
array array: Array<any>
) => { ) => {
const arr = array.slice() const arr = array.slice()
for (let i = 0; i <= arr.length - 2; i++) for (let i = 0; i <= arr.length - 2; i++)
@ -69,7 +56,7 @@ export const shuffle = (
return arr return arr
} }
function encodeShape(data) { function encodeShape(data: any): number {
if (typeof data === 'number') { if (typeof data === 'number') {
return data return data
} }
@ -86,7 +73,7 @@ function encodeShape(data) {
| ((data.left + 1) << 6) | ((data.left + 1) << 6)
} }
function decodeShape(data) { function decodeShape(data: any) {
if (typeof data !== 'number') { if (typeof data !== 'number') {
return data return data
} }
@ -98,14 +85,14 @@ function decodeShape(data) {
} }
} }
function encodeTile(data) { function encodeTile(data: any): Array<any> {
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data return data
} }
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group] return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
} }
function decodeTile(data) { function decodeTile(data: any) {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
return data return data
} }
@ -121,7 +108,7 @@ function decodeTile(data) {
} }
} }
function encodePlayer(data) { function encodePlayer(data: any): Array<any> {
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data return data
} }
@ -138,7 +125,7 @@ function encodePlayer(data) {
] ]
} }
function decodePlayer(data) { function decodePlayer(data: any) {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
return data return data
} }
@ -155,7 +142,7 @@ function decodePlayer(data) {
} }
} }
function encodeGame(data) { function encodeGame(data: any): Array<any> {
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data return data
} }
@ -170,7 +157,7 @@ function encodeGame(data) {
] ]
} }
function decodeGame(data) { function decodeGame(data: any) {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
return data return data
} }
@ -187,7 +174,7 @@ function decodeGame(data) {
} }
} }
function coordByTileIdx(info, tileIdx) { function coordByTileIdx(info: any, tileIdx: number) {
const wTiles = info.width / info.tileSize const wTiles = info.width / info.tileSize
return { return {
x: tileIdx % wTiles, x: tileIdx % wTiles,
@ -195,7 +182,7 @@ function coordByTileIdx(info, tileIdx) {
} }
} }
const hash = (str) => { const hash = (str: string): number => {
let hash = 0 let hash = 0
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
@ -211,7 +198,6 @@ export default {
uniqId, uniqId,
randomInt, randomInt,
choice, choice,
throttle,
shuffle, shuffle,
encodeShape, encodeShape,

View file

@ -1,6 +1,4 @@
export default { <template>
name: 'app',
template: `
<div id="app"> <div id="app">
<ul class="nav" v-if="showNav"> <ul class="nav" v-if="showNav">
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li> <li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
@ -8,11 +6,18 @@ export default {
</ul> </ul>
<router-view /> <router-view />
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'app',
computed: { computed: {
showNav () { showNav () {
// TODO: add info wether to show nav to route props // TODO: add info wether to show nav to route props
return !['game', 'replay'].includes(this.$route.name) return !['game', 'replay'].includes(String(this.$route.name))
}, },
}, },
} })
</script>

View file

@ -1,31 +1,39 @@
"use strict"
const MIN_ZOOM = .1 const MIN_ZOOM = .1
const MAX_ZOOM = 6 const MAX_ZOOM = 6
const ZOOM_STEP = .05 const ZOOM_STEP = .05
type ZOOM_DIR = 'in'|'out'
interface Point {
x: number
y: number
}
interface Dim {
w: number
h: number
}
export default function Camera () { export default function Camera () {
let x = 0 let x = 0
let y = 0 let y = 0
let curZoom = 1 let curZoom = 1
const move = (byX, byY) => { const move = (byX: number, byY: number) => {
x += byX / curZoom x += byX / curZoom
y += byY / curZoom y += byY / curZoom
} }
const calculateNewZoom = (inout) => { const calculateNewZoom = (inout: ZOOM_DIR): number => {
const factor = inout === 'in' ? 1 : -1 const factor = inout === 'in' ? 1 : -1
const newzoom = curZoom + ZOOM_STEP * curZoom * factor const newzoom = curZoom + ZOOM_STEP * curZoom * factor
const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM) const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM)
return capped return capped
} }
const canZoom = (inout) => { const canZoom = (inout: ZOOM_DIR): boolean => {
return curZoom != calculateNewZoom(inout) return curZoom != calculateNewZoom(inout)
} }
const setZoom = (newzoom, viewportCoordCenter) => { const setZoom = (newzoom: number, viewportCoordCenter: Point): boolean => {
if (curZoom == newzoom) { if (curZoom == newzoom) {
return false return false
} }
@ -43,7 +51,7 @@ export default function Camera () {
* Zooms towards/away from the provided coordinate, if possible. * Zooms towards/away from the provided coordinate, if possible.
* If at max or min zoom respectively, no zooming is performed. * If at max or min zoom respectively, no zooming is performed.
*/ */
const zoom = (inout, viewportCoordCenter) => { const zoom = (inout: ZOOM_DIR, viewportCoordCenter: Point): boolean => {
return setZoom(calculateNewZoom(inout), viewportCoordCenter) return setZoom(calculateNewZoom(inout), viewportCoordCenter)
} }
@ -52,7 +60,7 @@ export default function Camera () {
* coordinate in the world, rounded * coordinate in the world, rounded
* @param {x, y} viewportCoord * @param {x, y} viewportCoord
*/ */
const viewportToWorld = (viewportCoord) => { const viewportToWorld = (viewportCoord: Point): Point => {
const { x, y } = viewportToWorldRaw(viewportCoord) const { x, y } = viewportToWorldRaw(viewportCoord)
return { x: Math.round(x), y: Math.round(y) } return { x: Math.round(x), y: Math.round(y) }
} }
@ -62,7 +70,7 @@ export default function Camera () {
* coordinate in the world, not rounded * coordinate in the world, not rounded
* @param {x, y} viewportCoord * @param {x, y} viewportCoord
*/ */
const viewportToWorldRaw = (viewportCoord) => { const viewportToWorldRaw = (viewportCoord: Point): Point => {
return { return {
x: (viewportCoord.x / curZoom) - x, x: (viewportCoord.x / curZoom) - x,
y: (viewportCoord.y / curZoom) - y, y: (viewportCoord.y / curZoom) - y,
@ -74,7 +82,7 @@ export default function Camera () {
* coordinate in the viewport, rounded * coordinate in the viewport, rounded
* @param {x, y} worldCoord * @param {x, y} worldCoord
*/ */
const worldToViewport = (worldCoord) => { const worldToViewport = (worldCoord: Point): Point => {
const { x, y } = worldToViewportRaw(worldCoord) const { x, y } = worldToViewportRaw(worldCoord)
return { x: Math.round(x), y: Math.round(y) } return { x: Math.round(x), y: Math.round(y) }
} }
@ -84,7 +92,7 @@ export default function Camera () {
* coordinate in the viewport, not rounded * coordinate in the viewport, not rounded
* @param {x, y} worldCoord * @param {x, y} worldCoord
*/ */
const worldToViewportRaw = (worldCoord) => { const worldToViewportRaw = (worldCoord: Point): Point => {
return { return {
x: (worldCoord.x + x) * curZoom, x: (worldCoord.x + x) * curZoom,
y: (worldCoord.y + y) * curZoom, y: (worldCoord.y + y) * curZoom,
@ -96,7 +104,7 @@ export default function Camera () {
* one in the viewport, rounded * one in the viewport, rounded
* @param {w, h} worldDim * @param {w, h} worldDim
*/ */
const worldDimToViewport = (worldDim) => { const worldDimToViewport = (worldDim: Dim): Dim => {
const { w, h } = worldDimToViewportRaw(worldDim) const { w, h } = worldDimToViewportRaw(worldDim)
return { w: Math.round(w), h: Math.round(h) } return { w: Math.round(w), h: Math.round(h) }
} }
@ -107,7 +115,7 @@ export default function Camera () {
* one in the viewport, not rounded * one in the viewport, not rounded
* @param {w, h} worldDim * @param {w, h} worldDim
*/ */
const worldDimToViewportRaw = (worldDim) => { const worldDimToViewportRaw = (worldDim: Dim): Dim => {
return { return {
w: worldDim.w * curZoom, w: worldDim.w * curZoom,
h: worldDim.h * curZoom, h: worldDim.h * curZoom,

View file

@ -1,7 +1,7 @@
"use strict" "use strict"
import { logger } from '../common/Util.js' import { logger } from '../common/Util'
import Protocol from './../common/Protocol.js' import Protocol from './../common/Protocol'
const log = logger('Communication.js') const log = logger('Communication.js')
@ -14,27 +14,26 @@ const CONN_STATE_CONNECTED = 2 // connected
const CONN_STATE_CONNECTING = 3 // connecting const CONN_STATE_CONNECTING = 3 // connecting
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose) const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
/** @type WebSocket */ let ws: WebSocket
let ws let changesCallback = (msg: Array<any>) => {}
let changesCallback = () => {} let connectionStateChangeCallback = (state: number) => {}
let connectionStateChangeCallback = () => {}
// TODO: change these to something like on(EVT, cb) // TODO: change these to something like on(EVT, cb)
function onServerChange(callback) { function onServerChange(callback: (msg: Array<any>) => void) {
changesCallback = callback changesCallback = callback
} }
function onConnectionStateChange(callback) { function onConnectionStateChange(callback: (state: number) => void) {
connectionStateChangeCallback = callback connectionStateChangeCallback = callback
} }
let connectionState = CONN_STATE_NOT_CONNECTED let connectionState = CONN_STATE_NOT_CONNECTED
const setConnectionState = (v) => { const setConnectionState = (state: number) => {
if (connectionState !== v) { if (connectionState !== state) {
connectionState = v connectionState = state
connectionStateChangeCallback(v) connectionStateChangeCallback(state)
} }
} }
function send(message) { function send(message: Array<any>): void {
if (connectionState === CONN_STATE_CONNECTED) { if (connectionState === CONN_STATE_CONNECTED) {
try { try {
ws.send(JSON.stringify(message)) ws.send(JSON.stringify(message))
@ -45,9 +44,14 @@ function send(message) {
} }
let clientSeq let clientSeq: number
let events let events: Record<number, any>
function connect(address, gameId, clientId) {
function connect(
address: string,
gameId: string,
clientId: string
): Promise<any> {
clientSeq = 0 clientSeq = 0
events = {} events = {}
setConnectionState(CONN_STATE_CONNECTING) setConnectionState(CONN_STATE_CONNECTING)
@ -55,7 +59,6 @@ function connect(address, gameId, clientId) {
ws = new WebSocket(address, clientId + '|' + gameId) ws = new WebSocket(address, clientId + '|' + gameId)
ws.onopen = (e) => { ws.onopen = (e) => {
setConnectionState(CONN_STATE_CONNECTED) setConnectionState(CONN_STATE_CONNECTED)
connectionStateChangeCallback()
send([Protocol.EV_CLIENT_INIT]) send([Protocol.EV_CLIENT_INIT])
} }
ws.onmessage = (e) => { ws.onmessage = (e) => {
@ -94,7 +97,11 @@ function connect(address, gameId, clientId) {
} }
// TOOD: change replay stuff // TOOD: change replay stuff
function connectReplay(address, gameId, clientId) { function connectReplay(
address: string,
gameId: string,
clientId: string
): Promise<{ game: any, log: Array<any> }> {
clientSeq = 0 clientSeq = 0
events = {} events = {}
setConnectionState(CONN_STATE_CONNECTING) setConnectionState(CONN_STATE_CONNECTING)
@ -110,7 +117,8 @@ function connectReplay(address, gameId, clientId) {
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) { if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
const game = msg[1] const game = msg[1]
const log = msg[2] const log = msg[2]
resolve({game, log}) const replay: { game: any, log: Array<any> } = { game, log }
resolve(replay)
} else { } else {
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]` throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
} }
@ -131,7 +139,7 @@ function connectReplay(address, gameId, clientId) {
}) })
} }
function disconnect() { function disconnect(): void {
if (ws) { if (ws) {
ws.close(CODE_CUSTOM_DISCONNECT) ws.close(CODE_CUSTOM_DISCONNECT)
} }
@ -139,7 +147,7 @@ function disconnect() {
events = {} events = {}
} }
function sendClientEvent(evt) { function sendClientEvent(evt: any): void {
// when sending event, increase number of sent events // when sending event, increase number of sent events
// and add the event locally // and add the event locally
clientSeq++; clientSeq++;

View file

@ -1,18 +1,18 @@
"use strict" "use strict"
import { logger } from '../common/Util.js' import { logger } from '../common/Util'
const log = logger('Debug.js') const log = logger('Debug.js')
let _pt = 0 let _pt = 0
let _mindiff = 0 let _mindiff = 0
const checkpoint_start = (mindiff) => { const checkpoint_start = (mindiff: number) => {
_pt = performance.now() _pt = performance.now()
_mindiff = mindiff _mindiff = mindiff
} }
const checkpoint = (label) => { const checkpoint = (label: string) => {
const now = performance.now() const now = performance.now()
const diff = now - _pt const diff = now - _pt
if (diff > _mindiff) { if (diff > _mindiff) {

View file

@ -1,7 +1,7 @@
"use strict" "use strict"
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng'
import Util from '../common/Util.js' import Util from '../common/Util'
let minVx = -10 let minVx = -10
let deltaVx = 20 let deltaVx = 20
@ -20,7 +20,7 @@ const explosionDividerFactor = 10
const nBombs = 1 const nBombs = 1
const percentChanceNewBomb = 5 const percentChanceNewBomb = 5
function color(/** @type Rng */ rng) { function color(rng: Rng) {
const r = Util.randomInt(rng, 0, 255) const r = Util.randomInt(rng, 0, 255)
const g = Util.randomInt(rng, 0, 255) const g = Util.randomInt(rng, 0, 255)
const b = Util.randomInt(rng, 0, 255) const b = Util.randomInt(rng, 0, 255)
@ -29,7 +29,19 @@ function color(/** @type Rng */ rng) {
// A Bomb. Or firework. // A Bomb. Or firework.
class Bomb { class Bomb {
constructor(/** @type Rng */ rng) { radius: number
previousRadius: number
explodingDuration: number
hasExploded: boolean
alive: boolean
color: string
px: number
py: number
vx: number
vy: number
duration: number
constructor(rng: Rng) {
this.radius = bombRadius this.radius = bombRadius
this.previousRadius = bombRadius this.previousRadius = bombRadius
this.explodingDuration = explodingDuration this.explodingDuration = explodingDuration
@ -46,7 +58,7 @@ class Bomb {
this.duration = 0 this.duration = 0
} }
update(particlesVector) { update(particlesVector?: Array<Particle>) {
if (this.hasExploded) { if (this.hasExploded) {
const deltaRadius = explosionRadius - this.radius const deltaRadius = explosionRadius - this.radius
this.previousRadius = this.radius this.previousRadius = this.radius
@ -60,7 +72,9 @@ class Bomb {
this.vx += 0 this.vx += 0
this.vy += gravity this.vy += gravity
if (this.vy >= 0) { // invertion point if (this.vy >= 0) { // invertion point
this.explode(particlesVector) if (particlesVector) {
this.explode(particlesVector)
}
} }
this.px += this.vx this.px += this.vx
@ -68,7 +82,7 @@ class Bomb {
} }
} }
draw(ctx) { draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath() ctx.beginPath()
ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false) ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false)
if (!this.hasExploded) { if (!this.hasExploded) {
@ -78,7 +92,7 @@ class Bomb {
} }
} }
explode(particlesVector) { explode(particlesVector: Array<Particle>) {
this.hasExploded = true this.hasExploded = true
const e = 3 + Math.floor(Math.random() * 3) const e = 3 + Math.floor(Math.random() * 3)
for (let j = 0; j < e; j++) { for (let j = 0; j < e; j++) {
@ -94,7 +108,15 @@ class Bomb {
} }
class Particle { class Particle {
constructor(parent, angle, speed) { px: any
py: any
vx: number
vy: number
color: any
duration: number
alive: boolean
radius: number
constructor(parent: Bomb, angle: number, speed: number) {
this.px = parent.px this.px = parent.px
this.py = parent.py this.py = parent.py
this.vx = Math.cos(angle) * speed this.vx = Math.cos(angle) * speed
@ -102,6 +124,7 @@ class Particle {
this.color = parent.color this.color = parent.color
this.duration = 40 + Math.floor(Math.random() * 20) this.duration = 40 + Math.floor(Math.random() * 20)
this.alive = true this.alive = true
this.radius = 0
} }
update() { update() {
this.vx += 0 this.vx += 0
@ -117,7 +140,7 @@ class Particle {
} }
} }
draw(ctx) { draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath() ctx.beginPath()
ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false) ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false)
ctx.fillStyle = this.color ctx.fillStyle = this.color
@ -127,12 +150,22 @@ class Particle {
} }
class Controller { class Controller {
constructor(canvas, /** @type Rng */ rng) { canvas: HTMLCanvasElement
rng: Rng
ctx: CanvasRenderingContext2D
readyBombs: Array<Bomb>
explodedBombs: Array<Bomb>
particles: Array<Particle>
constructor(canvas: HTMLCanvasElement, rng: Rng) {
this.canvas = canvas this.canvas = canvas
this.rng = rng this.rng = rng
this.ctx = this.canvas.getContext('2d') this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
this.resize() this.resize()
this.readyBombs = []
this.explodedBombs = []
this.particles = []
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.resize() this.resize()
}) })
@ -177,6 +210,9 @@ class Controller {
const aliveBombs = [] const aliveBombs = []
while (this.explodedBombs.length > 0) { while (this.explodedBombs.length > 0) {
const bomb = this.explodedBombs.shift() const bomb = this.explodedBombs.shift()
if (!bomb) {
break;
}
bomb.update() bomb.update()
if (bomb.alive) { if (bomb.alive) {
aliveBombs.push(bomb) aliveBombs.push(bomb)
@ -187,6 +223,9 @@ class Controller {
const notExplodedBombs = [] const notExplodedBombs = []
while (this.readyBombs.length > 0) { while (this.readyBombs.length > 0) {
const bomb = this.readyBombs.shift() const bomb = this.readyBombs.shift()
if (!bomb) {
break
}
bomb.update(this.particles) bomb.update(this.particles)
if (bomb.hasExploded) { if (bomb.hasExploded) {
this.explodedBombs.push(bomb) this.explodedBombs.push(bomb)
@ -200,6 +239,9 @@ class Controller {
const aliveParticles = [] const aliveParticles = []
while (this.particles.length > 0) { while (this.particles.length > 0) {
const particle = this.particles.shift() const particle = this.particles.shift()
if (!particle) {
break
}
particle.update() particle.update()
if (particle.alive) { if (particle.alive) {
aliveParticles.push(particle) aliveParticles.push(particle)

View file

@ -1,13 +1,13 @@
"use strict" "use strict"
function createCanvas(width = 0, height = 0) { function createCanvas(width:number = 0, height:number = 0): HTMLCanvasElement {
const c = document.createElement('canvas') const c = document.createElement('canvas')
c.width = width c.width = width
c.height = height c.height = height
return c return c
} }
async function loadImageToBitmap(imagePath) { async function loadImageToBitmap(imagePath: string): Promise<ImageBitmap> {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image() const img = new Image()
img.onload = () => { img.onload = () => {
@ -17,24 +17,16 @@ async function loadImageToBitmap(imagePath) {
}) })
} }
async function resizeBitmap (bitmap, width, height) { async function resizeBitmap (bitmap: ImageBitmap, width: number, height: number): Promise<ImageBitmap> {
const c = createCanvas(width, height) const c = createCanvas(width, height)
const ctx = c.getContext('2d') const ctx = c.getContext('2d') as CanvasRenderingContext2D
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height) ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height)
return await createImageBitmap(c) return await createImageBitmap(c)
} }
async function createBitmap(width, height, color) { async function colorize(image: ImageBitmap, mask: ImageBitmap, color: string): Promise<ImageBitmap> {
const c = createCanvas(width, height)
const ctx = c.getContext('2d')
ctx.fillStyle = color
ctx.fillRect(0, 0, width, height)
return await createImageBitmap(c)
}
async function colorize(image, mask, color) {
const c = createCanvas(image.width, image.height) const c = createCanvas(image.width, image.height)
const ctx = c.getContext('2d') const ctx = c.getContext('2d') as CanvasRenderingContext2D
ctx.save() ctx.save()
ctx.drawImage(mask, 0, 0) ctx.drawImage(mask, 0, 0)
ctx.fillStyle = color ctx.fillStyle = color
@ -49,7 +41,6 @@ async function colorize(image, mask, color) {
} }
export default { export default {
createBitmap,
createCanvas, createCanvas,
loadImageToBitmap, loadImageToBitmap,
resizeBitmap, resizeBitmap,

View file

@ -1,12 +1,13 @@
"use strict" "use strict"
import Geometry from '../common/Geometry.js' import Geometry from '../common/Geometry'
import Graphics from './Graphics.js' import Graphics from './Graphics'
import Util, { logger } from './../common/Util.js' import Util, { logger } from './../common/Util'
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
const log = logger('PuzzleGraphics.js') const log = logger('PuzzleGraphics.js')
async function createPuzzleTileBitmaps(img, tiles, info) { async function createPuzzleTileBitmaps(img: ImageBitmap, tiles: Array<any>, info: PuzzleInfo) {
log.log('start createPuzzleTileBitmaps') log.log('start createPuzzleTileBitmaps')
var tileSize = info.tileSize var tileSize = info.tileSize
var tileMarginWidth = info.tileMarginWidth var tileMarginWidth = info.tileMarginWidth
@ -24,8 +25,8 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
const bitmaps = new Array(tiles.length) const bitmaps = new Array(tiles.length)
const paths = {} const paths: Record<string, Path2D> = {}
function pathForShape(shape) { function pathForShape(shape: PieceShape) {
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}` const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
if (paths[key]) { if (paths[key]) {
return paths[key] return paths[key]
@ -83,10 +84,10 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
} }
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize) const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
const ctx = c.getContext('2d') const ctx = c.getContext('2d') as CanvasRenderingContext2D
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize) const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
const ctx2 = c2.getContext('2d') const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
for (let t of tiles) { for (let t of tiles) {
const tile = Util.decodeTile(t) const tile = Util.decodeTile(t)
@ -197,7 +198,7 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
return bitmaps return bitmaps
} }
function srcRectByIdx(puzzleInfo, idx) { function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number) {
const c = Util.coordByTileIdx(puzzleInfo, idx) const c = Util.coordByTileIdx(puzzleInfo, idx)
return { return {
x: c.x * puzzleInfo.tileSize, x: c.x * puzzleInfo.tileSize,
@ -207,7 +208,7 @@ function srcRectByIdx(puzzleInfo, idx) {
} }
} }
async function loadPuzzleBitmaps(puzzle) { async function loadPuzzleBitmaps(puzzle: Puzzle) {
// load bitmap, to determine the original size of the image // load bitmap, to determine the original size of the image
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl) const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)

View file

@ -1,10 +1,4 @@
"use strict" <template>
import Communication from './../Communication.js'
export default {
name: 'connection-overlay',
template: `
<div class="overlay connection-lost" v-if="show"> <div class="overlay connection-lost" v-if="show">
<div class="overlay-content" v-if="lostConnection"> <div class="overlay-content" v-if="lostConnection">
<div> LOST CONNECTION </div> <div> LOST CONNECTION </div>
@ -13,7 +7,15 @@ export default {
<div class="overlay-content" v-if="connecting"> <div class="overlay-content" v-if="connecting">
<div>Connecting...</div> <div>Connecting...</div>
</div> </div>
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Communication from './../Communication'
export default defineComponent({
name: 'connection-overlay',
emits: { emits: {
reconnect: null, reconnect: null,
}, },
@ -21,14 +23,15 @@ export default {
connectionState: Number, connectionState: Number,
}, },
computed: { computed: {
lostConnection () { lostConnection (): boolean {
return this.connectionState === Communication.CONN_STATE_DISCONNECTED return this.connectionState === Communication.CONN_STATE_DISCONNECTED
}, },
connecting () { connecting (): boolean {
return this.connectionState === Communication.CONN_STATE_CONNECTING return this.connectionState === Communication.CONN_STATE_CONNECTING
}, },
show () { show (): boolean {
return this.lostConnection || this.connecting return !!(this.lostConnection || this.connecting)
}, },
} }
} })
</script>

View file

@ -1,13 +1,4 @@
"use strict" <template>
import Time from './../../common/Time.js'
export default {
name: 'game-teaser',
props: {
game: Object,
},
template: `
<div class="game-teaser" :style="style"> <div class="game-teaser" :style="style">
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }"> <router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
<span class="game-info-text"> <span class="game-info-text">
@ -19,9 +10,22 @@ export default {
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }"> <router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
Watch replay Watch replay
</router-link> </router-link>
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Time from './../../common/Time'
export default defineComponent({
name: 'game-teaser',
props: {
game: {
type: Object,
required: true,
},
},
computed: { computed: {
style() { style (): object {
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp' const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
return { return {
'background-image': `url("${url}")`, 'background-image': `url("${url}")`,
@ -29,7 +33,7 @@ export default {
}, },
}, },
methods: { methods: {
time(start, end) { time(start: number, end: number) {
const icon = end ? '🏁' : '⏳' const icon = end ? '🏁' : '⏳'
const from = start; const from = start;
const to = end || Time.timestamp() const to = end || Time.timestamp()
@ -37,4 +41,5 @@ export default {
return `${icon} ${timeDiffStr}` return `${icon} ${timeDiffStr}`
}, },
}, },
} })
</script>

View file

@ -1,11 +1,5 @@
"use strict" <template>
<div class="overlay transparent" @click="$emit('bgclick')">
// ingame component
// shows the help (key bindings)
export default {
name: 'help-overlay',
template: `<div class="overlay transparent" @click="$emit('bgclick')">
<table class="overlay-content help" @click.stop=""> <table class="overlay-content help" @click.stop="">
<tr><td> Move up:</td><td><div><kbd>W</kbd>/<kbd></kbd>/🖱</div></td></tr> <tr><td> Move up:</td><td><div><kbd>W</kbd>/<kbd></kbd>/🖱</div></td></tr>
<tr><td> Move down:</td><td><div><kbd>S</kbd>/<kbd></kbd>/🖱</div></td></tr> <tr><td> Move down:</td><td><div><kbd>S</kbd>/<kbd></kbd>/🖱</div></td></tr>
@ -19,8 +13,15 @@ export default {
<tr><td>🧩 Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr> <tr><td>🧩 Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
<tr><td>🧩 Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr> <tr><td>🧩 Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
</table> </table>
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'help-overlay',
emits: { emits: {
bgclick: null, bgclick: null,
}, },
} })
</script>

View 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>

View file

@ -1,16 +1,4 @@
"use strict" <template>
import GameCommon from './../../common/GameCommon.js'
import Upload from './../components/Upload.vue.js'
import ImageTeaser from './../components/ImageTeaser.vue.js'
export default {
name: 'new-game-dialog',
components: {
Upload,
ImageTeaser,
},
template: `
<div> <div>
<h1>New game</h1> <h1>New game</h1>
<table> <table>
@ -49,9 +37,23 @@ export default {
<h1>Image lib</h1> <h1>Image lib</h1>
<div> <div>
<image-teaser v-for="i in images" :image="i" @click="image = i" /> <image-teaser v-for="(i,idx) in images" :image="i" @click="image = i" :key="idx" />
</div> </div>
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import GameCommon from './../../common/GameCommon'
import Upload from './../components/Upload.vue'
import ImageTeaser from './../components/ImageTeaser.vue'
export default defineComponent({
name: 'new-game-dialog',
components: {
Upload,
ImageTeaser,
},
props: { props: {
images: Array, images: Array,
}, },
@ -66,8 +68,9 @@ export default {
} }
}, },
methods: { methods: {
mediaImgUploaded(j) { // TODO: ts type UploadedImage
this.image = j.image mediaImgUploaded(data: any) {
this.image = data.image
}, },
canStartNewGame () { canStartNewGame () {
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) { if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
@ -84,11 +87,12 @@ export default {
}, },
}, },
computed: { computed: {
scoreModeInt () { scoreModeInt (): number {
return parseInt(this.scoreMode, 10) return parseInt(`${this.scoreMode}`, 10)
}, },
tilesInt () { tilesInt (): number {
return parseInt(this.tiles, 10) return parseInt(`${this.tiles}`, 10)
}, },
}, },
} })
</script>

View 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>

View 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>

View file

@ -1,11 +1,4 @@
"use strict" <template>
// ingame component
// shows player scores
export default {
name: "scores",
template: `
<div class="scores"> <div class="scores">
<div>Scores</div> <div>Scores</div>
<table> <table>
@ -21,21 +14,33 @@ export default {
</tr> </tr>
</table> </table>
</div> </div>
`, </template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: "scores",
props: {
activePlayers: {
type: Array,
required: true,
},
idlePlayers: {
type: Array,
required: true,
},
},
computed: { computed: {
actives () { actives (): Array<any> {
// TODO: dont sort in place // TODO: dont sort in place
this.activePlayers.sort((a, b) => b.points - a.points) this.activePlayers.sort((a: any, b: any) => b.points - a.points)
return this.activePlayers return this.activePlayers
}, },
idles () { idles (): Array<any> {
// TODO: dont sort in place // TODO: dont sort in place
this.idlePlayers.sort((a, b) => b.points - a.points) this.idlePlayers.sort((a: any, b: any) => b.points - a.points)
return this.idlePlayers return this.idlePlayers
}, },
}, },
props: { })
activePlayers: Array, </script>
idlePlayers: Array,
},
}

View 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>

View file

@ -1,20 +1,23 @@
"use strict" <template>
<label>
<input type="file" style="display: none" @change="upload" :accept="accept" />
<span class="btn">{{label || 'Upload File'}}</span>
</label>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default { export default defineComponent({
name: 'upload', name: 'upload',
props: { props: {
accept: String, accept: String,
label: String, label: String,
}, },
template: `
<label>
<input type="file" style="display: none" @change="upload" :accept="accept" />
<span class="btn">{{label || 'Upload File'}}</span>
</label>
`,
methods: { methods: {
async upload(evt) { async upload(evt: Event) {
const file = evt.target.files[0] const target = (evt.target as HTMLInputElement)
if (!target.files) return;
const file = target.files[0]
if (!file) return; if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file, file.name); formData.append('file', file, file.name);
@ -26,4 +29,5 @@ export default {
this.$emit('uploaded', j) this.$emit('uploaded', j)
}, },
} }
} })
</script>

View file

@ -1,18 +1,32 @@
"use strict" "use strict"
import {run} from './gameloop.js' import {run} from './gameloop'
import Camera from './Camera.js' import Camera from './Camera'
import Graphics from './Graphics.js' import Graphics from './Graphics'
import Debug from './Debug.js' import Debug from './Debug'
import Communication from './Communication.js' import Communication from './Communication'
import Util, { logger } from './../common/Util.js' import Util from './../common/Util'
import PuzzleGraphics from './PuzzleGraphics.js' import PuzzleGraphics from './PuzzleGraphics'
import Game from './../common/GameCommon.js' import Game, { Player, Piece } from './../common/GameCommon'
import fireworksController from './Fireworks.js' import fireworksController from './Fireworks'
import Protocol from '../common/Protocol.js' import Protocol from '../common/Protocol'
import Time from '../common/Time.js' import Time from '../common/Time'
// const log = logger('game.js') declare global {
interface Window {
DEBUG?: boolean
}
}
// @see https://stackoverflow.com/a/59906630/392905
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
// @ts-ignore
const images = import.meta.globEager('./*.png')
export const MODE_PLAY = 'play' export const MODE_PLAY = 'play'
export const MODE_REPLAY = 'replay' export const MODE_REPLAY = 'replay'
@ -20,7 +34,34 @@ export const MODE_REPLAY = 'replay'
let PIECE_VIEW_FIXED = true let PIECE_VIEW_FIXED = true
let PIECE_VIEW_LOOSE = true let PIECE_VIEW_LOOSE = true
const shouldDrawPiece = (piece) => { interface Point {
x: number
y: number
}
interface Hud {
setActivePlayers: (v: Array<any>) => void
setIdlePlayers: (v: Array<any>) => void
setFinished: (v: boolean) => void
setDuration: (v: number) => void
setPiecesDone: (v: number) => void
setPiecesTotal: (v: number) => void
setConnectionState: (v: number) => void
togglePreview: () => void
setReplaySpeed?: (v: number) => void
setReplayPaused?: (v: boolean) => void
}
interface Replay {
log: Array<any>
logIdx: number
speeds: Array<number>
speedIdx: number
paused: boolean
lastRealTs: number
lastGameTs: number
gameStartTs: number
}
const shouldDrawPiece = (piece: Piece) => {
if (piece.owner === -1) { if (piece.owner === -1) {
return PIECE_VIEW_FIXED return PIECE_VIEW_FIXED
} }
@ -29,7 +70,7 @@ const shouldDrawPiece = (piece) => {
let RERENDER = true let RERENDER = true
function addCanvasToDom(TARGET_EL, canvas) { function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
canvas.width = window.innerWidth canvas.width = window.innerWidth
canvas.height = window.innerHeight canvas.height = window.innerHeight
TARGET_EL.appendChild(canvas) TARGET_EL.appendChild(canvas)
@ -41,8 +82,8 @@ function addCanvasToDom(TARGET_EL, canvas) {
return canvas return canvas
} }
function EventAdapter (canvas, window, viewport) { function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
let events = [] let events: Array<Array<any>> = []
let KEYS_ON = true let KEYS_ON = true
@ -54,15 +95,15 @@ function EventAdapter (canvas, window, viewport) {
let ZOOM_OUT = false let ZOOM_OUT = false
let SHIFT = false let SHIFT = false
const toWorldPoint = (x, y) => { const toWorldPoint = (x: number, y: number) => {
const pos = viewport.viewportToWorld({x, y}) const pos = viewport.viewportToWorld({x, y})
return [pos.x, pos.y] return [pos.x, pos.y]
} }
const mousePos = (ev) => toWorldPoint(ev.offsetX, ev.offsetY) const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2) const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
const key = (state, ev) => { const key = (state: boolean, ev: KeyboardEvent) => {
if (!KEYS_ON) { if (!KEYS_ON) {
return return
} }
@ -108,10 +149,10 @@ function EventAdapter (canvas, window, viewport) {
} }
}) })
window.addEventListener('keydown', (ev) => key(true, ev)) window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
window.addEventListener('keyup', (ev) => key(false, ev)) window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
window.addEventListener('keypress', (ev) => { window.addEventListener('keypress', (ev: KeyboardEvent) => {
if (!KEYS_ON) { if (!KEYS_ON) {
return return
} }
@ -128,7 +169,7 @@ function EventAdapter (canvas, window, viewport) {
} }
}) })
const addEvent = (event) => { const addEvent = (event: Array<any>) => {
events.push(event) events.push(event)
} }
@ -162,7 +203,7 @@ function EventAdapter (canvas, window, viewport) {
} }
} }
const setHotkeys = (state) => { const setHotkeys = (state: boolean) => {
KEYS_ON = state KEYS_ON = state
} }
@ -175,23 +216,23 @@ function EventAdapter (canvas, window, viewport) {
} }
export async function main( export async function main(
gameId, gameId: string,
clientId, clientId: string,
wsAddress, wsAddress: string,
MODE, MODE: string,
TARGET_EL, TARGET_EL: HTMLElement,
HUD HUD: Hud
) { ) {
if (typeof DEBUG === 'undefined') window.DEBUG = false if (typeof window.DEBUG === 'undefined') window.DEBUG = false
const shouldDrawPlayerText = (player) => { const shouldDrawPlayerText = (player: Player) => {
return MODE === MODE_REPLAY || player.id !== clientId return MODE === MODE_REPLAY || player.id !== clientId
} }
const cursorGrab = await Graphics.loadImageToBitmap('/grab.png') const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
const cursorHand = await Graphics.loadImageToBitmap('/hand.png') const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
const cursorGrabMask = await Graphics.loadImageToBitmap('/grab_mask.png') const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
const cursorHandMask = await Graphics.loadImageToBitmap('/hand_mask.png') const cursorHandMask = await Graphics.loadImageToBitmap(images['./hand_mask.png'].default)
// all cursors must be of the same dimensions // all cursors must be of the same dimensions
const CURSOR_W = cursorGrab.width const CURSOR_W = cursorGrab.width
@ -199,13 +240,17 @@ export async function main(
const CURSOR_H = cursorGrab.height const CURSOR_H = cursorGrab.height
const CURSOR_H_2 = Math.round(CURSOR_H / 2) const CURSOR_H_2 = Math.round(CURSOR_H / 2)
const cursors = {} const cursors: Record<string, ImageBitmap> = {}
const getPlayerCursor = async (p) => { const getPlayerCursor = async (p: Player) => {
const key = p.color + ' ' + p.d const key = p.color + ' ' + p.d
if (!cursors[key]) { if (!cursors[key]) {
const cursor = p.d ? cursorGrab : cursorHand const cursor = p.d ? cursorGrab : cursorHand
const mask = p.d ? cursorGrabMask : cursorHandMask if (p.color) {
cursors[key] = await Graphics.colorize(cursor, mask, p.color) const mask = p.d ? cursorGrabMask : cursorHandMask
cursors[key] = await Graphics.colorize(cursor, mask, p.color)
} else {
cursors[key] = cursor
}
} }
return cursors[key] return cursors[key]
} }
@ -215,22 +260,22 @@ export async function main(
// stuff only available in replay mode... // stuff only available in replay mode...
// TODO: refactor // TODO: refactor
const REPLAY = { const REPLAY: Replay = {
log: null, log: [],
logIdx: 0, logIdx: 0,
speeds: [0.5, 1, 2, 5, 10, 20, 50], speeds: [0.5, 1, 2, 5, 10, 20, 50],
speedIdx: 1, speedIdx: 1,
paused: false, paused: false,
lastRealTs: null, lastRealTs: 0,
lastGameTs: null, lastGameTs: 0,
gameStartTs: null, gameStartTs: 0,
} }
Communication.onConnectionStateChange((state) => { Communication.onConnectionStateChange((state) => {
HUD.setConnectionState(state) HUD.setConnectionState(state)
}) })
let TIME let TIME: () => number = () => 0
const connect = async () => { const connect = async () => {
if (MODE === MODE_PLAY) { if (MODE === MODE_PLAY) {
const game = await Communication.connect(wsAddress, gameId, clientId) const game = await Communication.connect(wsAddress, gameId, clientId)
@ -239,12 +284,12 @@ export async function main(
TIME = () => Time.timestamp() TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) { } else if (MODE === MODE_REPLAY) {
// TODO: change how replay connect is done... // TODO: change how replay connect is done...
const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId) const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game) const gameObject = Util.decodeGame(replay.game)
Game.setGame(gameObject.id, gameObject) Game.setGame(gameObject.id, gameObject)
REPLAY.log = log REPLAY.log = replay.log
REPLAY.lastRealTs = Time.timestamp() REPLAY.lastRealTs = Time.timestamp()
REPLAY.gameStartTs = REPLAY.log[0][REPLAY.log[0].length - 2] REPLAY.gameStartTs = parseInt(REPLAY.log[0][REPLAY.log[0].length - 2], 10)
REPLAY.lastGameTs = REPLAY.gameStartTs REPLAY.lastGameTs = REPLAY.gameStartTs
TIME = () => REPLAY.lastGameTs TIME = () => REPLAY.lastGameTs
} else { } else {
@ -280,21 +325,21 @@ export async function main(
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId)) const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
const fireworks = new fireworksController(canvas, Game.getRng(gameId)) const fireworks = new fireworksController(canvas, Game.getRng(gameId))
fireworks.init(canvas) fireworks.init()
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.classList.add('loaded') canvas.classList.add('loaded')
// initialize some view data // initialize some view data
// this global data will change according to input events // this global data will change according to input events
const viewport = new Camera() const viewport = Camera()
// center viewport // center viewport
viewport.move( viewport.move(
-(TABLE_WIDTH - canvas.width) /2, -(TABLE_WIDTH - canvas.width) /2,
-(TABLE_HEIGHT - canvas.height) /2 -(TABLE_HEIGHT - canvas.height) /2
) )
const evts = new EventAdapter(canvas, window, viewport) const evts = EventAdapter(canvas, window, viewport)
const previewImageUrl = Game.getImageUrl(gameId) const previewImageUrl = Game.getImageUrl(gameId)
@ -335,8 +380,12 @@ export async function main(
} }
const doSetSpeedStatus = () => { const doSetSpeedStatus = () => {
HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx]) if (HUD.setReplaySpeed) {
HUD.setReplayPaused(REPLAY.paused) HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx])
}
if (HUD.setReplayPaused) {
HUD.setReplayPaused(REPLAY.paused)
}
} }
const replayOnSpeedUp = () => { const replayOnSpeedUp = () => {
@ -419,17 +468,18 @@ export async function main(
} }
const entryWithTs = logEntry.slice() const entryWithTs = logEntry.slice()
entryWithTs[entryWithTs.length - 1] = nextTs
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) { if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
Game.addPlayer(gameId, ...entryWithTs.slice(1)) const playerId = entryWithTs[1]
Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) { } else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1]) const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
Game.addPlayer(gameId, playerId, ...entryWithTs.slice(2)) Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) { } else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1]) const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
Game.handleInput(gameId, playerId, ...entryWithTs.slice(2)) const input = entryWithTs[2]
Game.handleInput(gameId, playerId, input, nextTs)
RERENDER = true RERENDER = true
} }
REPLAY.logIdx = nextIdx REPLAY.logIdx = nextIdx
@ -440,7 +490,7 @@ export async function main(
}, 50) }, 50)
} }
let _last_mouse_down = null let _last_mouse_down: Point|null = null
const onUpdate = () => { const onUpdate = () => {
// handle key downs once per onUpdate // handle key downs once per onUpdate
// this will create Protocol.INPUT_EV_MOVE events if something // this will create Protocol.INPUT_EV_MOVE events if something
@ -552,13 +602,13 @@ export async function main(
let dim let dim
let bmp let bmp
if (DEBUG) Debug.checkpoint_start(0) if (window.DEBUG) Debug.checkpoint_start(0)
// CLEAR CTX // CLEAR CTX
// --------------------------------------------------------------- // ---------------------------------------------------------------
ctx.fillStyle = playerBgColor() ctx.fillStyle = playerBgColor()
ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillRect(0, 0, canvas.width, canvas.height)
if (DEBUG) Debug.checkpoint('clear done') if (window.DEBUG) Debug.checkpoint('clear done')
// --------------------------------------------------------------- // ---------------------------------------------------------------
@ -568,14 +618,14 @@ export async function main(
dim = viewport.worldDimToViewportRaw(BOARD_DIM) dim = viewport.worldDimToViewportRaw(BOARD_DIM)
ctx.fillStyle = 'rgba(255, 255, 255, .3)' ctx.fillStyle = 'rgba(255, 255, 255, .3)'
ctx.fillRect(pos.x, pos.y, dim.w, dim.h) ctx.fillRect(pos.x, pos.y, dim.w, dim.h)
if (DEBUG) Debug.checkpoint('board done') if (window.DEBUG) Debug.checkpoint('board done')
// --------------------------------------------------------------- // ---------------------------------------------------------------
// DRAW TILES // DRAW TILES
// --------------------------------------------------------------- // ---------------------------------------------------------------
const tiles = Game.getTilesSortedByZIndex(gameId) const tiles = Game.getTilesSortedByZIndex(gameId)
if (DEBUG) Debug.checkpoint('get tiles done') if (window.DEBUG) Debug.checkpoint('get tiles done')
dim = viewport.worldDimToViewportRaw(PIECE_DIM) dim = viewport.worldDimToViewportRaw(PIECE_DIM)
for (const tile of tiles) { for (const tile of tiles) {
@ -592,13 +642,13 @@ export async function main(
pos.x, pos.y, dim.w, dim.h pos.x, pos.y, dim.w, dim.h
) )
} }
if (DEBUG) Debug.checkpoint('tiles done') if (window.DEBUG) Debug.checkpoint('tiles done')
// --------------------------------------------------------------- // ---------------------------------------------------------------
// DRAW PLAYERS // DRAW PLAYERS
// --------------------------------------------------------------- // ---------------------------------------------------------------
const texts = [] const texts: Array<FixedLengthArray<[string, number, number]>> = []
// Cursors // Cursors
for (const p of Game.getActivePlayers(gameId, ts)) { for (const p of Game.getActivePlayers(gameId, ts)) {
bmp = await getPlayerCursor(p) bmp = await getPlayerCursor(p)
@ -619,14 +669,14 @@ export async function main(
ctx.fillText(txt, x, y) ctx.fillText(txt, x, y)
} }
if (DEBUG) Debug.checkpoint('players done') if (window.DEBUG) Debug.checkpoint('players done')
// propagate HUD changes // propagate HUD changes
// --------------------------------------------------------------- // ---------------------------------------------------------------
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts)) HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts)) HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
HUD.setPiecesDone(Game.getFinishedTileCount(gameId)) HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
if (DEBUG) Debug.checkpoint('HUD done') if (window.DEBUG) Debug.checkpoint('HUD done')
// --------------------------------------------------------------- // ---------------------------------------------------------------
if (justFinished()) { if (justFinished()) {
@ -642,18 +692,18 @@ export async function main(
}) })
return { return {
setHotkeys: (state) => { setHotkeys: (state: boolean) => {
evts.setHotkeys(state) evts.setHotkeys(state)
}, },
onBgChange: (value) => { onBgChange: (value: string) => {
localStorage.setItem('bg_color', value) localStorage.setItem('bg_color', value)
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value]) evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
}, },
onColorChange: (value) => { onColorChange: (value: string) => {
localStorage.setItem('player_color', value) localStorage.setItem('player_color', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value]) evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
}, },
onNameChange: (value) => { onNameChange: (value: string) => {
localStorage.setItem('player_name', value) localStorage.setItem('player_name', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value]) evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
}, },

View file

@ -1,6 +1,12 @@
"use strict" "use strict"
export const run = options => { interface GameLoopOptions {
fps?: number
slow?: number
update: (step: number) => any
render: (passed: number) => any
}
export const run = (options: GameLoopOptions) => {
const fps = options.fps || 60 const fps = options.fps || 60
const slow = options.slow || 1 const slow = options.slow || 1
const update = options.update const update = options.update

View file

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 148 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 125 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 170 B

After

Width:  |  Height:  |  Size: 170 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 140 B

Before After
Before After

10
src/frontend/index.html Normal file
View 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
View 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
View file

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View file

@ -1,25 +1,5 @@
"use strict" <template>
<div id="game">
import Scores from './../components/Scores.vue.js'
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
import ConnectionOverlay from './../components/ConnectionOverlay.vue.js'
import HelpOverlay from './../components/HelpOverlay.vue.js'
import { main, MODE_PLAY } from './../game.js'
export default {
name: 'game',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
ConnectionOverlay,
HelpOverlay,
},
template: `<div id="game">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" /> <settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" /> <preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" /> <help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
@ -46,18 +26,42 @@ export default {
</div> </div>
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" /> <scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_PLAY } from './../game'
export default defineComponent({
name: 'game',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
ConnectionOverlay,
HelpOverlay,
},
data() { data() {
return { return {
activePlayers: [], // TODO: ts Array<Player> type
idlePlayers: [], activePlayers: [] as PropType<Array<any>>,
idlePlayers: [] as PropType<Array<any>>,
finished: false, finished: false,
duration: 0, duration: 0,
piecesDone: 0, piecesDone: 0,
piecesTotal: 0, piecesTotal: 0,
overlay: null, overlay: '',
connectionState: 0, connectionState: 0,
@ -68,10 +72,10 @@ export default {
name: '', name: '',
}, },
previewImageUrl: '', previewImageUrl: '',
setHotkeys: () => {}, setHotkeys: (v: boolean) => {},
onBgChange: () => {}, onBgChange: (v: string) => {},
onColorChange: () => {}, onColorChange: (v: string) => {},
onNameChange: () => {}, onNameChange: (v: string) => {},
disconnect: () => {}, disconnect: () => {},
connect: () => {}, connect: () => {},
}, },
@ -81,29 +85,31 @@ export default {
if (!this.$route.params.id) { if (!this.$route.params.id) {
return return
} }
this.$watch(() => this.g.player.background, (value) => { this.$watch(() => this.g.player.background, (value: string) => {
this.g.onBgChange(value) this.g.onBgChange(value)
}) })
this.$watch(() => this.g.player.color, (value) => { this.$watch(() => this.g.player.color, (value: string) => {
this.g.onColorChange(value) this.g.onColorChange(value)
}) })
this.$watch(() => this.g.player.name, (value) => { this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value) this.g.onNameChange(value)
}) })
this.g = await main( this.g = await main(
this.$route.params.id, `${this.$route.params.id}`,
// @ts-ignore
this.$clientId, this.$clientId,
// @ts-ignore
this.$config.WS_ADDRESS, this.$config.WS_ADDRESS,
MODE_PLAY, MODE_PLAY,
this.$el, this.$el,
{ {
setActivePlayers: (v) => { this.activePlayers = v }, setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
setIdlePlayers: (v) => { this.idlePlayers = v }, setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
setFinished: (v) => { this.finished = v }, setFinished: (v: boolean) => { this.finished = v },
setDuration: (v) => { this.duration = v }, setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v) => { this.piecesDone = v }, setPiecesDone: (v: number) => { this.piecesDone = v },
setPiecesTotal: (v) => { this.piecesTotal = v }, setPiecesTotal: (v: number) => { this.piecesTotal = v },
setConnectionState: (v) => { this.connectionState = v }, setConnectionState: (v: number) => { this.connectionState = v },
togglePreview: () => { this.toggle('preview', false) }, togglePreview: () => { this.toggle('preview', false) },
} }
) )
@ -112,22 +118,23 @@ export default {
this.g.disconnect() this.g.disconnect()
}, },
methods: { methods: {
reconnect() { reconnect(): void {
this.g.connect() this.g.connect()
}, },
toggle(overlay, affectsHotkeys) { toggle(overlay: string, affectsHotkeys: boolean): void {
if (this.overlay === null) { if (this.overlay === '') {
this.overlay = overlay this.overlay = overlay
if (affectsHotkeys) { if (affectsHotkeys) {
this.g.setHotkeys(false) this.g.setHotkeys(false)
} }
} else { } else {
// could check if overlay was the provided one // could check if overlay was the provided one
this.overlay = null this.overlay = ''
if (affectsHotkeys) { if (affectsHotkeys) {
this.g.setHotkeys(true) this.g.setHotkeys(true)
} }
} }
}, },
}, },
} })
</script>

View 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>

View file

@ -1,16 +1,19 @@
"use strict" <template>
<div>
<new-game-dialog :images="images" @newGame="onNewGame" />
</div>
</template>
import NewGameDialog from './../components/NewGameDialog.vue.js' <script lang="ts">
import { defineComponent } from 'vue'
export default { // TODO: maybe move dialog back, now that this is a view on its own
import NewGameDialog from './../components/NewGameDialog.vue'
export default defineComponent({
components: { components: {
NewGameDialog, NewGameDialog,
}, },
// TODO: maybe move dialog back, now that this is a view on its own
template: `
<div>
<new-game-dialog :images="images" @newGame="onNewGame" />
</div>`,
data() { data() {
return { return {
images: [], images: [],
@ -22,7 +25,8 @@ export default {
this.images = json.images this.images = json.images
}, },
methods: { methods: {
async onNewGame(gameSettings) { // TODO: ts GameSettings type
async onNewGame(gameSettings: any) {
const res = await fetch('/newgame', { const res = await fetch('/newgame', {
method: 'post', method: 'post',
headers: { headers: {
@ -37,4 +41,5 @@ export default {
} }
} }
} }
} })
</script>

View file

@ -1,23 +1,5 @@
"use strict" <template>
<div id="replay">
import Scores from './../components/Scores.vue.js'
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
import HelpOverlay from './../components/HelpOverlay.vue.js'
import { main, MODE_REPLAY } from './../game.js'
export default {
name: 'replay',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
HelpOverlay,
},
template: `<div id="replay">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" /> <settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" /> <preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" /> <help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
@ -46,18 +28,41 @@ export default {
</div> </div>
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" /> <scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
</div>`, </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_REPLAY } from './../game'
export default defineComponent({
name: 'replay',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
HelpOverlay,
},
data() { data() {
return { return {
activePlayers: [], activePlayers: [] as Array<any>,
idlePlayers: [], idlePlayers: [] as Array<any>,
finished: false, finished: false,
duration: 0, duration: 0,
piecesDone: 0, piecesDone: 0,
piecesTotal: 0, piecesTotal: 0,
overlay: null, overlay: '',
connectionState: 0,
g: { g: {
player: { player: {
@ -66,10 +71,10 @@ export default {
name: '', name: '',
}, },
previewImageUrl: '', previewImageUrl: '',
setHotkeys: () => {}, setHotkeys: (v: boolean) => {},
onBgChange: () => {}, onBgChange: (v: string) => {},
onColorChange: () => {}, onColorChange: (v: string) => {},
onNameChange: () => {}, onNameChange: (v: string) => {},
replayOnSpeedUp: () => {}, replayOnSpeedUp: () => {},
replayOnSpeedDown: () => {}, replayOnSpeedDown: () => {},
replayOnPauseToggle: () => {}, replayOnPauseToggle: () => {},
@ -86,59 +91,62 @@ export default {
if (!this.$route.params.id) { if (!this.$route.params.id) {
return return
} }
this.$watch(() => this.g.player.background, (value) => { this.$watch(() => this.g.player.background, (value: string) => {
this.g.onBgChange(value) this.g.onBgChange(value)
}) })
this.$watch(() => this.g.player.color, (value) => { this.$watch(() => this.g.player.color, (value: string) => {
this.g.onColorChange(value) this.g.onColorChange(value)
}) })
this.$watch(() => this.g.player.name, (value) => { this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value) this.g.onNameChange(value)
}) })
this.g = await main( this.g = await main(
this.$route.params.id, `${this.$route.params.id}`,
// @ts-ignore
this.$clientId, this.$clientId,
// @ts-ignore
this.$config.WS_ADDRESS, this.$config.WS_ADDRESS,
MODE_REPLAY, MODE_REPLAY,
this.$el, this.$el,
{ {
setActivePlayers: (v) => { this.activePlayers = v }, setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
setIdlePlayers: (v) => { this.idlePlayers = v }, setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
setFinished: (v) => { this.finished = v }, setFinished: (v: boolean) => { this.finished = v },
setDuration: (v) => { this.duration = v }, setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v) => { this.piecesDone = v }, setPiecesDone: (v: number) => { this.piecesDone = v },
setPiecesTotal: (v) => { this.piecesTotal = v }, setPiecesTotal: (v: number) => { this.piecesTotal = v },
togglePreview: () => { this.toggle('preview', false) }, togglePreview: () => { this.toggle('preview', false) },
setConnectionState: (v) => { this.connectionState = v }, setConnectionState: (v: number) => { this.connectionState = v },
setReplaySpeed: (v) => { this.replay.speed = v }, setReplaySpeed: (v: number) => { this.replay.speed = v },
setReplayPaused: (v) => { this.replay.paused = v }, setReplayPaused: (v: boolean) => { this.replay.paused = v },
} }
) )
}, },
unmounted () { unmounted () {
this.g.disconnect() this.g.disconnect()
}, },
computed: {
replayText () {
return 'Replay-Speed: ' +
(this.replay.speed + 'x') +
(this.replay.paused ? ' Paused' : '')
},
},
methods: { methods: {
toggle(overlay, affectsHotkeys) { toggle(overlay: string, affectsHotkeys: boolean): void {
if (this.overlay === null) { if (this.overlay === '') {
this.overlay = overlay this.overlay = overlay
if (affectsHotkeys) { if (affectsHotkeys) {
this.g.setHotkeys(false) this.g.setHotkeys(false)
} }
} else { } else {
// could check if overlay was the provided one // could check if overlay was the provided one
this.overlay = null this.overlay = ''
if (affectsHotkeys) { if (affectsHotkeys) {
this.g.setHotkeys(true) this.g.setHotkeys(true)
} }
} }
}, },
}, },
} computed: {
replayText (): string {
return 'Replay-Speed: ' +
(this.replay.speed + 'x') +
(this.replay.paused ? ' Paused' : '')
},
},
})
</script>

View file

@ -4,10 +4,9 @@ import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
const BASE_DIR = `${__dirname}/..` const BASE_DIR = `${__dirname}/../..`
export const DATA_DIR = `${BASE_DIR}/data` export const DATA_DIR = `${BASE_DIR}/data`
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads` export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
export const UPLOAD_URL = `/uploads` export const UPLOAD_URL = `/uploads`
export const COMMON_DIR = `${BASE_DIR}/common/` export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
export const PUBLIC_DIR = `${BASE_DIR}/public/`

View file

@ -1,12 +1,12 @@
import GameCommon from './../common/GameCommon.js' import GameCommon from './../common/GameCommon'
import Util from './../common/Util.js' import Util from './../common/Util'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng'
import GameLog from './GameLog.js' import GameLog from './GameLog'
import { createPuzzle } from './Puzzle.js' import { createPuzzle } from './Puzzle'
import Protocol from '../common/Protocol.js' import Protocol from '../common/Protocol'
import GameStorage from './GameStorage.js' import GameStorage from './GameStorage'
async function createGameObject(gameId, targetTiles, image, ts, scoreMode) { async function createGameObject(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const seed = Util.hash(gameId + ' ' + ts) const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed) const rng = new Rng(seed)
return { return {
@ -19,7 +19,7 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
} }
} }
async function createGame(gameId, targetTiles, image, ts, scoreMode) { async function createGame(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode) const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
GameLog.create(gameId) GameLog.create(gameId)
@ -29,7 +29,7 @@ async function createGame(gameId, targetTiles, image, ts, scoreMode) {
GameStorage.setDirty(gameId) GameStorage.setDirty(gameId)
} }
function addPlayer(gameId, playerId, ts) { function addPlayer(gameId: string, playerId: string, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId) const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId) const diff = ts - GameCommon.getStartTs(gameId)
if (idx === -1) { if (idx === -1) {
@ -42,7 +42,7 @@ function addPlayer(gameId, playerId, ts) {
GameStorage.setDirty(gameId) GameStorage.setDirty(gameId)
} }
function handleInput(gameId, playerId, input, ts) { function handleInput(gameId: string, playerId: string, input: any, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId) const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId) const diff = ts - GameCommon.getStartTs(gameId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff) GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)

View file

@ -4,21 +4,21 @@ import { DATA_DIR } from '../server/Dirs.js'
const log = logger('GameLog.js') const log = logger('GameLog.js')
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log` const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
const create = (gameId) => { const create = (gameId: string) => {
const file = filename(gameId) const file = filename(gameId)
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
fs.appendFileSync(file, '') fs.appendFileSync(file, '')
} }
} }
const exists = (gameId) => { const exists = (gameId: string) => {
const file = filename(gameId) const file = filename(gameId)
return fs.existsSync(file) return fs.existsSync(file)
} }
const _log = (gameId, ...args) => { const _log = (gameId: string, ...args: Array<any>) => {
const file = filename(gameId) const file = filename(gameId)
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
return return
@ -27,13 +27,13 @@ const _log = (gameId, ...args) => {
fs.appendFileSync(file, str + "\n") fs.appendFileSync(file, str + "\n")
} }
const get = (gameId) => { const get = (gameId: string) => {
const file = filename(gameId) const file = filename(gameId)
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
return [] return []
} }
const lines = fs.readFileSync(file, 'utf-8').split("\n") const lines = fs.readFileSync(file, 'utf-8').split("\n")
return lines.filter(line => !!line).map((line) => { return lines.filter((line: string) => !!line).map((line: string) => {
try { try {
return JSON.parse(line) return JSON.parse(line)
} catch (e) { } catch (e) {

View file

@ -1,27 +1,28 @@
import { logger } from '../common/Util.js' import { logger } from '../common/Util.js'
import WebSocket from 'ws'
const log = logger('GameSocket.js') const log = logger('GameSocket.js')
// Map<gameId, Socket[]> // Map<gameId, Socket[]>
const SOCKETS = {} const SOCKETS = {} as Record<string, Array<WebSocket>>
function socketExists(gameId, socket) { function socketExists(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) { if (!(gameId in SOCKETS)) {
return false return false
} }
return SOCKETS[gameId].includes(socket) return SOCKETS[gameId].includes(socket)
} }
function removeSocket(gameId, socket) { function removeSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) { if (!(gameId in SOCKETS)) {
return return
} }
SOCKETS[gameId] = SOCKETS[gameId].filter(s => s !== socket) SOCKETS[gameId] = SOCKETS[gameId].filter((s: WebSocket) => s !== socket)
log.log('removed socket: ', gameId, socket.protocol) log.log('removed socket: ', gameId, socket.protocol)
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length) log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
} }
function addSocket(gameId, socket) { function addSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) { if (!(gameId in SOCKETS)) {
SOCKETS[gameId] = [] SOCKETS[gameId] = []
} }
@ -32,7 +33,7 @@ function addSocket(gameId, socket) {
} }
} }
function getSockets(gameId) { function getSockets(gameId: string) {
if (!(gameId in SOCKETS)) { if (!(gameId in SOCKETS)) {
return [] return []
} }

View file

@ -1,21 +1,21 @@
import fs from 'fs' import fs from 'fs'
import GameCommon from './../common/GameCommon.js' import GameCommon from './../common/GameCommon'
import Util, { logger } from './../common/Util.js' import Util, { logger } from './../common/Util'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng'
import { DATA_DIR } from './Dirs.js' import { DATA_DIR } from './Dirs'
import Time from '../common/Time.js' import Time from './../common/Time'
const log = logger('GameStorage.js') const log = logger('GameStorage.js')
const DIRTY_GAMES = {} const DIRTY_GAMES = {} as any
function setDirty(gameId) { function setDirty(gameId: string): void {
DIRTY_GAMES[gameId] = true DIRTY_GAMES[gameId] = true
} }
function setClean(gameId) { function setClean(gameId: string): void {
delete DIRTY_GAMES[gameId] delete DIRTY_GAMES[gameId]
} }
function loadGames() { function loadGames(): void {
const files = fs.readdirSync(DATA_DIR) const files = fs.readdirSync(DATA_DIR)
for (const f of files) { for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/) const m = f.match(/^([a-z0-9]+)\.json$/)
@ -27,7 +27,7 @@ function loadGames() {
} }
} }
function loadGame(gameId) { function loadGame(gameId: string): void {
const file = `${DATA_DIR}/${gameId}.json` const file = `${DATA_DIR}/${gameId}.json`
const contents = fs.readFileSync(file, 'utf-8') const contents = fs.readFileSync(file, 'utf-8')
let game let game
@ -40,7 +40,7 @@ function loadGame(gameId) {
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs) game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
} }
if (typeof game.puzzle.data.finished === 'undefined') { if (typeof game.puzzle.data.finished === 'undefined') {
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find(t => t.owner !== -1) let unfinished = game.puzzle.tiles.map(Util.decodeTile).find((t: any) => t.owner !== -1)
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp() game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
} }
if (!Array.isArray(game.players)) { if (!Array.isArray(game.players)) {
@ -50,7 +50,7 @@ function loadGame(gameId) {
id: game.id, id: game.id,
rng: { rng: {
type: game.rng ? game.rng.type : '_fake_', type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(), obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
}, },
puzzle: game.puzzle, puzzle: game.puzzle,
players: game.players, players: game.players,
@ -66,7 +66,7 @@ function persistGames() {
} }
} }
function persistGame(gameId) { function persistGame(gameId: string) {
const game = GameCommon.get(gameId) const game = GameCommon.get(gameId)
if (game.id in DIRTY_GAMES) { if (game.id in DIRTY_GAMES) {
setClean(game.id) setClean(game.id)

View file

@ -2,9 +2,10 @@ import sizeOf from 'image-size'
import fs from 'fs' import fs from 'fs'
import exif from 'exif' import exif from 'exif'
import sharp from 'sharp' import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js' import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js'
const resizeImage = async (filename) => { const resizeImage = async (filename: string) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) { if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return return
} }
@ -33,7 +34,7 @@ const resizeImage = async (filename) => {
} }
} }
async function getExifOrientation(imagePath) { async function getExifOrientation(imagePath: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) { new exif.ExifImage({ image: imagePath }, function (error, exifData) {
if (error) { if (error) {
@ -60,7 +61,7 @@ const allImages = () => {
return images return images
} }
async function getDimensions(imagePath) { async function getDimensions(imagePath: string) {
let dimensions = sizeOf(imagePath) let dimensions = sizeOf(imagePath)
const orientation = await getExifOrientation(imagePath) const orientation = await getExifOrientation(imagePath)
// when image is rotated to the left or right, switch width/height // when image is rotated to the left or right, switch width/height

View file

@ -1,22 +1,36 @@
import Util from '../common/Util.js' import Util from '../common/Util'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng'
import Images from './Images.js' import Images from './Images.js'
interface PuzzleInfo {
width: number
height: number
tileSize: number
tileMarginWidth: number
tileDrawSize: number
tiles: number
tilesX: number
tilesY: number
}
// cut size of each puzzle tile in the // cut size of each puzzle tile in the
// final resized version of the puzzle image // final resized version of the puzzle image
const TILE_SIZE = 64 const TILE_SIZE = 64
async function createPuzzle( async function createPuzzle(
/** @type Rng */ rng, rng: Rng,
targetTiles, targetTiles: number,
image, image: { file: string, url: string },
ts ts: number
) { ) {
const imagePath = image.file const imagePath = image.file
const imageUrl = image.url const imageUrl = image.url
// determine puzzle information from the image dimensions // determine puzzle information from the image dimensions
const dim = await Images.getDimensions(imagePath) const dim = await Images.getDimensions(imagePath)
if (!dim || !dim.width || !dim.height) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
}
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles) const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
let tiles = new Array(info.tiles) let tiles = new Array(info.tiles)
@ -143,8 +157,8 @@ async function createPuzzle(
} }
function determinePuzzleTileShapes( function determinePuzzleTileShapes(
/** @type Rng */ rng, rng: Rng,
info info: PuzzleInfo
) { ) {
const tabs = [-1, 1] const tabs = [-1, 1]
@ -161,7 +175,7 @@ function determinePuzzleTileShapes(
return shapes.map(Util.encodeShape) return shapes.map(Util.encodeShape)
} }
const determineTilesXY = (w, h, targetTiles) => { const determineTilesXY = (w: number, h: number, targetTiles: number) => {
const w_ = w < h ? (w * h) : (w * w) const w_ = w < h ? (w * h) : (w * w)
const h_ = w < h ? (h * h) : (w * h) const h_ = w < h ? (h * h) : (w * h)
let size = 0 let size = 0
@ -177,7 +191,7 @@ const determineTilesXY = (w, h, targetTiles) => {
} }
} }
const determinePuzzleInfo = (w, h, targetTiles) => { const determinePuzzleInfo = (w: number, h: number, targetTiles: number) => {
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles) const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
const tiles = tilesX * tilesY const tiles = tilesX * tilesY
const tileSize = TILE_SIZE const tileSize = TILE_SIZE

View file

@ -14,44 +14,49 @@ config = {
*/ */
class EvtBus { class EvtBus {
private _on: any
constructor() { constructor() {
this._on = {} this._on = {} as any
} }
on(type, callback) { on(type: string, callback: Function) {
this._on[type] = this._on[type] || [] this._on[type] = this._on[type] || []
this._on[type].push(callback) this._on[type].push(callback)
} }
dispatch(type, ...args) { dispatch(type: string, ...args: Array<any>) {
(this._on[type] || []).forEach(cb => { (this._on[type] || []).forEach((cb: Function) => {
cb(...args) cb(...args)
}) })
} }
} }
class WebSocketServer { class WebSocketServer {
constructor(config) { evt: EvtBus
private _websocketserver: WebSocket.Server|null
config: any
constructor(config: any) {
this.config = config this.config = config
this._websocketserver = null this._websocketserver = null
this.evt = new EvtBus() this.evt = new EvtBus()
} }
on(type, callback) { on(type: string, callback: Function) {
this.evt.on(type, callback) this.evt.on(type, callback)
} }
listen() { listen() {
this._websocketserver = new WebSocket.Server(this.config) this._websocketserver = new WebSocket.Server(this.config)
this._websocketserver.on('connection', (socket, request, client) => { this._websocketserver.on('connection', (socket: WebSocket, request: Request) => {
const pathname = new URL(this.config.connectstring).pathname const pathname = new URL(this.config.connectstring).pathname
if (request.url.indexOf(pathname) !== 0) { if (request.url.indexOf(pathname) !== 0) {
log.log('bad request url: ', request.url) log.log('bad request url: ', request.url)
socket.close() socket.close()
return return
} }
socket.on('message', (data) => { socket.on('message', (data: any) => {
log.log(`ws`, socket.protocol, data) log.log(`ws`, socket.protocol, data)
this.evt.dispatch('message', {socket, data}) this.evt.dispatch('message', {socket, data})
}) })
@ -62,10 +67,12 @@ class WebSocketServer {
} }
close() { close() {
this._websocketserver.close() if (this._websocketserver) {
this._websocketserver.close()
}
} }
notifyOne(data, socket) { notifyOne(data: any, socket: WebSocket) {
socket.send(JSON.stringify(data)) socket.send(JSON.stringify(data))
} }
} }

View file

@ -1,27 +1,41 @@
import WebSocketServer from './WebSocketServer.js' import WebSocketServer from './WebSocketServer'
import WebSocket from 'ws'
import express from 'express' import express from 'express'
import multer from 'multer' import multer from 'multer'
import config from './../config.js' import Protocol from './../common/Protocol'
import Protocol from './../common/Protocol.js' import Util, { logger } from './../common/Util'
import Util, { logger } from './../common/Util.js' import Game from './Game'
import Game from './Game.js'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import v8 from 'v8' import v8 from 'v8'
import GameLog from './GameLog.js' import fs from 'fs'
import GameSockets from './GameSockets.js' import GameLog from './GameLog'
import Time from '../common/Time.js' import GameSockets from './GameSockets'
import Images from './Images.js' import Time from '../common/Time'
import Images from './Images'
import { import {
UPLOAD_DIR, UPLOAD_DIR,
UPLOAD_URL, UPLOAD_URL,
COMMON_DIR,
PUBLIC_DIR, PUBLIC_DIR,
} from './Dirs.js' } from './Dirs'
import GameCommon from '../common/GameCommon.js' import GameCommon from '../common/GameCommon'
import GameStorage from './GameStorage.js' import GameStorage from './GameStorage'
const log = logger('index.js') let configFile = ''
let last = ''
for (const val of process.argv) {
if (last === '-c') {
configFile = val
}
last = val
}
if (configFile === '') {
process.exit(2)
}
const config = JSON.parse(String(fs.readFileSync(configFile)))
const log = logger('main.js')
const port = config.http.port const port = config.http.port
const hostname = config.http.hostname const hostname = config.http.hostname
@ -50,7 +64,7 @@ app.get('/api/newgame-data', (req, res) => {
app.get('/api/index-data', (req, res) => { app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp() const ts = Time.timestamp()
const games = [ const games = [
...Game.getAllGames().map(game => ({ ...Game.getAllGames().map((game: any) => ({
id: game.id, id: game.id,
hasReplay: GameLog.exists(game.id), hasReplay: GameLog.exists(game.id),
started: Game.getStartTs(game.id), started: Game.getStartTs(game.id),
@ -69,7 +83,7 @@ app.get('/api/index-data', (req, res) => {
}) })
app.post('/upload', (req, res) => { app.post('/upload', (req, res) => {
upload(req, res, async (err) => { upload(req, res, async (err: any) => {
if (err) { if (err) {
log.log(err) log.log(err)
res.status(400).send("Something went wrong!"); res.status(400).send("Something went wrong!");
@ -107,20 +121,19 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
res.send({ id: gameId }) res.send({ id: gameId })
}) })
app.use('/common/', express.static(COMMON_DIR))
app.use('/uploads/', express.static(UPLOAD_DIR)) app.use('/uploads/', express.static(UPLOAD_DIR))
app.use('/', express.static(PUBLIC_DIR)) app.use('/', express.static(PUBLIC_DIR))
const wss = new WebSocketServer(config.ws); const wss = new WebSocketServer(config.ws);
const notify = (data, sockets) => { const notify = (data: any, sockets: Array<WebSocket>) => {
// TODO: throttle? // TODO: throttle?
for (let socket of sockets) { for (let socket of sockets) {
wss.notifyOne(data, socket) wss.notifyOne(data, socket)
} }
} }
wss.on('close', async ({socket}) => { wss.on('close', async ({socket} : {socket: WebSocket}) => {
try { try {
const proto = socket.protocol.split('|') const proto = socket.protocol.split('|')
const clientId = proto[0] const clientId = proto[0]
@ -131,7 +144,7 @@ wss.on('close', async ({socket}) => {
} }
}) })
wss.on('message', async ({socket, data}) => { wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
try { try {
const proto = socket.protocol.split('|') const proto = socket.protocol.split('|')
const clientId = proto[0] const clientId = proto[0]
@ -236,7 +249,7 @@ const persistInterval = setInterval(() => {
memoryUsageHuman() memoryUsageHuman()
}, config.persistence.interval) }, config.persistence.interval)
const gracefulShutdown = (signal) => { const gracefulShutdown = (signal: any) => {
log.log(`${signal} received...`) log.log(`${signal} received...`)
log.log('clearing persist interval...') log.log('clearing persist interval...')

14
tsconfig.server.json Normal file
View 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
View 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,
},
})