Compare commits
1 commit
master
...
gamelog-db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b332ac6f |
73 changed files with 1627 additions and 5548 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
node_modules
|
|
||||||
build
|
|
||||||
src/frontend/shims-vue.d.ts
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: [
|
|
||||||
'@typescript-eslint',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-inferrable-types': 'off'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
1
build/public/assets/index.50ee8245.js
Normal file
1
build/public/assets/index.50ee8245.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/public/assets/index.f7304069.css
Normal file
1
build/public/assets/index.f7304069.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
build/public/assets/vendor.b622ee49.js
Normal file
6
build/public/assets/vendor.b622ee49.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -4,9 +4,9 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<title>🧩 jigsaw.hyottoko.club</title>
|
<title>🧩 jigsaw.hyottoko.club</title>
|
||||||
<script type="module" crossorigin src="/assets/index.63ff8630.js"></script>
|
<script type="module" crossorigin src="/assets/index.50ee8245.js"></script>
|
||||||
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
|
<link rel="modulepreload" href="/assets/vendor.b622ee49.js">
|
||||||
<link rel="stylesheet" href="/assets/index.22dc307c.css">
|
<link rel="stylesheet" href="/assets/index.f7304069.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
1094
build/server/main.js
1094
build/server/main.js
File diff suppressed because it is too large
Load diff
1981
package-lock.json
generated
1981
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
|
@ -2,6 +2,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^7.4.0",
|
"better-sqlite3": "^7.4.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
"exif": "^0.6.0",
|
"exif": "^0.6.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"image-size": "^0.9.3",
|
"image-size": "^0.9.3",
|
||||||
|
|
@ -13,24 +14,19 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^5.4.1",
|
"@types/better-sqlite3": "^5.4.1",
|
||||||
"@types/compression": "^1.7.0",
|
|
||||||
"@types/exif": "^0.6.2",
|
"@types/exif": "^0.6.2",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/multer": "^1.4.5",
|
"@types/multer": "^1.4.5",
|
||||||
"@types/sharp": "^0.28.1",
|
"@types/sharp": "^0.28.1",
|
||||||
"@types/ws": "^7.4.4",
|
"@types/ws": "^7.4.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
|
||||||
"@typescript-eslint/parser": "^4.25.0",
|
|
||||||
"@vitejs/plugin-vue": "^1.2.2",
|
"@vitejs/plugin-vue": "^1.2.2",
|
||||||
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
||||||
"compression": "^1.7.4",
|
|
||||||
"eslint": "^7.27.0",
|
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"rollup": "^2.48.0",
|
"rollup": "^2.48.0",
|
||||||
"rollup-plugin-typescript2": "^0.30.0",
|
"rollup-plugin-typescript2": "^0.30.0",
|
||||||
"rollup-plugin-vue": "^6.0.0-beta.10",
|
"rollup-plugin-vue": "^6.0.0-beta.10",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.2.4",
|
||||||
"vite": "^2.3.2"
|
"vite": "^2.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -39,7 +35,6 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rollup": "rollup",
|
"rollup": "rollup",
|
||||||
"vite": "vite",
|
"vite": "vite"
|
||||||
"eslint": "eslint"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,17 @@ export default {
|
||||||
format: 'es',
|
format: 'es',
|
||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
"better-sqlite3",
|
|
||||||
"compression",
|
|
||||||
"exif",
|
|
||||||
"express",
|
"express",
|
||||||
"fs",
|
|
||||||
"image-size",
|
|
||||||
"multer",
|
"multer",
|
||||||
"path",
|
"body-parser",
|
||||||
|
"v8",
|
||||||
|
"fs",
|
||||||
|
"ws",
|
||||||
|
"image-size",
|
||||||
|
"exif",
|
||||||
"sharp",
|
"sharp",
|
||||||
"url",
|
"url",
|
||||||
"v8",
|
"path",
|
||||||
"ws",
|
|
||||||
],
|
],
|
||||||
plugins: [typescript()],
|
plugins: [typescript()],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
import GameCommon from '../src/common/GameCommon'
|
|
||||||
import GameLog from '../src/server/GameLog'
|
|
||||||
import { Game } from '../src/common/Types'
|
|
||||||
import { logger } from '../src/common/Util'
|
|
||||||
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
|
|
||||||
import Db from '../src/server/Db'
|
|
||||||
import GameStorage from '../src/server/GameStorage'
|
|
||||||
import fs from 'fs'
|
|
||||||
|
|
||||||
const log = logger('fix_games_image_info.ts')
|
|
||||||
|
|
||||||
import Images from '../src/server/Images'
|
|
||||||
|
|
||||||
console.log(DB_FILE)
|
|
||||||
|
|
||||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
|
||||||
db.patch(true)
|
|
||||||
|
|
||||||
// ;(async () => {
|
|
||||||
// let images = db.getMany('images')
|
|
||||||
// for (let image of images) {
|
|
||||||
// console.log(image.filename)
|
|
||||||
// let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
|
|
||||||
// console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
|
|
||||||
// image.width = dim.w
|
|
||||||
// image.height = dim.h
|
|
||||||
// db.upsert('images', image, { id: image.id })
|
|
||||||
// }
|
|
||||||
// })()
|
|
||||||
|
|
||||||
function fixOne(gameId: string) {
|
|
||||||
let g = GameCommon.get(gameId)
|
|
||||||
if (!g) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!g.puzzle.info.image && g.puzzle.info.imageUrl) {
|
|
||||||
log.log('game id: ', gameId)
|
|
||||||
const parts = g.puzzle.info.imageUrl.split('/')
|
|
||||||
const fileName = parts[parts.length - 1]
|
|
||||||
const imageRow = db.get('images', {filename: fileName})
|
|
||||||
if (!imageRow) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
g.puzzle.info.image = Images.imageFromDb(db, imageRow.id)
|
|
||||||
|
|
||||||
log.log(g.puzzle.info.image.title, imageRow.id)
|
|
||||||
|
|
||||||
GameStorage.persistGameToDb(db, gameId)
|
|
||||||
} else if (g.puzzle.info.image?.id) {
|
|
||||||
const imageId = g.puzzle.info.image.id
|
|
||||||
|
|
||||||
g.puzzle.info.image = Images.imageFromDb(db, imageId)
|
|
||||||
|
|
||||||
log.log(g.puzzle.info.image.title, imageId)
|
|
||||||
|
|
||||||
GameStorage.persistGameToDb(db, gameId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix log
|
|
||||||
const file = GameLog.filename(gameId, 0)
|
|
||||||
if (!fs.existsSync(file)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
|
||||||
const l = lines.filter(line => !!line).map(line => {
|
|
||||||
return JSON.parse(`[${line}]`)
|
|
||||||
})
|
|
||||||
if (l && l[0] && !l[0][3].id) {
|
|
||||||
log.log(l[0][3])
|
|
||||||
l[0][3] = g.puzzle.info.image
|
|
||||||
const newlines = l.map(ll => {
|
|
||||||
return JSON.stringify(ll).slice(1, -1)
|
|
||||||
}).join("\n") + "\n"
|
|
||||||
console.log(g.puzzle.info.image)
|
|
||||||
// process.exit(0)
|
|
||||||
fs.writeFileSync(file, newlines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fix() {
|
|
||||||
GameStorage.loadGamesFromDisk()
|
|
||||||
GameCommon.getAllGames().forEach((game: Game) => {
|
|
||||||
fixOne(game.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fix()
|
|
||||||
23
scripts/fix_image.ts
Normal file
23
scripts/fix_image.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import GameCommon from '../src/common/GameCommon'
|
||||||
|
import { logger } from '../src/common/Util'
|
||||||
|
import GameStorage from '../src/server/GameStorage'
|
||||||
|
|
||||||
|
const log = logger('fix_image.js')
|
||||||
|
|
||||||
|
function fix(gameId) {
|
||||||
|
GameStorage.loadGame(gameId)
|
||||||
|
let changed = false
|
||||||
|
|
||||||
|
let imgUrl = GameCommon.getImageUrl(gameId)
|
||||||
|
if (imgUrl.match(/^\/example-images\//)) {
|
||||||
|
log.log(`found bad imgUrl: ${imgUrl}`)
|
||||||
|
imgUrl = imgUrl.replace(/^\/example-images\//, '/uploads/')
|
||||||
|
GameCommon.setImageUrl(gameId, imgUrl)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
GameStorage.persistGame(gameId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fix(process.argv[2])
|
||||||
|
|
@ -1,38 +1,33 @@
|
||||||
import GameCommon from '../src/common/GameCommon'
|
import GameCommon from '../src/common/GameCommon'
|
||||||
import { logger } from '../src/common/Util'
|
import { logger } from '../src/common/Util'
|
||||||
import Db from '../src/server/Db'
|
|
||||||
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
|
|
||||||
import GameStorage from '../src/server/GameStorage'
|
import GameStorage from '../src/server/GameStorage'
|
||||||
|
|
||||||
const log = logger('fix_tiles.js')
|
const log = logger('fix_tiles.js')
|
||||||
|
|
||||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
|
||||||
db.patch(true)
|
|
||||||
|
|
||||||
function fix_tiles(gameId) {
|
function fix_tiles(gameId) {
|
||||||
GameStorage.loadGameFromDb(db, gameId)
|
GameStorage.loadGame(gameId)
|
||||||
let changed = false
|
let changed = false
|
||||||
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
|
const tiles = GameCommon.getTilesSortedByZIndex(gameId)
|
||||||
for (let tile of tiles) {
|
for (let tile of tiles) {
|
||||||
if (tile.owner === -1) {
|
if (tile.owner === -1) {
|
||||||
const p = GameCommon.getFinalPiecePos(gameId, tile.idx)
|
const p = GameCommon.getFinalTilePos(gameId, tile.idx)
|
||||||
if (p.x === tile.pos.x && p.y === tile.pos.y) {
|
if (p.x === tile.pos.x && p.y === tile.pos.y) {
|
||||||
// log.log('all good', tile.pos)
|
// log.log('all good', tile.pos)
|
||||||
} else {
|
} else {
|
||||||
log.log('bad tile pos', tile.pos, 'should be: ', p)
|
log.log('bad tile pos', tile.pos, 'should be: ', p)
|
||||||
tile.pos = p
|
tile.pos = p
|
||||||
GameCommon.setPiece(gameId, tile.idx, tile)
|
GameCommon.setTile(gameId, tile.idx, tile)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
} else if (tile.owner !== 0) {
|
} else if (tile.owner !== 0) {
|
||||||
tile.owner = 0
|
tile.owner = 0
|
||||||
log.log('unowning tile', tile.idx)
|
log.log('unowning tile', tile.idx)
|
||||||
GameCommon.setPiece(gameId, tile.idx, tile)
|
GameCommon.setTile(gameId, tile.idx, tile)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
GameStorage.persistGameToDb(db, gameId)
|
GameStorage.persistGame(gameId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
47
scripts/import_game_logs.ts
Normal file
47
scripts/import_game_logs.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { DB_FILE, DB_PATCHES_DIR, DATA_DIR } from '../src/server/Dirs'
|
||||||
|
import Db from '../src/server/Db'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { logger } from '../src/common/Util'
|
||||||
|
|
||||||
|
const log = logger('import_game_logs.ts')
|
||||||
|
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||||
|
db.patch(true)
|
||||||
|
|
||||||
|
|
||||||
|
for (const file of fs.readdirSync(DATA_DIR)) {
|
||||||
|
const m = file.match(/^log_(.*)\.log$/)
|
||||||
|
if (!m) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameId = m[1]
|
||||||
|
log.info(`Importing log for game ${file}`)
|
||||||
|
|
||||||
|
const contents = fs.readFileSync(`${DATA_DIR}/${file}`, 'utf-8')
|
||||||
|
let t = 0
|
||||||
|
let datas = []
|
||||||
|
for (const line of contents.split("\n")) {
|
||||||
|
if (line === '') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let parsed
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(line)
|
||||||
|
} catch (e) {
|
||||||
|
log.error('bad game', e)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (t === 0) {
|
||||||
|
t = parsed[4]
|
||||||
|
} else {
|
||||||
|
t += parsed[parsed.length - 1]
|
||||||
|
}
|
||||||
|
datas.push({
|
||||||
|
game_id: gameId,
|
||||||
|
created: t / 1000,
|
||||||
|
entry: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
db.insertMany('game_log', datas)
|
||||||
|
log.info(`Done.`)
|
||||||
|
}
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import GameCommon from '../src/common/GameCommon'
|
|
||||||
import { Game } from '../src/common/Types'
|
|
||||||
import { logger } from '../src/common/Util'
|
|
||||||
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
|
|
||||||
import Db from '../src/server/Db'
|
|
||||||
import GameStorage from '../src/server/GameStorage'
|
|
||||||
|
|
||||||
const log = logger('import_games.ts')
|
|
||||||
|
|
||||||
console.log(DB_FILE)
|
|
||||||
|
|
||||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
|
||||||
db.patch(true)
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
GameStorage.loadGamesFromDisk()
|
|
||||||
GameCommon.getAllGames().forEach((game: Game) => {
|
|
||||||
if (!game.puzzle.info.image?.id) {
|
|
||||||
log.error(game.id + " has no image")
|
|
||||||
log.error(game.puzzle.info.image)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
GameStorage.persistGameToDb(db, game.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
run()
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
|
|
||||||
import Db from '../src/server/Db'
|
|
||||||
import Images from '../src/server/Images'
|
|
||||||
|
|
||||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
|
||||||
db.patch(true)
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
let images = db.getMany('images')
|
|
||||||
for (let image of images) {
|
|
||||||
console.log(image.filename)
|
|
||||||
let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
|
|
||||||
console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
|
|
||||||
image.width = dim.w
|
|
||||||
image.height = dim.h
|
|
||||||
db.upsert('images', image, { id: image.id })
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
cd "$RUN_DIR"
|
|
||||||
|
|
||||||
npm run eslint src
|
|
||||||
82
scripts/rewrite_logs.ts
Normal file
82
scripts/rewrite_logs.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import Protocol from '../src/common/Protocol'
|
||||||
|
import { logger } from '../src/common/Util'
|
||||||
|
import { DATA_DIR } from '../src/server/Dirs'
|
||||||
|
|
||||||
|
const log = logger('rewrite_logs')
|
||||||
|
|
||||||
|
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`
|
||||||
|
|
||||||
|
const rewrite = (gameId) => {
|
||||||
|
const file = filename(gameId)
|
||||||
|
log.log(file)
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let playerIds = [];
|
||||||
|
let startTs = null
|
||||||
|
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
||||||
|
const linesNew = lines.filter(line => !!line).map((line) => {
|
||||||
|
const json = JSON.parse(line)
|
||||||
|
const m = {
|
||||||
|
createGame: Protocol.LOG_HEADER,
|
||||||
|
addPlayer: Protocol.LOG_ADD_PLAYER,
|
||||||
|
handleInput: Protocol.LOG_HANDLE_INPUT,
|
||||||
|
}
|
||||||
|
const action = json[0]
|
||||||
|
if (action in m) {
|
||||||
|
json[0] = m[action]
|
||||||
|
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||||
|
const inputm = {
|
||||||
|
down: Protocol.INPUT_EV_MOUSE_DOWN,
|
||||||
|
up: Protocol.INPUT_EV_MOUSE_UP,
|
||||||
|
move: Protocol.INPUT_EV_MOUSE_MOVE,
|
||||||
|
zoomin: Protocol.INPUT_EV_ZOOM_IN,
|
||||||
|
zoomout: Protocol.INPUT_EV_ZOOM_OUT,
|
||||||
|
bg_color: Protocol.INPUT_EV_BG_COLOR,
|
||||||
|
player_color: Protocol.INPUT_EV_PLAYER_COLOR,
|
||||||
|
player_name: Protocol.INPUT_EV_PLAYER_NAME,
|
||||||
|
}
|
||||||
|
const inputa = json[2][0]
|
||||||
|
if (inputa in inputm) {
|
||||||
|
json[2][0] = inputm[inputa]
|
||||||
|
} else {
|
||||||
|
throw '[ invalid input log line: "' + line + '" ]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw '[ invalid general log line: "' + line + '" ]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json[0] === Protocol.LOG_ADD_PLAYER) {
|
||||||
|
if (playerIds.indexOf(json[1]) === -1) {
|
||||||
|
playerIds.push(json[1])
|
||||||
|
} else {
|
||||||
|
json[0] = Protocol.LOG_UPDATE_PLAYER
|
||||||
|
json[1] = playerIds.indexOf(json[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||||
|
json[1] = playerIds.indexOf(json[1])
|
||||||
|
if (json[1] === -1) {
|
||||||
|
throw '[ invalid player ... "' + line + '" ]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json[0] === Protocol.LOG_HEADER) {
|
||||||
|
startTs = json[json.length - 1]
|
||||||
|
json[4] = json[3]
|
||||||
|
json[3] = json[2]
|
||||||
|
json[2] = json[1]
|
||||||
|
json[1] = 1
|
||||||
|
} else {
|
||||||
|
json[json.length - 1] = json[json.length - 1] - startTs
|
||||||
|
}
|
||||||
|
return JSON.stringify(json)
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(file, linesNew.join("\n") + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite(process.argv[2])
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# server for built files
|
# server for built files
|
||||||
nodemon --watch build --max-old-space-size=64 -e js build/server/main.js -c config.json
|
nodemon --max-old-space-size=64 build/server/main.js -c config.json
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import fs from 'fs'
|
|
||||||
import { logger } from '../src/common/Util'
|
|
||||||
import { DATA_DIR } from '../src/server/Dirs'
|
|
||||||
import { filename } from '../src/server/GameLog'
|
|
||||||
|
|
||||||
const log = logger('rewrite_logs')
|
|
||||||
|
|
||||||
interface IdxOld {
|
|
||||||
total: number
|
|
||||||
currentFile: string
|
|
||||||
perFile: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Idx {
|
|
||||||
gameId: string
|
|
||||||
total: number
|
|
||||||
lastTs: number
|
|
||||||
currentFile: string
|
|
||||||
perFile: number
|
|
||||||
}
|
|
||||||
const doit = (idxfile: string): void => {
|
|
||||||
const gameId: string = (idxfile.match(/^log_([a-z0-9]+)\.idx\.log$/) as any[])[1]
|
|
||||||
const idxOld: IdxOld = JSON.parse(fs.readFileSync(DATA_DIR + '/' + idxfile, 'utf-8'))
|
|
||||||
|
|
||||||
let currentFile = filename(gameId, 0)
|
|
||||||
const idxNew: Idx = {
|
|
||||||
gameId: gameId,
|
|
||||||
total: 0,
|
|
||||||
lastTs: 0,
|
|
||||||
currentFile: currentFile,
|
|
||||||
perFile: idxOld.perFile
|
|
||||||
}
|
|
||||||
|
|
||||||
let firstTs = 0
|
|
||||||
while (fs.existsSync(currentFile)) {
|
|
||||||
idxNew.currentFile = currentFile
|
|
||||||
const log = fs.readFileSync(currentFile, 'utf-8').split("\n")
|
|
||||||
const newLines = []
|
|
||||||
const lines = log.filter(line => !!line).map(line => {
|
|
||||||
return JSON.parse(line)
|
|
||||||
})
|
|
||||||
for (const l of lines) {
|
|
||||||
if (idxNew.total === 0) {
|
|
||||||
firstTs = l[4]
|
|
||||||
idxNew.lastTs = l[4]
|
|
||||||
newLines.push(JSON.stringify(l).slice(1, -1))
|
|
||||||
} else {
|
|
||||||
const ts = firstTs + l[l.length - 1]
|
|
||||||
const diff = ts - idxNew.lastTs
|
|
||||||
idxNew.lastTs = ts
|
|
||||||
const newL = l.slice(0, -1)
|
|
||||||
newL.push(diff)
|
|
||||||
newLines.push(JSON.stringify(newL).slice(1, -1))
|
|
||||||
}
|
|
||||||
idxNew.total++
|
|
||||||
}
|
|
||||||
fs.writeFileSync(idxNew.currentFile, newLines.join("\n") + "\n")
|
|
||||||
currentFile = filename(gameId, idxNew.total)
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(DATA_DIR + '/' + idxfile, JSON.stringify(idxNew))
|
|
||||||
console.log('done: ' + gameId)
|
|
||||||
}
|
|
||||||
|
|
||||||
let indexfiles = fs.readdirSync(DATA_DIR)
|
|
||||||
.filter(f => f.toLowerCase().match(/^log_[a-z0-9]+\.idx\.log$/))
|
|
||||||
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
for (const file of indexfiles) {
|
|
||||||
await doit(file)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh -e
|
#!/bin/sh
|
||||||
|
|
||||||
node --experimental-vm-modules node_modules/.bin/jest
|
node --experimental-vm-modules node_modules/.bin/jest
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
|
|
||||||
node --max-old-space-size=256 --experimental-specifier-resolution=node --loader ts-node/esm $@
|
node --experimental-specifier-resolution=node --loader ts-node/esm $@
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,139 @@ import Geometry, { Point, Rect } from './Geometry'
|
||||||
import Protocol from './Protocol'
|
import Protocol from './Protocol'
|
||||||
import { Rng } from './Rng'
|
import { Rng } from './Rng'
|
||||||
import Time from './Time'
|
import Time from './Time'
|
||||||
import {
|
|
||||||
Change,
|
|
||||||
EncodedPiece,
|
|
||||||
EvtInfo,
|
|
||||||
Game,
|
|
||||||
Input,
|
|
||||||
Piece,
|
|
||||||
PieceChange,
|
|
||||||
Player,
|
|
||||||
PlayerChange,
|
|
||||||
Puzzle,
|
|
||||||
PuzzleData,
|
|
||||||
PuzzleDataChange,
|
|
||||||
ScoreMode,
|
|
||||||
SnapMode,
|
|
||||||
Timestamp
|
|
||||||
} from './Types'
|
|
||||||
import Util from './Util'
|
import Util from './Util'
|
||||||
|
|
||||||
|
export type Timestamp = number
|
||||||
|
export type EncodedPlayer = Array<any>
|
||||||
|
export type EncodedPiece = Array<any>
|
||||||
|
export type EncodedPieceShape = number
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id: number
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameRng {
|
||||||
|
obj: Rng
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Game {
|
||||||
|
id: string
|
||||||
|
players: Array<EncodedPlayer>
|
||||||
|
puzzle: Puzzle
|
||||||
|
evtInfos: Record<string, EvtInfo>
|
||||||
|
scoreMode?: ScoreMode
|
||||||
|
rng: GameRng
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
id: number
|
||||||
|
filename: string
|
||||||
|
file: string
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
tags: Array<Tag>
|
||||||
|
created: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameSettings {
|
||||||
|
tiles: number
|
||||||
|
image: Image
|
||||||
|
scoreMode: ScoreMode
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PieceEdge {
|
||||||
|
Flat = 0,
|
||||||
|
Out = 1,
|
||||||
|
In = -1,
|
||||||
|
}
|
||||||
|
export interface PieceShape {
|
||||||
|
top: PieceEdge
|
||||||
|
bottom: PieceEdge
|
||||||
|
left: PieceEdge
|
||||||
|
right: PieceEdge
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Piece {
|
||||||
|
owner: string|number
|
||||||
|
idx: number
|
||||||
|
pos: Point
|
||||||
|
z: number
|
||||||
|
group: 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
|
||||||
|
|
||||||
|
shapes: Array<EncodedPieceShape>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ScoreMode {
|
||||||
|
FINAL = 0,
|
||||||
|
ANY = 1,
|
||||||
|
}
|
||||||
|
|
||||||
const IDLE_TIMEOUT_SEC = 30
|
const IDLE_TIMEOUT_SEC = 30
|
||||||
|
|
||||||
// Map<gameId, Game>
|
// Map<gameId, Game>
|
||||||
const GAMES: Record<string, Game> = {}
|
const GAMES: Record<string, Game> = {}
|
||||||
|
|
||||||
function exists(gameId: string): boolean {
|
function exists(gameId: string) {
|
||||||
return (!!GAMES[gameId]) || false
|
return (!!GAMES[gameId]) || false
|
||||||
}
|
}
|
||||||
|
|
||||||
function __createPlayerObject(id: string, ts: Timestamp): Player {
|
function __createPlayerObject(id: string, ts: number): Player {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|
@ -50,7 +154,7 @@ function setGame(gameId: string, game: Game): void {
|
||||||
|
|
||||||
function getPlayerIndexById(gameId: string, playerId: string): number {
|
function getPlayerIndexById(gameId: string, playerId: string): number {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const player of GAMES[gameId].players) {
|
for (let player of GAMES[gameId].players) {
|
||||||
if (Util.decodePlayer(player).id === playerId) {
|
if (Util.decodePlayer(player).id === playerId) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
@ -66,11 +170,8 @@ function getPlayerIdByIndex(gameId: string, playerIndex: number): string|null {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayer(gameId: string, playerId: string): Player|null {
|
function getPlayer(gameId: string, playerId: string): Player {
|
||||||
const idx = getPlayerIndexById(gameId, playerId)
|
const idx = getPlayerIndexById(gameId, playerId)
|
||||||
if (idx === -1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Util.decodePlayer(GAMES[gameId].players[idx])
|
return Util.decodePlayer(GAMES[gameId].players[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,8 +188,8 @@ function setPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPiece(gameId: string, pieceIdx: number, piece: Piece): void {
|
function setTile(gameId: string, tileIdx: number, tile: Piece): void {
|
||||||
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
|
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPuzzleData(gameId: string, data: PuzzleData): void {
|
function setPuzzleData(gameId: string, data: PuzzleData): void {
|
||||||
|
|
@ -110,7 +211,7 @@ function getIdlePlayers(gameId: string, ts: number): Array<Player> {
|
||||||
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
|
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
|
function addPlayer(gameId: string, playerId: string, ts: number): void {
|
||||||
if (!playerExists(gameId, playerId)) {
|
if (!playerExists(gameId, playerId)) {
|
||||||
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -138,16 +239,12 @@ function setEvtInfo(
|
||||||
|
|
||||||
function getAllGames(): Array<Game> {
|
function getAllGames(): Array<Game> {
|
||||||
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
||||||
const finished = isFinished(a.id)
|
|
||||||
// when both have same finished state, sort by started
|
// when both have same finished state, sort by started
|
||||||
if (finished === isFinished(b.id)) {
|
if (isFinished(a.id) === isFinished(b.id)) {
|
||||||
if (finished) {
|
|
||||||
return b.puzzle.data.finished - a.puzzle.data.finished
|
|
||||||
}
|
|
||||||
return b.puzzle.data.started - a.puzzle.data.started
|
return b.puzzle.data.started - a.puzzle.data.started
|
||||||
}
|
}
|
||||||
// otherwise, sort: unfinished, finished
|
// otherwise, sort: unfinished, finished
|
||||||
return finished ? 1 : -1
|
return isFinished(a.id) ? 1 : -1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,107 +254,84 @@ function getAllPlayers(gameId: string): Array<Player> {
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(gameId: string): Game|null {
|
function get(gameId: string) {
|
||||||
return GAMES[gameId] || null
|
return GAMES[gameId]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPieceCount(gameId: string): number {
|
function getTileCount(gameId: string): number {
|
||||||
return GAMES[gameId].puzzle.tiles.length
|
return GAMES[gameId].puzzle.tiles.length
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageUrl(gameId: string): string {
|
function getImageUrl(gameId: string): string {
|
||||||
const imageUrl = GAMES[gameId].puzzle.info.image?.url
|
return GAMES[gameId].puzzle.info.imageUrl
|
||||||
|| GAMES[gameId].puzzle.info.imageUrl
|
}
|
||||||
if (!imageUrl) {
|
|
||||||
throw new Error('[2021-07-11] no image url set')
|
function setImageUrl(gameId: string, imageUrl: string): void {
|
||||||
}
|
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
||||||
return imageUrl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScoreMode(gameId: string): ScoreMode {
|
function getScoreMode(gameId: string): ScoreMode {
|
||||||
return GAMES[gameId].scoreMode
|
return GAMES[gameId].scoreMode || ScoreMode.FINAL
|
||||||
}
|
|
||||||
|
|
||||||
function getSnapMode(gameId: string): SnapMode {
|
|
||||||
return GAMES[gameId].snapMode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFinished(gameId: string): boolean {
|
function isFinished(gameId: string): boolean {
|
||||||
return getFinishedPiecesCount(gameId) === getPieceCount(gameId)
|
return getFinishedTileCount(gameId) === getTileCount(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFinishedPiecesCount(gameId: string): number {
|
function getFinishedTileCount(gameId: string): number {
|
||||||
let count = 0
|
let count = 0
|
||||||
for (const t of GAMES[gameId].puzzle.tiles) {
|
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||||
if (Util.decodePiece(t).owner === -1) {
|
if (Util.decodeTile(t).owner === -1) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPiecesSortedByZIndex(gameId: string): Piece[] {
|
function getTilesSortedByZIndex(gameId: string): Piece[] {
|
||||||
const pieces = GAMES[gameId].puzzle.tiles.map(Util.decodePiece)
|
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
|
||||||
return pieces.sort((t1, t2) => t1.z - t2.z)
|
return tiles.sort((t1, t2) => t1.z - t2.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePlayer(
|
function changePlayer(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
playerId: string,
|
playerId: string,
|
||||||
change: PlayerChange
|
change: any
|
||||||
): void {
|
): void {
|
||||||
const player = getPlayer(gameId, playerId)
|
const player = getPlayer(gameId, playerId)
|
||||||
if (player === null) {
|
for (let k of Object.keys(change)) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k of Object.keys(change)) {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
player[k] = change[k]
|
player[k] = change[k]
|
||||||
}
|
}
|
||||||
setPlayer(gameId, playerId, player)
|
setPlayer(gameId, playerId, player)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeData(gameId: string, change: PuzzleDataChange): void {
|
function changeData(gameId: string, change: any): void {
|
||||||
for (const k of Object.keys(change)) {
|
for (let k of Object.keys(change)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
GAMES[gameId].puzzle.data[k] = change[k]
|
GAMES[gameId].puzzle.data[k] = change[k]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePiece(
|
function changeTile(gameId: string, tileIdx: number, change: any): void {
|
||||||
gameId: string,
|
for (let k of Object.keys(change)) {
|
||||||
pieceIdx: number,
|
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||||
change: PieceChange
|
|
||||||
): void {
|
|
||||||
for (const k of Object.keys(change)) {
|
|
||||||
const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
piece[k] = change[k]
|
tile[k] = change[k]
|
||||||
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
|
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPiece = (gameId: string, pieceIdx: number): Piece => {
|
const getTile = (gameId: string, tileIdx: number): Piece => {
|
||||||
return Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
|
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceGroup = (gameId: string, tileIdx: number): number => {
|
const getTileGroup = (gameId: string, tileIdx: number): number => {
|
||||||
const tile = getPiece(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return tile.group
|
return tile.group
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCornerPiece = (gameId: string, tileIdx: number): boolean => {
|
const getFinalTilePos = (gameId: string, tileIdx: number): Point => {
|
||||||
const info = GAMES[gameId].puzzle.info
|
|
||||||
return (
|
|
||||||
tileIdx === 0 // top left corner
|
|
||||||
|| tileIdx === (info.tilesX - 1) // top right corner
|
|
||||||
|| tileIdx === (info.tiles - info.tilesX) // bottom left corner
|
|
||||||
|| tileIdx === (info.tiles - 1) // bottom right corner
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFinalPiecePos = (gameId: string, tileIdx: number): Point => {
|
|
||||||
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,
|
||||||
|
|
@ -267,8 +341,8 @@ const getFinalPiecePos = (gameId: string, tileIdx: number): Point => {
|
||||||
return Geometry.pointAdd(boardPos, srcPos)
|
return Geometry.pointAdd(boardPos, srcPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPiecePos = (gameId: string, tileIdx: number): Point => {
|
const getTilePos = (gameId: string, tileIdx: number): Point => {
|
||||||
const tile = getPiece(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return tile.pos
|
return tile.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,9 +361,9 @@ const getBounds = (gameId: string): Rect => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
|
const getTileBounds = (gameId: string, tileIdx: number): Rect => {
|
||||||
const s = getPieceSize(gameId)
|
const s = getTileSize(gameId)
|
||||||
const tile = getPiece(gameId, tileIdx)
|
const tile = getTile(gameId, tileIdx)
|
||||||
return {
|
return {
|
||||||
x: tile.pos.x,
|
x: tile.pos.x,
|
||||||
y: tile.pos.y,
|
y: tile.pos.y,
|
||||||
|
|
@ -298,13 +372,14 @@ const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceZIndex = (gameId: string, pieceIdx: number): number => {
|
const getTileZIndex = (gameId: string, tileIdx: number): number => {
|
||||||
return getPiece(gameId, pieceIdx).z
|
const tile = getTile(gameId, tileIdx)
|
||||||
|
return tile.z
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
|
const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => {
|
||||||
for (const t of GAMES[gameId].puzzle.tiles) {
|
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||||
const tile = Util.decodePiece(t)
|
const tile = Util.decodeTile(t)
|
||||||
if (tile.owner === playerId) {
|
if (tile.owner === playerId) {
|
||||||
return tile.idx
|
return tile.idx
|
||||||
}
|
}
|
||||||
|
|
@ -312,23 +387,20 @@ const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstOwnedPiece = (
|
const getFirstOwnedTile = (gameId: string, playerId: string): EncodedPiece|null => {
|
||||||
gameId: string,
|
const idx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
playerId: string
|
|
||||||
): EncodedPiece|null => {
|
|
||||||
const idx = getFirstOwnedPieceIdx(gameId, playerId)
|
|
||||||
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceDrawOffset = (gameId: string): number => {
|
const getTileDrawOffset = (gameId: string): number => {
|
||||||
return GAMES[gameId].puzzle.info.tileDrawOffset
|
return GAMES[gameId].puzzle.info.tileDrawOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceDrawSize = (gameId: string): number => {
|
const getTileDrawSize = (gameId: string): number => {
|
||||||
return GAMES[gameId].puzzle.info.tileDrawSize
|
return GAMES[gameId].puzzle.info.tileDrawSize
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceSize = (gameId: string): number => {
|
const getTileSize = (gameId: string): number => {
|
||||||
return GAMES[gameId].puzzle.info.tileSize
|
return GAMES[gameId].puzzle.info.tileSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,12 +420,12 @@ const getMaxZIndex = (gameId: string): number => {
|
||||||
return GAMES[gameId].puzzle.data.maxZ
|
return GAMES[gameId].puzzle.data.maxZ
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMaxZIndexByPieceIdxs = (gameId: string, pieceIdxs: Array<number>): number => {
|
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>): number => {
|
||||||
let maxZ = 0
|
let maxZ = 0
|
||||||
for (const pieceIdx of pieceIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
const curZ = getPieceZIndex(gameId, pieceIdx)
|
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
||||||
if (curZ > maxZ) {
|
if (tileZIndex > maxZ) {
|
||||||
maxZ = curZ
|
maxZ = tileZIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return maxZ
|
return maxZ
|
||||||
|
|
@ -362,7 +434,7 @@ const getMaxZIndexByPieceIdxs = (gameId: string, pieceIdxs: Array<number>): numb
|
||||||
function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
|
function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
|
||||||
const info = GAMES[gameId].puzzle.info
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
const c = Util.coordByPieceIdx(info, tileIdx)
|
const c = Util.coordByTileIdx(info, tileIdx)
|
||||||
const cx = c.x * info.tileSize
|
const cx = c.x * info.tileSize
|
||||||
const cy = c.y * info.tileSize
|
const cy = c.y * info.tileSize
|
||||||
|
|
||||||
|
|
@ -372,7 +444,7 @@ function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
|
||||||
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
||||||
const info = GAMES[gameId].puzzle.info
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
const c = Util.coordByPieceIdx(info, tileIdx)
|
const c = Util.coordByTileIdx(info, tileIdx)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// top
|
// top
|
||||||
|
|
@ -386,117 +458,109 @@ function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPiecesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => {
|
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => {
|
||||||
for (const tilesIdx of tileIdxs) {
|
for (let tilesIdx of tileIdxs) {
|
||||||
changePiece(gameId, tilesIdx, { z: zIndex })
|
changeTile(gameId, tilesIdx, { z: zIndex })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => {
|
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => {
|
||||||
const oldPos = getPiecePos(gameId, tileIdx)
|
const oldPos = getTilePos(gameId, tileIdx)
|
||||||
const pos = Geometry.pointAdd(oldPos, diff)
|
const pos = Geometry.pointAdd(oldPos, diff)
|
||||||
changePiece(gameId, tileIdx, { pos })
|
changeTile(gameId, tileIdx, { pos })
|
||||||
}
|
}
|
||||||
|
|
||||||
const movePiecesDiff = (
|
const moveTilesDiff = (
|
||||||
gameId: string,
|
gameId: string,
|
||||||
pieceIdxs: Array<number>,
|
tileIdxs: Array<number>,
|
||||||
diff: Point
|
diff: Point
|
||||||
): void => {
|
): void => {
|
||||||
const drawSize = getPieceDrawSize(gameId)
|
const tileDrawSize = getTileDrawSize(gameId)
|
||||||
const bounds = getBounds(gameId)
|
const bounds = getBounds(gameId)
|
||||||
const cappedDiff = diff
|
const cappedDiff = diff
|
||||||
|
|
||||||
for (const pieceIdx of pieceIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
const t = getPiece(gameId, pieceIdx)
|
const t = getTile(gameId, tileIdx)
|
||||||
if (t.pos.x + diff.x < bounds.x) {
|
if (t.pos.x + diff.x < bounds.x) {
|
||||||
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x)
|
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x)
|
||||||
} else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) {
|
} else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) {
|
||||||
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + drawSize, cappedDiff.x)
|
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x)
|
||||||
}
|
}
|
||||||
if (t.pos.y + diff.y < bounds.y) {
|
if (t.pos.y + diff.y < bounds.y) {
|
||||||
cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y)
|
cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y)
|
||||||
} else if (t.pos.y + drawSize + diff.y > bounds.y + bounds.h) {
|
} else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) {
|
||||||
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + drawSize, cappedDiff.y)
|
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pieceIdx of pieceIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
moveTileDiff(gameId, pieceIdx, cappedDiff)
|
moveTileDiff(gameId, tileIdx, cappedDiff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFinishedPiece = (gameId: string, pieceIdx: number): boolean => {
|
const finishTiles = (gameId: string, tileIdxs: Array<number>): void => {
|
||||||
return getPieceOwner(gameId, pieceIdx) === -1
|
for (let tileIdx of tileIdxs) {
|
||||||
}
|
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
|
||||||
|
|
||||||
const getPieceOwner = (gameId: string, pieceIdx: number): string|number => {
|
|
||||||
return getPiece(gameId, pieceIdx).owner
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishPieces = (gameId: string, pieceIdxs: Array<number>): void => {
|
|
||||||
for (const pieceIdx of pieceIdxs) {
|
|
||||||
changePiece(gameId, pieceIdx, { owner: -1, z: 1 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTilesOwner = (
|
const setTilesOwner = (
|
||||||
gameId: string,
|
gameId: string,
|
||||||
pieceIdxs: Array<number>,
|
tileIdxs: Array<number>,
|
||||||
owner: string|number
|
owner: string|number
|
||||||
): void => {
|
): void => {
|
||||||
for (const pieceIdx of pieceIdxs) {
|
for (let tileIdx of tileIdxs) {
|
||||||
changePiece(gameId, pieceIdx, { owner })
|
changeTile(gameId, tileIdx, { owner })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all grouped tiles for a tile
|
// get all grouped tiles for a tile
|
||||||
function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] {
|
function getGroupedTileIdxs(gameId: string, tileIdx: number): number[] {
|
||||||
const pieces = GAMES[gameId].puzzle.tiles
|
const tiles = GAMES[gameId].puzzle.tiles
|
||||||
const piece = Util.decodePiece(pieces[pieceIdx])
|
const tile = Util.decodeTile(tiles[tileIdx])
|
||||||
|
|
||||||
const grouped = []
|
const grouped = []
|
||||||
if (piece.group) {
|
if (tile.group) {
|
||||||
for (const other of pieces) {
|
for (let other of tiles) {
|
||||||
const otherPiece = Util.decodePiece(other)
|
const otherTile = Util.decodeTile(other)
|
||||||
if (otherPiece.group === piece.group) {
|
if (otherTile.group === tile.group) {
|
||||||
grouped.push(otherPiece.idx)
|
grouped.push(otherTile.idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
grouped.push(piece.idx)
|
grouped.push(tile.idx)
|
||||||
}
|
}
|
||||||
return grouped
|
return grouped
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 freePieceIdxByPos = (gameId: string, pos: Point): number => {
|
const freeTileIdxByPos = (gameId: string, pos: Point): number => {
|
||||||
const info = GAMES[gameId].puzzle.info
|
let info = GAMES[gameId].puzzle.info
|
||||||
const pieces = GAMES[gameId].puzzle.tiles
|
let tiles = GAMES[gameId].puzzle.tiles
|
||||||
|
|
||||||
let maxZ = -1
|
let maxZ = -1
|
||||||
let pieceIdx = -1
|
let tileIdx = -1
|
||||||
for (let idx = 0; idx < pieces.length; idx++) {
|
for (let idx = 0; idx < tiles.length; idx++) {
|
||||||
const piece = Util.decodePiece(pieces[idx])
|
const tile = Util.decodeTile(tiles[idx])
|
||||||
if (piece.owner !== 0) {
|
if (tile.owner !== 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const collisionRect: Rect = {
|
const collisionRect: Rect = {
|
||||||
x: piece.pos.x,
|
x: tile.pos.x,
|
||||||
y: piece.pos.y,
|
y: tile.pos.y,
|
||||||
w: info.tileSize,
|
w: info.tileSize,
|
||||||
h: info.tileSize,
|
h: info.tileSize,
|
||||||
}
|
}
|
||||||
if (Geometry.pointInBounds(pos, collisionRect)) {
|
if (Geometry.pointInBounds(pos, collisionRect)) {
|
||||||
if (maxZ === -1 || piece.z > maxZ) {
|
if (maxZ === -1 || tile.z > maxZ) {
|
||||||
maxZ = piece.z
|
maxZ = tile.z
|
||||||
pieceIdx = idx
|
tileIdx = idx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pieceIdx
|
return tileIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlayerBgColor = (gameId: string, playerId: string): string|null => {
|
const getPlayerBgColor = (gameId: string, playerId: string): string|null => {
|
||||||
|
|
@ -525,8 +589,8 @@ const areGrouped = (
|
||||||
tileIdx1: number,
|
tileIdx1: number,
|
||||||
tileIdx2: number
|
tileIdx2: number
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const g1 = getPieceGroup(gameId, tileIdx1)
|
const g1 = getTileGroup(gameId, tileIdx1)
|
||||||
const g2 = getPieceGroup(gameId, tileIdx2)
|
const g2 = getTileGroup(gameId, tileIdx2)
|
||||||
return !!(g1 && g1 === g2)
|
return !!(g1 && g1 === g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,52 +621,47 @@ const getPuzzleHeight = (gameId: string): number => {
|
||||||
function handleInput(
|
function handleInput(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
playerId: string,
|
playerId: string,
|
||||||
input: Input,
|
input: any,
|
||||||
ts: Timestamp,
|
ts: number
|
||||||
onSnap?: (playerId: string) => void
|
): Array<Array<any>> {
|
||||||
): Array<Change> {
|
|
||||||
const puzzle = GAMES[gameId].puzzle
|
const puzzle = GAMES[gameId].puzzle
|
||||||
const evtInfo = getEvtInfo(gameId, playerId)
|
const evtInfo = getEvtInfo(gameId, playerId)
|
||||||
|
|
||||||
const changes: Array<Change> = []
|
const changes = [] as Array<Array<any>>
|
||||||
|
|
||||||
const _dataChange = (): void => {
|
const _dataChange = (): void => {
|
||||||
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
||||||
}
|
}
|
||||||
|
|
||||||
const _pieceChange = (pieceIdx: number): void => {
|
const _tileChange = (tileIdx: number): void => {
|
||||||
changes.push([
|
changes.push([
|
||||||
Protocol.CHANGE_TILE,
|
Protocol.CHANGE_TILE,
|
||||||
Util.encodePiece(getPiece(gameId, pieceIdx)),
|
Util.encodeTile(getTile(gameId, tileIdx)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const _pieceChanges = (pieceIdxs: Array<number>): void => {
|
const _tileChanges = (tileIdxs: Array<number>): void => {
|
||||||
for (const pieceIdx of pieceIdxs) {
|
for (const tileIdx of tileIdxs) {
|
||||||
_pieceChange(pieceIdx)
|
_tileChange(tileIdx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _playerChange = (): void => {
|
const _playerChange = (): void => {
|
||||||
const player = getPlayer(gameId, playerId)
|
|
||||||
if (!player) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
changes.push([
|
changes.push([
|
||||||
Protocol.CHANGE_PLAYER,
|
Protocol.CHANGE_PLAYER,
|
||||||
Util.encodePlayer(player),
|
Util.encodePlayer(getPlayer(gameId, playerId)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// put both tiles (and their grouped tiles) in the same group
|
// put both tiles (and their grouped tiles) in the same group
|
||||||
const groupTiles = (
|
const groupTiles = (
|
||||||
gameId: string,
|
gameId: string,
|
||||||
pieceIdx1: number,
|
tileIdx1: number,
|
||||||
pieceIdx2: number
|
tileIdx2: number
|
||||||
): void => {
|
): void => {
|
||||||
const pieces = GAMES[gameId].puzzle.tiles
|
const tiles = GAMES[gameId].puzzle.tiles
|
||||||
const group1 = getPieceGroup(gameId, pieceIdx1)
|
const group1 = getTileGroup(gameId, tileIdx1)
|
||||||
const group2 = getPieceGroup(gameId, pieceIdx2)
|
const group2 = getTileGroup(gameId, tileIdx2)
|
||||||
|
|
||||||
let group
|
let group
|
||||||
const searchGroups = []
|
const searchGroups = []
|
||||||
|
|
@ -623,18 +682,18 @@ function handleInput(
|
||||||
group = getMaxGroup(gameId)
|
group = getMaxGroup(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
changePiece(gameId, pieceIdx1, { group })
|
changeTile(gameId, tileIdx1, { group })
|
||||||
_pieceChange(pieceIdx1)
|
_tileChange(tileIdx1)
|
||||||
changePiece(gameId, pieceIdx2, { group })
|
changeTile(gameId, tileIdx2, { group })
|
||||||
_pieceChange(pieceIdx2)
|
_tileChange(tileIdx2)
|
||||||
|
|
||||||
// TODO: strange
|
// TODO: strange
|
||||||
if (searchGroups.length > 0) {
|
if (searchGroups.length > 0) {
|
||||||
for (const p of pieces) {
|
for (const t of tiles) {
|
||||||
const piece = Util.decodePiece(p)
|
const tile = Util.decodeTile(t)
|
||||||
if (searchGroups.includes(piece.group)) {
|
if (searchGroups.includes(tile.group)) {
|
||||||
changePiece(gameId, piece.idx, { group })
|
changeTile(gameId, tile.idx, { group })
|
||||||
_pieceChange(piece.idx)
|
_tileChange(tile.idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -653,16 +712,6 @@ function handleInput(
|
||||||
const name = `${input[1]}`.substr(0, 16)
|
const name = `${input[1]}`.substr(0, 16)
|
||||||
changePlayer(gameId, playerId, { name, ts })
|
changePlayer(gameId, playerId, { name, ts })
|
||||||
_playerChange()
|
_playerChange()
|
||||||
} else if (type === Protocol.INPUT_EV_MOVE) {
|
|
||||||
const w = input[1]
|
|
||||||
const h = input[2]
|
|
||||||
const player = getPlayer(gameId, playerId)
|
|
||||||
if (player) {
|
|
||||||
const x = player.x - w
|
|
||||||
const y = player.y - h
|
|
||||||
changePlayer(gameId, playerId, { ts, x, y })
|
|
||||||
_playerChange()
|
|
||||||
}
|
|
||||||
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
||||||
const x = input[1]
|
const x = input[1]
|
||||||
const y = input[2]
|
const y = input[2]
|
||||||
|
|
@ -672,15 +721,15 @@ function handleInput(
|
||||||
_playerChange()
|
_playerChange()
|
||||||
evtInfo._last_mouse_down = pos
|
evtInfo._last_mouse_down = pos
|
||||||
|
|
||||||
const tileIdxAtPos = freePieceIdxByPos(gameId, pos)
|
const tileIdxAtPos = freeTileIdxByPos(gameId, pos)
|
||||||
if (tileIdxAtPos >= 0) {
|
if (tileIdxAtPos >= 0) {
|
||||||
const maxZ = getMaxZIndex(gameId) + 1
|
let maxZ = getMaxZIndex(gameId) + 1
|
||||||
changeData(gameId, { maxZ })
|
changeData(gameId, { maxZ })
|
||||||
_dataChange()
|
_dataChange()
|
||||||
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos)
|
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos)
|
||||||
setPiecesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
|
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
|
||||||
setTilesOwner(gameId, tileIdxs, playerId)
|
setTilesOwner(gameId, tileIdxs, playerId)
|
||||||
_pieceChanges(tileIdxs)
|
_tileChanges(tileIdxs)
|
||||||
}
|
}
|
||||||
evtInfo._last_mouse = pos
|
evtInfo._last_mouse = pos
|
||||||
|
|
||||||
|
|
@ -694,19 +743,19 @@ function handleInput(
|
||||||
changePlayer(gameId, playerId, {x, y, ts})
|
changePlayer(gameId, playerId, {x, y, ts})
|
||||||
_playerChange()
|
_playerChange()
|
||||||
} else {
|
} else {
|
||||||
const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
|
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
if (pieceIdx >= 0) {
|
if (tileIdx >= 0) {
|
||||||
// player is moving a tile (and hand)
|
// player is moving a tile (and hand)
|
||||||
changePlayer(gameId, playerId, {x, y, ts})
|
changePlayer(gameId, playerId, {x, y, ts})
|
||||||
_playerChange()
|
_playerChange()
|
||||||
|
|
||||||
// check if pos is on the tile, otherwise dont move
|
// check if pos is on the tile, otherwise dont move
|
||||||
// (mouse could be out of table, but tile stays on it)
|
// (mouse could be out of table, but tile stays on it)
|
||||||
const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
|
const tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
|
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
|
||||||
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId))
|
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId))
|
||||||
for (const idx of pieceIdxs) {
|
for (let idx of tileIdxs) {
|
||||||
const bounds = getPieceBounds(gameId, idx)
|
const bounds = getTileBounds(gameId, idx)
|
||||||
if (Geometry.pointInBounds(pos, bounds)) {
|
if (Geometry.pointInBounds(pos, bounds)) {
|
||||||
anyOk = true
|
anyOk = true
|
||||||
break
|
break
|
||||||
|
|
@ -717,9 +766,9 @@ function handleInput(
|
||||||
const diffY = y - evtInfo._last_mouse_down.y
|
const diffY = y - evtInfo._last_mouse_down.y
|
||||||
|
|
||||||
const diff = { x: diffX, y: diffY }
|
const diff = { x: diffX, y: diffY }
|
||||||
movePiecesDiff(gameId, pieceIdxs, diff)
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
|
|
||||||
_pieceChanges(pieceIdxs)
|
_tileChanges(tileIdxs)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// player is just moving map, so no change in position!
|
// player is just moving map, so no change in position!
|
||||||
|
|
@ -739,44 +788,26 @@ function handleInput(
|
||||||
|
|
||||||
evtInfo._last_mouse_down = null
|
evtInfo._last_mouse_down = null
|
||||||
|
|
||||||
const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
|
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
if (pieceIdx >= 0) {
|
if (tileIdx >= 0) {
|
||||||
// drop the tile(s)
|
// drop the tile(s)
|
||||||
const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
|
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
setTilesOwner(gameId, pieceIdxs, 0)
|
setTilesOwner(gameId, tileIdxs, 0)
|
||||||
_pieceChanges(pieceIdxs)
|
_tileChanges(tileIdxs)
|
||||||
|
|
||||||
// Check if the tile was dropped near the final location
|
// Check if the tile was dropped near the final location
|
||||||
const tilePos = getPiecePos(gameId, pieceIdx)
|
let tilePos = getTilePos(gameId, tileIdx)
|
||||||
const finalPos = getFinalPiecePos(gameId, pieceIdx)
|
let finalPos = getFinalTilePos(gameId, tileIdx)
|
||||||
|
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
|
||||||
let canSnapToFinal = false
|
let diff = Geometry.pointSub(finalPos, tilePos)
|
||||||
if (getSnapMode(gameId) === SnapMode.REAL) {
|
|
||||||
// only can snap to final if any of the grouped pieces are
|
|
||||||
// corner pieces
|
|
||||||
for (const pieceIdxTmp of pieceIdxs) {
|
|
||||||
if (isCornerPiece(gameId, pieceIdxTmp)) {
|
|
||||||
canSnapToFinal = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canSnapToFinal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
canSnapToFinal
|
|
||||||
&& Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance
|
|
||||||
) {
|
|
||||||
const diff = Geometry.pointSub(finalPos, tilePos)
|
|
||||||
// Snap the tile to the final destination
|
// Snap the tile to the final destination
|
||||||
movePiecesDiff(gameId, pieceIdxs, diff)
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
finishPieces(gameId, pieceIdxs)
|
finishTiles(gameId, tileIdxs)
|
||||||
_pieceChanges(pieceIdxs)
|
_tileChanges(tileIdxs)
|
||||||
|
|
||||||
let points = getPlayerPoints(gameId, playerId)
|
let points = getPlayerPoints(gameId, playerId)
|
||||||
if (getScoreMode(gameId) === ScoreMode.FINAL) {
|
if (getScoreMode(gameId) === ScoreMode.FINAL) {
|
||||||
points += pieceIdxs.length
|
points += tileIdxs.length
|
||||||
} else if (getScoreMode(gameId) === ScoreMode.ANY) {
|
} else if (getScoreMode(gameId) === ScoreMode.ANY) {
|
||||||
points += 1
|
points += 1
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -787,13 +818,10 @@ function handleInput(
|
||||||
_playerChange()
|
_playerChange()
|
||||||
|
|
||||||
// check if the puzzle is finished
|
// check if the puzzle is finished
|
||||||
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
|
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
|
||||||
changeData(gameId, { finished: ts })
|
changeData(gameId, { finished: ts })
|
||||||
_dataChange()
|
_dataChange()
|
||||||
}
|
}
|
||||||
if (onSnap) {
|
|
||||||
onSnap(playerId)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Snap to other tiles
|
// Snap to other tiles
|
||||||
const check = (
|
const check = (
|
||||||
|
|
@ -802,44 +830,40 @@ function handleInput(
|
||||||
otherTileIdx: number,
|
otherTileIdx: number,
|
||||||
off: Array<number>
|
off: Array<number>
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const info = GAMES[gameId].puzzle.info
|
let info = GAMES[gameId].puzzle.info
|
||||||
if (otherTileIdx < 0) {
|
if (otherTileIdx < 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
|
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const tilePos = getPiecePos(gameId, tileIdx)
|
const tilePos = getTilePos(gameId, tileIdx)
|
||||||
const dstPos = Geometry.pointAdd(
|
const dstPos = Geometry.pointAdd(
|
||||||
getPiecePos(gameId, otherTileIdx),
|
getTilePos(gameId, otherTileIdx),
|
||||||
{x: off[0] * info.tileSize, y: off[1] * info.tileSize}
|
{x: off[0] * info.tileSize, y: off[1] * info.tileSize}
|
||||||
)
|
)
|
||||||
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
|
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
|
||||||
const diff = Geometry.pointSub(dstPos, tilePos)
|
let diff = Geometry.pointSub(dstPos, tilePos)
|
||||||
let pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
|
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
movePiecesDiff(gameId, pieceIdxs, diff)
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
groupTiles(gameId, tileIdx, otherTileIdx)
|
groupTiles(gameId, tileIdx, otherTileIdx)
|
||||||
pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
|
tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
if (isFinishedPiece(gameId, otherTileIdx)) {
|
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs)
|
||||||
finishPieces(gameId, pieceIdxs)
|
setTilesZIndex(gameId, tileIdxs, zIndex)
|
||||||
} else {
|
_tileChanges(tileIdxs)
|
||||||
const zIndex = getMaxZIndexByPieceIdxs(gameId, pieceIdxs)
|
|
||||||
setPiecesZIndex(gameId, pieceIdxs, zIndex)
|
|
||||||
}
|
|
||||||
_pieceChanges(pieceIdxs)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapped = false
|
let snapped = false
|
||||||
for (const pieceIdxTmp of getGroupedPieceIdxs(gameId, pieceIdx)) {
|
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
|
||||||
const othersIdxs = getSurroundingTilesByIdx(gameId, pieceIdxTmp)
|
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp)
|
||||||
if (
|
if (
|
||||||
check(gameId, pieceIdxTmp, othersIdxs[0], [0, 1]) // top
|
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
|
||||||
|| check(gameId, pieceIdxTmp, othersIdxs[1], [-1, 0]) // right
|
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right
|
||||||
|| check(gameId, pieceIdxTmp, othersIdxs[2], [0, -1]) // bottom
|
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom
|
||||||
|| check(gameId, pieceIdxTmp, othersIdxs[3], [1, 0]) // left
|
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left
|
||||||
) {
|
) {
|
||||||
snapped = true
|
snapped = true
|
||||||
break
|
break
|
||||||
|
|
@ -853,16 +877,6 @@ function handleInput(
|
||||||
changePlayer(gameId, playerId, { d, ts })
|
changePlayer(gameId, playerId, { d, ts })
|
||||||
_playerChange()
|
_playerChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapped && getSnapMode(gameId) === SnapMode.REAL) {
|
|
||||||
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
|
|
||||||
changeData(gameId, { finished: ts })
|
|
||||||
_dataChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (snapped && onSnap) {
|
|
||||||
onSnap(playerId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
changePlayer(gameId, playerId, { d, ts })
|
changePlayer(gameId, playerId, { d, ts })
|
||||||
|
|
@ -891,15 +905,17 @@ function handleInput(
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
__createPlayerObject,
|
||||||
setGame,
|
setGame,
|
||||||
exists,
|
exists,
|
||||||
playerExists,
|
playerExists,
|
||||||
getActivePlayers,
|
getActivePlayers,
|
||||||
getIdlePlayers,
|
getIdlePlayers,
|
||||||
addPlayer,
|
addPlayer,
|
||||||
getFinishedPiecesCount,
|
getFinishedTileCount,
|
||||||
getPieceCount,
|
getTileCount,
|
||||||
getImageUrl,
|
getImageUrl,
|
||||||
|
setImageUrl,
|
||||||
get,
|
get,
|
||||||
getAllGames,
|
getAllGames,
|
||||||
getPlayerBgColor,
|
getPlayerBgColor,
|
||||||
|
|
@ -909,7 +925,7 @@ export default {
|
||||||
getPlayerIdByIndex,
|
getPlayerIdByIndex,
|
||||||
changePlayer,
|
changePlayer,
|
||||||
setPlayer,
|
setPlayer,
|
||||||
setPiece,
|
setTile,
|
||||||
setPuzzleData,
|
setPuzzleData,
|
||||||
getTableWidth,
|
getTableWidth,
|
||||||
getTableHeight,
|
getTableHeight,
|
||||||
|
|
@ -917,11 +933,11 @@ export default {
|
||||||
getRng,
|
getRng,
|
||||||
getPuzzleWidth,
|
getPuzzleWidth,
|
||||||
getPuzzleHeight,
|
getPuzzleHeight,
|
||||||
getPiecesSortedByZIndex,
|
getTilesSortedByZIndex,
|
||||||
getFirstOwnedPiece,
|
getFirstOwnedTile,
|
||||||
getPieceDrawOffset,
|
getTileDrawOffset,
|
||||||
getPieceDrawSize,
|
getTileDrawSize,
|
||||||
getFinalPiecePos,
|
getFinalTilePos,
|
||||||
getStartTs,
|
getStartTs,
|
||||||
getFinishTs,
|
getFinishTs,
|
||||||
handleInput,
|
handleInput,
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,10 @@ EV_SERVER_INIT: event sent to one client after that client
|
||||||
*/
|
*/
|
||||||
const EV_SERVER_EVENT = 1
|
const EV_SERVER_EVENT = 1
|
||||||
const EV_SERVER_INIT = 4
|
const EV_SERVER_INIT = 4
|
||||||
|
const EV_SERVER_INIT_REPLAY = 5
|
||||||
const EV_CLIENT_EVENT = 2
|
const EV_CLIENT_EVENT = 2
|
||||||
const EV_CLIENT_INIT = 3
|
const EV_CLIENT_INIT = 3
|
||||||
|
const EV_CLIENT_INIT_REPLAY = 6
|
||||||
|
|
||||||
const LOG_HEADER = 1
|
const LOG_HEADER = 1
|
||||||
const LOG_ADD_PLAYER = 2
|
const LOG_ADD_PLAYER = 2
|
||||||
|
|
@ -58,17 +60,6 @@ const INPUT_EV_PLAYER_COLOR = 7
|
||||||
const INPUT_EV_PLAYER_NAME = 8
|
const INPUT_EV_PLAYER_NAME = 8
|
||||||
const INPUT_EV_MOVE = 9
|
const INPUT_EV_MOVE = 9
|
||||||
const INPUT_EV_TOGGLE_PREVIEW = 10
|
const INPUT_EV_TOGGLE_PREVIEW = 10
|
||||||
const INPUT_EV_TOGGLE_SOUNDS = 11
|
|
||||||
|
|
||||||
const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12
|
|
||||||
const INPUT_EV_REPLAY_SPEED_UP = 13
|
|
||||||
const INPUT_EV_REPLAY_SPEED_DOWN = 14
|
|
||||||
|
|
||||||
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15
|
|
||||||
const INPUT_EV_CENTER_FIT_PUZZLE = 16
|
|
||||||
|
|
||||||
const INPUT_EV_TOGGLE_FIXED_PIECES = 17
|
|
||||||
const INPUT_EV_TOGGLE_LOOSE_PIECES = 18
|
|
||||||
|
|
||||||
const CHANGE_DATA = 1
|
const CHANGE_DATA = 1
|
||||||
const CHANGE_TILE = 2
|
const CHANGE_TILE = 2
|
||||||
|
|
@ -77,8 +68,10 @@ const CHANGE_PLAYER = 3
|
||||||
export default {
|
export default {
|
||||||
EV_SERVER_EVENT,
|
EV_SERVER_EVENT,
|
||||||
EV_SERVER_INIT,
|
EV_SERVER_INIT,
|
||||||
|
EV_SERVER_INIT_REPLAY,
|
||||||
EV_CLIENT_EVENT,
|
EV_CLIENT_EVENT,
|
||||||
EV_CLIENT_INIT,
|
EV_CLIENT_INIT,
|
||||||
|
EV_CLIENT_INIT_REPLAY,
|
||||||
|
|
||||||
LOG_HEADER,
|
LOG_HEADER,
|
||||||
LOG_ADD_PLAYER,
|
LOG_ADD_PLAYER,
|
||||||
|
|
@ -98,17 +91,6 @@ export default {
|
||||||
INPUT_EV_PLAYER_NAME,
|
INPUT_EV_PLAYER_NAME,
|
||||||
|
|
||||||
INPUT_EV_TOGGLE_PREVIEW,
|
INPUT_EV_TOGGLE_PREVIEW,
|
||||||
INPUT_EV_TOGGLE_SOUNDS,
|
|
||||||
|
|
||||||
INPUT_EV_REPLAY_TOGGLE_PAUSE,
|
|
||||||
INPUT_EV_REPLAY_SPEED_UP,
|
|
||||||
INPUT_EV_REPLAY_SPEED_DOWN,
|
|
||||||
|
|
||||||
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
|
||||||
INPUT_EV_CENTER_FIT_PUZZLE,
|
|
||||||
|
|
||||||
INPUT_EV_TOGGLE_FIXED_PIECES,
|
|
||||||
INPUT_EV_TOGGLE_LOOSE_PIECES,
|
|
||||||
|
|
||||||
CHANGE_DATA,
|
CHANGE_DATA,
|
||||||
CHANGE_TILE,
|
CHANGE_TILE,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export interface RngSerialized {
|
interface RngSerialized {
|
||||||
rand_high: number,
|
rand_high: number,
|
||||||
rand_low: number,
|
rand_low: number,
|
||||||
}
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ export class Rng {
|
||||||
random (min: number, max: number): number {
|
random (min: number, max: number): 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;
|
||||||
const 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
import { Point } from "./Geometry"
|
|
||||||
import { Rng, RngSerialized } from "./Rng"
|
|
||||||
|
|
||||||
// @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
|
|
||||||
export type FixedLengthArray<T extends any[]> =
|
|
||||||
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
|
|
||||||
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
|
|
||||||
|
|
||||||
export type Timestamp = number
|
|
||||||
|
|
||||||
export type Input = any
|
|
||||||
export type Change = Array<any>
|
|
||||||
|
|
||||||
export type GameEvent = Array<any>
|
|
||||||
|
|
||||||
export type ServerEvent = Array<any>
|
|
||||||
export type ClientEvent = Array<any>
|
|
||||||
|
|
||||||
export type EncodedPlayer = FixedLengthArray<[
|
|
||||||
string,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
0|1,
|
|
||||||
string|null,
|
|
||||||
string|null,
|
|
||||||
string|null,
|
|
||||||
number,
|
|
||||||
Timestamp,
|
|
||||||
]>
|
|
||||||
|
|
||||||
export type EncodedPiece = FixedLengthArray<[
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
string|number,
|
|
||||||
number,
|
|
||||||
]>
|
|
||||||
|
|
||||||
export type EncodedPieceShape = number
|
|
||||||
|
|
||||||
export type EncodedGame = FixedLengthArray<[
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
RngSerialized,
|
|
||||||
Puzzle,
|
|
||||||
Array<EncodedPlayer>,
|
|
||||||
Record<string, EvtInfo>,
|
|
||||||
ScoreMode,
|
|
||||||
ShapeMode,
|
|
||||||
SnapMode,
|
|
||||||
number|null,
|
|
||||||
]>
|
|
||||||
|
|
||||||
export interface ReplayData {
|
|
||||||
log: any[],
|
|
||||||
game: EncodedGame|null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
id: number
|
|
||||||
slug: string
|
|
||||||
title: string
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameRng {
|
|
||||||
obj: Rng
|
|
||||||
type?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Game {
|
|
||||||
id: string
|
|
||||||
creatorUserId: number|null
|
|
||||||
players: Array<EncodedPlayer>
|
|
||||||
puzzle: Puzzle
|
|
||||||
evtInfos: Record<string, EvtInfo>
|
|
||||||
scoreMode: ScoreMode
|
|
||||||
shapeMode: ShapeMode
|
|
||||||
snapMode: SnapMode
|
|
||||||
rng: GameRng
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Image {
|
|
||||||
id: number
|
|
||||||
filename: string
|
|
||||||
file: string
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
tags: Array<Tag>
|
|
||||||
created: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameSettings {
|
|
||||||
tiles: number
|
|
||||||
image: ImageInfo
|
|
||||||
scoreMode: ScoreMode
|
|
||||||
shapeMode: ShapeMode
|
|
||||||
snapMode: SnapMode
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Puzzle {
|
|
||||||
tiles: Array<EncodedPiece>
|
|
||||||
data: PuzzleData
|
|
||||||
info: PuzzleInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PuzzleData {
|
|
||||||
started: number
|
|
||||||
finished: number
|
|
||||||
maxGroup: number
|
|
||||||
maxZ: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PuzzleDataChange {
|
|
||||||
started?: number
|
|
||||||
finished?: number
|
|
||||||
maxGroup?: number
|
|
||||||
maxZ?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PuzzleTable {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PieceEdge {
|
|
||||||
Flat = 0,
|
|
||||||
Out = 1,
|
|
||||||
In = -1,
|
|
||||||
}
|
|
||||||
export interface PieceShape {
|
|
||||||
top: PieceEdge
|
|
||||||
bottom: PieceEdge
|
|
||||||
left: PieceEdge
|
|
||||||
right: PieceEdge
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Piece {
|
|
||||||
owner: string|number
|
|
||||||
idx: number
|
|
||||||
pos: Point
|
|
||||||
z: number
|
|
||||||
group: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PieceChange {
|
|
||||||
owner?: string|number
|
|
||||||
idx?: number
|
|
||||||
pos?: Point
|
|
||||||
z?: number
|
|
||||||
group?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageInfo
|
|
||||||
{
|
|
||||||
id: number
|
|
||||||
uploaderUserId: number|null
|
|
||||||
filename: string
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
tags: Tag[]
|
|
||||||
created: Timestamp
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PuzzleInfo {
|
|
||||||
table: PuzzleTable
|
|
||||||
targetTiles: number
|
|
||||||
imageUrl?: string // deprecated, use image.url instead
|
|
||||||
image?: ImageInfo
|
|
||||||
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
tileSize: number
|
|
||||||
tileDrawSize: number
|
|
||||||
tileMarginWidth: number
|
|
||||||
tileDrawOffset: number
|
|
||||||
snapDistance: number
|
|
||||||
|
|
||||||
tiles: number
|
|
||||||
tilesX: number
|
|
||||||
tilesY: number
|
|
||||||
|
|
||||||
shapes: Array<EncodedPieceShape>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Player {
|
|
||||||
id: string
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
d: 0|1
|
|
||||||
name: string|null
|
|
||||||
color: string|null
|
|
||||||
bgcolor: string|null
|
|
||||||
points: number
|
|
||||||
ts: Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlayerChange {
|
|
||||||
id?: string
|
|
||||||
x?: number
|
|
||||||
y?: number
|
|
||||||
d?: 0|1
|
|
||||||
name?: string|null
|
|
||||||
color?: string|null
|
|
||||||
bgcolor?: string|null
|
|
||||||
points?: number
|
|
||||||
ts?: Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EvtInfo {
|
|
||||||
_last_mouse: Point|null
|
|
||||||
_last_mouse_down: Point|null
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ScoreMode {
|
|
||||||
FINAL = 0,
|
|
||||||
ANY = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ShapeMode {
|
|
||||||
NORMAL = 0,
|
|
||||||
ANY = 1,
|
|
||||||
FLAT = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SnapMode {
|
|
||||||
NORMAL = 0,
|
|
||||||
REAL = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultScoreMode = (v: any): ScoreMode => {
|
|
||||||
if (v === ScoreMode.FINAL || v === ScoreMode.ANY) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ScoreMode.FINAL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultShapeMode = (v: any): ShapeMode => {
|
|
||||||
if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ShapeMode.NORMAL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultSnapMode = (v: any): SnapMode => {
|
|
||||||
if (v === SnapMode.NORMAL || v === SnapMode.REAL) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return SnapMode.NORMAL
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +1,15 @@
|
||||||
import { PuzzleCreationInfo } from '../server/Puzzle'
|
import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Player } from './GameCommon'
|
||||||
import {
|
|
||||||
EncodedGame,
|
|
||||||
EncodedPiece,
|
|
||||||
EncodedPieceShape,
|
|
||||||
EncodedPlayer,
|
|
||||||
Game,
|
|
||||||
Piece,
|
|
||||||
PieceShape,
|
|
||||||
Player,
|
|
||||||
PuzzleInfo,
|
|
||||||
ScoreMode,
|
|
||||||
ShapeMode,
|
|
||||||
SnapMode
|
|
||||||
} from './Types'
|
|
||||||
import { Point } from './Geometry'
|
import { Point } from './Geometry'
|
||||||
import { Rng } from './Rng'
|
import { Rng } from './Rng'
|
||||||
|
|
||||||
const slug = (str: string): string => {
|
const slug = (str: string) => {
|
||||||
let tmp = str.toLowerCase()
|
let tmp = str.toLowerCase()
|
||||||
tmp = tmp.replace(/[^a-z0-9]+/g, '-')
|
tmp = tmp.replace(/[^a-z0-9]+/g, '-')
|
||||||
tmp = tmp.replace(/^-|-$/, '')
|
tmp = tmp.replace(/^-|-$/, '')
|
||||||
return tmp
|
return tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
const pad = (x: number, pad: string): string => {
|
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
|
||||||
|
|
@ -31,11 +17,8 @@ const pad = (x: number, pad: string): string => {
|
||||||
return pad.substr(0, pad.length - str.length) + str
|
return pad.substr(0, pad.length - str.length) + str
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogArgs = Array<any>
|
export const logger = (...pre: Array<any>) => {
|
||||||
type LogFn = (...args: LogArgs) => void
|
const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
|
||||||
|
|
||||||
export const logger = (...pre: string[]): { log: LogFn, error: LogFn, info: LogFn } => {
|
|
||||||
const log = (m: 'log'|'info'|'error') => (...args: LogArgs): void => {
|
|
||||||
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')
|
||||||
|
|
@ -50,9 +33,7 @@ export const logger = (...pre: string[]): { log: LogFn, error: LogFn, info: LogF
|
||||||
}
|
}
|
||||||
|
|
||||||
// get a unique id
|
// get a unique id
|
||||||
export const uniqId = (): string => {
|
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
|
||||||
return Date.now().toString(36) + Math.random().toString(36).substring(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeShape(data: PieceShape): EncodedPieceShape {
|
function encodeShape(data: PieceShape): EncodedPieceShape {
|
||||||
/* encoded in 1 byte:
|
/* encoded in 1 byte:
|
||||||
|
|
@ -77,11 +58,11 @@ function decodeShape(data: EncodedPieceShape): PieceShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodePiece(data: Piece): EncodedPiece {
|
function encodeTile(data: Piece): EncodedPiece {
|
||||||
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 decodePiece(data: EncodedPiece): Piece {
|
function decodeTile(data: EncodedPiece): Piece {
|
||||||
return {
|
return {
|
||||||
idx: data[0],
|
idx: data[0],
|
||||||
pos: {
|
pos: {
|
||||||
|
|
@ -122,22 +103,25 @@ function decodePlayer(data: EncodedPlayer): Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeGame(data: Game): EncodedGame {
|
function encodeGame(data: any): Array<any> {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
data.id,
|
data.id,
|
||||||
data.rng.type || '',
|
data.rng.type,
|
||||||
Rng.serialize(data.rng.obj),
|
Rng.serialize(data.rng.obj),
|
||||||
data.puzzle,
|
data.puzzle,
|
||||||
data.players,
|
data.players,
|
||||||
data.evtInfos,
|
data.evtInfos,
|
||||||
data.scoreMode,
|
data.scoreMode,
|
||||||
data.shapeMode,
|
|
||||||
data.snapMode,
|
|
||||||
data.creatorUserId,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeGame(data: EncodedGame): Game {
|
function decodeGame(data: any) {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: data[0],
|
id: data[0],
|
||||||
rng: {
|
rng: {
|
||||||
|
|
@ -148,17 +132,14 @@ function decodeGame(data: EncodedGame): Game {
|
||||||
players: data[4],
|
players: data[4],
|
||||||
evtInfos: data[5],
|
evtInfos: data[5],
|
||||||
scoreMode: data[6],
|
scoreMode: data[6],
|
||||||
shapeMode: data[7],
|
|
||||||
snapMode: data[8],
|
|
||||||
creatorUserId: data[9],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function coordByPieceIdx(info: PuzzleInfo|PuzzleCreationInfo, pieceIdx: number): Point {
|
function coordByTileIdx(info: any, tileIdx: number): Point {
|
||||||
const wTiles = info.width / info.tileSize
|
const wTiles = info.width / info.tileSize
|
||||||
return {
|
return {
|
||||||
x: pieceIdx % wTiles,
|
x: tileIdx % wTiles,
|
||||||
y: Math.floor(pieceIdx / wTiles),
|
y: Math.floor(tileIdx / wTiles),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,16 +147,16 @@ 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++) {
|
||||||
const chr = str.charCodeAt(i);
|
let chr = str.charCodeAt(i);
|
||||||
hash = ((hash << 5) - hash) + chr;
|
hash = ((hash << 5) - hash) + chr;
|
||||||
hash |= 0; // Convert to 32bit integer
|
hash |= 0; // Convert to 32bit integer
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asQueryArgs(data: Record<string, any>): string {
|
function asQueryArgs(data: any) {
|
||||||
const q = []
|
const q = []
|
||||||
for (const k in data) {
|
for (let k in data) {
|
||||||
const pair = [k, data[k]].map(encodeURIComponent)
|
const pair = [k, data[k]].map(encodeURIComponent)
|
||||||
q.push(pair.join('='))
|
q.push(pair.join('='))
|
||||||
}
|
}
|
||||||
|
|
@ -193,8 +174,8 @@ export default {
|
||||||
encodeShape,
|
encodeShape,
|
||||||
decodeShape,
|
decodeShape,
|
||||||
|
|
||||||
encodePiece,
|
encodeTile,
|
||||||
decodePiece,
|
decodeTile,
|
||||||
|
|
||||||
encodePlayer,
|
encodePlayer,
|
||||||
decodePlayer,
|
decodePlayer,
|
||||||
|
|
@ -202,7 +183,7 @@ export default {
|
||||||
encodeGame,
|
encodeGame,
|
||||||
decodeGame,
|
decodeGame,
|
||||||
|
|
||||||
coordByPieceIdx,
|
coordByTileIdx,
|
||||||
|
|
||||||
asQueryArgs,
|
asQueryArgs,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
src/dbpatches/02_gamelog.sqlite
Normal file
7
src/dbpatches/02_gamelog.sqlite
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE game_log (
|
||||||
|
game_id TEXT,
|
||||||
|
|
||||||
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
entry TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
-- Add width/height to images table
|
|
||||||
|
|
||||||
CREATE TABLE images_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
|
|
||||||
created TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
filename TEXT NOT NULL UNIQUE,
|
|
||||||
filename_original TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
|
|
||||||
width INTEGER NOT NULL,
|
|
||||||
height INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO images_new
|
|
||||||
SELECT id, created, filename, filename_original, title, 0, 0
|
|
||||||
FROM images;
|
|
||||||
|
|
||||||
DROP TABLE images;
|
|
||||||
|
|
||||||
ALTER TABLE images_new RENAME TO images;
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
CREATE TABLE users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
|
|
||||||
created TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
client_id TEXT NOT NULL,
|
|
||||||
client_secret TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE images_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
uploader_user_id INTEGER,
|
|
||||||
|
|
||||||
created TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
filename TEXT NOT NULL UNIQUE,
|
|
||||||
filename_original TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
|
|
||||||
width INTEGER NOT NULL,
|
|
||||||
height INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE image_x_category_new (
|
|
||||||
image_id INTEGER NOT NULL,
|
|
||||||
category_id INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO images_new
|
|
||||||
SELECT id, NULL, created, filename, filename_original, title, width, height
|
|
||||||
FROM images;
|
|
||||||
|
|
||||||
INSERT INTO image_x_category_new
|
|
||||||
SELECT image_id, category_id
|
|
||||||
FROM image_x_category;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = OFF;
|
|
||||||
|
|
||||||
DROP TABLE images;
|
|
||||||
DROP TABLE image_x_category;
|
|
||||||
|
|
||||||
ALTER TABLE images_new RENAME TO images;
|
|
||||||
ALTER TABLE image_x_category_new RENAME TO image_x_category;
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
CREATE TABLE games (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
|
|
||||||
creator_user_id INTEGER,
|
|
||||||
image_id INTEGER NOT NULL,
|
|
||||||
|
|
||||||
created TIMESTAMP NOT NULL,
|
|
||||||
finished TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
data TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<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'}">Games overview</router-link></li>
|
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
|
||||||
<li><router-link class="btn" :to="{name: 'new-game'}">New game</router-link></li>
|
<li><router-link class="btn" :to="{name: 'new-game'}">New game</router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@ export default function Camera () {
|
||||||
let y = 0
|
let y = 0
|
||||||
let curZoom = 1
|
let curZoom = 1
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
x = 0
|
|
||||||
y = 0
|
|
||||||
curZoom = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = (byX: number, byY: number) => {
|
const move = (byX: number, byY: number) => {
|
||||||
x += byX / curZoom
|
x += byX / curZoom
|
||||||
y += byY / curZoom
|
y += byY / curZoom
|
||||||
|
|
@ -122,32 +116,15 @@ export default function Camera () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportDimToWorld = (viewportDim: Dim): Dim => {
|
|
||||||
const { w, h } = viewportDimToWorldRaw(viewportDim)
|
|
||||||
return { w: Math.round(w), h: Math.round(h) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewportDimToWorldRaw = (viewportDim: Dim): Dim => {
|
|
||||||
return {
|
return {
|
||||||
w: viewportDim.w / curZoom,
|
|
||||||
h: viewportDim.h / curZoom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getCurrentZoom: () => curZoom,
|
|
||||||
reset,
|
|
||||||
move,
|
move,
|
||||||
canZoom,
|
canZoom,
|
||||||
zoom,
|
zoom,
|
||||||
setZoom,
|
|
||||||
worldToViewport,
|
worldToViewport,
|
||||||
worldToViewportRaw,
|
worldToViewportRaw,
|
||||||
worldDimToViewport, // not used outside
|
worldDimToViewport, // not used outside
|
||||||
worldDimToViewportRaw,
|
worldDimToViewportRaw,
|
||||||
viewportToWorld,
|
viewportToWorld,
|
||||||
viewportToWorldRaw, // not used outside
|
viewportToWorldRaw, // not used outside
|
||||||
viewportDimToWorld,
|
|
||||||
viewportDimToWorldRaw,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types'
|
import { logger } from '../common/Util'
|
||||||
import Util, { logger } from '../common/Util'
|
|
||||||
import Protocol from './../common/Protocol'
|
import Protocol from './../common/Protocol'
|
||||||
import xhr from './xhr'
|
|
||||||
|
|
||||||
const log = logger('Communication.js')
|
const log = logger('Communication.js')
|
||||||
|
|
||||||
|
|
@ -17,41 +15,25 @@ 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)
|
||||||
|
|
||||||
let ws: WebSocket
|
let ws: WebSocket
|
||||||
|
let changesCallback = (msg: Array<any>) => {}
|
||||||
|
let connectionStateChangeCallback = (state: number) => {}
|
||||||
|
|
||||||
let missedMessages: ServerEvent[] = []
|
// TODO: change these to something like on(EVT, cb)
|
||||||
let changesCallback = (msg: ServerEvent) => {
|
function onServerChange(callback: (msg: Array<any>) => void) {
|
||||||
missedMessages.push(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
let missedStateChanges: Array<number> = []
|
|
||||||
let connectionStateChangeCallback = (state: number) => {
|
|
||||||
missedStateChanges.push(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onServerChange(callback: (msg: ServerEvent) => void): void {
|
|
||||||
changesCallback = callback
|
changesCallback = callback
|
||||||
for (const missedMessage of missedMessages) {
|
|
||||||
changesCallback(missedMessage)
|
|
||||||
}
|
|
||||||
missedMessages = []
|
|
||||||
}
|
}
|
||||||
|
function onConnectionStateChange(callback: (state: number) => void) {
|
||||||
function onConnectionStateChange(callback: (state: number) => void): void {
|
|
||||||
connectionStateChangeCallback = callback
|
connectionStateChangeCallback = callback
|
||||||
for (const missedStateChange of missedStateChanges) {
|
|
||||||
connectionStateChangeCallback(missedStateChange)
|
|
||||||
}
|
|
||||||
missedStateChanges = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectionState = CONN_STATE_NOT_CONNECTED
|
let connectionState = CONN_STATE_NOT_CONNECTED
|
||||||
const setConnectionState = (state: number): void => {
|
const setConnectionState = (state: number) => {
|
||||||
if (connectionState !== state) {
|
if (connectionState !== state) {
|
||||||
connectionState = state
|
connectionState = state
|
||||||
connectionStateChangeCallback(state)
|
connectionStateChangeCallback(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function send(message: ClientEvent): void {
|
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))
|
||||||
|
|
@ -61,25 +43,26 @@ function send(message: ClientEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let clientSeq: number
|
let clientSeq: number
|
||||||
let events: Record<number, GameEvent>
|
let events: Record<number, any>
|
||||||
|
|
||||||
function connect(
|
function connect(
|
||||||
address: string,
|
address: string,
|
||||||
gameId: string,
|
gameId: string,
|
||||||
clientId: string
|
clientId: string
|
||||||
): Promise<EncodedGame> {
|
): Promise<any> {
|
||||||
clientSeq = 0
|
clientSeq = 0
|
||||||
events = {}
|
events = {}
|
||||||
setConnectionState(CONN_STATE_CONNECTING)
|
setConnectionState(CONN_STATE_CONNECTING)
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
ws = new WebSocket(address, clientId + '|' + gameId)
|
ws = new WebSocket(address, clientId + '|' + gameId)
|
||||||
ws.onopen = () => {
|
ws.onopen = (e) => {
|
||||||
setConnectionState(CONN_STATE_CONNECTED)
|
setConnectionState(CONN_STATE_CONNECTED)
|
||||||
send([Protocol.EV_CLIENT_INIT])
|
send([Protocol.EV_CLIENT_INIT])
|
||||||
}
|
}
|
||||||
ws.onmessage = (e: MessageEvent) => {
|
ws.onmessage = (e) => {
|
||||||
const msg: ServerEvent = JSON.parse(e.data)
|
const msg = JSON.parse(e.data)
|
||||||
const msgType = msg[0]
|
const msgType = msg[0]
|
||||||
if (msgType === Protocol.EV_SERVER_INIT) {
|
if (msgType === Protocol.EV_SERVER_INIT) {
|
||||||
const game = msg[1]
|
const game = msg[1]
|
||||||
|
|
@ -98,12 +81,12 @@ function connect(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = (e) => {
|
||||||
setConnectionState(CONN_STATE_DISCONNECTED)
|
setConnectionState(CONN_STATE_DISCONNECTED)
|
||||||
throw `[ 2021-05-15 onerror ]`
|
throw `[ 2021-05-15 onerror ]`
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (e: CloseEvent) => {
|
ws.onclose = (e) => {
|
||||||
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
|
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
|
||||||
setConnectionState(CONN_STATE_CLOSED)
|
setConnectionState(CONN_STATE_CLOSED)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -113,14 +96,47 @@ function connect(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestReplayData(
|
// TOOD: change replay stuff
|
||||||
|
function connectReplay(
|
||||||
|
address: string,
|
||||||
gameId: string,
|
gameId: string,
|
||||||
offset: number
|
clientId: string
|
||||||
): Promise<ReplayData> {
|
): Promise<{ game: any, log: Array<any> }> {
|
||||||
const args = { gameId, offset }
|
clientSeq = 0
|
||||||
const res = await xhr.get(`/api/replay-data${Util.asQueryArgs(args)}`, {})
|
events = {}
|
||||||
const json: ReplayData = await res.json()
|
setConnectionState(CONN_STATE_CONNECTING)
|
||||||
return json
|
return new Promise(resolve => {
|
||||||
|
ws = new WebSocket(address, clientId + '|' + gameId)
|
||||||
|
ws.onopen = (e) => {
|
||||||
|
setConnectionState(CONN_STATE_CONNECTED)
|
||||||
|
send([Protocol.EV_CLIENT_INIT_REPLAY])
|
||||||
|
}
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
const msg = JSON.parse(e.data)
|
||||||
|
const msgType = msg[0]
|
||||||
|
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
|
||||||
|
const game = msg[1]
|
||||||
|
const log = msg[2]
|
||||||
|
const replay: { game: any, log: Array<any> } = { game, log }
|
||||||
|
resolve(replay)
|
||||||
|
} else {
|
||||||
|
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (e) => {
|
||||||
|
setConnectionState(CONN_STATE_DISCONNECTED)
|
||||||
|
throw `[ 2021-05-15 onerror ]`
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = (e) => {
|
||||||
|
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
|
||||||
|
setConnectionState(CONN_STATE_CLOSED)
|
||||||
|
} else {
|
||||||
|
setConnectionState(CONN_STATE_DISCONNECTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect(): void {
|
function disconnect(): void {
|
||||||
|
|
@ -131,7 +147,7 @@ function disconnect(): void {
|
||||||
events = {}
|
events = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendClientEvent(evt: GameEvent): void {
|
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++;
|
||||||
|
|
@ -141,7 +157,7 @@ function sendClientEvent(evt: GameEvent): void {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
connect,
|
connect,
|
||||||
requestReplayData,
|
connectReplay,
|
||||||
disconnect,
|
disconnect,
|
||||||
sendClientEvent,
|
sendClientEvent,
|
||||||
onServerChange,
|
onServerChange,
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ const log = logger('Debug.js')
|
||||||
let _pt = 0
|
let _pt = 0
|
||||||
let _mindiff = 0
|
let _mindiff = 0
|
||||||
|
|
||||||
const checkpoint_start = (mindiff: number): void => {
|
const checkpoint_start = (mindiff: number) => {
|
||||||
_pt = performance.now()
|
_pt = performance.now()
|
||||||
_mindiff = mindiff
|
_mindiff = mindiff
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkpoint = (label: string): void => {
|
const checkpoint = (label: string) => {
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
const diff = now - _pt
|
const diff = now - _pt
|
||||||
if (diff > _mindiff) {
|
if (diff > _mindiff) {
|
||||||
|
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import Protocol from "../common/Protocol"
|
|
||||||
import { GameEvent } from "../common/Types"
|
|
||||||
import { MODE_REPLAY } from "./game"
|
|
||||||
|
|
||||||
function EventAdapter (
|
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
window: any,
|
|
||||||
viewport: any,
|
|
||||||
MODE: string
|
|
||||||
) {
|
|
||||||
let events: Array<GameEvent> = []
|
|
||||||
|
|
||||||
let KEYS_ON = true
|
|
||||||
|
|
||||||
let LEFT = false
|
|
||||||
let RIGHT = false
|
|
||||||
let UP = false
|
|
||||||
let DOWN = false
|
|
||||||
let ZOOM_IN = false
|
|
||||||
let ZOOM_OUT = false
|
|
||||||
let SHIFT = false
|
|
||||||
|
|
||||||
const toWorldPoint = (x: number, y: number): [number, number] => {
|
|
||||||
const pos = viewport.viewportToWorld({x, y})
|
|
||||||
return [pos.x, pos.y]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
|
|
||||||
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
|
||||||
|
|
||||||
const key = (state: boolean, ev: KeyboardEvent) => {
|
|
||||||
if (!KEYS_ON) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.code === 'ShiftLeft' || ev.code === 'ShiftRight') {
|
|
||||||
SHIFT = state
|
|
||||||
} else if (ev.code === 'ArrowUp' || ev.code === 'KeyW') {
|
|
||||||
UP = state
|
|
||||||
} else if (ev.code === 'ArrowDown' || ev.code === 'KeyS') {
|
|
||||||
DOWN = state
|
|
||||||
} else if (ev.code === 'ArrowLeft' || ev.code === 'KeyA') {
|
|
||||||
LEFT = state
|
|
||||||
} else if (ev.code === 'ArrowRight' || ev.code === 'KeyD') {
|
|
||||||
RIGHT = state
|
|
||||||
} else if (ev.code === 'KeyQ') {
|
|
||||||
ZOOM_OUT = state
|
|
||||||
} else if (ev.code === 'KeyE') {
|
|
||||||
ZOOM_IN = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastMouse: [number, number]|null = null
|
|
||||||
canvas.addEventListener('mousedown', (ev) => {
|
|
||||||
lastMouse = mousePos(ev)
|
|
||||||
if (ev.button === 0) {
|
|
||||||
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...lastMouse])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', (ev) => {
|
|
||||||
lastMouse = mousePos(ev)
|
|
||||||
if (ev.button === 0) {
|
|
||||||
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...lastMouse])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (ev) => {
|
|
||||||
lastMouse = mousePos(ev)
|
|
||||||
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...lastMouse])
|
|
||||||
})
|
|
||||||
|
|
||||||
canvas.addEventListener('wheel', (ev) => {
|
|
||||||
lastMouse = mousePos(ev)
|
|
||||||
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
|
|
||||||
const evt = ev.deltaY < 0
|
|
||||||
? Protocol.INPUT_EV_ZOOM_IN
|
|
||||||
: Protocol.INPUT_EV_ZOOM_OUT
|
|
||||||
addEvent([evt, ...lastMouse])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
|
||||||
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
|
||||||
|
|
||||||
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
|
||||||
if (!KEYS_ON) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ev.code === 'Space') {
|
|
||||||
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MODE === MODE_REPLAY) {
|
|
||||||
if (ev.code === 'KeyI') {
|
|
||||||
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_UP])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.code === 'KeyO') {
|
|
||||||
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_DOWN])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.code === 'KeyP') {
|
|
||||||
addEvent([Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyF') {
|
|
||||||
addEvent([Protocol.INPUT_EV_TOGGLE_FIXED_PIECES])
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyG') {
|
|
||||||
addEvent([Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES])
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyM') {
|
|
||||||
addEvent([Protocol.INPUT_EV_TOGGLE_SOUNDS])
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyN') {
|
|
||||||
addEvent([Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES])
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyC') {
|
|
||||||
addEvent([Protocol.INPUT_EV_CENTER_FIT_PUZZLE])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const addEvent = (event: GameEvent) => {
|
|
||||||
events.push(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const consumeAll = (): GameEvent[] => {
|
|
||||||
if (events.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const all = events.slice()
|
|
||||||
events = []
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
const createKeyEvents = (): void => {
|
|
||||||
const w = (LEFT ? 1 : 0) - (RIGHT ? 1 : 0)
|
|
||||||
const h = (UP ? 1 : 0) - (DOWN ? 1 : 0)
|
|
||||||
if (w !== 0 || h !== 0) {
|
|
||||||
const amount = (SHIFT ? 24 : 12) * Math.sqrt(viewport.getCurrentZoom())
|
|
||||||
const pos = viewport.viewportDimToWorld({w: w * amount, h: h * amount})
|
|
||||||
addEvent([Protocol.INPUT_EV_MOVE, pos.w, pos.h])
|
|
||||||
if (lastMouse) {
|
|
||||||
lastMouse[0] -= pos.w
|
|
||||||
lastMouse[1] -= pos.h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ZOOM_IN && ZOOM_OUT) {
|
|
||||||
// cancel each other out
|
|
||||||
} else if (ZOOM_IN) {
|
|
||||||
if (viewport.canZoom('in')) {
|
|
||||||
const target = lastMouse || canvasCenter()
|
|
||||||
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...target])
|
|
||||||
}
|
|
||||||
} else if (ZOOM_OUT) {
|
|
||||||
if (viewport.canZoom('out')) {
|
|
||||||
const target = lastMouse || canvasCenter()
|
|
||||||
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...target])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setHotkeys = (state: boolean) => {
|
|
||||||
KEYS_ON = state
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
addEvent,
|
|
||||||
consumeAll,
|
|
||||||
createKeyEvents,
|
|
||||||
setHotkeys,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventAdapter
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { Rng } from '../common/Rng'
|
import { Rng } from '../common/Rng'
|
||||||
|
import Util from '../common/Util'
|
||||||
|
|
||||||
let minVx = -10
|
let minVx = -10
|
||||||
let deltaVx = 20
|
let deltaVx = 20
|
||||||
|
|
@ -107,11 +108,11 @@ class Bomb {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Particle {
|
class Particle {
|
||||||
px: number
|
px: any
|
||||||
py: number
|
py: any
|
||||||
vx: number
|
vx: number
|
||||||
vy: number
|
vy: number
|
||||||
color: string
|
color: any
|
||||||
duration: number
|
duration: number
|
||||||
alive: boolean
|
alive: boolean
|
||||||
radius: number
|
radius: number
|
||||||
|
|
@ -170,7 +171,7 @@ class Controller {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpeedParams(): void {
|
setSpeedParams() {
|
||||||
let heightReached = 0
|
let heightReached = 0
|
||||||
let vy = 0
|
let vy = 0
|
||||||
|
|
||||||
|
|
@ -187,11 +188,11 @@ class Controller {
|
||||||
deltaVx = 2 * vx
|
deltaVx = 2 * vx
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(): void {
|
resize() {
|
||||||
this.setSpeedParams()
|
this.setSpeedParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(): void {
|
init() {
|
||||||
this.readyBombs = []
|
this.readyBombs = []
|
||||||
this.explodedBombs = []
|
this.explodedBombs = []
|
||||||
this.particles = []
|
this.particles = []
|
||||||
|
|
@ -201,7 +202,7 @@ class Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(): void {
|
update() {
|
||||||
if (Math.random() * 100 < percentChanceNewBomb) {
|
if (Math.random() * 100 < percentChanceNewBomb) {
|
||||||
this.readyBombs.push(new Bomb(this.rng))
|
this.readyBombs.push(new Bomb(this.rng))
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +250,7 @@ class Controller {
|
||||||
this.particles = aliveParticles
|
this.particles = aliveParticles
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): void {
|
render() {
|
||||||
this.ctx.beginPath()
|
this.ctx.beginPath()
|
||||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' // Ghostly effect
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' // Ghostly effect
|
||||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,22 @@
|
||||||
import Geometry, { Rect } from '../common/Geometry'
|
import Geometry, { Rect } from '../common/Geometry'
|
||||||
import Graphics from './Graphics'
|
import Graphics from './Graphics'
|
||||||
import Util, { logger } from './../common/Util'
|
import Util, { logger } from './../common/Util'
|
||||||
import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/Types'
|
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
|
||||||
|
|
||||||
const log = logger('PuzzleGraphics.js')
|
const log = logger('PuzzleGraphics.js')
|
||||||
|
|
||||||
async function createPuzzleTileBitmaps(
|
async function createPuzzleTileBitmaps(
|
||||||
img: ImageBitmap,
|
img: ImageBitmap,
|
||||||
pieces: EncodedPiece[],
|
tiles: Array<any>,
|
||||||
info: PuzzleInfo
|
info: PuzzleInfo
|
||||||
): Promise<Array<ImageBitmap>> {
|
): Promise<Array<ImageBitmap>> {
|
||||||
log.log('start createPuzzleTileBitmaps')
|
log.log('start createPuzzleTileBitmaps')
|
||||||
const tileSize = info.tileSize
|
var tileSize = info.tileSize
|
||||||
const tileMarginWidth = info.tileMarginWidth
|
var tileMarginWidth = info.tileMarginWidth
|
||||||
const tileDrawSize = info.tileDrawSize
|
var tileDrawSize = info.tileDrawSize
|
||||||
const tileRatio = tileSize / 100.0
|
var tileRatio = tileSize / 100.0
|
||||||
|
|
||||||
const curvyCoords = [
|
var curvyCoords = [
|
||||||
0, 0, 40, 15, 37, 5,
|
0, 0, 40, 15, 37, 5,
|
||||||
37, 5, 40, 0, 38, -5,
|
37, 5, 40, 0, 38, -5,
|
||||||
38, -5, 20, -20, 50, -20,
|
38, -5, 20, -20, 50, -20,
|
||||||
|
|
@ -27,7 +27,7 @@ async function createPuzzleTileBitmaps(
|
||||||
63, 5, 65, 15, 100, 0
|
63, 5, 65, 15, 100, 0
|
||||||
];
|
];
|
||||||
|
|
||||||
const bitmaps: Array<ImageBitmap> = new Array(pieces.length)
|
const bitmaps: Array<ImageBitmap> = new Array(tiles.length)
|
||||||
|
|
||||||
const paths: Record<string, Path2D> = {}
|
const paths: Record<string, Path2D> = {}
|
||||||
function pathForShape(shape: PieceShape) {
|
function pathForShape(shape: PieceShape) {
|
||||||
|
|
@ -65,9 +65,9 @@ async function createPuzzleTileBitmaps(
|
||||||
}
|
}
|
||||||
if (shape.bottom !== 0) {
|
if (shape.bottom !== 0) {
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
const p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
|
let p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
|
||||||
const p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
|
let p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
|
||||||
const p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
|
let p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
|
||||||
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -75,9 +75,9 @@ async function createPuzzleTileBitmaps(
|
||||||
}
|
}
|
||||||
if (shape.left !== 0) {
|
if (shape.left !== 0) {
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
const p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
let p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
||||||
const p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
|
let p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
|
||||||
const p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
|
let p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
|
||||||
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -93,10 +93,10 @@ async function createPuzzleTileBitmaps(
|
||||||
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
||||||
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
|
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
|
||||||
|
|
||||||
for (const p of pieces) {
|
for (const t of tiles) {
|
||||||
const piece = Util.decodePiece(p)
|
const tile = Util.decodeTile(t)
|
||||||
const srcRect = srcRectByIdx(info, piece.idx)
|
const srcRect = srcRectByIdx(info, tile.idx)
|
||||||
const path = pathForShape(Util.decodeShape(info.shapes[piece.idx]))
|
const path = pathForShape(Util.decodeShape(info.shapes[tile.idx]))
|
||||||
|
|
||||||
ctx.clearRect(0, 0, tileDrawSize, tileDrawSize)
|
ctx.clearRect(0, 0, tileDrawSize, tileDrawSize)
|
||||||
|
|
||||||
|
|
@ -195,7 +195,7 @@ async function createPuzzleTileBitmaps(
|
||||||
ctx2.restore()
|
ctx2.restore()
|
||||||
ctx.drawImage(c2, 0, 0)
|
ctx.drawImage(c2, 0, 0)
|
||||||
|
|
||||||
bitmaps[piece.idx] = await createImageBitmap(c)
|
bitmaps[tile.idx] = await createImageBitmap(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.log('end createPuzzleTileBitmaps')
|
log.log('end createPuzzleTileBitmaps')
|
||||||
|
|
@ -203,7 +203,7 @@ async function createPuzzleTileBitmaps(
|
||||||
}
|
}
|
||||||
|
|
||||||
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number): Rect {
|
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number): Rect {
|
||||||
const c = Util.coordByPieceIdx(puzzleInfo, idx)
|
const c = Util.coordByTileIdx(puzzleInfo, idx)
|
||||||
return {
|
return {
|
||||||
x: c.x * puzzleInfo.tileSize,
|
x: c.x * puzzleInfo.tileSize,
|
||||||
y: c.y * puzzleInfo.tileSize,
|
y: c.y * puzzleInfo.tileSize,
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -23,7 +23,7 @@
|
||||||
<!-- TODO: autocomplete tags -->
|
<!-- TODO: autocomplete tags -->
|
||||||
<td><label>Tags</label></td>
|
<td><label>Tags</label></td>
|
||||||
<td>
|
<td>
|
||||||
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
|
<tags-input v-model="tags" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue'
|
import { defineComponent, PropType } from 'vue'
|
||||||
import { Image, Tag } from '../../common/Types'
|
import { Image, Tag } from '../../common/GameCommon'
|
||||||
|
|
||||||
import ResponsiveImage from './ResponsiveImage.vue'
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
import TagsInput from './TagsInput.vue'
|
import TagsInput from './TagsInput.vue'
|
||||||
|
|
@ -54,9 +54,6 @@ export default defineComponent({
|
||||||
type: Object as PropType<Image>,
|
type: Object as PropType<Image>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
autocompleteTags: {
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
bgclick: null,
|
bgclick: null,
|
||||||
|
|
@ -96,17 +93,7 @@ export default defineComponent({
|
||||||
height: 90%;
|
height: 90%;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
@media (max-width: 1400px) and (min-height: 720px),
|
|
||||||
(max-width: 1000px) {
|
|
||||||
.edit-image-dialog .overlay-content {
|
|
||||||
grid-template-columns: auto;
|
|
||||||
grid-template-rows: 1fr min-content min-content;
|
|
||||||
grid-template-areas:
|
|
||||||
"image"
|
|
||||||
"settings"
|
|
||||||
"buttons";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edit-image-dialog .area-image {
|
.edit-image-dialog .area-image {
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{{time(game.started, game.finished)}}<br />
|
{{time(game.started, game.finished)}}<br />
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="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>
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,8 @@
|
||||||
<tr><td>🔍+ Zoom in:</td><td><div><kbd>E</kbd>/🖱️-Wheel</div></td></tr>
|
<tr><td>🔍+ Zoom in:</td><td><div><kbd>E</kbd>/🖱️-Wheel</div></td></tr>
|
||||||
<tr><td>🔍- Zoom out:</td><td><div><kbd>Q</kbd>/🖱️-Wheel</div></td></tr>
|
<tr><td>🔍- Zoom out:</td><td><div><kbd>Q</kbd>/🖱️-Wheel</div></td></tr>
|
||||||
<tr><td>🖼️ Toggle preview:</td><td><div><kbd>Space</kbd></div></td></tr>
|
<tr><td>🖼️ Toggle preview:</td><td><div><kbd>Space</kbd></div></td></tr>
|
||||||
<tr><td>🎯 Center puzzle in screen:</td><td><div><kbd>C</kbd></div></td></tr>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<tr><td>👤 Toggle player names:</td><td><div><kbd>N</kbd></div></td></tr>
|
|
||||||
<tr><td>🔉 Toggle sounds:</td><td><div><kbd>M</kbd></div></td></tr>
|
|
||||||
|
|
||||||
<tr><td>⏫ Speed up (replay):</td><td><div><kbd>I</kbd></div></td></tr>
|
|
||||||
<tr><td>⏬ Speed down (replay):</td><td><div><kbd>O</kbd></div></td></tr>
|
|
||||||
<tr><td>⏸️ Pause (replay):</td><td><div><kbd>P</kbd></div></td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { Image } from '../../common/Types'
|
import { Image } from '../../common/GameCommon'
|
||||||
|
|
||||||
import ImageTeaser from './ImageTeaser.vue'
|
import ImageTeaser from './ImageTeaser.vue'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class="imageteaser"
|
class="imageteaser"
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="onClick">
|
@click="onClick">
|
||||||
<div class="btn edit" v-if="canEdit" @click.stop="onEditClick">✏️</div>
|
<div class="btn edit" @click.stop="onEditClick">✏️</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -18,18 +18,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style(): object {
|
style (): object {
|
||||||
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
||||||
return {
|
return {
|
||||||
'backgroundImage': `url("${url}")`,
|
'backgroundImage': `url("${url}")`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
canEdit(): boolean {
|
|
||||||
if (!this.$me.id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return this.$me.id === this.image.uploaderUserId
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
click: null,
|
click: null,
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
|
||||||
<table class="overlay-content help" @click.stop="">
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">Info about this puzzle</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Image Title: </td>
|
|
||||||
<td>{{game.puzzle.info.image.title}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Scoring: </td>
|
|
||||||
<td><span :title="snapMode[1]">{{scoreMode[0]}}</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Shapes: </td>
|
|
||||||
<td><span :title="snapMode[1]">{{shapeMode[0]}}</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Snapping: </td>
|
|
||||||
<td><span :title="snapMode[1]">{{snapMode[0]}}</span></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType } from 'vue'
|
|
||||||
import { Game, ScoreMode, ShapeMode, SnapMode } from '../../common/Types'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'help-overlay',
|
|
||||||
emits: {
|
|
||||||
bgclick: null,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
game: {
|
|
||||||
type: Object as PropType<Game>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
scoreMode () {
|
|
||||||
switch (this.game.scoreMode) {
|
|
||||||
case ScoreMode.ANY: return ['Any', 'Score when pieces are connected to each other or on final location']
|
|
||||||
case ScoreMode.FINAL:
|
|
||||||
default: return ['Final', 'Score when pieces are put to their final location']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shapeMode () {
|
|
||||||
switch (this.game.shapeMode) {
|
|
||||||
case ShapeMode.FLAT: return ['Flat', 'All pieces flat on all sides']
|
|
||||||
case ShapeMode.ANY: return ['Any', 'Flat pieces can occur anywhere']
|
|
||||||
case ShapeMode.NORMAL:
|
|
||||||
default:
|
|
||||||
return ['Normal', '']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
snapMode () {
|
|
||||||
switch (this.game.snapMode) {
|
|
||||||
case SnapMode.REAL: return ['Real', 'Pieces snap only to corners, already snapped pieces and to each other']
|
|
||||||
case SnapMode.NORMAL:
|
|
||||||
default:
|
|
||||||
return ['Normal', 'Pieces snap to final destination and to each other']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -6,10 +6,6 @@
|
||||||
<div class="has-image">
|
<div class="has-image">
|
||||||
<responsive-image :src="image.url" :title="image.title" />
|
<responsive-image :src="image.url" :title="image.title" />
|
||||||
</div>
|
</div>
|
||||||
<div class="image-title" v-if="image.title || image.width || image.height">
|
|
||||||
<span class="image-title-title" v-if="image.title">"{{image.title}}"</span>
|
|
||||||
<span class="image-title-dim" v-if="image.width || image.height">({{image.width}} ✕ {{image.height}})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="area-settings">
|
<div class="area-settings">
|
||||||
|
|
@ -21,34 +17,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><label>Scoring: </label></td>
|
<td><label>Scoring: </label></td>
|
||||||
<td>
|
<td>
|
||||||
<label><input type="radio" v-model="scoreMode" value="1" />
|
<label><input type="radio" v-model="scoreMode" value="1" /> Any (Score when pieces are connected to each other or on final location)</label>
|
||||||
Any (Score when pieces are connected to each other or on final location)</label>
|
|
||||||
<br />
|
<br />
|
||||||
<label><input type="radio" v-model="scoreMode" value="0" />
|
<label><input type="radio" v-model="scoreMode" value="0" /> Final (Score when pieces are put to their final location)</label>
|
||||||
Final (Score when pieces are put to their final location)</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Shapes: </label></td>
|
|
||||||
<td>
|
|
||||||
<label><input type="radio" v-model="shapeMode" value="0" />
|
|
||||||
Normal</label>
|
|
||||||
<br />
|
|
||||||
<label><input type="radio" v-model="shapeMode" value="1" />
|
|
||||||
Any (Flat pieces can occur anywhere)</label>
|
|
||||||
<br />
|
|
||||||
<label><input type="radio" v-model="shapeMode" value="2" />
|
|
||||||
Flat (All pieces flat on all sides)</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Snapping: </label></td>
|
|
||||||
<td>
|
|
||||||
<label><input type="radio" v-model="snapMode" value="0" />
|
|
||||||
Normal (Pieces snap to final destination and to each other)</label>
|
|
||||||
<br />
|
|
||||||
<label><input type="radio" v-model="snapMode" value="1" />
|
|
||||||
Real (Pieces snap only to corners, already snapped pieces and to each other)</label>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -65,7 +36,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
import { GameSettings, ScoreMode, ShapeMode, SnapMode } from './../../common/Types'
|
import { GameSettings, ScoreMode } from './../../common/GameCommon'
|
||||||
import ResponsiveImage from './ResponsiveImage.vue'
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -87,8 +58,6 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
tiles: 1000,
|
tiles: 1000,
|
||||||
scoreMode: ScoreMode.ANY,
|
scoreMode: ScoreMode.ANY,
|
||||||
shapeMode: ShapeMode.NORMAL,
|
|
||||||
snapMode: SnapMode.NORMAL,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -97,8 +66,6 @@ export default defineComponent({
|
||||||
tiles: this.tilesInt,
|
tiles: this.tilesInt,
|
||||||
image: this.image,
|
image: this.image,
|
||||||
scoreMode: this.scoreModeInt,
|
scoreMode: this.scoreModeInt,
|
||||||
shapeMode: this.shapeModeInt,
|
|
||||||
snapMode: this.snapModeInt,
|
|
||||||
} as GameSettings)
|
} as GameSettings)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -117,12 +84,6 @@ export default defineComponent({
|
||||||
scoreModeInt (): number {
|
scoreModeInt (): number {
|
||||||
return parseInt(`${this.scoreMode}`, 10)
|
return parseInt(`${this.scoreMode}`, 10)
|
||||||
},
|
},
|
||||||
shapeModeInt (): number {
|
|
||||||
return parseInt(`${this.shapeMode}`, 10)
|
|
||||||
},
|
|
||||||
snapModeInt (): number {
|
|
||||||
return parseInt(`${this.snapMode}`, 10)
|
|
||||||
},
|
|
||||||
tilesInt (): number {
|
tilesInt (): number {
|
||||||
return parseInt(`${this.tiles}`, 10)
|
return parseInt(`${this.tiles}`, 10)
|
||||||
},
|
},
|
||||||
|
|
@ -145,26 +106,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
.new-game-dialog .area-image {
|
.new-game-dialog .area-image {
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
display: grid;
|
margin: 20px;
|
||||||
grid-template-rows: 1fr min-content;
|
|
||||||
grid-template-areas:
|
|
||||||
"image"
|
|
||||||
"image-title";
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
@media (max-width: 1400px) and (min-height: 720px),
|
|
||||||
(max-width: 1000px) {
|
|
||||||
.new-game-dialog .overlay-content {
|
|
||||||
grid-template-columns: auto;
|
|
||||||
grid-template-rows: 1fr min-content min-content;
|
|
||||||
grid-template-areas:
|
|
||||||
"image"
|
|
||||||
"settings"
|
|
||||||
"buttons";
|
|
||||||
}
|
|
||||||
.new-game-dialog .area-image {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.new-game-dialog .area-settings {
|
.new-game-dialog .area-settings {
|
||||||
grid-area: settings;
|
grid-area: settings;
|
||||||
|
|
@ -182,29 +124,13 @@ export default defineComponent({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.new-game-dialog .has-image {
|
.new-game-dialog .has-image {
|
||||||
box-sizing: border-box;
|
|
||||||
grid-area: image;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: solid 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-game-dialog .image-title {
|
|
||||||
grid-area: image-title;
|
|
||||||
text-align: center;
|
|
||||||
padding: .5em 0;
|
|
||||||
background: var(--main-color);
|
|
||||||
color: #262523;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-game-dialog .has-image .remove {
|
.new-game-dialog .has-image .remove {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: .5em;
|
top: .5em;
|
||||||
left: .5em;
|
left: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-game-dialog .image-title > span { margin-right: .5em; }
|
|
||||||
.new-game-dialog .image-title > span:last-child { margin-right: 0; }
|
|
||||||
.image-title-dim { display: inline-block; white-space: no-wrap; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,15 @@ gallery", if possible!
|
||||||
<div class="overlay new-image-dialog" @click="$emit('bgclick')">
|
<div class="overlay new-image-dialog" @click="$emit('bgclick')">
|
||||||
<div class="overlay-content" @click.stop="">
|
<div class="overlay-content" @click.stop="">
|
||||||
|
|
||||||
<div
|
<div class="area-image" :class="{'has-image': !!previewUrl, 'no-image': !previewUrl}">
|
||||||
class="area-image"
|
|
||||||
:class="{'has-image': !!previewUrl, 'no-image': !previewUrl, droppable: droppable}"
|
|
||||||
@drop="onDrop"
|
|
||||||
@dragover="onDragover"
|
|
||||||
@dragleave="onDragleave">
|
|
||||||
<!-- TODO: ... -->
|
<!-- TODO: ... -->
|
||||||
<div class="drop-target"></div>
|
|
||||||
<div v-if="previewUrl" class="has-image">
|
<div v-if="previewUrl" class="has-image">
|
||||||
<span class="remove btn" @click="previewUrl=''">X</span>
|
<span class="remove btn" @click="previewUrl=''">X</span>
|
||||||
<responsive-image :src="previewUrl" />
|
<responsive-image :src="previewUrl" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<label class="upload">
|
<label class="upload">
|
||||||
<input type="file" style="display: none" @change="onFileSelect" accept="image/*" />
|
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
||||||
<span class="btn">Upload File</span>
|
<span class="btn">Upload File</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,57 +36,32 @@ gallery", if possible!
|
||||||
<!-- TODO: autocomplete tags -->
|
<!-- TODO: autocomplete tags -->
|
||||||
<td><label>Tags</label></td>
|
<td><label>Tags</label></td>
|
||||||
<td>
|
<td>
|
||||||
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
|
<tags-input v-model="tags" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="area-buttons">
|
<div class="area-buttons">
|
||||||
<button class="btn"
|
<button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button>
|
||||||
:disabled="!canPostToGallery"
|
<button class="btn" :disabled="!canSetupGameClick" @click="setupGameClick">🧩 Post to gallery <br /> + set up game</button>
|
||||||
@click="postToGallery"
|
|
||||||
>
|
|
||||||
<template v-if="uploading === 'postToGallery'">Uploading ({{uploadProgressPercent}}%)</template>
|
|
||||||
<template v-else>🖼️ Post to gallery</template>
|
|
||||||
</button>
|
|
||||||
<button class="btn"
|
|
||||||
:disabled="!canSetupGameClick"
|
|
||||||
@click="setupGameClick"
|
|
||||||
>
|
|
||||||
<template v-if="uploading === 'setupGame'">Uploading ({{uploadProgressPercent}}%)</template>
|
|
||||||
<template v-else>🧩 Post to gallery <br /> + set up game</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { logger } from '../../common/Util'
|
|
||||||
|
|
||||||
import ResponsiveImage from './ResponsiveImage.vue'
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
import TagsInput from './TagsInput.vue'
|
import TagsInput from './TagsInput.vue'
|
||||||
|
|
||||||
const log = logger('NewImageDialog.vue')
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'new-image-dialog',
|
name: 'new-image-dialog',
|
||||||
components: {
|
components: {
|
||||||
ResponsiveImage,
|
ResponsiveImage,
|
||||||
TagsInput,
|
TagsInput,
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
autocompleteTags: {
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
uploadProgress: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
uploading: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
emits: {
|
||||||
bgclick: null,
|
bgclick: null,
|
||||||
setupGameClick: null,
|
setupGameClick: null,
|
||||||
|
|
@ -104,47 +73,23 @@ export default defineComponent({
|
||||||
file: null as File|null,
|
file: null as File|null,
|
||||||
title: '',
|
title: '',
|
||||||
tags: [] as string[],
|
tags: [] as string[],
|
||||||
droppable: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
uploadProgressPercent (): number {
|
|
||||||
return this.uploadProgress ? Math.round(this.uploadProgress * 100) : 0
|
|
||||||
},
|
|
||||||
canPostToGallery (): boolean {
|
canPostToGallery (): boolean {
|
||||||
if (this.uploading) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !!(this.previewUrl && this.file)
|
return !!(this.previewUrl && this.file)
|
||||||
},
|
},
|
||||||
canSetupGameClick (): boolean {
|
canSetupGameClick (): boolean {
|
||||||
if (this.uploading) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !!(this.previewUrl && this.file)
|
return !!(this.previewUrl && this.file)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
imageFromDragEvt (evt: DragEvent): DataTransferItem|null {
|
preview (evt: Event) {
|
||||||
const items = evt.dataTransfer?.items
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const item = items[0]
|
|
||||||
if (!item.type.startsWith('image/')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
},
|
|
||||||
onFileSelect (evt: Event) {
|
|
||||||
const target = (evt.target as HTMLInputElement)
|
const target = (evt.target as HTMLInputElement)
|
||||||
if (!target.files) return;
|
if (!target.files) return;
|
||||||
const file = target.files[0]
|
const file = target.files[0]
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
this.preview(file)
|
|
||||||
},
|
|
||||||
preview (file: File) {
|
|
||||||
const r = new FileReader()
|
const r = new FileReader()
|
||||||
r.readAsDataURL(file)
|
r.readAsDataURL(file)
|
||||||
r.onload = (ev: any) => {
|
r.onload = (ev: any) => {
|
||||||
|
|
@ -166,34 +111,6 @@ export default defineComponent({
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onDrop (evt: DragEvent): boolean {
|
|
||||||
this.droppable = false
|
|
||||||
const img = this.imageFromDragEvt(evt)
|
|
||||||
if (!img) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const f = img.getAsFile()
|
|
||||||
if (!f) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
this.file = f
|
|
||||||
this.preview(f)
|
|
||||||
evt.preventDefault()
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
onDragover (evt: DragEvent): boolean {
|
|
||||||
const img = this.imageFromDragEvt(evt)
|
|
||||||
if (!img) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
this.droppable = true
|
|
||||||
evt.preventDefault()
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
onDragleave () {
|
|
||||||
log.info('onDragleave')
|
|
||||||
this.droppable = false
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -210,35 +127,17 @@ export default defineComponent({
|
||||||
height: 90%;
|
height: 90%;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
@media (max-width: 1400px) and (min-height: 720px),
|
|
||||||
(max-width: 1000px) {
|
|
||||||
.new-image-dialog .overlay-content {
|
|
||||||
grid-template-columns: auto;
|
|
||||||
grid-template-rows: 1fr min-content min-content;
|
|
||||||
grid-template-areas:
|
|
||||||
"image"
|
|
||||||
"settings"
|
|
||||||
"buttons";
|
|
||||||
}
|
|
||||||
.new-image-dialog .overlay-content .area-buttons .btn br {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-image-dialog .area-image {
|
.new-image-dialog .area-image {
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
margin: .5em;
|
margin: 20px;
|
||||||
border: solid 6px transparent;
|
|
||||||
}
|
}
|
||||||
.new-image-dialog .area-image.no-image {
|
.new-image-dialog .area-image.no-image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: solid 6px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.new-image-dialog .area-image.droppable {
|
|
||||||
border: dashed 6px;
|
border: dashed 6px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.new-image-dialog .area-image .has-image {
|
.new-image-dialog .area-image .has-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -281,16 +180,4 @@ export default defineComponent({
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%,-50%);
|
transform: translate(-50%,-50%);
|
||||||
}
|
}
|
||||||
.area-image .drop-target {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.area-image.droppable .drop-target {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,6 @@
|
||||||
<td><label>Name: </label></td>
|
<td><label>Name: </label></td>
|
||||||
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
|
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><label>Sounds: </label></td>
|
|
||||||
<td><input type="checkbox" v-model="modelValue.soundsEnabled" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Sounds Volume: </label></td>
|
|
||||||
<td class="sound-volume">
|
|
||||||
<span @click="decreaseVolume">🔉</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
:value="modelValue.soundsVolume"
|
|
||||||
@change="updateVolume"
|
|
||||||
/>
|
|
||||||
<span @click="increaseVolume">🔊</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label>Show player names: </label></td>
|
|
||||||
<td><input type="checkbox" v-model="modelValue.showPlayerNames" /></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -48,23 +26,7 @@ export default defineComponent({
|
||||||
'update:modelValue': null,
|
'update:modelValue': null,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: Object,
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateVolume (ev: Event): void {
|
|
||||||
(this.modelValue as any).soundsVolume = (ev.target as HTMLInputElement).value
|
|
||||||
},
|
|
||||||
decreaseVolume (): void {
|
|
||||||
const vol = parseInt(this.modelValue.soundsVolume, 10) - 5
|
|
||||||
this.modelValue.soundsVolume = Math.max(0, vol)
|
|
||||||
},
|
|
||||||
increaseVolume (): void {
|
|
||||||
const vol = parseInt(this.modelValue.soundsVolume, 10) + 5
|
|
||||||
this.modelValue.soundsVolume = Math.min(100, vol)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
// TODO: ts type PlayerSettings
|
// TODO: ts type PlayerSettings
|
||||||
|
|
@ -74,7 +36,3 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
.sound-volume span { cursor: pointer; user-select: none; }
|
|
||||||
.sound-volume input { vertical-align: middle; }
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input class="input" type="text" v-model="input" placeholder="Plants, People" @keydown.enter="add" @keyup="onKeyUp" />
|
||||||
ref="input"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
v-model="input"
|
|
||||||
placeholder="Plants, People"
|
|
||||||
@change="onChange"
|
|
||||||
@keydown.enter="add"
|
|
||||||
@keyup="onKeyUp"
|
|
||||||
/>
|
|
||||||
<div v-if="autocomplete.values" class="autocomplete">
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="(val,idx) in autocomplete.values"
|
|
||||||
:key="idx"
|
|
||||||
:class="{active: idx===autocomplete.idx}"
|
|
||||||
@click="addVal(val)"
|
|
||||||
>{{val}}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} ✖</span>
|
<span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} ✖</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue'
|
import { defineComponent, PropType } from 'vue'
|
||||||
import { Tag } from '../../common/Types'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'tags-input',
|
name: 'tags-input',
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<Array<string>>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
autocompleteTags: {
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
'update:modelValue': null,
|
'update:modelValue': null,
|
||||||
|
|
@ -44,11 +21,7 @@ export default defineComponent({
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
input: '',
|
input: '',
|
||||||
values: [] as string[],
|
values: [] as Array<string>,
|
||||||
autocomplete: {
|
|
||||||
idx: -1,
|
|
||||||
values: [] as string[],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|
@ -56,39 +29,14 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onKeyUp (ev: KeyboardEvent) {
|
onKeyUp (ev: KeyboardEvent) {
|
||||||
if (ev.code === 'ArrowDown' && this.autocomplete.values.length > 0) {
|
|
||||||
if (this.autocomplete.idx < this.autocomplete.values.length - 1) {
|
|
||||||
this.autocomplete.idx++
|
|
||||||
}
|
|
||||||
ev.stopPropagation()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (ev.code === 'ArrowUp' && this.autocomplete.values.length > 0) {
|
|
||||||
if (this.autocomplete.idx > 0) {
|
|
||||||
this.autocomplete.idx--
|
|
||||||
}
|
|
||||||
ev.stopPropagation()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (ev.key === ',') {
|
if (ev.key === ',') {
|
||||||
this.add()
|
this.add()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.input && this.autocompleteTags) {
|
|
||||||
this.autocomplete.values = this.autocompleteTags(
|
|
||||||
this.input,
|
|
||||||
this.values
|
|
||||||
)
|
|
||||||
this.autocomplete.idx = -1
|
|
||||||
} else {
|
|
||||||
this.autocomplete.values = []
|
|
||||||
this.autocomplete.idx = -1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
addVal (value: string) {
|
add () {
|
||||||
const newval = value.replace(/,/g, '').trim()
|
const newval = this.input.replace(/,/g, '').trim()
|
||||||
if (!newval) {
|
if (!newval) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -96,16 +44,7 @@ export default defineComponent({
|
||||||
this.values.push(newval)
|
this.values.push(newval)
|
||||||
}
|
}
|
||||||
this.input = ''
|
this.input = ''
|
||||||
this.autocomplete.values = []
|
|
||||||
this.autocomplete.idx = -1
|
|
||||||
this.$emit('update:modelValue', this.values)
|
this.$emit('update:modelValue', this.values)
|
||||||
;(this.$refs.input as HTMLInputElement).focus()
|
|
||||||
},
|
|
||||||
add () {
|
|
||||||
const value = this.autocomplete.idx >= 0
|
|
||||||
? this.autocomplete.values[this.autocomplete.idx]
|
|
||||||
: this.input
|
|
||||||
this.addVal(value)
|
|
||||||
},
|
},
|
||||||
rm (val: string) {
|
rm (val: string) {
|
||||||
this.values = this.values.filter(v => v !== val)
|
this.values = this.values.filter(v => v !== val)
|
||||||
|
|
@ -118,31 +57,4 @@ export default defineComponent({
|
||||||
.input {
|
.input {
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
.autocomplete {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.autocomplete ul { list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: #333230;
|
|
||||||
top: -.5em;
|
|
||||||
}
|
|
||||||
.autocomplete ul li {
|
|
||||||
position: relative;
|
|
||||||
padding: .5em .5em .5em 1.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.autocomplete ul li.active {
|
|
||||||
color: var(--link-hover-color);
|
|
||||||
background: var(--input-bg-color);
|
|
||||||
}
|
|
||||||
.autocomplete ul li.active:before {
|
|
||||||
content: '▶';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: .5em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import xhr from '../xhr'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'upload',
|
name: 'upload',
|
||||||
|
|
@ -22,7 +21,8 @@ export default defineComponent({
|
||||||
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);
|
||||||
const res = await xhr.post('/upload', {
|
const res = await fetch('/upload', {
|
||||||
|
method: 'post',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
const j = await res.json()
|
const j = await res.json()
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,34 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import { GameLoopInstance, run } from './gameloop'
|
import {run} from './gameloop'
|
||||||
import Camera from './Camera'
|
import Camera from './Camera'
|
||||||
import Graphics from './Graphics'
|
import Graphics from './Graphics'
|
||||||
import Debug from './Debug'
|
import Debug from './Debug'
|
||||||
import Communication from './Communication'
|
import Communication from './Communication'
|
||||||
import Util, { logger } from './../common/Util'
|
import Util from './../common/Util'
|
||||||
import PuzzleGraphics from './PuzzleGraphics'
|
import PuzzleGraphics from './PuzzleGraphics'
|
||||||
import Game from './../common/GameCommon'
|
import Game, { Player, Piece } from './../common/GameCommon'
|
||||||
import fireworksController from './Fireworks'
|
import fireworksController from './Fireworks'
|
||||||
import Protocol from '../common/Protocol'
|
import Protocol from '../common/Protocol'
|
||||||
import Time from '../common/Time'
|
import Time from '../common/Time'
|
||||||
import settings from './settings'
|
|
||||||
import { SETTINGS } from './settings'
|
|
||||||
import { Dim, Point } from '../common/Geometry'
|
import { Dim, Point } from '../common/Geometry'
|
||||||
import {
|
|
||||||
FixedLengthArray,
|
|
||||||
Game as GameType,
|
|
||||||
Player,
|
|
||||||
Piece,
|
|
||||||
EncodedGame,
|
|
||||||
ReplayData,
|
|
||||||
Timestamp,
|
|
||||||
ServerEvent,
|
|
||||||
} from '../common/Types'
|
|
||||||
import EventAdapter from './EventAdapter'
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
DEBUG?: boolean
|
DEBUG?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const log = logger('game.ts')
|
// @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
|
// @ts-ignore
|
||||||
const images = import.meta.globEager('./*.png')
|
const images = import.meta.globEager('./*.png')
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const sounds = import.meta.globEager('./*.mp3')
|
|
||||||
|
|
||||||
export const MODE_PLAY = 'play'
|
export const MODE_PLAY = 'play'
|
||||||
export const MODE_REPLAY = 'replay'
|
export const MODE_REPLAY = 'replay'
|
||||||
|
|
||||||
|
|
@ -46,7 +36,6 @@ let PIECE_VIEW_FIXED = true
|
||||||
let PIECE_VIEW_LOOSE = true
|
let PIECE_VIEW_LOOSE = true
|
||||||
|
|
||||||
interface Hud {
|
interface Hud {
|
||||||
setPuzzleCut: () => void
|
|
||||||
setActivePlayers: (v: Array<any>) => void
|
setActivePlayers: (v: Array<any>) => void
|
||||||
setIdlePlayers: (v: Array<any>) => void
|
setIdlePlayers: (v: Array<any>) => void
|
||||||
setFinished: (v: boolean) => void
|
setFinished: (v: boolean) => void
|
||||||
|
|
@ -55,24 +44,18 @@ interface Hud {
|
||||||
setPiecesTotal: (v: number) => void
|
setPiecesTotal: (v: number) => void
|
||||||
setConnectionState: (v: number) => void
|
setConnectionState: (v: number) => void
|
||||||
togglePreview: () => void
|
togglePreview: () => void
|
||||||
toggleSoundsEnabled: () => void
|
|
||||||
togglePlayerNames: () => void
|
|
||||||
setReplaySpeed?: (v: number) => void
|
setReplaySpeed?: (v: number) => void
|
||||||
setReplayPaused?: (v: boolean) => void
|
setReplayPaused?: (v: boolean) => void
|
||||||
}
|
}
|
||||||
interface Replay {
|
interface Replay {
|
||||||
final: boolean
|
log: Array<any>
|
||||||
log: Array<any> // current log entries
|
logIdx: number
|
||||||
logPointer: number // pointer to current item in the log array
|
|
||||||
speeds: Array<number>
|
speeds: Array<number>
|
||||||
speedIdx: number
|
speedIdx: number
|
||||||
paused: boolean
|
paused: boolean
|
||||||
lastRealTs: number
|
lastRealTs: number
|
||||||
lastGameTs: number
|
lastGameTs: number
|
||||||
gameStartTs: number
|
gameStartTs: number
|
||||||
skipNonActionPhases: boolean
|
|
||||||
//
|
|
||||||
dataOffset: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDrawPiece = (piece: Piece) => {
|
const shouldDrawPiece = (piece: Piece) => {
|
||||||
|
|
@ -96,6 +79,139 @@ function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
|
||||||
|
let events: Array<Array<any>> = []
|
||||||
|
|
||||||
|
let KEYS_ON = true
|
||||||
|
|
||||||
|
let LEFT = false
|
||||||
|
let RIGHT = false
|
||||||
|
let UP = false
|
||||||
|
let DOWN = false
|
||||||
|
let ZOOM_IN = false
|
||||||
|
let ZOOM_OUT = false
|
||||||
|
let SHIFT = false
|
||||||
|
|
||||||
|
const toWorldPoint = (x: number, y: number) => {
|
||||||
|
const pos = viewport.viewportToWorld({x, y})
|
||||||
|
return [pos.x, pos.y]
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
|
||||||
|
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
||||||
|
|
||||||
|
const key = (state: boolean, ev: KeyboardEvent) => {
|
||||||
|
if (!KEYS_ON) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ev.key === 'Shift') {
|
||||||
|
SHIFT = state
|
||||||
|
} else if (ev.key === 'ArrowUp' || ev.key === 'w' || ev.key === 'W') {
|
||||||
|
UP = state
|
||||||
|
} else if (ev.key === 'ArrowDown' || ev.key === 's' || ev.key === 'S') {
|
||||||
|
DOWN = state
|
||||||
|
} else if (ev.key === 'ArrowLeft' || ev.key === 'a' || ev.key === 'A') {
|
||||||
|
LEFT = state
|
||||||
|
} else if (ev.key === 'ArrowRight' || ev.key === 'd' || ev.key === 'D') {
|
||||||
|
RIGHT = state
|
||||||
|
} else if (ev.key === 'q') {
|
||||||
|
ZOOM_OUT = state
|
||||||
|
} else if (ev.key === 'e') {
|
||||||
|
ZOOM_IN = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', (ev) => {
|
||||||
|
if (ev.button === 0) {
|
||||||
|
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...mousePos(ev)])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseup', (ev) => {
|
||||||
|
if (ev.button === 0) {
|
||||||
|
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...mousePos(ev)])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.addEventListener('mousemove', (ev) => {
|
||||||
|
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...mousePos(ev)])
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.addEventListener('wheel', (ev) => {
|
||||||
|
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
|
||||||
|
const evt = ev.deltaY < 0
|
||||||
|
? Protocol.INPUT_EV_ZOOM_IN
|
||||||
|
: Protocol.INPUT_EV_ZOOM_OUT
|
||||||
|
addEvent([evt, ...mousePos(ev)])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
||||||
|
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
||||||
|
|
||||||
|
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
||||||
|
if (!KEYS_ON) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ev.key === ' ') {
|
||||||
|
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
|
||||||
|
}
|
||||||
|
if (ev.key === 'F' || ev.key === 'f') {
|
||||||
|
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
||||||
|
RERENDER = true
|
||||||
|
}
|
||||||
|
if (ev.key === 'G' || ev.key === 'g') {
|
||||||
|
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
||||||
|
RERENDER = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addEvent = (event: Array<any>) => {
|
||||||
|
events.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumeAll = () => {
|
||||||
|
if (events.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const all = events.slice()
|
||||||
|
events = []
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
const createKeyEvents = () => {
|
||||||
|
const amount = SHIFT ? 20 : 10
|
||||||
|
const x = (LEFT ? amount : 0) - (RIGHT ? amount : 0)
|
||||||
|
const y = (UP ? amount : 0) - (DOWN ? amount : 0)
|
||||||
|
if (x !== 0 || y !== 0) {
|
||||||
|
addEvent([Protocol.INPUT_EV_MOVE, x, y])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ZOOM_IN && ZOOM_OUT) {
|
||||||
|
// cancel each other out
|
||||||
|
} else if (ZOOM_IN) {
|
||||||
|
if (viewport.canZoom('in')) {
|
||||||
|
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...canvasCenter()])
|
||||||
|
}
|
||||||
|
} else if (ZOOM_OUT) {
|
||||||
|
if (viewport.canZoom('out')) {
|
||||||
|
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...canvasCenter()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setHotkeys = (state: boolean) => {
|
||||||
|
KEYS_ON = state
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addEvent,
|
||||||
|
consumeAll,
|
||||||
|
createKeyEvents,
|
||||||
|
setHotkeys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function main(
|
export async function main(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
|
|
@ -110,9 +226,6 @@ export async function main(
|
||||||
return MODE === MODE_REPLAY || player.id !== clientId
|
return MODE === MODE_REPLAY || player.id !== clientId
|
||||||
}
|
}
|
||||||
|
|
||||||
const click = sounds['./click.mp3'].default
|
|
||||||
const clickAudio = new Audio(click)
|
|
||||||
|
|
||||||
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
|
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
|
||||||
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
|
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
|
||||||
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
|
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
|
||||||
|
|
@ -147,63 +260,36 @@ export async function main(
|
||||||
// stuff only available in replay mode...
|
// stuff only available in replay mode...
|
||||||
// TODO: refactor
|
// TODO: refactor
|
||||||
const REPLAY: Replay = {
|
const REPLAY: Replay = {
|
||||||
final: false,
|
|
||||||
log: [],
|
log: [],
|
||||||
logPointer: 0,
|
logIdx: 0,
|
||||||
speeds: [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500],
|
speeds: [0.5, 1, 2, 5, 10, 20, 50],
|
||||||
speedIdx: 1,
|
speedIdx: 1,
|
||||||
paused: false,
|
paused: false,
|
||||||
lastRealTs: 0,
|
lastRealTs: 0,
|
||||||
lastGameTs: 0,
|
lastGameTs: 0,
|
||||||
gameStartTs: 0,
|
gameStartTs: 0,
|
||||||
skipNonActionPhases: true,
|
|
||||||
dataOffset: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Communication.onConnectionStateChange((state) => {
|
Communication.onConnectionStateChange((state) => {
|
||||||
HUD.setConnectionState(state)
|
HUD.setConnectionState(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryNextReplayBatch = async (
|
|
||||||
gameId: string
|
|
||||||
): Promise<ReplayData> => {
|
|
||||||
const offset = REPLAY.dataOffset
|
|
||||||
REPLAY.dataOffset += 10000 // meh
|
|
||||||
const replay: ReplayData = await Communication.requestReplayData(
|
|
||||||
gameId,
|
|
||||||
offset
|
|
||||||
)
|
|
||||||
|
|
||||||
// cut log that was already handled
|
|
||||||
REPLAY.log = REPLAY.log.slice(REPLAY.logPointer)
|
|
||||||
REPLAY.logPointer = 0
|
|
||||||
REPLAY.log.push(...replay.log)
|
|
||||||
|
|
||||||
if (replay.log.length === 0) {
|
|
||||||
REPLAY.final = true
|
|
||||||
}
|
|
||||||
return replay
|
|
||||||
}
|
|
||||||
|
|
||||||
let TIME: () => number = () => 0
|
let TIME: () => number = () => 0
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
if (MODE === MODE_PLAY) {
|
if (MODE === MODE_PLAY) {
|
||||||
const game: EncodedGame = await Communication.connect(wsAddress, gameId, clientId)
|
const game = await Communication.connect(wsAddress, gameId, clientId)
|
||||||
const gameObject: GameType = Util.decodeGame(game)
|
const gameObject = Util.decodeGame(game)
|
||||||
Game.setGame(gameObject.id, gameObject)
|
Game.setGame(gameObject.id, gameObject)
|
||||||
TIME = () => Time.timestamp()
|
TIME = () => Time.timestamp()
|
||||||
} else if (MODE === MODE_REPLAY) {
|
} else if (MODE === MODE_REPLAY) {
|
||||||
const replay: ReplayData = await queryNextReplayBatch(gameId)
|
// TODO: change how replay connect is done...
|
||||||
if (!replay.game) {
|
const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
|
||||||
throw '[ 2021-05-29 no game received ]'
|
const gameObject = Util.decodeGame(replay.game)
|
||||||
}
|
|
||||||
const gameObject: GameType = Util.decodeGame(replay.game)
|
|
||||||
Game.setGame(gameObject.id, gameObject)
|
Game.setGame(gameObject.id, gameObject)
|
||||||
|
REPLAY.log = replay.log
|
||||||
REPLAY.lastRealTs = Time.timestamp()
|
REPLAY.lastRealTs = Time.timestamp()
|
||||||
REPLAY.gameStartTs = parseInt(replay.log[0][4], 10)
|
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 {
|
||||||
throw '[ 2020-12-22 MODE invalid, must be play|replay ]'
|
throw '[ 2020-12-22 MODE invalid, must be play|replay ]'
|
||||||
|
|
@ -215,8 +301,8 @@ export async function main(
|
||||||
|
|
||||||
await connect()
|
await connect()
|
||||||
|
|
||||||
const PIECE_DRAW_OFFSET = Game.getPieceDrawOffset(gameId)
|
const TILE_DRAW_OFFSET = Game.getTileDrawOffset(gameId)
|
||||||
const PIECE_DRAW_SIZE = Game.getPieceDrawSize(gameId)
|
const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId)
|
||||||
const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId)
|
const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId)
|
||||||
const PUZZLE_HEIGHT = Game.getPuzzleHeight(gameId)
|
const PUZZLE_HEIGHT = Game.getPuzzleHeight(gameId)
|
||||||
const TABLE_WIDTH = Game.getTableWidth(gameId)
|
const TABLE_WIDTH = Game.getTableWidth(gameId)
|
||||||
|
|
@ -231,8 +317,8 @@ export async function main(
|
||||||
h: PUZZLE_HEIGHT,
|
h: PUZZLE_HEIGHT,
|
||||||
}
|
}
|
||||||
const PIECE_DIM = {
|
const PIECE_DIM = {
|
||||||
w: PIECE_DRAW_SIZE,
|
w: TILE_DRAW_SIZE,
|
||||||
h: PIECE_DRAW_SIZE,
|
h: TILE_DRAW_SIZE,
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
|
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
|
||||||
|
|
@ -242,40 +328,17 @@ export async function main(
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||||
canvas.classList.add('loaded')
|
canvas.classList.add('loaded')
|
||||||
HUD.setPuzzleCut()
|
|
||||||
|
|
||||||
// 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 = Camera()
|
const viewport = Camera()
|
||||||
|
// center viewport
|
||||||
const centerPuzzle = () => {
|
|
||||||
// center on the puzzle
|
|
||||||
viewport.reset()
|
|
||||||
viewport.move(
|
viewport.move(
|
||||||
-(TABLE_WIDTH - canvas.width) /2,
|
-(TABLE_WIDTH - canvas.width) /2,
|
||||||
-(TABLE_HEIGHT - canvas.height) /2
|
-(TABLE_HEIGHT - canvas.height) /2
|
||||||
)
|
)
|
||||||
|
|
||||||
// zoom viewport to fit whole puzzle in
|
const evts = EventAdapter(canvas, window, viewport)
|
||||||
const x = viewport.worldDimToViewport(BOARD_DIM)
|
|
||||||
const border = 20
|
|
||||||
const targetW = canvas.width - (border * 2)
|
|
||||||
const targetH = canvas.height - (border * 2)
|
|
||||||
if (
|
|
||||||
(x.w > targetW || x.h > targetH)
|
|
||||||
|| (x.w < targetW && x.h < targetH)
|
|
||||||
) {
|
|
||||||
const zoom = Math.min(targetW / x.w, targetH / x.h)
|
|
||||||
viewport.setZoom(zoom, {
|
|
||||||
x: canvas.width / 2,
|
|
||||||
y: canvas.height / 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
centerPuzzle()
|
|
||||||
|
|
||||||
const evts = EventAdapter(canvas, window, viewport, MODE)
|
|
||||||
|
|
||||||
const previewImageUrl = Game.getImageUrl(gameId)
|
const previewImageUrl = Game.getImageUrl(gameId)
|
||||||
|
|
||||||
|
|
@ -289,8 +352,8 @@ export async function main(
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTimerElements()
|
updateTimerElements()
|
||||||
HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId))
|
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
|
||||||
HUD.setPiecesTotal(Game.getPieceCount(gameId))
|
HUD.setPiecesTotal(Game.getTileCount(gameId))
|
||||||
const ts = TIME()
|
const ts = TIME()
|
||||||
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
|
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
|
||||||
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
|
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
|
||||||
|
|
@ -299,42 +362,20 @@ export async function main(
|
||||||
let finished = longFinished
|
let finished = longFinished
|
||||||
const justFinished = () => finished && !longFinished
|
const justFinished = () => finished && !longFinished
|
||||||
|
|
||||||
const playerSoundVolume = (): number => {
|
|
||||||
return settings.getInt(SETTINGS.SOUND_VOLUME, 100)
|
|
||||||
}
|
|
||||||
const playerSoundEnabled = (): boolean => {
|
|
||||||
return settings.getBool(SETTINGS.SOUND_ENABLED, false)
|
|
||||||
}
|
|
||||||
const showPlayerNames = (): boolean => {
|
|
||||||
return settings.getBool(SETTINGS.SHOW_PLAYER_NAMES, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const playClick = () => {
|
|
||||||
const vol = playerSoundVolume()
|
|
||||||
clickAudio.volume = vol / 100
|
|
||||||
clickAudio.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerBgColor = () => {
|
const playerBgColor = () => {
|
||||||
if (MODE === MODE_REPLAY) {
|
return (Game.getPlayerBgColor(gameId, clientId)
|
||||||
return settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
|
|| localStorage.getItem('bg_color')
|
||||||
}
|
|| '#222222')
|
||||||
return Game.getPlayerBgColor(gameId, clientId)
|
|
||||||
|| settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
|
|
||||||
}
|
}
|
||||||
const playerColor = () => {
|
const playerColor = () => {
|
||||||
if (MODE === MODE_REPLAY) {
|
return (Game.getPlayerColor(gameId, clientId)
|
||||||
return settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
|
|| localStorage.getItem('player_color')
|
||||||
}
|
|| '#ffffff')
|
||||||
return Game.getPlayerColor(gameId, clientId)
|
|
||||||
|| settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
|
|
||||||
}
|
}
|
||||||
const playerName = () => {
|
const playerName = () => {
|
||||||
if (MODE === MODE_REPLAY) {
|
return (Game.getPlayerName(gameId, clientId)
|
||||||
return settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
|
|| localStorage.getItem('player_name')
|
||||||
}
|
|| 'anon')
|
||||||
return Game.getPlayerName(gameId, clientId)
|
|
||||||
|| settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursorDown: string = ''
|
let cursorDown: string = ''
|
||||||
|
|
@ -378,35 +419,14 @@ export async function main(
|
||||||
doSetSpeedStatus()
|
doSetSpeedStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const intervals: NodeJS.Timeout[] = []
|
|
||||||
let to: NodeJS.Timeout
|
|
||||||
const clearIntervals = () => {
|
|
||||||
intervals.forEach(inter => {
|
|
||||||
clearInterval(inter)
|
|
||||||
})
|
|
||||||
if (to) {
|
|
||||||
clearTimeout(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let gameLoopInstance: GameLoopInstance
|
|
||||||
const unload = () => {
|
|
||||||
clearIntervals()
|
|
||||||
if (gameLoopInstance) {
|
|
||||||
gameLoopInstance.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MODE === MODE_PLAY) {
|
if (MODE === MODE_PLAY) {
|
||||||
intervals.push(setInterval(() => {
|
setInterval(updateTimerElements, 1000)
|
||||||
updateTimerElements()
|
|
||||||
}, 1000))
|
|
||||||
} else if (MODE === MODE_REPLAY) {
|
} else if (MODE === MODE_REPLAY) {
|
||||||
doSetSpeedStatus()
|
doSetSpeedStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MODE === MODE_PLAY) {
|
if (MODE === MODE_PLAY) {
|
||||||
Communication.onServerChange((msg: ServerEvent) => {
|
Communication.onServerChange((msg) => {
|
||||||
const msgType = msg[0]
|
const msgType = msg[0]
|
||||||
const evClientId = msg[1]
|
const evClientId = msg[1]
|
||||||
const evClientSeq = msg[2]
|
const evClientSeq = msg[2]
|
||||||
|
|
@ -421,8 +441,8 @@ export async function main(
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case Protocol.CHANGE_TILE: {
|
case Protocol.CHANGE_TILE: {
|
||||||
const t = Util.decodePiece(changeData)
|
const t = Util.decodeTile(changeData)
|
||||||
Game.setPiece(gameId, t.idx, t)
|
Game.setTile(gameId, t.idx, t)
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
} break;
|
} break;
|
||||||
case Protocol.CHANGE_DATA: {
|
case Protocol.CHANGE_DATA: {
|
||||||
|
|
@ -434,91 +454,64 @@ export async function main(
|
||||||
finished = !! Game.getFinishTs(gameId)
|
finished = !! Game.getFinishTs(gameId)
|
||||||
})
|
})
|
||||||
} else if (MODE === MODE_REPLAY) {
|
} else if (MODE === MODE_REPLAY) {
|
||||||
const handleLogEntry = (logEntry: any[], ts: Timestamp) => {
|
// no external communication for replay mode,
|
||||||
const entry = logEntry
|
// only the REPLAY.log is relevant
|
||||||
if (entry[0] === Protocol.LOG_ADD_PLAYER) {
|
let inter = setInterval(() => {
|
||||||
const playerId = entry[1]
|
|
||||||
Game.addPlayer(gameId, playerId, ts)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (entry[0] === Protocol.LOG_UPDATE_PLAYER) {
|
|
||||||
const playerId = Game.getPlayerIdByIndex(gameId, entry[1])
|
|
||||||
if (!playerId) {
|
|
||||||
throw '[ 2021-05-17 player not found (update player) ]'
|
|
||||||
}
|
|
||||||
Game.addPlayer(gameId, playerId, ts)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (entry[0] === Protocol.LOG_HANDLE_INPUT) {
|
|
||||||
const playerId = Game.getPlayerIdByIndex(gameId, entry[1])
|
|
||||||
if (!playerId) {
|
|
||||||
throw '[ 2021-05-17 player not found (handle input) ]'
|
|
||||||
}
|
|
||||||
const input = entry[2]
|
|
||||||
Game.handleInput(gameId, playerId, input, ts)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let GAME_TS = REPLAY.lastGameTs
|
|
||||||
const next = async () => {
|
|
||||||
if (REPLAY.logPointer + 1 >= REPLAY.log.length) {
|
|
||||||
await queryNextReplayBatch(gameId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const realTs = Time.timestamp()
|
const realTs = Time.timestamp()
|
||||||
if (REPLAY.paused) {
|
if (REPLAY.paused) {
|
||||||
REPLAY.lastRealTs = realTs
|
REPLAY.lastRealTs = realTs
|
||||||
to = setTimeout(next, 50)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const timePassedReal = realTs - REPLAY.lastRealTs
|
const timePassedReal = realTs - REPLAY.lastRealTs
|
||||||
const timePassedGame = timePassedReal * REPLAY.speeds[REPLAY.speedIdx]
|
const timePassedGame = timePassedReal * REPLAY.speeds[REPLAY.speedIdx]
|
||||||
let maxGameTs = REPLAY.lastGameTs + timePassedGame
|
const maxGameTs = REPLAY.lastGameTs + timePassedGame
|
||||||
do {
|
do {
|
||||||
if (REPLAY.paused) {
|
if (REPLAY.paused) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const nextIdx = REPLAY.logPointer + 1
|
const nextIdx = REPLAY.logIdx + 1
|
||||||
if (nextIdx >= REPLAY.log.length) {
|
if (nextIdx >= REPLAY.log.length) {
|
||||||
|
clearInterval(inter)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const currLogEntry = REPLAY.log[REPLAY.logPointer]
|
const logEntry = REPLAY.log[nextIdx]
|
||||||
const currTs: Timestamp = GAME_TS + currLogEntry[currLogEntry.length - 1]
|
const nextTs = REPLAY.gameStartTs + logEntry[logEntry.length - 1]
|
||||||
|
|
||||||
const nextLogEntry = REPLAY.log[nextIdx]
|
|
||||||
const diffToNext = nextLogEntry[nextLogEntry.length - 1]
|
|
||||||
const nextTs: Timestamp = currTs + diffToNext
|
|
||||||
if (nextTs > maxGameTs) {
|
if (nextTs > maxGameTs) {
|
||||||
// next log entry is too far into the future
|
|
||||||
if (REPLAY.skipNonActionPhases && (maxGameTs + 500 * Time.MS < nextTs)) {
|
|
||||||
maxGameTs += diffToNext
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
GAME_TS = currTs
|
const entryWithTs = logEntry.slice()
|
||||||
if (handleLogEntry(nextLogEntry, nextTs)) {
|
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
|
||||||
|
const playerId = entryWithTs[1]
|
||||||
|
Game.addPlayer(gameId, playerId, nextTs)
|
||||||
|
RERENDER = true
|
||||||
|
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
|
||||||
|
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
||||||
|
if (!playerId) {
|
||||||
|
throw '[ 2021-05-17 player not found (update player) ]'
|
||||||
|
}
|
||||||
|
Game.addPlayer(gameId, playerId, nextTs)
|
||||||
|
RERENDER = true
|
||||||
|
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||||
|
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
|
||||||
|
if (!playerId) {
|
||||||
|
throw '[ 2021-05-17 player not found (handle input) ]'
|
||||||
|
}
|
||||||
|
const input = entryWithTs[2]
|
||||||
|
Game.handleInput(gameId, playerId, input, nextTs)
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
}
|
}
|
||||||
REPLAY.logPointer = nextIdx
|
REPLAY.logIdx = nextIdx
|
||||||
} while (true)
|
} while (true)
|
||||||
REPLAY.lastRealTs = realTs
|
REPLAY.lastRealTs = realTs
|
||||||
REPLAY.lastGameTs = maxGameTs
|
REPLAY.lastGameTs = maxGameTs
|
||||||
updateTimerElements()
|
updateTimerElements()
|
||||||
|
}, 50)
|
||||||
if (!REPLAY.final) {
|
|
||||||
to = setTimeout(next, 50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _last_mouse_down: Point|null = null
|
let _last_mouse_down: Point|null = null
|
||||||
const onUpdate = (): void => {
|
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
|
||||||
// relevant is pressed
|
// relevant is pressed
|
||||||
|
|
@ -530,13 +523,12 @@ export async function main(
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
const type = evt[0]
|
const type = evt[0]
|
||||||
if (type === Protocol.INPUT_EV_MOVE) {
|
if (type === Protocol.INPUT_EV_MOVE) {
|
||||||
const w = evt[1]
|
const diffX = evt[1]
|
||||||
const h = evt[2]
|
const diffY = evt[2]
|
||||||
const dim = viewport.worldDimToViewport({w, h})
|
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
viewport.move(dim.w, dim.h)
|
viewport.move(diffX, diffY)
|
||||||
} else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
|
} else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
|
||||||
if (_last_mouse_down && !Game.getFirstOwnedPiece(gameId, clientId)) {
|
if (_last_mouse_down && !Game.getFirstOwnedTile(gameId, clientId)) {
|
||||||
// move the cam
|
// move the cam
|
||||||
const pos = { x: evt[1], y: evt[2] }
|
const pos = { x: evt[1], y: evt[2] }
|
||||||
const mouse = viewport.worldToViewport(pos)
|
const mouse = viewport.worldToViewport(pos)
|
||||||
|
|
@ -566,34 +558,12 @@ export async function main(
|
||||||
viewport.zoom('out', viewport.worldToViewport(pos))
|
viewport.zoom('out', viewport.worldToViewport(pos))
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
|
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
|
||||||
HUD.togglePreview()
|
HUD.togglePreview()
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
|
|
||||||
HUD.toggleSoundsEnabled()
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
|
|
||||||
HUD.togglePlayerNames()
|
|
||||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
|
||||||
centerPuzzle()
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
|
|
||||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
|
||||||
RERENDER = true
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
|
|
||||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
|
||||||
RERENDER = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOCAL + SERVER CHANGES
|
// LOCAL + SERVER CHANGES
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
const ts = TIME()
|
const ts = TIME()
|
||||||
const changes = Game.handleInput(
|
const changes = Game.handleInput(gameId, clientId, evt, ts)
|
||||||
gameId,
|
|
||||||
clientId,
|
|
||||||
evt,
|
|
||||||
ts,
|
|
||||||
(playerId: string) => {
|
|
||||||
if (playerSoundEnabled()) {
|
|
||||||
playClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
}
|
}
|
||||||
|
|
@ -602,13 +572,7 @@ export async function main(
|
||||||
// LOCAL ONLY CHANGES
|
// LOCAL ONLY CHANGES
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
const type = evt[0]
|
const type = evt[0]
|
||||||
if (type === Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE) {
|
if (type === Protocol.INPUT_EV_MOVE) {
|
||||||
replayOnPauseToggle()
|
|
||||||
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_DOWN) {
|
|
||||||
replayOnSpeedDown()
|
|
||||||
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_UP) {
|
|
||||||
replayOnSpeedUp()
|
|
||||||
} else if (type === Protocol.INPUT_EV_MOVE) {
|
|
||||||
const diffX = evt[1]
|
const diffX = evt[1]
|
||||||
const diffY = evt[2]
|
const diffY = evt[2]
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
|
|
@ -625,15 +589,11 @@ export async function main(
|
||||||
|
|
||||||
_last_mouse_down = mouse
|
_last_mouse_down = mouse
|
||||||
}
|
}
|
||||||
} else if (type === Protocol.INPUT_EV_PLAYER_COLOR) {
|
|
||||||
updatePlayerCursorColor(evt[1])
|
|
||||||
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
||||||
const pos = { x: evt[1], y: evt[2] }
|
const pos = { x: evt[1], y: evt[2] }
|
||||||
_last_mouse_down = viewport.worldToViewport(pos)
|
_last_mouse_down = viewport.worldToViewport(pos)
|
||||||
updatePlayerCursorState(true)
|
|
||||||
} else if (type === Protocol.INPUT_EV_MOUSE_UP) {
|
} else if (type === Protocol.INPUT_EV_MOUSE_UP) {
|
||||||
_last_mouse_down = null
|
_last_mouse_down = null
|
||||||
updatePlayerCursorState(false)
|
|
||||||
} else if (type === Protocol.INPUT_EV_ZOOM_IN) {
|
} else if (type === Protocol.INPUT_EV_ZOOM_IN) {
|
||||||
const pos = { x: evt[1], y: evt[2] }
|
const pos = { x: evt[1], y: evt[2] }
|
||||||
RERENDER = true
|
RERENDER = true
|
||||||
|
|
@ -644,18 +604,6 @@ export async function main(
|
||||||
viewport.zoom('out', viewport.worldToViewport(pos))
|
viewport.zoom('out', viewport.worldToViewport(pos))
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
|
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
|
||||||
HUD.togglePreview()
|
HUD.togglePreview()
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
|
|
||||||
HUD.toggleSoundsEnabled()
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
|
|
||||||
HUD.togglePlayerNames()
|
|
||||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
|
||||||
centerPuzzle()
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
|
|
||||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
|
||||||
RERENDER = true
|
|
||||||
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
|
|
||||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
|
||||||
RERENDER = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -667,7 +615,7 @@ export async function main(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRender = async (): Promise<void> => {
|
const onRender = async () => {
|
||||||
if (!RERENDER) {
|
if (!RERENDER) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -700,7 +648,7 @@ export async function main(
|
||||||
|
|
||||||
// DRAW TILES
|
// DRAW TILES
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
const tiles = Game.getPiecesSortedByZIndex(gameId)
|
const tiles = Game.getTilesSortedByZIndex(gameId)
|
||||||
if (window.DEBUG) Debug.checkpoint('get tiles done')
|
if (window.DEBUG) Debug.checkpoint('get tiles done')
|
||||||
|
|
||||||
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
|
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
|
||||||
|
|
@ -710,8 +658,8 @@ export async function main(
|
||||||
}
|
}
|
||||||
bmp = bitmaps[tile.idx]
|
bmp = bitmaps[tile.idx]
|
||||||
pos = viewport.worldToViewportRaw({
|
pos = viewport.worldToViewportRaw({
|
||||||
x: PIECE_DRAW_OFFSET + tile.pos.x,
|
x: TILE_DRAW_OFFSET + tile.pos.x,
|
||||||
y: PIECE_DRAW_OFFSET + tile.pos.y,
|
y: TILE_DRAW_OFFSET + tile.pos.y,
|
||||||
})
|
})
|
||||||
ctx.drawImage(bmp,
|
ctx.drawImage(bmp,
|
||||||
0, 0, bmp.width, bmp.height,
|
0, 0, bmp.width, bmp.height,
|
||||||
|
|
@ -731,14 +679,12 @@ export async function main(
|
||||||
bmp = await getPlayerCursor(p)
|
bmp = await getPlayerCursor(p)
|
||||||
pos = viewport.worldToViewport(p)
|
pos = viewport.worldToViewport(p)
|
||||||
ctx.drawImage(bmp, pos.x - CURSOR_W_2, pos.y - CURSOR_H_2)
|
ctx.drawImage(bmp, pos.x - CURSOR_W_2, pos.y - CURSOR_H_2)
|
||||||
if (showPlayerNames()) {
|
|
||||||
// performance:
|
// performance:
|
||||||
// not drawing text directly here, to have less ctx
|
// not drawing text directly here, to have less ctx
|
||||||
// switches between drawImage and fillTxt
|
// switches between drawImage and fillTxt
|
||||||
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
|
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Names
|
// Names
|
||||||
ctx.fillStyle = 'white'
|
ctx.fillStyle = 'white'
|
||||||
|
|
@ -753,7 +699,7 @@ export async function main(
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
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.getFinishedPiecesCount(gameId))
|
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
|
||||||
if (window.DEBUG) Debug.checkpoint('HUD done')
|
if (window.DEBUG) Debug.checkpoint('HUD done')
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -764,7 +710,7 @@ export async function main(
|
||||||
RERENDER = false
|
RERENDER = false
|
||||||
}
|
}
|
||||||
|
|
||||||
gameLoopInstance = run({
|
run({
|
||||||
update: onUpdate,
|
update: onUpdate,
|
||||||
render: onRender,
|
render: onRender,
|
||||||
})
|
})
|
||||||
|
|
@ -774,27 +720,17 @@ export async function main(
|
||||||
evts.setHotkeys(state)
|
evts.setHotkeys(state)
|
||||||
},
|
},
|
||||||
onBgChange: (value: string) => {
|
onBgChange: (value: string) => {
|
||||||
settings.setStr(SETTINGS.COLOR_BACKGROUND, value)
|
localStorage.setItem('bg_color', value)
|
||||||
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
||||||
},
|
},
|
||||||
onColorChange: (value: string) => {
|
onColorChange: (value: string) => {
|
||||||
settings.setStr(SETTINGS.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: string) => {
|
onNameChange: (value: string) => {
|
||||||
settings.setStr(SETTINGS.PLAYER_NAME, value)
|
localStorage.setItem('player_name', value)
|
||||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
||||||
},
|
},
|
||||||
onSoundsEnabledChange: (value: boolean) => {
|
|
||||||
settings.setBool(SETTINGS.SOUND_ENABLED, value)
|
|
||||||
},
|
|
||||||
onSoundsVolumeChange: (value: number) => {
|
|
||||||
settings.setInt(SETTINGS.SOUND_VOLUME, value)
|
|
||||||
playClick()
|
|
||||||
},
|
|
||||||
onShowPlayerNamesChange: (value: boolean) => {
|
|
||||||
settings.setBool(SETTINGS.SHOW_PLAYER_NAMES, value)
|
|
||||||
},
|
|
||||||
replayOnSpeedUp,
|
replayOnSpeedUp,
|
||||||
replayOnSpeedDown,
|
replayOnSpeedDown,
|
||||||
replayOnPauseToggle,
|
replayOnPauseToggle,
|
||||||
|
|
@ -803,13 +739,8 @@ export async function main(
|
||||||
background: playerBgColor(),
|
background: playerBgColor(),
|
||||||
color: playerColor(),
|
color: playerColor(),
|
||||||
name: playerName(),
|
name: playerName(),
|
||||||
soundsEnabled: playerSoundEnabled(),
|
|
||||||
soundsVolume: playerSoundVolume(),
|
|
||||||
showPlayerNames: showPlayerNames(),
|
|
||||||
},
|
},
|
||||||
game: Game.get(gameId),
|
|
||||||
disconnect: Communication.disconnect,
|
disconnect: Communication.disconnect,
|
||||||
connect: connect,
|
connect: connect,
|
||||||
unload: unload,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,10 @@
|
||||||
interface GameLoopOptions {
|
interface GameLoopOptions {
|
||||||
fps?: number
|
fps?: number
|
||||||
slow?: number
|
slow?: number
|
||||||
update: (step: number) => void
|
update: (step: number) => any
|
||||||
render: (passed: number) => void
|
render: (passed: number) => any
|
||||||
}
|
}
|
||||||
|
export const run = (options: GameLoopOptions) => {
|
||||||
export interface GameLoopInstance {
|
|
||||||
stop: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = (options: GameLoopOptions): GameLoopInstance => {
|
|
||||||
let stopped = false
|
|
||||||
const stop = () => {
|
|
||||||
stopped = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -38,15 +28,10 @@ export const run = (options: GameLoopOptions): GameLoopInstance => {
|
||||||
}
|
}
|
||||||
render(dt / slow)
|
render(dt / slow)
|
||||||
last = now
|
last = now
|
||||||
if (!stopped) {
|
|
||||||
raf(frame)
|
raf(frame)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
raf(frame)
|
raf(frame)
|
||||||
return {
|
|
||||||
stop,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,19 @@ import NewGame from './views/NewGame.vue'
|
||||||
import Game from './views/Game.vue'
|
import Game from './views/Game.vue'
|
||||||
import Replay from './views/Replay.vue'
|
import Replay from './views/Replay.vue'
|
||||||
import Util from './../common/Util'
|
import Util from './../common/Util'
|
||||||
import settings from './settings'
|
|
||||||
import xhr from './xhr'
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
function initClientSecret() {
|
const res = await fetch(`/api/conf`)
|
||||||
let SECRET = settings.getStr('SECRET', '')
|
const conf = await res.json()
|
||||||
if (!SECRET) {
|
|
||||||
SECRET = Util.uniqId()
|
function initme() {
|
||||||
settings.setStr('SECRET', SECRET)
|
let ID = localStorage.getItem('ID')
|
||||||
}
|
|
||||||
return SECRET
|
|
||||||
}
|
|
||||||
function initClientId() {
|
|
||||||
let ID = settings.getStr('ID', '')
|
|
||||||
if (!ID) {
|
if (!ID) {
|
||||||
ID = Util.uniqId()
|
ID = Util.uniqId()
|
||||||
settings.setStr('ID', ID)
|
localStorage.setItem('ID', ID)
|
||||||
}
|
}
|
||||||
return ID
|
return ID
|
||||||
}
|
}
|
||||||
const clientId = initClientId()
|
|
||||||
const clientSecret = initClientSecret()
|
|
||||||
xhr.setClientId(clientId)
|
|
||||||
xhr.setClientSecret(clientSecret)
|
|
||||||
|
|
||||||
const meRes = await xhr.get(`/api/me`, {})
|
|
||||||
const me = await meRes.json()
|
|
||||||
|
|
||||||
const confRes = await xhr.get(`/api/conf`, {})
|
|
||||||
const conf = await confRes.json()
|
|
||||||
|
|
||||||
const router = VueRouter.createRouter({
|
const router = VueRouter.createRouter({
|
||||||
history: VueRouter.createWebHashHistory(),
|
history: VueRouter.createWebHashHistory(),
|
||||||
|
|
@ -56,9 +39,8 @@ import xhr from './xhr'
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = Vue.createApp(App)
|
const app = Vue.createApp(App)
|
||||||
app.config.globalProperties.$me = me
|
|
||||||
app.config.globalProperties.$config = conf
|
app.config.globalProperties.$config = conf
|
||||||
app.config.globalProperties.$clientId = clientId
|
app.config.globalProperties.$clientId = initme()
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* Player settings
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const SETTINGS = {
|
|
||||||
SOUND_VOLUME: 'sound_volume',
|
|
||||||
SOUND_ENABLED: 'sound_enabled',
|
|
||||||
COLOR_BACKGROUND: 'bg_color',
|
|
||||||
PLAYER_COLOR: 'player_color',
|
|
||||||
PLAYER_NAME: 'player_name',
|
|
||||||
SHOW_PLAYER_NAMES: 'show_player_names',
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = (setting: string, value: string): void => {
|
|
||||||
localStorage.setItem(setting, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = (setting: string): any => {
|
|
||||||
return localStorage.getItem(setting)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setInt = (setting: string, val: number): void => {
|
|
||||||
set(setting, `${val}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInt = (setting: string, def: number): number => {
|
|
||||||
const value = get(setting)
|
|
||||||
if (value === null) {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
const vol = parseInt(value, 10)
|
|
||||||
return isNaN(vol) ? def : vol
|
|
||||||
}
|
|
||||||
|
|
||||||
const setBool = (setting: string, val: boolean): void => {
|
|
||||||
set(setting, val ? '1' : '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBool = (setting: string, def: boolean): boolean => {
|
|
||||||
const value = get(setting)
|
|
||||||
if (value === null) {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return value === '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
const setStr = (setting: string, val: string): void => {
|
|
||||||
set(setting, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStr = (setting: string, def: string): string => {
|
|
||||||
const value = get(setting)
|
|
||||||
if (value === null) {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setInt,
|
|
||||||
getInt,
|
|
||||||
setBool,
|
|
||||||
getBool,
|
|
||||||
setStr,
|
|
||||||
getStr,
|
|
||||||
}
|
|
||||||
|
|
@ -127,7 +127,7 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,8 @@
|
||||||
<div id="game">
|
<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" />
|
||||||
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
|
|
||||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||||
|
|
||||||
<div class="overlay" v-if="cuttingPuzzle">
|
|
||||||
<div class="overlay-content">
|
|
||||||
<div>⏳ Cutting puzzle, please wait... ⏳</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<connection-overlay
|
<connection-overlay
|
||||||
:connectionState="connectionState"
|
:connectionState="connectionState"
|
||||||
@reconnect="reconnect"
|
@reconnect="reconnect"
|
||||||
|
|
@ -28,8 +21,7 @@
|
||||||
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
||||||
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
||||||
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
||||||
<div class="opener" @click="toggle('info', true)">ℹ️ Info</div>
|
<div class="opener" @click="toggle('help', true)">ℹ️ Help</div>
|
||||||
<div class="opener" @click="toggle('help', true)">⌨️ Hotkeys</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -43,12 +35,11 @@ import Scores from './../components/Scores.vue'
|
||||||
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||||
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||||
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||||
import InfoOverlay from './../components/InfoOverlay.vue'
|
|
||||||
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
|
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
|
||||||
import HelpOverlay from './../components/HelpOverlay.vue'
|
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||||
|
|
||||||
import { main, MODE_PLAY } from './../game'
|
import { main, MODE_PLAY } from './../game'
|
||||||
import { Game, Player } from '../../common/Types'
|
import { Player } from '../../common/GameCommon'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'game',
|
name: 'game',
|
||||||
|
|
@ -57,7 +48,6 @@ export default defineComponent({
|
||||||
Scores,
|
Scores,
|
||||||
SettingsOverlay,
|
SettingsOverlay,
|
||||||
PreviewOverlay,
|
PreviewOverlay,
|
||||||
InfoOverlay,
|
|
||||||
ConnectionOverlay,
|
ConnectionOverlay,
|
||||||
HelpOverlay,
|
HelpOverlay,
|
||||||
},
|
},
|
||||||
|
|
@ -74,29 +64,20 @@ export default defineComponent({
|
||||||
overlay: '',
|
overlay: '',
|
||||||
|
|
||||||
connectionState: 0,
|
connectionState: 0,
|
||||||
cuttingPuzzle: true,
|
|
||||||
|
|
||||||
g: {
|
g: {
|
||||||
player: {
|
player: {
|
||||||
background: '',
|
background: '',
|
||||||
color: '',
|
color: '',
|
||||||
name: '',
|
name: '',
|
||||||
soundsEnabled: false,
|
|
||||||
soundsVolume: 100,
|
|
||||||
showPlayerNames: true,
|
|
||||||
},
|
},
|
||||||
game: null as Game|null,
|
|
||||||
previewImageUrl: '',
|
previewImageUrl: '',
|
||||||
setHotkeys: (v: boolean) => {},
|
setHotkeys: (v: boolean) => {},
|
||||||
onBgChange: (v: string) => {},
|
onBgChange: (v: string) => {},
|
||||||
onColorChange: (v: string) => {},
|
onColorChange: (v: string) => {},
|
||||||
onNameChange: (v: string) => {},
|
onNameChange: (v: string) => {},
|
||||||
onSoundsEnabledChange: (v: boolean) => {},
|
|
||||||
onSoundsVolumeChange: (v: number) => {},
|
|
||||||
onShowPlayerNamesChange: (v: boolean) => {},
|
|
||||||
connect: () => {},
|
|
||||||
disconnect: () => {},
|
disconnect: () => {},
|
||||||
unload: () => {},
|
connect: () => {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -113,15 +94,6 @@ export default defineComponent({
|
||||||
this.$watch(() => this.g.player.name, (value: string) => {
|
this.$watch(() => this.g.player.name, (value: string) => {
|
||||||
this.g.onNameChange(value)
|
this.g.onNameChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
|
|
||||||
this.g.onSoundsEnabledChange(value)
|
|
||||||
})
|
|
||||||
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
|
|
||||||
this.g.onSoundsVolumeChange(value)
|
|
||||||
})
|
|
||||||
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
|
|
||||||
this.g.onShowPlayerNamesChange(value)
|
|
||||||
})
|
|
||||||
this.g = await main(
|
this.g = await main(
|
||||||
`${this.$route.params.id}`,
|
`${this.$route.params.id}`,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -131,22 +103,18 @@ export default defineComponent({
|
||||||
MODE_PLAY,
|
MODE_PLAY,
|
||||||
this.$el,
|
this.$el,
|
||||||
{
|
{
|
||||||
setPuzzleCut: () => { this.cuttingPuzzle = false },
|
|
||||||
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
|
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
|
||||||
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
|
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
|
||||||
setFinished: (v: boolean) => { this.finished = v },
|
setFinished: (v: boolean) => { this.finished = v },
|
||||||
setDuration: (v: number) => { this.duration = v },
|
setDuration: (v: number) => { this.duration = v },
|
||||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||||
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||||
togglePreview: () => { this.toggle('preview', false) },
|
|
||||||
setConnectionState: (v: number) => { this.connectionState = v },
|
setConnectionState: (v: number) => { this.connectionState = v },
|
||||||
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
|
togglePreview: () => { this.toggle('preview', false) },
|
||||||
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
this.g.unload()
|
|
||||||
this.g.disconnect()
|
this.g.disconnect()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import xhr from '../xhr'
|
|
||||||
|
|
||||||
import GameTeaser from './../components/GameTeaser.vue'
|
import GameTeaser from './../components/GameTeaser.vue'
|
||||||
|
|
||||||
|
|
@ -28,7 +27,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
const res = await xhr.get('/api/index-data', {})
|
const res = await fetch('/api/index-data')
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
this.gamesRunning = json.gamesRunning
|
this.gamesRunning = json.gamesRunning
|
||||||
this.gamesFinished = json.gamesFinished
|
this.gamesFinished = json.gamesFinished
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,7 @@ in jigsawpuzzles.io
|
||||||
<div>
|
<div>
|
||||||
<label v-if="tags.length > 0">
|
<label v-if="tags.length > 0">
|
||||||
Tags:
|
Tags:
|
||||||
<span
|
<span class="bit" v-for="(t,idx) in tags" :key="idx" @click="toggleTag(t)" :class="{on: filters.tags.includes(t.slug)}">{{t.title}}</span>
|
||||||
class="bit"
|
|
||||||
v-for="(t,idx) in relevantTags"
|
|
||||||
:key="idx"
|
|
||||||
@click="toggleTag(t)"
|
|
||||||
:class="{on: filters.tags.includes(t.slug)}">{{t.title}} ({{t.total}})</span>
|
|
||||||
<!-- <select v-model="filters.tags" @change="filtersChanged">
|
<!-- <select v-model="filters.tags" @change="filtersChanged">
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
<option v-for="(c, idx) in tags" :key="idx" :value="c.slug">{{c.title}}</option>
|
<option v-for="(c, idx) in tags" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||||
|
|
@ -36,30 +31,10 @@ in jigsawpuzzles.io
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<image-library
|
<image-library :images="images" @imageClicked="onImageClicked" @imageEditClicked="onImageEditClicked" />
|
||||||
:images="images"
|
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
||||||
@imageClicked="onImageClicked"
|
<edit-image-dialog v-if="dialog==='edit-image'" @bgclick="dialog=''" @saveClick="onSaveImageClick" :image="image" />
|
||||||
@imageEditClicked="onImageEditClicked" />
|
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
||||||
<new-image-dialog
|
|
||||||
v-if="dialog==='new-image'"
|
|
||||||
:autocompleteTags="autocompleteTags"
|
|
||||||
@bgclick="dialog=''"
|
|
||||||
:uploadProgress="uploadProgress"
|
|
||||||
:uploading="uploading"
|
|
||||||
@postToGalleryClick="postToGalleryClick"
|
|
||||||
@setupGameClick="setupGameClick"
|
|
||||||
/>
|
|
||||||
<edit-image-dialog
|
|
||||||
v-if="dialog==='edit-image'"
|
|
||||||
:autocompleteTags="autocompleteTags"
|
|
||||||
@bgclick="dialog=''"
|
|
||||||
@saveClick="onSaveImageClick"
|
|
||||||
:image="image" />
|
|
||||||
<new-game-dialog
|
|
||||||
v-if="image && dialog==='new-game'"
|
|
||||||
@bgclick="dialog=''"
|
|
||||||
@newGame="onNewGame"
|
|
||||||
:image="image" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -70,9 +45,8 @@ import ImageLibrary from './../components/ImageLibrary.vue'
|
||||||
import NewImageDialog from './../components/NewImageDialog.vue'
|
import NewImageDialog from './../components/NewImageDialog.vue'
|
||||||
import EditImageDialog from './../components/EditImageDialog.vue'
|
import EditImageDialog from './../components/EditImageDialog.vue'
|
||||||
import NewGameDialog from './../components/NewGameDialog.vue'
|
import NewGameDialog from './../components/NewGameDialog.vue'
|
||||||
import { GameSettings, Image, Tag } from '../../common/Types'
|
import { GameSettings, Image, Tag } from '../../common/GameCommon'
|
||||||
import Util from '../../common/Util'
|
import Util from '../../common/Util'
|
||||||
import xhr from '../xhr'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -88,7 +62,7 @@ export default defineComponent({
|
||||||
tags: [] as string[],
|
tags: [] as string[],
|
||||||
},
|
},
|
||||||
images: [],
|
images: [],
|
||||||
tags: [] as Tag[],
|
tags: [],
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
|
@ -101,29 +75,12 @@ export default defineComponent({
|
||||||
} as Image,
|
} as Image,
|
||||||
|
|
||||||
dialog: '',
|
dialog: '',
|
||||||
|
|
||||||
uploading: '',
|
|
||||||
uploadProgress: 0,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
relevantTags (): Tag[] {
|
|
||||||
return this.tags.filter((tag: Tag) => tag.total > 0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
autocompleteTags (input: string, exclude: string[]): string[] {
|
|
||||||
return this.tags
|
|
||||||
.filter((tag: Tag) => {
|
|
||||||
return !exclude.includes(tag.title)
|
|
||||||
&& tag.title.toLowerCase().startsWith(input.toLowerCase())
|
|
||||||
})
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((tag: Tag) => tag.title)
|
|
||||||
},
|
|
||||||
toggleTag (t: Tag) {
|
toggleTag (t: Tag) {
|
||||||
if (this.filters.tags.includes(t.slug)) {
|
if (this.filters.tags.includes(t.slug)) {
|
||||||
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)
|
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)
|
||||||
|
|
@ -133,7 +90,7 @@ export default defineComponent({
|
||||||
this.filtersChanged()
|
this.filtersChanged()
|
||||||
},
|
},
|
||||||
async loadImages () {
|
async loadImages () {
|
||||||
const res = await xhr.get(`/api/newgame-data${Util.asQueryArgs(this.filters)}`, {})
|
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
this.images = json.images
|
this.images = json.images
|
||||||
this.tags = json.tags
|
this.tags = json.tags
|
||||||
|
|
@ -150,22 +107,20 @@ export default defineComponent({
|
||||||
this.dialog = 'edit-image'
|
this.dialog = 'edit-image'
|
||||||
},
|
},
|
||||||
async uploadImage (data: any) {
|
async uploadImage (data: any) {
|
||||||
this.uploadProgress = 0
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', data.file, data.file.name);
|
formData.append('file', data.file, data.file.name);
|
||||||
formData.append('title', data.title)
|
formData.append('title', data.title)
|
||||||
formData.append('tags', data.tags)
|
formData.append('tags', data.tags)
|
||||||
const res = await xhr.post('/api/upload', {
|
|
||||||
|
const res = await fetch('/api/upload', {
|
||||||
|
method: 'post',
|
||||||
body: formData,
|
body: formData,
|
||||||
onUploadProgress: (evt: ProgressEvent<XMLHttpRequestEventTarget>): void => {
|
|
||||||
this.uploadProgress = evt.loaded / evt.total
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
this.uploadProgress = 1
|
|
||||||
return await res.json()
|
return await res.json()
|
||||||
},
|
},
|
||||||
async saveImage (data: any) {
|
async saveImage (data: any) {
|
||||||
const res = await xhr.post('/api/save-image', {
|
const res = await fetch('/api/save-image', {
|
||||||
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
@ -179,31 +134,24 @@ export default defineComponent({
|
||||||
return await res.json()
|
return await res.json()
|
||||||
},
|
},
|
||||||
async onSaveImageClick(data: any) {
|
async onSaveImageClick(data: any) {
|
||||||
const res = await this.saveImage(data)
|
await this.saveImage(data)
|
||||||
if (res.ok) {
|
|
||||||
this.dialog = ''
|
this.dialog = ''
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
} else {
|
|
||||||
alert(res.error)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async postToGalleryClick(data: any) {
|
async postToGalleryClick(data: any) {
|
||||||
this.uploading = 'postToGallery'
|
|
||||||
await this.uploadImage(data)
|
await this.uploadImage(data)
|
||||||
this.uploading = ''
|
|
||||||
this.dialog = ''
|
this.dialog = ''
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
},
|
},
|
||||||
async setupGameClick (data: any) {
|
async setupGameClick (data: any) {
|
||||||
this.uploading = 'setupGame'
|
|
||||||
const image = await this.uploadImage(data)
|
const image = await this.uploadImage(data)
|
||||||
this.uploading = ''
|
|
||||||
this.loadImages() // load images in background
|
this.loadImages() // load images in background
|
||||||
this.image = image
|
this.image = image
|
||||||
this.dialog = 'new-game'
|
this.dialog = 'new-game'
|
||||||
},
|
},
|
||||||
async onNewGame(gameSettings: GameSettings) {
|
async onNewGame(gameSettings: GameSettings) {
|
||||||
const res = await xhr.post('/api/newgame', {
|
const res = await fetch('/newgame', {
|
||||||
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,8 @@
|
||||||
<div id="replay">
|
<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" />
|
||||||
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
|
|
||||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||||
|
|
||||||
<div class="overlay" v-if="cuttingPuzzle">
|
|
||||||
<div class="overlay-content">
|
|
||||||
<div>⏳ Cutting puzzle, please wait... ⏳</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<puzzle-status
|
<puzzle-status
|
||||||
:finished="finished"
|
:finished="finished"
|
||||||
:duration="duration"
|
:duration="duration"
|
||||||
|
|
@ -30,8 +23,7 @@
|
||||||
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
||||||
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
||||||
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
||||||
<div class="opener" @click="toggle('info', true)">ℹ️ Info</div>
|
<div class="opener" @click="toggle('help', true)">ℹ️ Help</div>
|
||||||
<div class="opener" @click="toggle('help', true)">⌨️ Hotkeys</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -45,11 +37,9 @@ import Scores from './../components/Scores.vue'
|
||||||
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||||
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||||
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||||
import InfoOverlay from './../components/InfoOverlay.vue'
|
|
||||||
import HelpOverlay from './../components/HelpOverlay.vue'
|
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||||
|
|
||||||
import { main, MODE_REPLAY } from './../game'
|
import { main, MODE_REPLAY } from './../game'
|
||||||
import { Game, Player } from '../../common/Types'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'replay',
|
name: 'replay',
|
||||||
|
|
@ -58,13 +48,12 @@ export default defineComponent({
|
||||||
Scores,
|
Scores,
|
||||||
SettingsOverlay,
|
SettingsOverlay,
|
||||||
PreviewOverlay,
|
PreviewOverlay,
|
||||||
InfoOverlay,
|
|
||||||
HelpOverlay,
|
HelpOverlay,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activePlayers: [] as Array<Player>,
|
activePlayers: [] as Array<any>,
|
||||||
idlePlayers: [] as Array<Player>,
|
idlePlayers: [] as Array<any>,
|
||||||
|
|
||||||
finished: false,
|
finished: false,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|
@ -74,32 +63,22 @@ export default defineComponent({
|
||||||
overlay: '',
|
overlay: '',
|
||||||
|
|
||||||
connectionState: 0,
|
connectionState: 0,
|
||||||
cuttingPuzzle: true,
|
|
||||||
|
|
||||||
g: {
|
g: {
|
||||||
player: {
|
player: {
|
||||||
background: '',
|
background: '',
|
||||||
color: '',
|
color: '',
|
||||||
name: '',
|
name: '',
|
||||||
soundsEnabled: false,
|
|
||||||
soundsVolume: 100,
|
|
||||||
showPlayerNames: true,
|
|
||||||
},
|
},
|
||||||
game: null as Game|null,
|
|
||||||
previewImageUrl: '',
|
previewImageUrl: '',
|
||||||
setHotkeys: (v: boolean) => {},
|
setHotkeys: (v: boolean) => {},
|
||||||
onBgChange: (v: string) => {},
|
onBgChange: (v: string) => {},
|
||||||
onColorChange: (v: string) => {},
|
onColorChange: (v: string) => {},
|
||||||
onNameChange: (v: string) => {},
|
onNameChange: (v: string) => {},
|
||||||
onSoundsEnabledChange: (v: boolean) => {},
|
|
||||||
onSoundsVolumeChange: (v: number) => {},
|
|
||||||
onShowPlayerNamesChange: (v: boolean) => {},
|
|
||||||
replayOnSpeedUp: () => {},
|
replayOnSpeedUp: () => {},
|
||||||
replayOnSpeedDown: () => {},
|
replayOnSpeedDown: () => {},
|
||||||
replayOnPauseToggle: () => {},
|
replayOnPauseToggle: () => {},
|
||||||
connect: () => {},
|
|
||||||
disconnect: () => {},
|
disconnect: () => {},
|
||||||
unload: () => {},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
replay: {
|
replay: {
|
||||||
|
|
@ -121,15 +100,6 @@ export default defineComponent({
|
||||||
this.$watch(() => this.g.player.name, (value: string) => {
|
this.$watch(() => this.g.player.name, (value: string) => {
|
||||||
this.g.onNameChange(value)
|
this.g.onNameChange(value)
|
||||||
})
|
})
|
||||||
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
|
|
||||||
this.g.onSoundsEnabledChange(value)
|
|
||||||
})
|
|
||||||
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
|
|
||||||
this.g.onSoundsVolumeChange(value)
|
|
||||||
})
|
|
||||||
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
|
|
||||||
this.g.onShowPlayerNamesChange(value)
|
|
||||||
})
|
|
||||||
this.g = await main(
|
this.g = await main(
|
||||||
`${this.$route.params.id}`,
|
`${this.$route.params.id}`,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -139,9 +109,8 @@ export default defineComponent({
|
||||||
MODE_REPLAY,
|
MODE_REPLAY,
|
||||||
this.$el,
|
this.$el,
|
||||||
{
|
{
|
||||||
setPuzzleCut: () => { this.cuttingPuzzle = false },
|
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||||
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
|
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||||
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
|
|
||||||
setFinished: (v: boolean) => { this.finished = v },
|
setFinished: (v: boolean) => { this.finished = v },
|
||||||
setDuration: (v: number) => { this.duration = v },
|
setDuration: (v: number) => { this.duration = v },
|
||||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||||
|
|
@ -150,13 +119,10 @@ export default defineComponent({
|
||||||
setConnectionState: (v: number) => { this.connectionState = v },
|
setConnectionState: (v: number) => { this.connectionState = v },
|
||||||
setReplaySpeed: (v: number) => { this.replay.speed = v },
|
setReplaySpeed: (v: number) => { this.replay.speed = v },
|
||||||
setReplayPaused: (v: boolean) => { this.replay.paused = v },
|
setReplayPaused: (v: boolean) => { this.replay.paused = v },
|
||||||
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
|
|
||||||
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
this.g.unload()
|
|
||||||
this.g.disconnect()
|
this.g.disconnect()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
export interface Response {
|
|
||||||
status: number,
|
|
||||||
text: string,
|
|
||||||
json: () => Promise<any>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options {
|
|
||||||
body: FormData|string,
|
|
||||||
headers?: any,
|
|
||||||
onUploadProgress?: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => any,
|
|
||||||
}
|
|
||||||
|
|
||||||
let xhrClientId: string = ''
|
|
||||||
let xhrClientSecret: string = ''
|
|
||||||
const request = async (
|
|
||||||
method: string,
|
|
||||||
url: string,
|
|
||||||
options: Options
|
|
||||||
): Promise<Response> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new window.XMLHttpRequest()
|
|
||||||
xhr.open(method, url, true)
|
|
||||||
xhr.withCredentials = true
|
|
||||||
for (const k in options.headers || {}) {
|
|
||||||
xhr.setRequestHeader(k, options.headers[k])
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.setRequestHeader('Client-Id', xhrClientId)
|
|
||||||
xhr.setRequestHeader('Client-Secret', xhrClientSecret)
|
|
||||||
|
|
||||||
xhr.addEventListener('load', function (ev: ProgressEvent<XMLHttpRequestEventTarget>
|
|
||||||
) {
|
|
||||||
resolve({
|
|
||||||
status: this.status,
|
|
||||||
text: this.responseText,
|
|
||||||
json: async () => JSON.parse(this.responseText),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
xhr.addEventListener('error', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
|
|
||||||
reject(new Error('xhr error'))
|
|
||||||
})
|
|
||||||
if (xhr.upload && options.onUploadProgress) {
|
|
||||||
xhr.upload.addEventListener('progress', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
|
|
||||||
// typescript complains without this extra check
|
|
||||||
if (options.onUploadProgress) {
|
|
||||||
options.onUploadProgress(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
xhr.send(options.body || null)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
request,
|
|
||||||
get: (url: string, options: any): Promise<Response> => {
|
|
||||||
return request('get', url, options)
|
|
||||||
},
|
|
||||||
post: (url: string, options: any): Promise<Response> => {
|
|
||||||
return request('post', url, options)
|
|
||||||
},
|
|
||||||
setClientId: (clientId: string): void => {
|
|
||||||
xhrClientId = clientId
|
|
||||||
},
|
|
||||||
setClientSecret: (clientSecret: string): void => {
|
|
||||||
xhrClientSecret = clientSecret
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,10 @@ import { logger } from '../common/Util'
|
||||||
|
|
||||||
const log = logger('Db.ts')
|
const log = logger('Db.ts')
|
||||||
|
|
||||||
|
// assume 32766 SQLITE_MAX_VARIABLE_NUMBER
|
||||||
|
// @see https://sqlite.org/limits.html
|
||||||
|
const SQLITE_MAX_VARIABLE_NUMBER = 32766
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: create a more specific type for OrderBy.
|
* TODO: create a more specific type for OrderBy.
|
||||||
* It looks like this (example):
|
* It looks like this (example):
|
||||||
|
|
@ -13,12 +17,11 @@ const log = logger('Db.ts')
|
||||||
* {name: 1}, // then by name ascending
|
* {name: 1}, // then by name ascending
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
|
type OrderBy = Array<any>
|
||||||
type Data = Record<string, any>
|
type Data = Record<string, any>
|
||||||
|
type WhereRaw = Record<string, any>
|
||||||
type Params = Array<any>
|
type Params = Array<any>
|
||||||
|
|
||||||
export type WhereRaw = Record<string, any>
|
|
||||||
export type OrderBy = Array<Record<string, 1|-1>>
|
|
||||||
|
|
||||||
interface Where {
|
interface Where {
|
||||||
sql: string
|
sql: string
|
||||||
values: Array<any>
|
values: Array<any>
|
||||||
|
|
@ -89,7 +92,7 @@ class Db {
|
||||||
let prop = '$nin'
|
let prop = '$nin'
|
||||||
if (where[k][prop]) {
|
if (where[k][prop]) {
|
||||||
if (where[k][prop].length > 0) {
|
if (where[k][prop].length > 0) {
|
||||||
wheres.push(k + ' NOT IN (' + where[k][prop].map(() => '?') + ')')
|
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||||
values.push(...where[k][prop])
|
values.push(...where[k][prop])
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -97,7 +100,7 @@ class Db {
|
||||||
prop = '$in'
|
prop = '$in'
|
||||||
if (where[k][prop]) {
|
if (where[k][prop]) {
|
||||||
if (where[k][prop].length > 0) {
|
if (where[k][prop].length > 0) {
|
||||||
wheres.push(k + ' IN (' + where[k][prop].map(() => '?') + ')')
|
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||||
values.push(...where[k][prop])
|
values.push(...where[k][prop])
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -187,15 +190,57 @@ class Db {
|
||||||
return this.get(table, check)[idcol] // get id manually
|
return this.get(table, check)[idcol] // get id manually
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts data into table and returns the last insert id
|
||||||
|
*/
|
||||||
insert (table: string, data: Data): Integer.IntLike {
|
insert (table: string, data: Data): Integer.IntLike {
|
||||||
const keys = Object.keys(data)
|
const keys = Object.keys(data)
|
||||||
const values = keys.map(k => data[k])
|
const values = keys.map(k => data[k])
|
||||||
const sql = 'INSERT INTO '+ table
|
const sql = 'INSERT INTO '+ table
|
||||||
+ ' (' + keys.join(',') + ')'
|
+ ' (' + keys.join(',') + ')'
|
||||||
+ ' VALUES (' + keys.map(() => '?').join(',') + ')'
|
+ ' VALUES (' + keys.map(k => '?').join(',') + ')'
|
||||||
return this.run(sql, values).lastInsertRowid
|
return this.run(sql, values).lastInsertRowid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts multiple datas into table. Returns the total number
|
||||||
|
* of changes.
|
||||||
|
*/
|
||||||
|
insertMany (table: string, datas: Data[]): number {
|
||||||
|
if (datas.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(datas[0])
|
||||||
|
|
||||||
|
const runChunk = (vars: string[], values: any[]) => {
|
||||||
|
const sql = `INSERT INTO ${table}
|
||||||
|
(${keys.join(',')})
|
||||||
|
VALUES ${vars.join(',')}`
|
||||||
|
return this.run(sql, values).changes
|
||||||
|
}
|
||||||
|
|
||||||
|
let len: number = 0
|
||||||
|
let vars: string[] = []
|
||||||
|
let values: any[] = []
|
||||||
|
let changes = 0
|
||||||
|
for (const data of datas) {
|
||||||
|
if (len + keys.length > SQLITE_MAX_VARIABLE_NUMBER) {
|
||||||
|
changes += runChunk(vars, values)
|
||||||
|
len = 0
|
||||||
|
vars = []
|
||||||
|
values = []
|
||||||
|
}
|
||||||
|
len += keys.length
|
||||||
|
vars.push('(' + keys.map(_ => '?').join(',') + ')')
|
||||||
|
values.push(...keys.map(k => data[k]))
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
changes += runChunk(vars, values)
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
|
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
|
||||||
const keys = Object.keys(data)
|
const keys = Object.keys(data)
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,53 @@
|
||||||
import GameCommon from './../common/GameCommon'
|
import GameCommon, { ScoreMode } from './../common/GameCommon'
|
||||||
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode,ImageInfo, Timestamp, GameSettings } from './../common/Types'
|
import Util from './../common/Util'
|
||||||
import Util, { logger } from './../common/Util'
|
|
||||||
import { Rng } from './../common/Rng'
|
import { Rng } from './../common/Rng'
|
||||||
import GameLog from './GameLog'
|
import GameLog from './GameLog'
|
||||||
import { createPuzzle } from './Puzzle'
|
import { createPuzzle } from './Puzzle'
|
||||||
import Protocol from './../common/Protocol'
|
import Protocol from './../common/Protocol'
|
||||||
import GameStorage from './GameStorage'
|
import GameStorage from './GameStorage'
|
||||||
|
|
||||||
const log = logger('Game.ts')
|
|
||||||
|
|
||||||
async function createGameObject(
|
async function createGameObject(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
targetTiles: number,
|
targetTiles: number,
|
||||||
image: ImageInfo,
|
image: { file: string, url: string },
|
||||||
ts: Timestamp,
|
ts: number,
|
||||||
scoreMode: ScoreMode,
|
scoreMode: ScoreMode
|
||||||
shapeMode: ShapeMode,
|
) {
|
||||||
snapMode: SnapMode,
|
|
||||||
creatorUserId: number|null
|
|
||||||
): Promise<Game> {
|
|
||||||
const seed = Util.hash(gameId + ' ' + ts)
|
const seed = Util.hash(gameId + ' ' + ts)
|
||||||
const rng = new Rng(seed)
|
const rng = new Rng(seed)
|
||||||
return {
|
return {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
creatorUserId,
|
|
||||||
rng: { type: 'Rng', obj: rng },
|
rng: { type: 'Rng', obj: rng },
|
||||||
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
|
puzzle: await createPuzzle(rng, targetTiles, image, ts),
|
||||||
players: [],
|
players: [],
|
||||||
evtInfos: {},
|
evtInfos: {},
|
||||||
scoreMode,
|
scoreMode,
|
||||||
shapeMode,
|
|
||||||
snapMode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewGame(
|
async function createGame(
|
||||||
gameSettings: GameSettings,
|
gameId: string,
|
||||||
ts: Timestamp,
|
targetTiles: number,
|
||||||
creatorUserId: number
|
image: { file: string, url: string },
|
||||||
): Promise<string> {
|
ts: number,
|
||||||
let gameId;
|
scoreMode: ScoreMode
|
||||||
do {
|
): Promise<void> {
|
||||||
gameId = Util.uniqId()
|
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
|
||||||
} while (GameCommon.exists(gameId))
|
|
||||||
|
|
||||||
const gameObject = await createGameObject(
|
GameLog.create(gameId)
|
||||||
gameId,
|
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode)
|
||||||
gameSettings.tiles,
|
|
||||||
gameSettings.image,
|
|
||||||
ts,
|
|
||||||
gameSettings.scoreMode,
|
|
||||||
gameSettings.shapeMode,
|
|
||||||
gameSettings.snapMode,
|
|
||||||
creatorUserId
|
|
||||||
)
|
|
||||||
|
|
||||||
GameLog.create(gameId, ts)
|
|
||||||
GameLog.log(
|
|
||||||
gameId,
|
|
||||||
Protocol.LOG_HEADER,
|
|
||||||
1,
|
|
||||||
gameSettings.tiles,
|
|
||||||
gameSettings.image,
|
|
||||||
ts,
|
|
||||||
gameSettings.scoreMode,
|
|
||||||
gameSettings.shapeMode,
|
|
||||||
gameSettings.snapMode,
|
|
||||||
gameObject.creatorUserId
|
|
||||||
)
|
|
||||||
|
|
||||||
GameCommon.setGame(gameObject.id, gameObject)
|
GameCommon.setGame(gameObject.id, gameObject)
|
||||||
GameStorage.setDirty(gameId)
|
GameStorage.setDirty(gameId)
|
||||||
|
|
||||||
return gameId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
|
function addPlayer(gameId: string, playerId: string, ts: number): void {
|
||||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
|
|
||||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||||
|
const diff = ts - GameCommon.getStartTs(gameId)
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, ts)
|
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, diff)
|
||||||
} else {
|
} else {
|
||||||
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, ts)
|
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GameCommon.addPlayer(gameId, playerId, ts)
|
GameCommon.addPlayer(gameId, playerId, ts)
|
||||||
|
|
@ -92,13 +57,12 @@ function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
|
||||||
function handleInput(
|
function handleInput(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
playerId: string,
|
playerId: string,
|
||||||
input: Input,
|
input: any,
|
||||||
ts: Timestamp
|
ts: number
|
||||||
): Array<Change> {
|
): Array<Array<any>> {
|
||||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
|
|
||||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, ts)
|
const diff = ts - GameCommon.getStartTs(gameId)
|
||||||
}
|
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
|
||||||
|
|
||||||
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
|
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
|
||||||
GameStorage.setDirty(gameId)
|
GameStorage.setDirty(gameId)
|
||||||
|
|
@ -107,7 +71,17 @@ function handleInput(
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createGameObject,
|
createGameObject,
|
||||||
createNewGame,
|
createGame,
|
||||||
addPlayer,
|
addPlayer,
|
||||||
handleInput,
|
handleInput,
|
||||||
|
getAllGames: GameCommon.getAllGames,
|
||||||
|
getActivePlayers: GameCommon.getActivePlayers,
|
||||||
|
getFinishedTileCount: GameCommon.getFinishedTileCount,
|
||||||
|
getImageUrl: GameCommon.getImageUrl,
|
||||||
|
getTileCount: GameCommon.getTileCount,
|
||||||
|
exists: GameCommon.exists,
|
||||||
|
playerExists: GameCommon.playerExists,
|
||||||
|
get: GameCommon.get,
|
||||||
|
getStartTs: GameCommon.getStartTs,
|
||||||
|
getFinishTs: GameCommon.getFinishTs,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,51 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import Protocol from '../common/Protocol'
|
|
||||||
import Time from '../common/Time'
|
|
||||||
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Timestamp } from '../common/Types'
|
|
||||||
import { logger } from './../common/Util'
|
import { logger } from './../common/Util'
|
||||||
import { DATA_DIR } from './../server/Dirs'
|
import { DATA_DIR } from './../server/Dirs'
|
||||||
|
|
||||||
const log = logger('GameLog.js')
|
const log = logger('GameLog.js')
|
||||||
|
|
||||||
const LINES_PER_LOG_FILE = 10000
|
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
|
||||||
const POST_GAME_LOG_DURATION = 5 * Time.MIN
|
|
||||||
|
|
||||||
const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
|
const create = (gameId: string) => {
|
||||||
// when not finished yet, always log
|
const file = filename(gameId)
|
||||||
if (!finishTs) {
|
if (!fs.existsSync(file)) {
|
||||||
return true
|
fs.appendFileSync(file, '')
|
||||||
}
|
|
||||||
|
|
||||||
// in finished games, log max POST_GAME_LOG_DURATION after
|
|
||||||
// the game finished, to record winning dance moves etc :P
|
|
||||||
const timeSinceGameEnd = currentTs - finishTs
|
|
||||||
return timeSinceGameEnd <= POST_GAME_LOG_DURATION
|
|
||||||
}
|
|
||||||
|
|
||||||
export const filename = (gameId: string, offset: number) => `${DATA_DIR}/log_${gameId}-${offset}.log`
|
|
||||||
export const idxname = (gameId: string) => `${DATA_DIR}/log_${gameId}.idx.log`
|
|
||||||
|
|
||||||
const create = (gameId: string, ts: Timestamp): void => {
|
|
||||||
const idxfile = idxname(gameId)
|
|
||||||
if (!fs.existsSync(idxfile)) {
|
|
||||||
fs.appendFileSync(idxfile, JSON.stringify({
|
|
||||||
gameId: gameId,
|
|
||||||
total: 0,
|
|
||||||
lastTs: ts,
|
|
||||||
currentFile: '',
|
|
||||||
perFile: LINES_PER_LOG_FILE,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = (gameId: string): boolean => {
|
const exists = (gameId: string) => {
|
||||||
const idxfile = idxname(gameId)
|
const file = filename(gameId)
|
||||||
return fs.existsSync(idxfile)
|
return fs.existsSync(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _log = (gameId: string, type: number, ...args: Array<any>): void => {
|
const _log = (gameId: string, ...args: Array<any>) => {
|
||||||
const idxfile = idxname(gameId)
|
const file = filename(gameId)
|
||||||
if (!fs.existsSync(idxfile)) {
|
if (!fs.existsSync(file)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const str = JSON.stringify(args)
|
||||||
const idxObj = JSON.parse(fs.readFileSync(idxfile, 'utf-8'))
|
fs.appendFileSync(file, str + "\n")
|
||||||
if (idxObj.total % idxObj.perFile === 0) {
|
|
||||||
idxObj.currentFile = filename(gameId, idxObj.total)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tsIdx = type === Protocol.LOG_HEADER ? 3 : (args.length - 1)
|
|
||||||
const ts: Timestamp = args[tsIdx]
|
|
||||||
if (type !== Protocol.LOG_HEADER) {
|
|
||||||
// for everything but header save the diff to last log entry
|
|
||||||
args[tsIdx] = ts - idxObj.lastTs
|
|
||||||
}
|
|
||||||
const line = JSON.stringify([type, ...args]).slice(1, -1)
|
|
||||||
fs.appendFileSync(idxObj.currentFile, line + "\n")
|
|
||||||
|
|
||||||
idxObj.total++
|
|
||||||
idxObj.lastTs = ts
|
|
||||||
fs.writeFileSync(idxfile, JSON.stringify(idxObj))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = (
|
const get = (gameId: string) => {
|
||||||
gameId: string,
|
const file = filename(gameId)
|
||||||
offset: number = 0,
|
|
||||||
): any[] => {
|
|
||||||
const idxfile = idxname(gameId)
|
|
||||||
if (!fs.existsSync(idxfile)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = filename(gameId, offset)
|
|
||||||
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")
|
||||||
const log = lines.filter(line => !!line).map(line => {
|
return lines.filter((line: string) => !!line).map((line: string) => {
|
||||||
return JSON.parse(`[${line}]`)
|
try {
|
||||||
})
|
return JSON.parse(line)
|
||||||
if (offset === 0 && log.length > 0) {
|
} catch (e) {
|
||||||
log[0][5] = DefaultScoreMode(log[0][5])
|
log.log(line)
|
||||||
log[0][6] = DefaultShapeMode(log[0][6])
|
log.log(e)
|
||||||
log[0][7] = DefaultSnapMode(log[0][7])
|
|
||||||
log[0][8] = log[0][8] || null // creatorUserId
|
|
||||||
}
|
}
|
||||||
return log
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
shouldLog,
|
|
||||||
create,
|
create,
|
||||||
exists,
|
exists,
|
||||||
log: _log,
|
log: _log,
|
||||||
get,
|
get,
|
||||||
filename,
|
|
||||||
idxname,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,21 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import GameCommon from './../common/GameCommon'
|
import GameCommon, { Piece, ScoreMode } from './../common/GameCommon'
|
||||||
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Game, Piece } from './../common/Types'
|
|
||||||
import Util, { logger } from './../common/Util'
|
import Util, { logger } from './../common/Util'
|
||||||
import { Rng } from './../common/Rng'
|
import { Rng } from './../common/Rng'
|
||||||
import { DATA_DIR } from './Dirs'
|
import { DATA_DIR } from './Dirs'
|
||||||
import Time from './../common/Time'
|
import Time from './../common/Time'
|
||||||
import Db from './Db'
|
|
||||||
|
|
||||||
const log = logger('GameStorage.js')
|
const log = logger('GameStorage.js')
|
||||||
|
|
||||||
const dirtyGames: Record<string, boolean> = {}
|
const DIRTY_GAMES = {} as any
|
||||||
function setDirty(gameId: string): void {
|
function setDirty(gameId: string): void {
|
||||||
dirtyGames[gameId] = true
|
DIRTY_GAMES[gameId] = true
|
||||||
}
|
}
|
||||||
function setClean(gameId: string): void {
|
function setClean(gameId: string): void {
|
||||||
delete dirtyGames[gameId]
|
delete DIRTY_GAMES[gameId]
|
||||||
}
|
|
||||||
function loadGamesFromDb(db: Db): void {
|
|
||||||
const gameRows = db.getMany('games')
|
|
||||||
for (const gameRow of gameRows) {
|
|
||||||
loadGameFromDb(db, gameRow.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGameFromDb(db: Db, gameId: string): void {
|
function loadGames(): void {
|
||||||
const gameRow = db.get('games', {id: gameId})
|
|
||||||
|
|
||||||
let game
|
|
||||||
try {
|
|
||||||
game = JSON.parse(gameRow.data)
|
|
||||||
} catch {
|
|
||||||
log.log(`[ERR] unable to load game from db ${gameId}`);
|
|
||||||
}
|
|
||||||
if (typeof game.puzzle.data.started === 'undefined') {
|
|
||||||
game.puzzle.data.started = gameRow.created
|
|
||||||
}
|
|
||||||
if (typeof game.puzzle.data.finished === 'undefined') {
|
|
||||||
game.puzzle.data.finished = gameRow.finished
|
|
||||||
}
|
|
||||||
if (!Array.isArray(game.players)) {
|
|
||||||
game.players = Object.values(game.players)
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameObject: Game = storeDataToGame(game, game.creator_user_id)
|
|
||||||
GameCommon.setGame(gameObject.id, gameObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistGamesToDb(db: Db): void {
|
|
||||||
for (const gameId of Object.keys(dirtyGames)) {
|
|
||||||
persistGameToDb(db, gameId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistGameToDb(db: Db, gameId: string): void {
|
|
||||||
const game: Game|null = GameCommon.get(gameId)
|
|
||||||
if (!game) {
|
|
||||||
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.id in dirtyGames) {
|
|
||||||
setClean(game.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.upsert('games', {
|
|
||||||
id: game.id,
|
|
||||||
|
|
||||||
creator_user_id: game.creatorUserId,
|
|
||||||
image_id: game.puzzle.info.image?.id,
|
|
||||||
|
|
||||||
created: game.puzzle.data.started,
|
|
||||||
finished: game.puzzle.data.finished,
|
|
||||||
|
|
||||||
data: gameToStoreData(game)
|
|
||||||
}, {
|
|
||||||
id: game.id,
|
|
||||||
})
|
|
||||||
log.info(`[INFO] persisted game ${game.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
function loadGamesFromDisk(): 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$/)
|
||||||
|
|
@ -90,14 +23,11 @@ function loadGamesFromDisk(): void {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const gameId = m[1]
|
const gameId = m[1]
|
||||||
loadGameFromDisk(gameId)
|
loadGame(gameId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function loadGame(gameId: string): void {
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
function loadGameFromDisk(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
|
||||||
|
|
@ -111,36 +41,39 @@ function loadGameFromDisk(gameId: string): void {
|
||||||
}
|
}
|
||||||
if (typeof game.puzzle.data.finished === 'undefined') {
|
if (typeof game.puzzle.data.finished === 'undefined') {
|
||||||
const unfinished = game.puzzle.tiles
|
const unfinished = game.puzzle.tiles
|
||||||
.map(Util.decodePiece)
|
.map(Util.decodeTile)
|
||||||
.find((t: Piece) => t.owner !== -1)
|
.find((t: Piece) => 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)) {
|
||||||
game.players = Object.values(game.players)
|
game.players = Object.values(game.players)
|
||||||
}
|
}
|
||||||
const gameObject: Game = storeDataToGame(game, null)
|
const gameObject = {
|
||||||
|
id: game.id,
|
||||||
|
rng: {
|
||||||
|
type: game.rng ? game.rng.type : '_fake_',
|
||||||
|
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
|
||||||
|
},
|
||||||
|
puzzle: game.puzzle,
|
||||||
|
players: game.players,
|
||||||
|
evtInfos: {},
|
||||||
|
scoreMode: game.scoreMode || ScoreMode.FINAL,
|
||||||
|
}
|
||||||
GameCommon.setGame(gameObject.id, gameObject)
|
GameCommon.setGame(gameObject.id, gameObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeDataToGame(storeData: any, creatorUserId: number|null): Game {
|
function persistGames(): void {
|
||||||
return {
|
for (const gameId of Object.keys(DIRTY_GAMES)) {
|
||||||
id: storeData.id,
|
persistGame(gameId)
|
||||||
creatorUserId,
|
|
||||||
rng: {
|
|
||||||
type: storeData.rng ? storeData.rng.type : '_fake_',
|
|
||||||
obj: storeData.rng ? Rng.unserialize(storeData.rng.obj) : new Rng(0),
|
|
||||||
},
|
|
||||||
puzzle: storeData.puzzle,
|
|
||||||
players: storeData.players,
|
|
||||||
evtInfos: {},
|
|
||||||
scoreMode: DefaultScoreMode(storeData.scoreMode),
|
|
||||||
shapeMode: DefaultShapeMode(storeData.shapeMode),
|
|
||||||
snapMode: DefaultSnapMode(storeData.snapMode),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function gameToStoreData(game: Game): string {
|
function persistGame(gameId: string): void {
|
||||||
return JSON.stringify({
|
const game = GameCommon.get(gameId)
|
||||||
|
if (game.id in DIRTY_GAMES) {
|
||||||
|
setClean(game.id)
|
||||||
|
}
|
||||||
|
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
rng: {
|
rng: {
|
||||||
type: game.rng.type,
|
type: game.rng.type,
|
||||||
|
|
@ -149,20 +82,14 @@ function gameToStoreData(game: Game): string {
|
||||||
puzzle: game.puzzle,
|
puzzle: game.puzzle,
|
||||||
players: game.players,
|
players: game.players,
|
||||||
scoreMode: game.scoreMode,
|
scoreMode: game.scoreMode,
|
||||||
shapeMode: game.shapeMode,
|
}))
|
||||||
snapMode: game.snapMode,
|
log.info(`[INFO] persisted game ${game.id}`)
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// disk functions are deprecated
|
loadGames,
|
||||||
loadGamesFromDisk,
|
loadGame,
|
||||||
loadGameFromDisk,
|
persistGames,
|
||||||
|
persistGame,
|
||||||
loadGamesFromDb,
|
|
||||||
loadGameFromDb,
|
|
||||||
persistGamesToDb,
|
|
||||||
persistGameToDb,
|
|
||||||
|
|
||||||
setDirty,
|
setDirty,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,10 @@ import exif from 'exif'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
|
||||||
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
|
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
|
||||||
import Db, { OrderBy, WhereRaw } from './Db'
|
import Db from './Db'
|
||||||
import { Dim } from '../common/Geometry'
|
import { Dim } from '../common/Geometry'
|
||||||
import Util, { logger } from '../common/Util'
|
|
||||||
import { Tag, ImageInfo } from '../common/Types'
|
|
||||||
|
|
||||||
const log = logger('Images.ts')
|
const resizeImage = async (filename: string) => {
|
||||||
|
|
||||||
const resizeImage = async (filename: string): Promise<void> => {
|
|
||||||
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -34,82 +30,55 @@ const resizeImage = async (filename: string): Promise<void> => {
|
||||||
[150, 100],
|
[150, 100],
|
||||||
[375, 210],
|
[375, 210],
|
||||||
]
|
]
|
||||||
for (const [w,h] of sizes) {
|
for (let [w,h] of sizes) {
|
||||||
log.info(w, h, imagePath)
|
console.log(w, h, imagePath)
|
||||||
await sharpImg
|
await sharpImg.resize(w, h, { fit: 'contain' }).toFile(`${imageOutPath}-${w}x${h}.webp`)
|
||||||
.resize(w, h, { fit: 'contain' })
|
|
||||||
.toFile(`${imageOutPath}-${w}x${h}.webp`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExifOrientation(imagePath: string): Promise<number> {
|
async function getExifOrientation(imagePath: string) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
new exif.ExifImage({ image: imagePath }, (error, exifData) => {
|
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
|
||||||
if (error) {
|
if (error) {
|
||||||
resolve(0)
|
resolve(0)
|
||||||
} else {
|
} else {
|
||||||
resolve(exifData.image.Orientation || 0)
|
resolve(exifData.image.Orientation)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllTags = (db: Db): Tag[] => {
|
const getTags = (db: Db, imageId: number) => {
|
||||||
const query = `
|
|
||||||
select c.id, c.slug, c.title, count(*) as total from categories c
|
|
||||||
inner join image_x_category ixc on c.id = ixc.category_id
|
|
||||||
inner join images i on i.id = ixc.image_id
|
|
||||||
group by c.id order by total desc;`
|
|
||||||
return db._getMany(query).map(row => ({
|
|
||||||
id: parseInt(row.id, 10) || 0,
|
|
||||||
slug: row.slug,
|
|
||||||
title: row.title,
|
|
||||||
total: parseInt(row.total, 10) || 0,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTags = (db: Db, imageId: number): Tag[] => {
|
|
||||||
const query = `
|
const query = `
|
||||||
select * from categories c
|
select * from categories c
|
||||||
inner join image_x_category ixc on c.id = ixc.category_id
|
inner join image_x_category ixc on c.id = ixc.category_id
|
||||||
where ixc.image_id = ?`
|
where ixc.image_id = ?`
|
||||||
return db._getMany(query, [imageId]).map(row => ({
|
return db._getMany(query, [imageId])
|
||||||
id: parseInt(row.id, 10) || 0,
|
|
||||||
slug: row.slug,
|
|
||||||
title: row.title,
|
|
||||||
total: 0,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageFromDb = (db: Db, imageId: number): ImageInfo => {
|
const imageFromDb = (db: Db, imageId: number) => {
|
||||||
const i = db.get('images', { id: imageId })
|
const i = db.get('images', { id: imageId })
|
||||||
return {
|
return {
|
||||||
id: i.id,
|
id: i.id,
|
||||||
uploaderUserId: i.uploader_user_id,
|
|
||||||
filename: i.filename,
|
filename: i.filename,
|
||||||
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
tags: getTags(db, i.id),
|
tags: getTags(db, i.id) as any[],
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
width: i.width,
|
|
||||||
height: i.height,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allImagesFromDb = (
|
const allImagesFromDb = (db: Db, tagSlugs: string[], sort: string) => {
|
||||||
db: Db,
|
const sortMap = {
|
||||||
tagSlugs: string[],
|
|
||||||
orderBy: string
|
|
||||||
): ImageInfo[] => {
|
|
||||||
const orderByMap = {
|
|
||||||
alpha_asc: [{filename: 1}],
|
alpha_asc: [{filename: 1}],
|
||||||
alpha_desc: [{filename: -1}],
|
alpha_desc: [{filename: -1}],
|
||||||
date_asc: [{created: 1}],
|
date_asc: [{created: 1}],
|
||||||
date_desc: [{created: -1}],
|
date_desc: [{created: -1}],
|
||||||
} as Record<string, OrderBy>
|
} as Record<string, any>
|
||||||
|
|
||||||
// TODO: .... clean up
|
// TODO: .... clean up
|
||||||
const wheresRaw: WhereRaw = {}
|
const wheresRaw: Record<string, any> = {}
|
||||||
if (tagSlugs.length > 0) {
|
if (tagSlugs.length > 0) {
|
||||||
const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
|
const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
|
||||||
if (!c) {
|
if (!c) {
|
||||||
|
|
@ -127,52 +96,42 @@ inner join images i on i.id = ixc.image_id ${where.sql};
|
||||||
}
|
}
|
||||||
wheresRaw['id'] = {'$in': ids}
|
wheresRaw['id'] = {'$in': ids}
|
||||||
}
|
}
|
||||||
const images = db.getMany('images', wheresRaw, orderByMap[orderBy])
|
const images = db.getMany('images', wheresRaw, sortMap[sort])
|
||||||
|
|
||||||
return images.map(i => ({
|
return images.map(i => ({
|
||||||
id: i.id as number,
|
id: i.id as number,
|
||||||
uploaderUserId: i.uploader_user_id,
|
|
||||||
filename: i.filename,
|
filename: i.filename,
|
||||||
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
tags: getTags(db, i.id),
|
tags: getTags(db, i.id) as any[],
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
width: i.width,
|
|
||||||
height: i.height,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const allImagesFromDisk = (tags: string[], sort: string) => {
|
||||||
* @deprecated old function, now database is used
|
|
||||||
*/
|
|
||||||
const allImagesFromDisk = (
|
|
||||||
tags: string[],
|
|
||||||
sort: string
|
|
||||||
): ImageInfo[] => {
|
|
||||||
let images = fs.readdirSync(UPLOAD_DIR)
|
let images = fs.readdirSync(UPLOAD_DIR)
|
||||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
id: 0,
|
id: 0,
|
||||||
uploaderUserId: null,
|
|
||||||
filename: f,
|
filename: f,
|
||||||
|
file: `${UPLOAD_DIR}/${f}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||||
title: f.replace(/\.[a-z]+$/, ''),
|
title: f.replace(/\.[a-z]+$/, ''),
|
||||||
tags: [] as Tag[],
|
tags: [] as any[],
|
||||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||||
width: 0, // may have to fill when the function is used again
|
|
||||||
height: 0, // may have to fill when the function is used again
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'alpha_asc':
|
case 'alpha_asc':
|
||||||
images = images.sort((a, b) => {
|
images = images.sort((a, b) => {
|
||||||
return a.filename > b.filename ? 1 : -1
|
return a.file > b.file ? 1 : -1
|
||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'alpha_desc':
|
case 'alpha_desc':
|
||||||
images = images.sort((a, b) => {
|
images = images.sort((a, b) => {
|
||||||
return a.filename < b.filename ? 1 : -1
|
return a.file < b.file ? 1 : -1
|
||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -193,7 +152,7 @@ const allImagesFromDisk = (
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDimensions(imagePath: string): Promise<Dim> {
|
async function getDimensions(imagePath: string): Promise<Dim> {
|
||||||
const 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
|
||||||
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
||||||
|
|
@ -209,26 +168,10 @@ async function getDimensions(imagePath: string): Promise<Dim> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTags = (db: Db, imageId: number, tags: string[]): void => {
|
|
||||||
db.delete('image_x_category', { image_id: imageId })
|
|
||||||
tags.forEach((tag: string) => {
|
|
||||||
const slug = Util.slug(tag)
|
|
||||||
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
|
|
||||||
if (id) {
|
|
||||||
db.insert('image_x_category', {
|
|
||||||
image_id: imageId,
|
|
||||||
category_id: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
allImagesFromDisk,
|
allImagesFromDisk,
|
||||||
imageFromDb,
|
imageFromDb,
|
||||||
allImagesFromDb,
|
allImagesFromDb,
|
||||||
getAllTags,
|
|
||||||
resizeImage,
|
resizeImage,
|
||||||
getDimensions,
|
getDimensions,
|
||||||
setTags,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import Util from './../common/Util'
|
import Util from './../common/Util'
|
||||||
import { Rng } from './../common/Rng'
|
import { Rng } from './../common/Rng'
|
||||||
import Images from './Images'
|
import Images from './Images'
|
||||||
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle, ShapeMode, ImageInfo } from '../common/Types'
|
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/GameCommon'
|
||||||
import { Dim, Point } from '../common/Geometry'
|
import { Point } from '../common/Geometry'
|
||||||
import { UPLOAD_DIR } from './Dirs'
|
|
||||||
|
|
||||||
export interface PuzzleCreationInfo {
|
interface PuzzleCreationInfo {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
tileSize: number
|
tileSize: number
|
||||||
|
|
@ -23,11 +22,10 @@ const TILE_SIZE = 64
|
||||||
async function createPuzzle(
|
async function createPuzzle(
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
targetTiles: number,
|
targetTiles: number,
|
||||||
image: ImageInfo,
|
image: { file: string, url: string },
|
||||||
ts: number,
|
ts: number
|
||||||
shapeMode: ShapeMode
|
|
||||||
): Promise<Puzzle> {
|
): Promise<Puzzle> {
|
||||||
const imagePath = `${UPLOAD_DIR}/${image.filename}`
|
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
|
||||||
|
|
@ -35,18 +33,22 @@ async function createPuzzle(
|
||||||
if (!dim.w || !dim.h) {
|
if (!dim.w || !dim.h) {
|
||||||
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
|
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
|
||||||
}
|
}
|
||||||
const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles)
|
const info: PuzzleCreationInfo = determinePuzzleInfo(
|
||||||
|
dim.w,
|
||||||
|
dim.h,
|
||||||
|
targetTiles
|
||||||
|
)
|
||||||
|
|
||||||
const rawPieces = new Array(info.tiles)
|
let tiles = new Array(info.tiles)
|
||||||
for (let i = 0; i < rawPieces.length; i++) {
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
rawPieces[i] = { idx: i }
|
tiles[i] = { idx: i }
|
||||||
}
|
}
|
||||||
const shapes = determinePuzzleTileShapes(rng, info, shapeMode)
|
const shapes = determinePuzzleTileShapes(rng, info)
|
||||||
|
|
||||||
let positions: Point[] = new Array(info.tiles)
|
let positions: Point[] = new Array(info.tiles)
|
||||||
for (const piece of rawPieces) {
|
for (let tile of tiles) {
|
||||||
const coord = Util.coordByPieceIdx(info, piece.idx)
|
const coord = Util.coordByTileIdx(info, tile.idx)
|
||||||
positions[piece.idx] = {
|
positions[tile.idx] = {
|
||||||
// instead of info.tileSize, we use info.tileDrawSize
|
// instead of info.tileSize, we use info.tileDrawSize
|
||||||
// to spread the tiles a bit
|
// to spread the tiles a bit
|
||||||
x: coord.x * info.tileSize * 1.5,
|
x: coord.x * info.tileSize * 1.5,
|
||||||
|
|
@ -58,7 +60,7 @@ async function createPuzzle(
|
||||||
const tableHeight = info.height * 3
|
const tableHeight = info.height * 3
|
||||||
|
|
||||||
const off = info.tileSize * 1.5
|
const off = info.tileSize * 1.5
|
||||||
const last: Point = {
|
let last: Point = {
|
||||||
x: info.width - (1 * off),
|
x: info.width - (1 * off),
|
||||||
y: info.height - (2 * off),
|
y: info.height - (2 * off),
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +70,7 @@ async function createPuzzle(
|
||||||
let diffX = off
|
let diffX = off
|
||||||
let diffY = 0
|
let diffY = 0
|
||||||
let index = 0
|
let index = 0
|
||||||
for (const pos of positions) {
|
for (let pos of positions) {
|
||||||
pos.x = last.x
|
pos.x = last.x
|
||||||
pos.y = last.y
|
pos.y = last.y
|
||||||
last.x+=diffX
|
last.x+=diffX
|
||||||
|
|
@ -95,9 +97,9 @@ async function createPuzzle(
|
||||||
// then shuffle the positions
|
// then shuffle the positions
|
||||||
positions = rng.shuffle(positions)
|
positions = rng.shuffle(positions)
|
||||||
|
|
||||||
const pieces: Array<EncodedPiece> = rawPieces.map(piece => {
|
const pieces: Array<EncodedPiece> = tiles.map(tile => {
|
||||||
return Util.encodePiece({
|
return Util.encodeTile({
|
||||||
idx: piece.idx, // index of tile in the array
|
idx: tile.idx, // index of tile in the array
|
||||||
group: 0, // if grouped with other tiles
|
group: 0, // if grouped with other tiles
|
||||||
z: 0, // z index of the tile
|
z: 0, // z index of the tile
|
||||||
|
|
||||||
|
|
@ -110,7 +112,7 @@ async function createPuzzle(
|
||||||
// physical current position of the tile (x/y in pixels)
|
// physical current position of the tile (x/y in pixels)
|
||||||
// this position is the initial position only and is the
|
// this position is the initial position only and is the
|
||||||
// value that changes when moving a tile
|
// value that changes when moving a tile
|
||||||
pos: positions[piece.idx],
|
pos: positions[tile.idx],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -135,8 +137,7 @@ async function createPuzzle(
|
||||||
},
|
},
|
||||||
// information that was used to create the puzzle
|
// information that was used to create the puzzle
|
||||||
targetTiles: targetTiles,
|
targetTiles: targetTiles,
|
||||||
imageUrl, // todo: remove
|
imageUrl,
|
||||||
image: image,
|
|
||||||
|
|
||||||
width: info.width, // actual puzzle width (same as bitmap.width)
|
width: info.width, // actual puzzle width (same as bitmap.width)
|
||||||
height: info.height, // actual puzzle height (same as bitmap.height)
|
height: info.height, // actual puzzle height (same as bitmap.height)
|
||||||
|
|
@ -160,27 +161,16 @@ async function createPuzzle(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function determineTabs (shapeMode: ShapeMode): number[] {
|
|
||||||
switch(shapeMode) {
|
|
||||||
case ShapeMode.ANY:
|
|
||||||
return [-1, 0, 1]
|
|
||||||
case ShapeMode.FLAT:
|
|
||||||
return [0]
|
|
||||||
case ShapeMode.NORMAL:
|
|
||||||
default:
|
|
||||||
return [-1, 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function determinePuzzleTileShapes(
|
function determinePuzzleTileShapes(
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
info: PuzzleCreationInfo,
|
info: PuzzleCreationInfo
|
||||||
shapeMode: ShapeMode
|
|
||||||
): Array<EncodedPieceShape> {
|
): Array<EncodedPieceShape> {
|
||||||
const tabs: number[] = determineTabs(shapeMode)
|
const tabs = [-1, 1]
|
||||||
|
|
||||||
const shapes: Array<PieceShape> = new Array(info.tiles)
|
const shapes: Array<PieceShape> = new Array(info.tiles)
|
||||||
for (let i = 0; i < info.tiles; i++) {
|
for (let i = 0; i < info.tiles; i++) {
|
||||||
const coord = Util.coordByPieceIdx(info, i)
|
let coord = Util.coordByTileIdx(info, i)
|
||||||
shapes[i] = {
|
shapes[i] = {
|
||||||
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
|
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
|
||||||
right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs),
|
right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs),
|
||||||
|
|
@ -191,12 +181,9 @@ function determinePuzzleTileShapes(
|
||||||
return shapes.map(Util.encodeShape)
|
return shapes.map(Util.encodeShape)
|
||||||
}
|
}
|
||||||
|
|
||||||
const determineTilesXY = (
|
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
|
||||||
dim: Dim,
|
const w_ = w < h ? (w * h) : (w * w)
|
||||||
targetTiles: number
|
const h_ = w < h ? (h * h) : (w * h)
|
||||||
): { tilesX: number, tilesY: number } => {
|
|
||||||
const w_ = dim.w < dim.h ? (dim.w * dim.h) : (dim.w * dim.w)
|
|
||||||
const h_ = dim.w < dim.h ? (dim.h * dim.h) : (dim.w * dim.h)
|
|
||||||
let size = 0
|
let size = 0
|
||||||
let tiles = 0
|
let tiles = 0
|
||||||
do {
|
do {
|
||||||
|
|
@ -211,10 +198,11 @@ const determineTilesXY = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const determinePuzzleInfo = (
|
const determinePuzzleInfo = (
|
||||||
dim: Dim,
|
w: number,
|
||||||
|
h: number,
|
||||||
targetTiles: number
|
targetTiles: number
|
||||||
): PuzzleCreationInfo => {
|
): PuzzleCreationInfo => {
|
||||||
const {tilesX, tilesY} = determineTilesXY(dim, targetTiles)
|
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
|
||||||
const tiles = tilesX * tilesY
|
const tiles = tilesX * tilesY
|
||||||
const tileSize = TILE_SIZE
|
const tileSize = TILE_SIZE
|
||||||
const width = tilesX * tileSize
|
const width = tilesX * tileSize
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import Time from '../common/Time'
|
|
||||||
import Db from './Db'
|
|
||||||
|
|
||||||
const TABLE = 'users'
|
|
||||||
|
|
||||||
const HEADER_CLIENT_ID = 'client-id'
|
|
||||||
const HEADER_CLIENT_SECRET = 'client-secret'
|
|
||||||
|
|
||||||
const getOrCreateUser = (db: Db, req: any): any => {
|
|
||||||
let user = getUser(db, req)
|
|
||||||
if (!user) {
|
|
||||||
db.insert(TABLE, {
|
|
||||||
'client_id': req.headers[HEADER_CLIENT_ID],
|
|
||||||
'client_secret': req.headers[HEADER_CLIENT_SECRET],
|
|
||||||
'created': Time.timestamp(),
|
|
||||||
})
|
|
||||||
user = getUser(db, req)
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUser = (db: Db, req: any): any => {
|
|
||||||
const user = db.get(TABLE, {
|
|
||||||
'client_id': req.headers[HEADER_CLIENT_ID],
|
|
||||||
'client_secret': req.headers[HEADER_CLIENT_SECRET],
|
|
||||||
})
|
|
||||||
if (user) {
|
|
||||||
user.id = parseInt(user.id, 10)
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getOrCreateUser,
|
|
||||||
getUser,
|
|
||||||
}
|
|
||||||
|
|
@ -14,17 +14,17 @@ config = {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class EvtBus {
|
class EvtBus {
|
||||||
private _on: Record<string, Function[]>
|
private _on: any
|
||||||
constructor() {
|
constructor() {
|
||||||
this._on = {}
|
this._on = {} as any
|
||||||
}
|
}
|
||||||
|
|
||||||
on (type: string, callback: Function): void {
|
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: string, ...args: Array<any>): void {
|
dispatch(type: string, ...args: Array<any>) {
|
||||||
(this._on[type] || []).forEach((cb: Function) => {
|
(this._on[type] || []).forEach((cb: Function) => {
|
||||||
cb(...args)
|
cb(...args)
|
||||||
})
|
})
|
||||||
|
|
@ -43,20 +43,20 @@ class WebSocketServer {
|
||||||
this.evt = new EvtBus()
|
this.evt = new EvtBus()
|
||||||
}
|
}
|
||||||
|
|
||||||
on (type: string, callback: Function): void {
|
on(type: string, callback: Function) {
|
||||||
this.evt.on(type, callback)
|
this.evt.on(type, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
listen (): void {
|
listen() {
|
||||||
this._websocketserver = new WebSocket.Server(this.config)
|
this._websocketserver = new WebSocket.Server(this.config)
|
||||||
this._websocketserver.on('connection', (socket: WebSocket, request: Request): void => {
|
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: WebSocket.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})
|
||||||
})
|
})
|
||||||
|
|
@ -66,13 +66,13 @@ class WebSocketServer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
close (): void {
|
close() {
|
||||||
if (this._websocketserver) {
|
if (this._websocketserver) {
|
||||||
this._websocketserver.close()
|
this._websocketserver.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOne (data: any, socket: WebSocket): void {
|
notifyOne(data: any, socket: WebSocket) {
|
||||||
socket.send(JSON.stringify(data))
|
socket.send(JSON.stringify(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import WebSocketServer from './WebSocketServer'
|
import WebSocketServer from './WebSocketServer'
|
||||||
import WebSocket from 'ws'
|
import WebSocket from 'ws'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import compression from 'compression'
|
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import Protocol from './../common/Protocol'
|
import Protocol from './../common/Protocol'
|
||||||
import Util, { logger } from './../common/Util'
|
import Util, { logger } from './../common/Util'
|
||||||
import Game from './Game'
|
import Game from './Game'
|
||||||
|
import bodyParser from 'body-parser'
|
||||||
import v8 from 'v8'
|
import v8 from 'v8'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import GameLog from './GameLog'
|
import GameLog from './GameLog'
|
||||||
|
|
@ -16,13 +16,11 @@ import {
|
||||||
DB_FILE,
|
DB_FILE,
|
||||||
DB_PATCHES_DIR,
|
DB_PATCHES_DIR,
|
||||||
PUBLIC_DIR,
|
PUBLIC_DIR,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR
|
||||||
} from './Dirs'
|
} from './Dirs'
|
||||||
import GameCommon from '../common/GameCommon'
|
import { GameSettings, ScoreMode } from '../common/GameCommon'
|
||||||
import { ServerEvent, Game as GameType, GameSettings } from '../common/Types'
|
|
||||||
import GameStorage from './GameStorage'
|
import GameStorage from './GameStorage'
|
||||||
import Db from './Db'
|
import Db from './Db'
|
||||||
import Users from './Users'
|
|
||||||
|
|
||||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||||
db.patch()
|
db.patch()
|
||||||
|
|
@ -48,8 +46,6 @@ const port = config.http.port
|
||||||
const hostname = config.http.hostname
|
const hostname = config.http.hostname
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use(compression())
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: UPLOAD_DIR,
|
destination: UPLOAD_DIR,
|
||||||
filename: function (req, file, cb) {
|
filename: function (req, file, cb) {
|
||||||
|
|
@ -58,76 +54,33 @@ const storage = multer.diskStorage({
|
||||||
})
|
})
|
||||||
const upload = multer({storage}).single('file');
|
const upload = multer({storage}).single('file');
|
||||||
|
|
||||||
app.get('/api/me', (req, res): void => {
|
app.get('/api/conf', (req, res) => {
|
||||||
let user = Users.getUser(db, req)
|
|
||||||
res.send({
|
|
||||||
id: user ? user.id : null,
|
|
||||||
created: user ? user.created : null,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/api/conf', (req, res): void => {
|
|
||||||
res.send({
|
res.send({
|
||||||
WS_ADDRESS: config.ws.connectstring,
|
WS_ADDRESS: config.ws.connectstring,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/api/replay-data', async (req, res): Promise<void> => {
|
app.get('/api/newgame-data', (req, res) => {
|
||||||
const q: Record<string, any> = req.query
|
const q = req.query as any
|
||||||
const offset = parseInt(q.offset, 10) || 0
|
|
||||||
if (offset < 0) {
|
|
||||||
res.status(400).send({ reason: 'bad offset' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const size = parseInt(q.size, 10) || 10000
|
|
||||||
if (size < 0 || size > 10000) {
|
|
||||||
res.status(400).send({ reason: 'bad size' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const gameId = q.gameId || ''
|
|
||||||
if (!GameLog.exists(q.gameId)) {
|
|
||||||
res.status(404).send({ reason: 'no log found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const log = GameLog.get(gameId, offset)
|
|
||||||
let game: GameType|null = null
|
|
||||||
if (offset === 0) {
|
|
||||||
// also need the game
|
|
||||||
game = await Game.createGameObject(
|
|
||||||
gameId,
|
|
||||||
log[0][2],
|
|
||||||
log[0][3], // must be ImageInfo
|
|
||||||
log[0][4],
|
|
||||||
log[0][5],
|
|
||||||
log[0][6],
|
|
||||||
log[0][7],
|
|
||||||
log[0][8], // creatorUserId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
res.send({ log, game: game ? Util.encodeGame(game) : null })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/api/newgame-data', (req, res): void => {
|
|
||||||
const q: Record<string, any> = req.query
|
|
||||||
const tagSlugs: string[] = q.tags ? q.tags.split(',') : []
|
const tagSlugs: string[] = q.tags ? q.tags.split(',') : []
|
||||||
res.send({
|
res.send({
|
||||||
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
|
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
|
||||||
tags: Images.getAllTags(db),
|
tags: db.getMany('categories', {}, [{ title: 1 }]),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/api/index-data', (req, res): void => {
|
app.get('/api/index-data', (req, res) => {
|
||||||
const ts = Time.timestamp()
|
const ts = Time.timestamp()
|
||||||
const games = [
|
const games = [
|
||||||
...GameCommon.getAllGames().map((game: GameType) => ({
|
...Game.getAllGames().map((game: any) => ({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
hasReplay: GameLog.exists(game.id),
|
hasReplay: GameLog.exists(game.id),
|
||||||
started: GameCommon.getStartTs(game.id),
|
started: Game.getStartTs(game.id),
|
||||||
finished: GameCommon.getFinishTs(game.id),
|
finished: Game.getFinishTs(game.id),
|
||||||
tilesFinished: GameCommon.getFinishedPiecesCount(game.id),
|
tilesFinished: Game.getFinishedTileCount(game.id),
|
||||||
tilesTotal: GameCommon.getPieceCount(game.id),
|
tilesTotal: Game.getTileCount(game.id),
|
||||||
players: GameCommon.getActivePlayers(game.id, ts).length,
|
players: Game.getActivePlayers(game.id, ts).length,
|
||||||
imageUrl: GameCommon.getImageUrl(game.id),
|
imageUrl: Game.getImageUrl(game.id),
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -143,77 +96,78 @@ interface SaveImageRequestData {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/api/save-image', express.json(), (req, res): void => {
|
const setImageTags = (db: Db, imageId: number, tags: string[]) => {
|
||||||
const user = Users.getUser(db, req)
|
tags.forEach((tag: string) => {
|
||||||
if (!user || !user.id) {
|
const slug = Util.slug(tag)
|
||||||
res.status(403).send({ ok: false, error: 'forbidden' })
|
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
|
||||||
return
|
if (id) {
|
||||||
|
db.insert('image_x_category', {
|
||||||
|
image_id: imageId,
|
||||||
|
category_id: id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
const data = req.body as SaveImageRequestData
|
const data = req.body as SaveImageRequestData
|
||||||
const image = db.get('images', {id: data.id})
|
|
||||||
if (parseInt(image.uploader_user_id, 10) !== user.id) {
|
|
||||||
res.status(403).send({ ok: false, error: 'forbidden' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db.update('images', {
|
db.update('images', {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
}, {
|
}, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
Images.setTags(db, data.id, data.tags || [])
|
db.delete('image_x_category', { image_id: data.id })
|
||||||
|
|
||||||
|
if (data.tags) {
|
||||||
|
setImageTags(db, data.id, data.tags)
|
||||||
|
}
|
||||||
|
|
||||||
res.send({ ok: true })
|
res.send({ ok: true })
|
||||||
})
|
})
|
||||||
app.post('/api/upload', (req, res): void => {
|
app.post('/api/upload', (req, res) => {
|
||||||
upload(req, res, async (err: any): Promise<void> => {
|
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!");
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Images.resizeImage(req.file.filename)
|
await Images.resizeImage(req.file.filename)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.log(err)
|
log.log(err)
|
||||||
res.status(400).send("Something went wrong!")
|
res.status(400).send("Something went wrong!");
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = Users.getOrCreateUser(db, req)
|
|
||||||
|
|
||||||
const dim = await Images.getDimensions(
|
|
||||||
`${UPLOAD_DIR}/${req.file.filename}`
|
|
||||||
)
|
|
||||||
const imageId = db.insert('images', {
|
const imageId = db.insert('images', {
|
||||||
uploader_user_id: user.id,
|
|
||||||
filename: req.file.filename,
|
filename: req.file.filename,
|
||||||
filename_original: req.file.originalname,
|
filename_original: req.file.originalname,
|
||||||
title: req.body.title || '',
|
title: req.body.title || '',
|
||||||
created: Time.timestamp(),
|
created: Time.timestamp(),
|
||||||
width: dim.w,
|
|
||||||
height: dim.h,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (req.body.tags) {
|
if (req.body.tags) {
|
||||||
const tags = req.body.tags.split(',').filter((tag: string) => !!tag)
|
setImageTags(db, imageId as number, req.body.tags)
|
||||||
Images.setTags(db, imageId as number, tags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(Images.imageFromDb(db, imageId as number))
|
res.send(Images.imageFromDb(db, imageId as number))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
|
app.post('/newgame', bodyParser.json(), async (req, res) => {
|
||||||
const user = Users.getOrCreateUser(db, req)
|
const gameSettings = req.body as GameSettings
|
||||||
const gameId = await Game.createNewGame(
|
log.log(gameSettings)
|
||||||
req.body as GameSettings,
|
const gameId = Util.uniqId()
|
||||||
Time.timestamp(),
|
if (!Game.exists(gameId)) {
|
||||||
user.id
|
const ts = Time.timestamp()
|
||||||
|
await Game.createGame(
|
||||||
|
gameId,
|
||||||
|
gameSettings.tiles,
|
||||||
|
gameSettings.image,
|
||||||
|
ts,
|
||||||
|
gameSettings.scoreMode
|
||||||
)
|
)
|
||||||
|
}
|
||||||
res.send({ id: gameId })
|
res.send({ id: gameId })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -222,18 +176,17 @@ app.use('/', express.static(PUBLIC_DIR))
|
||||||
|
|
||||||
const wss = new WebSocketServer(config.ws);
|
const wss = new WebSocketServer(config.ws);
|
||||||
|
|
||||||
const notify = (data: ServerEvent, sockets: Array<WebSocket>): void => {
|
const notify = (data: any, sockets: Array<WebSocket>) => {
|
||||||
for (const socket of sockets) {
|
// TODO: throttle?
|
||||||
|
for (let socket of sockets) {
|
||||||
wss.notifyOne(data, socket)
|
wss.notifyOne(data, socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.on('close', async (
|
wss.on('close', async ({socket} : {socket: WebSocket}) => {
|
||||||
{socket} : { socket: WebSocket }
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const proto = socket.protocol.split('|')
|
const proto = socket.protocol.split('|')
|
||||||
// const clientId = proto[0]
|
const clientId = proto[0]
|
||||||
const gameId = proto[1]
|
const gameId = proto[1]
|
||||||
GameSockets.removeSocket(gameId, socket)
|
GameSockets.removeSocket(gameId, socket)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -241,30 +194,40 @@ wss.on('close', async (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wss.on('message', async (
|
wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
|
||||||
{socket, data} : { socket: WebSocket, data: WebSocket.Data }
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const proto = socket.protocol.split('|')
|
const proto = socket.protocol.split('|')
|
||||||
const clientId = proto[0]
|
const clientId = proto[0]
|
||||||
const gameId = proto[1]
|
const gameId = proto[1]
|
||||||
// TODO: maybe handle different types of data
|
const msg = JSON.parse(data)
|
||||||
// (but atm only string comes through)
|
|
||||||
const msg = JSON.parse(data as string)
|
|
||||||
const msgType = msg[0]
|
const msgType = msg[0]
|
||||||
switch (msgType) {
|
switch (msgType) {
|
||||||
|
case Protocol.EV_CLIENT_INIT_REPLAY: {
|
||||||
|
if (!GameLog.exists(gameId)) {
|
||||||
|
throw `[gamelog ${gameId} does not exist... ]`
|
||||||
|
}
|
||||||
|
const log = GameLog.get(gameId)
|
||||||
|
const game = await Game.createGameObject(
|
||||||
|
gameId,
|
||||||
|
log[0][2],
|
||||||
|
log[0][3],
|
||||||
|
log[0][4],
|
||||||
|
log[0][5] || ScoreMode.FINAL
|
||||||
|
)
|
||||||
|
notify(
|
||||||
|
[Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game), log],
|
||||||
|
[socket]
|
||||||
|
)
|
||||||
|
} break
|
||||||
|
|
||||||
case Protocol.EV_CLIENT_INIT: {
|
case Protocol.EV_CLIENT_INIT: {
|
||||||
if (!GameCommon.exists(gameId)) {
|
if (!Game.exists(gameId)) {
|
||||||
throw `[game ${gameId} does not exist... ]`
|
throw `[game ${gameId} does not exist... ]`
|
||||||
}
|
}
|
||||||
const ts = Time.timestamp()
|
const ts = Time.timestamp()
|
||||||
Game.addPlayer(gameId, clientId, ts)
|
Game.addPlayer(gameId, clientId, ts)
|
||||||
GameSockets.addSocket(gameId, socket)
|
GameSockets.addSocket(gameId, socket)
|
||||||
|
const game = Game.get(gameId)
|
||||||
const game: GameType|null = GameCommon.get(gameId)
|
|
||||||
if (!game) {
|
|
||||||
throw `[game ${gameId} does not exist (anymore)... ]`
|
|
||||||
}
|
|
||||||
notify(
|
notify(
|
||||||
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
|
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
|
||||||
[socket]
|
[socket]
|
||||||
|
|
@ -272,7 +235,7 @@ wss.on('message', async (
|
||||||
} break
|
} break
|
||||||
|
|
||||||
case Protocol.EV_CLIENT_EVENT: {
|
case Protocol.EV_CLIENT_EVENT: {
|
||||||
if (!GameCommon.exists(gameId)) {
|
if (!Game.exists(gameId)) {
|
||||||
throw `[game ${gameId} does not exist... ]`
|
throw `[game ${gameId} does not exist... ]`
|
||||||
}
|
}
|
||||||
const clientSeq = msg[1]
|
const clientSeq = msg[1]
|
||||||
|
|
@ -280,7 +243,7 @@ wss.on('message', async (
|
||||||
const ts = Time.timestamp()
|
const ts = Time.timestamp()
|
||||||
|
|
||||||
let sendGame = false
|
let sendGame = false
|
||||||
if (!GameCommon.playerExists(gameId, clientId)) {
|
if (!Game.playerExists(gameId, clientId)) {
|
||||||
Game.addPlayer(gameId, clientId, ts)
|
Game.addPlayer(gameId, clientId, ts)
|
||||||
sendGame = true
|
sendGame = true
|
||||||
}
|
}
|
||||||
|
|
@ -289,10 +252,7 @@ wss.on('message', async (
|
||||||
sendGame = true
|
sendGame = true
|
||||||
}
|
}
|
||||||
if (sendGame) {
|
if (sendGame) {
|
||||||
const game: GameType|null = GameCommon.get(gameId)
|
const game = Game.get(gameId)
|
||||||
if (!game) {
|
|
||||||
throw `[game ${gameId} does not exist (anymore)... ]`
|
|
||||||
}
|
|
||||||
notify(
|
notify(
|
||||||
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
|
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
|
||||||
[socket]
|
[socket]
|
||||||
|
|
@ -311,7 +271,7 @@ wss.on('message', async (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
GameStorage.loadGamesFromDb(db)
|
GameStorage.loadGames()
|
||||||
const server = app.listen(
|
const server = app.listen(
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
|
|
@ -320,9 +280,9 @@ const server = app.listen(
|
||||||
wss.listen()
|
wss.listen()
|
||||||
|
|
||||||
|
|
||||||
const memoryUsageHuman = (): void => {
|
const memoryUsageHuman = () => {
|
||||||
const totalHeapSize = v8.getHeapStatistics().total_available_size
|
const totalHeapSize = v8.getHeapStatistics().total_available_size
|
||||||
const totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
|
let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
|
||||||
|
|
||||||
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`)
|
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`)
|
||||||
const used = process.memoryUsage().heapUsed / 1024 / 1024
|
const used = process.memoryUsage().heapUsed / 1024 / 1024
|
||||||
|
|
@ -334,19 +294,19 @@ memoryUsageHuman()
|
||||||
// persist games in fixed interval
|
// persist games in fixed interval
|
||||||
const persistInterval = setInterval(() => {
|
const persistInterval = setInterval(() => {
|
||||||
log.log('Persisting games...')
|
log.log('Persisting games...')
|
||||||
GameStorage.persistGamesToDb(db)
|
GameStorage.persistGames()
|
||||||
|
|
||||||
memoryUsageHuman()
|
memoryUsageHuman()
|
||||||
}, config.persistence.interval)
|
}, config.persistence.interval)
|
||||||
|
|
||||||
const gracefulShutdown = (signal: string): void => {
|
const gracefulShutdown = (signal: any) => {
|
||||||
log.log(`${signal} received...`)
|
log.log(`${signal} received...`)
|
||||||
|
|
||||||
log.log('clearing persist interval...')
|
log.log('clearing persist interval...')
|
||||||
clearInterval(persistInterval)
|
clearInterval(persistInterval)
|
||||||
|
|
||||||
log.log('persisting games...')
|
log.log('persisting games...')
|
||||||
GameStorage.persistGamesToDb(db)
|
GameStorage.persistGames()
|
||||||
|
|
||||||
log.log('shutting down webserver...')
|
log.log('shutting down webserver...')
|
||||||
server.close()
|
server.close()
|
||||||
|
|
@ -359,14 +319,14 @@ const gracefulShutdown = (signal: string): void => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// used by nodemon
|
// used by nodemon
|
||||||
process.once('SIGUSR2', (): void => {
|
process.once('SIGUSR2', function () {
|
||||||
gracefulShutdown('SIGUSR2')
|
gracefulShutdown('SIGUSR2')
|
||||||
})
|
})
|
||||||
|
|
||||||
process.once('SIGINT', (): void => {
|
process.once('SIGINT', function (code) {
|
||||||
gracefulShutdown('SIGINT')
|
gracefulShutdown('SIGINT')
|
||||||
})
|
})
|
||||||
|
|
||||||
process.once('SIGTERM', (): void => {
|
process.once('SIGTERM', function (code) {
|
||||||
gracefulShutdown('SIGTERM')
|
gracefulShutdown('SIGTERM')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue