Compare commits
14 commits
feature/pu
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a267bd70 | ||
|
|
bf4897bf83 | ||
|
|
b4980e367c | ||
|
|
e7f86b5ef8 | ||
|
|
4e528cc83d | ||
|
|
126384e5bd | ||
|
|
e5fb49ecb1 | ||
|
|
c11229a5e5 | ||
|
|
1008106355 | ||
|
|
65daeb0247 | ||
|
|
d2d5968d02 | ||
|
|
8f31a669d5 | ||
|
|
e7628895c9 | ||
|
|
bbcfd42008 |
32 changed files with 775 additions and 353 deletions
1
build/public/assets/index.63ff8630.js
Normal file
1
build/public/assets/index.63ff8630.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
|
|
@ -4,7 +4,7 @@
|
||||||
<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.93936dee.js"></script>
|
<script type="module" crossorigin src="/assets/index.63ff8630.js"></script>
|
||||||
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
|
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
|
||||||
<link rel="stylesheet" href="/assets/index.22dc307c.css">
|
<link rel="stylesheet" href="/assets/index.22dc307c.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ function encodeGame(data) {
|
||||||
data.scoreMode,
|
data.scoreMode,
|
||||||
data.shapeMode,
|
data.shapeMode,
|
||||||
data.snapMode,
|
data.snapMode,
|
||||||
|
data.creatorUserId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
function decodeGame(data) {
|
function decodeGame(data) {
|
||||||
|
|
@ -170,6 +171,7 @@ function decodeGame(data) {
|
||||||
scoreMode: data[6],
|
scoreMode: data[6],
|
||||||
shapeMode: data[7],
|
shapeMode: data[7],
|
||||||
snapMode: data[8],
|
snapMode: data[8],
|
||||||
|
creatorUserId: data[9],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function coordByPieceIdx(info, pieceIdx) {
|
function coordByPieceIdx(info, pieceIdx) {
|
||||||
|
|
@ -340,6 +342,8 @@ const INPUT_EV_REPLAY_SPEED_UP = 13;
|
||||||
const INPUT_EV_REPLAY_SPEED_DOWN = 14;
|
const INPUT_EV_REPLAY_SPEED_DOWN = 14;
|
||||||
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15;
|
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15;
|
||||||
const INPUT_EV_CENTER_FIT_PUZZLE = 16;
|
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;
|
||||||
const CHANGE_PLAYER = 3;
|
const CHANGE_PLAYER = 3;
|
||||||
|
|
@ -368,6 +372,8 @@ var Protocol = {
|
||||||
INPUT_EV_REPLAY_SPEED_DOWN,
|
INPUT_EV_REPLAY_SPEED_DOWN,
|
||||||
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
||||||
INPUT_EV_CENTER_FIT_PUZZLE,
|
INPUT_EV_CENTER_FIT_PUZZLE,
|
||||||
|
INPUT_EV_TOGGLE_FIXED_PIECES,
|
||||||
|
INPUT_EV_TOGGLE_LOOSE_PIECES,
|
||||||
CHANGE_DATA,
|
CHANGE_DATA,
|
||||||
CHANGE_TILE,
|
CHANGE_TILE,
|
||||||
CHANGE_PLAYER,
|
CHANGE_PLAYER,
|
||||||
|
|
@ -606,12 +612,16 @@ function setEvtInfo(gameId, playerId, evtInfo) {
|
||||||
}
|
}
|
||||||
function getAllGames() {
|
function getAllGames() {
|
||||||
return Object.values(GAMES).sort((a, b) => {
|
return Object.values(GAMES).sort((a, b) => {
|
||||||
|
const finished = isFinished(a.id);
|
||||||
// when both have same finished state, sort by started
|
// when both have same finished state, sort by started
|
||||||
if (isFinished(a.id) === isFinished(b.id)) {
|
if (finished === 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 isFinished(a.id) ? 1 : -1;
|
return finished ? 1 : -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getAllPlayers(gameId) {
|
function getAllPlayers(gameId) {
|
||||||
|
|
@ -1350,6 +1360,7 @@ const get = (gameId, offset = 0) => {
|
||||||
log[0][5] = DefaultScoreMode(log[0][5]);
|
log[0][5] = DefaultScoreMode(log[0][5]);
|
||||||
log[0][6] = DefaultShapeMode(log[0][6]);
|
log[0][6] = DefaultShapeMode(log[0][6]);
|
||||||
log[0][7] = DefaultSnapMode(log[0][7]);
|
log[0][7] = DefaultSnapMode(log[0][7]);
|
||||||
|
log[0][8] = log[0][8] || null;
|
||||||
}
|
}
|
||||||
return log;
|
return log;
|
||||||
};
|
};
|
||||||
|
|
@ -1435,6 +1446,7 @@ const imageFromDb = (db, imageId) => {
|
||||||
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,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
|
|
@ -1473,6 +1485,7 @@ inner join images i on i.id = ixc.image_id ${where.sql};
|
||||||
const images = db.getMany('images', wheresRaw, orderByMap[orderBy]);
|
const images = db.getMany('images', wheresRaw, orderByMap[orderBy]);
|
||||||
return images.map(i => ({
|
return images.map(i => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
|
uploaderUserId: i.uploader_user_id,
|
||||||
filename: i.filename,
|
filename: i.filename,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
|
|
@ -1490,6 +1503,7 @@ const allImagesFromDisk = (tags, sort) => {
|
||||||
.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,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||||
title: f.replace(/\.[a-z]+$/, ''),
|
title: f.replace(/\.[a-z]+$/, ''),
|
||||||
|
|
@ -1742,7 +1756,63 @@ function setDirty(gameId) {
|
||||||
function setClean(gameId) {
|
function setClean(gameId) {
|
||||||
delete dirtyGames[gameId];
|
delete dirtyGames[gameId];
|
||||||
}
|
}
|
||||||
function loadGames() {
|
function loadGamesFromDb(db) {
|
||||||
|
const gameRows = db.getMany('games');
|
||||||
|
for (const gameRow of gameRows) {
|
||||||
|
loadGameFromDb(db, gameRow.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function loadGameFromDb(db, gameId) {
|
||||||
|
const gameRow = db.get('games', { id: gameId });
|
||||||
|
let game;
|
||||||
|
try {
|
||||||
|
game = JSON.parse(gameRow.data);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
log$3.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 = storeDataToGame(game, game.creator_user_id);
|
||||||
|
GameCommon.setGame(gameObject.id, gameObject);
|
||||||
|
}
|
||||||
|
function persistGamesToDb(db) {
|
||||||
|
for (const gameId of Object.keys(dirtyGames)) {
|
||||||
|
persistGameToDb(db, gameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function persistGameToDb(db, gameId) {
|
||||||
|
const game = GameCommon.get(gameId);
|
||||||
|
if (!game) {
|
||||||
|
log$3.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$3.info(`[INFO] persisted game ${game.id}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
function loadGamesFromDisk() {
|
||||||
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$/);
|
||||||
|
|
@ -1750,10 +1820,13 @@ function loadGames() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const gameId = m[1];
|
const gameId = m[1];
|
||||||
loadGame(gameId);
|
loadGameFromDisk(gameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function loadGame(gameId) {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
function loadGameFromDisk(gameId) {
|
||||||
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;
|
||||||
|
|
@ -1775,27 +1848,21 @@ function loadGame(gameId) {
|
||||||
if (!Array.isArray(game.players)) {
|
if (!Array.isArray(game.players)) {
|
||||||
game.players = Object.values(game.players);
|
game.players = Object.values(game.players);
|
||||||
}
|
}
|
||||||
const gameObject = {
|
const gameObject = storeDataToGame(game, null);
|
||||||
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: DefaultScoreMode(game.scoreMode),
|
|
||||||
shapeMode: DefaultShapeMode(game.shapeMode),
|
|
||||||
snapMode: DefaultSnapMode(game.snapMode),
|
|
||||||
};
|
|
||||||
GameCommon.setGame(gameObject.id, gameObject);
|
GameCommon.setGame(gameObject.id, gameObject);
|
||||||
}
|
}
|
||||||
function persistGames() {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
function persistGamesToDisk() {
|
||||||
for (const gameId of Object.keys(dirtyGames)) {
|
for (const gameId of Object.keys(dirtyGames)) {
|
||||||
persistGame(gameId);
|
persistGameToDisk(gameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function persistGame(gameId) {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
function persistGameToDisk(gameId) {
|
||||||
const game = GameCommon.get(gameId);
|
const game = GameCommon.get(gameId);
|
||||||
if (!game) {
|
if (!game) {
|
||||||
log$3.error(`[ERROR] unable to persist non existing game ${gameId}`);
|
log$3.error(`[ERROR] unable to persist non existing game ${gameId}`);
|
||||||
|
|
@ -1804,7 +1871,27 @@ function persistGame(gameId) {
|
||||||
if (game.id in dirtyGames) {
|
if (game.id in dirtyGames) {
|
||||||
setClean(game.id);
|
setClean(game.id);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
|
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, gameToStoreData(game));
|
||||||
|
log$3.info(`[INFO] persisted game ${game.id}`);
|
||||||
|
}
|
||||||
|
function storeDataToGame(storeData, creatorUserId) {
|
||||||
|
return {
|
||||||
|
id: storeData.id,
|
||||||
|
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) {
|
||||||
|
return JSON.stringify({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
rng: {
|
rng: {
|
||||||
type: game.rng.type,
|
type: game.rng.type,
|
||||||
|
|
@ -1815,22 +1902,27 @@ function persistGame(gameId) {
|
||||||
scoreMode: game.scoreMode,
|
scoreMode: game.scoreMode,
|
||||||
shapeMode: game.shapeMode,
|
shapeMode: game.shapeMode,
|
||||||
snapMode: game.snapMode,
|
snapMode: game.snapMode,
|
||||||
}));
|
});
|
||||||
log$3.info(`[INFO] persisted game ${game.id}`);
|
|
||||||
}
|
}
|
||||||
var GameStorage = {
|
var GameStorage = {
|
||||||
loadGames,
|
// disk functions are deprecated
|
||||||
loadGame,
|
loadGamesFromDisk,
|
||||||
persistGames,
|
loadGameFromDisk,
|
||||||
persistGame,
|
persistGamesToDisk,
|
||||||
|
persistGameToDisk,
|
||||||
|
loadGamesFromDb,
|
||||||
|
loadGameFromDb,
|
||||||
|
persistGamesToDb,
|
||||||
|
persistGameToDb,
|
||||||
setDirty,
|
setDirty,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) {
|
async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) {
|
||||||
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, shapeMode),
|
||||||
players: [],
|
players: [],
|
||||||
|
|
@ -1840,10 +1932,10 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shape
|
||||||
snapMode,
|
snapMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) {
|
async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) {
|
||||||
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode);
|
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId);
|
||||||
GameLog.create(gameId, ts);
|
GameLog.create(gameId, ts);
|
||||||
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode);
|
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode, gameObject.creatorUserId);
|
||||||
GameCommon.setGame(gameObject.id, gameObject);
|
GameCommon.setGame(gameObject.id, gameObject);
|
||||||
GameStorage.setDirty(gameId);
|
GameStorage.setDirty(gameId);
|
||||||
}
|
}
|
||||||
|
|
@ -2089,6 +2181,13 @@ const storage = multer.diskStorage({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const upload = multer({ storage }).single('file');
|
const upload = multer({ storage }).single('file');
|
||||||
|
app.get('/api/me', (req, res) => {
|
||||||
|
let user = getUser(db, req);
|
||||||
|
res.send({
|
||||||
|
id: user ? user.id : null,
|
||||||
|
created: user ? user.created : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
app.get('/api/conf', (req, res) => {
|
app.get('/api/conf', (req, res) => {
|
||||||
res.send({
|
res.send({
|
||||||
WS_ADDRESS: config.ws.connectstring,
|
WS_ADDRESS: config.ws.connectstring,
|
||||||
|
|
@ -2116,7 +2215,7 @@ app.get('/api/replay-data', async (req, res) => {
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
// also need the game
|
// also need the game
|
||||||
game = await Game.createGameObject(gameId, log[0][2], log[0][3], // must be ImageInfo
|
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][4], log[0][5], log[0][6], log[0][7], log[0][8]);
|
||||||
}
|
}
|
||||||
res.send({ log, game: game ? Util.encodeGame(game) : null });
|
res.send({ log, game: game ? Util.encodeGame(game) : null });
|
||||||
});
|
});
|
||||||
|
|
@ -2147,6 +2246,28 @@ app.get('/api/index-data', (req, res) => {
|
||||||
gamesFinished: games.filter(g => !!g.finished),
|
gamesFinished: games.filter(g => !!g.finished),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const getOrCreateUser = (db, req) => {
|
||||||
|
let user = getUser(db, req);
|
||||||
|
if (!user) {
|
||||||
|
db.insert('users', {
|
||||||
|
'client_id': req.headers['client-id'],
|
||||||
|
'client_secret': req.headers['client-secret'],
|
||||||
|
'created': Time.timestamp(),
|
||||||
|
});
|
||||||
|
user = getUser(db, req);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
const getUser = (db, req) => {
|
||||||
|
let user = db.get('users', {
|
||||||
|
'client_id': req.headers['client-id'],
|
||||||
|
'client_secret': req.headers['client-secret'],
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
user.id = parseInt(user.id, 10);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
const setImageTags = (db, imageId, tags) => {
|
const setImageTags = (db, imageId, tags) => {
|
||||||
tags.forEach((tag) => {
|
tags.forEach((tag) => {
|
||||||
const slug = Util.slug(tag);
|
const slug = Util.slug(tag);
|
||||||
|
|
@ -2160,7 +2281,17 @@ const setImageTags = (db, imageId, tags) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
app.post('/api/save-image', express.json(), (req, res) => {
|
app.post('/api/save-image', express.json(), (req, res) => {
|
||||||
|
let user = getUser(db, req);
|
||||||
|
if (!user || !user.id) {
|
||||||
|
res.status(403).send({ ok: false, error: 'forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
|
let 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,
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -2185,8 +2316,10 @@ app.post('/api/upload', (req, res) => {
|
||||||
log.log(err);
|
log.log(err);
|
||||||
res.status(400).send("Something went wrong!");
|
res.status(400).send("Something went wrong!");
|
||||||
}
|
}
|
||||||
|
const user = getOrCreateUser(db, req);
|
||||||
const dim = await Images.getDimensions(`${UPLOAD_DIR}/${req.file.filename}`);
|
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 || '',
|
||||||
|
|
@ -2202,12 +2335,17 @@ app.post('/api/upload', (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.post('/api/newgame', express.json(), async (req, res) => {
|
app.post('/api/newgame', express.json(), async (req, res) => {
|
||||||
|
let user = getOrCreateUser(db, req);
|
||||||
|
if (!user || !user.id) {
|
||||||
|
res.status(403).send({ ok: false, error: 'forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const gameSettings = req.body;
|
const gameSettings = req.body;
|
||||||
log.log(gameSettings);
|
log.log(gameSettings);
|
||||||
const gameId = Util.uniqId();
|
const gameId = Util.uniqId();
|
||||||
if (!GameCommon.exists(gameId)) {
|
if (!GameCommon.exists(gameId)) {
|
||||||
const ts = Time.timestamp();
|
const ts = Time.timestamp();
|
||||||
await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode);
|
await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode, user.id);
|
||||||
}
|
}
|
||||||
res.send({ id: gameId });
|
res.send({ id: gameId });
|
||||||
});
|
});
|
||||||
|
|
@ -2289,7 +2427,7 @@ wss.on('message', async ({ socket, data }) => {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
GameStorage.loadGames();
|
GameStorage.loadGamesFromDb(db);
|
||||||
const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`));
|
const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`));
|
||||||
wss.listen();
|
wss.listen();
|
||||||
const memoryUsageHuman = () => {
|
const memoryUsageHuman = () => {
|
||||||
|
|
@ -2303,7 +2441,7 @@ 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.persistGames();
|
GameStorage.persistGamesToDb(db);
|
||||||
memoryUsageHuman();
|
memoryUsageHuman();
|
||||||
}, config.persistence.interval);
|
}, config.persistence.interval);
|
||||||
const gracefulShutdown = (signal) => {
|
const gracefulShutdown = (signal) => {
|
||||||
|
|
@ -2311,7 +2449,7 @@ const gracefulShutdown = (signal) => {
|
||||||
log.log('clearing persist interval...');
|
log.log('clearing persist interval...');
|
||||||
clearInterval(persistInterval);
|
clearInterval(persistInterval);
|
||||||
log.log('persisting games...');
|
log.log('persisting games...');
|
||||||
GameStorage.persistGames();
|
GameStorage.persistGamesToDb(db);
|
||||||
log.log('shutting down webserver...');
|
log.log('shutting down webserver...');
|
||||||
server.close();
|
server.close();
|
||||||
log.log('shutting down websocketserver...');
|
log.log('shutting down websocketserver...');
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function fixOne(gameId: string) {
|
||||||
|
|
||||||
log.log(g.puzzle.info.image.title, imageRow.id)
|
log.log(g.puzzle.info.image.title, imageRow.id)
|
||||||
|
|
||||||
GameStorage.persistGame(gameId)
|
GameStorage.persistGameToDb(db, gameId)
|
||||||
} else if (g.puzzle.info.image?.id) {
|
} else if (g.puzzle.info.image?.id) {
|
||||||
const imageId = g.puzzle.info.image.id
|
const imageId = g.puzzle.info.image.id
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ function fixOne(gameId: string) {
|
||||||
|
|
||||||
log.log(g.puzzle.info.image.title, imageId)
|
log.log(g.puzzle.info.image.title, imageId)
|
||||||
|
|
||||||
GameStorage.persistGame(gameId)
|
GameStorage.persistGameToDb(db, gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix log
|
// fix log
|
||||||
|
|
@ -81,7 +81,7 @@ function fixOne(gameId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fix() {
|
function fix() {
|
||||||
GameStorage.loadGames()
|
GameStorage.loadGamesFromDisk()
|
||||||
GameCommon.getAllGames().forEach((game: Game) => {
|
GameCommon.getAllGames().forEach((game: Game) => {
|
||||||
fixOne(game.id)
|
fixOne(game.id)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
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.loadGame(gameId)
|
GameStorage.loadGameFromDb(db, gameId)
|
||||||
let changed = false
|
let changed = false
|
||||||
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
|
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
|
||||||
for (let tile of tiles) {
|
for (let tile of tiles) {
|
||||||
|
|
@ -27,7 +32,7 @@ function fix_tiles(gameId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
GameStorage.persistGame(gameId)
|
GameStorage.persistGameToDb(db, gameId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
27
scripts/import_games.ts
Normal file
27
scripts/import_games.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# server for built files
|
# server for built files
|
||||||
nodemon --max-old-space-size=64 -e js build/server/main.js -c config.json
|
nodemon --watch build --max-old-space-size=64 -e js build/server/main.js -c config.json
|
||||||
|
|
|
||||||
|
|
@ -138,12 +138,16 @@ 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 (isFinished(a.id) === isFinished(b.id)) {
|
if (finished === 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 isFinished(a.id) ? 1 : -1
|
return finished ? 1 : -1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ const INPUT_EV_REPLAY_SPEED_DOWN = 14
|
||||||
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15
|
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15
|
||||||
const INPUT_EV_CENTER_FIT_PUZZLE = 16
|
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
|
||||||
const CHANGE_PLAYER = 3
|
const CHANGE_PLAYER = 3
|
||||||
|
|
@ -104,6 +107,9 @@ export default {
|
||||||
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
||||||
INPUT_EV_CENTER_FIT_PUZZLE,
|
INPUT_EV_CENTER_FIT_PUZZLE,
|
||||||
|
|
||||||
|
INPUT_EV_TOGGLE_FIXED_PIECES,
|
||||||
|
INPUT_EV_TOGGLE_LOOSE_PIECES,
|
||||||
|
|
||||||
CHANGE_DATA,
|
CHANGE_DATA,
|
||||||
CHANGE_TILE,
|
CHANGE_TILE,
|
||||||
CHANGE_PLAYER,
|
CHANGE_PLAYER,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export type EncodedGame = FixedLengthArray<[
|
||||||
ScoreMode,
|
ScoreMode,
|
||||||
ShapeMode,
|
ShapeMode,
|
||||||
SnapMode,
|
SnapMode,
|
||||||
|
number|null,
|
||||||
]>
|
]>
|
||||||
|
|
||||||
export interface ReplayData {
|
export interface ReplayData {
|
||||||
|
|
@ -72,6 +73,7 @@ interface GameRng {
|
||||||
|
|
||||||
export interface Game {
|
export interface Game {
|
||||||
id: string
|
id: string
|
||||||
|
creatorUserId: number|null
|
||||||
players: Array<EncodedPlayer>
|
players: Array<EncodedPlayer>
|
||||||
puzzle: Puzzle
|
puzzle: Puzzle
|
||||||
evtInfos: Record<string, EvtInfo>
|
evtInfos: Record<string, EvtInfo>
|
||||||
|
|
@ -155,6 +157,7 @@ export interface PieceChange {
|
||||||
export interface ImageInfo
|
export interface ImageInfo
|
||||||
{
|
{
|
||||||
id: number
|
id: number
|
||||||
|
uploaderUserId: number|null
|
||||||
filename: string
|
filename: string
|
||||||
url: string
|
url: string
|
||||||
title: string
|
title: string
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ function encodeGame(data: Game): EncodedGame {
|
||||||
data.scoreMode,
|
data.scoreMode,
|
||||||
data.shapeMode,
|
data.shapeMode,
|
||||||
data.snapMode,
|
data.snapMode,
|
||||||
|
data.creatorUserId,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +150,7 @@ function decodeGame(data: EncodedGame): Game {
|
||||||
scoreMode: data[6],
|
scoreMode: data[6],
|
||||||
shapeMode: data[7],
|
shapeMode: data[7],
|
||||||
snapMode: data[8],
|
snapMode: data[8],
|
||||||
|
creatorUserId: data[9],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
src/dbpatches/03_users.sqlite
Normal file
45
src/dbpatches/03_users.sqlite
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
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;
|
||||||
11
src/dbpatches/04_games.sqlite
Normal file
11
src/dbpatches/04_games.sqlite
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types'
|
import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types'
|
||||||
import Util, { 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')
|
||||||
|
|
||||||
|
|
@ -117,7 +118,7 @@ async function requestReplayData(
|
||||||
offset: number
|
offset: number
|
||||||
): Promise<ReplayData> {
|
): Promise<ReplayData> {
|
||||||
const args = { gameId, offset }
|
const args = { gameId, offset }
|
||||||
const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`)
|
const res = await xhr.get(`/api/replay-data${Util.asQueryArgs(args)}`, {})
|
||||||
const json: ReplayData = await res.json()
|
const json: ReplayData = await res.json()
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
src/frontend/EventAdapter.ts
Normal file
177
src/frontend/EventAdapter.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
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
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class="imageteaser"
|
class="imageteaser"
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="onClick">
|
@click="onClick">
|
||||||
<div class="btn edit" @click.stop="onEditClick">✏️</div>
|
<div class="btn edit" v-if="canEdit" @click.stop="onEditClick">✏️</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -18,12 +18,18 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@
|
||||||
<td>{{game.puzzle.info.image.title}}</td>
|
<td>{{game.puzzle.info.image.title}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Snap Mode: </td>
|
<td>Scoring: </td>
|
||||||
<td><span :title="snapMode[1]">{{scoreMode[0]}}</span></td>
|
<td><span :title="snapMode[1]">{{scoreMode[0]}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Shape Mode: </td>
|
<td>Shapes: </td>
|
||||||
<td><span :title="snapMode[1]">{{shapeMode[0]}}</span></td>
|
<td><span :title="snapMode[1]">{{shapeMode[0]}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Score Mode: </td>
|
<td>Snapping: </td>
|
||||||
<td><span :title="snapMode[1]">{{snapMode[0]}}</span></td>
|
<td><span :title="snapMode[1]">{{snapMode[0]}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -48,8 +48,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
shapeMode () {
|
shapeMode () {
|
||||||
switch (this.game.shapeMode) {
|
switch (this.game.shapeMode) {
|
||||||
case ShapeMode.FLAT: return ['Flat', 'all pieces flat on all sides']
|
case ShapeMode.FLAT: return ['Flat', 'All pieces flat on all sides']
|
||||||
case ShapeMode.ANY: return ['Any', 'flat pieces can occur anywhere']
|
case ShapeMode.ANY: return ['Any', 'Flat pieces can occur anywhere']
|
||||||
case ShapeMode.NORMAL:
|
case ShapeMode.NORMAL:
|
||||||
default:
|
default:
|
||||||
return ['Normal', '']
|
return ['Normal', '']
|
||||||
|
|
@ -57,10 +57,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
snapMode () {
|
snapMode () {
|
||||||
switch (this.game.snapMode) {
|
switch (this.game.snapMode) {
|
||||||
case SnapMode.REAL: return ['Real', 'pieces snap only to corners, already snapped pieces and to each other']
|
case SnapMode.REAL: return ['Real', 'Pieces snap only to corners, already snapped pieces and to each other']
|
||||||
case SnapMode.NORMAL:
|
case SnapMode.NORMAL:
|
||||||
default:
|
default:
|
||||||
return ['Normal', 'pieces snap to final destination and to each other']
|
return ['Normal', 'Pieces snap to final destination and to each other']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -35,20 +35,20 @@
|
||||||
Normal</label>
|
Normal</label>
|
||||||
<br />
|
<br />
|
||||||
<label><input type="radio" v-model="shapeMode" value="1" />
|
<label><input type="radio" v-model="shapeMode" value="1" />
|
||||||
Any (flat pieces can occur anywhere)</label>
|
Any (Flat pieces can occur anywhere)</label>
|
||||||
<br />
|
<br />
|
||||||
<label><input type="radio" v-model="shapeMode" value="2" />
|
<label><input type="radio" v-model="shapeMode" value="2" />
|
||||||
Flat (all pieces flat on all sides)</label>
|
Flat (All pieces flat on all sides)</label>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><label>Snapping: </label></td>
|
<td><label>Snapping: </label></td>
|
||||||
<td>
|
<td>
|
||||||
<label><input type="radio" v-model="snapMode" value="0" />
|
<label><input type="radio" v-model="snapMode" value="0" />
|
||||||
Normal (pieces snap to final destination and to each other)</label>
|
Normal (Pieces snap to final destination and to each other)</label>
|
||||||
<br />
|
<br />
|
||||||
<label><input type="radio" v-model="snapMode" value="1" />
|
<label><input type="radio" v-model="snapMode" value="1" />
|
||||||
Real (pieces snap only to corners, already snapped pieces and to each other)</label>
|
Real (Pieces snap only to corners, already snapped pieces and to each other)</label>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
</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',
|
||||||
|
|
@ -21,8 +22,7 @@ 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 fetch('/upload', {
|
const res = await xhr.post('/upload', {
|
||||||
method: 'post',
|
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
const j = await res.json()
|
const j = await res.json()
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ import {
|
||||||
EncodedGame,
|
EncodedGame,
|
||||||
ReplayData,
|
ReplayData,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
GameEvent,
|
|
||||||
ServerEvent,
|
ServerEvent,
|
||||||
} from '../common/Types'
|
} from '../common/Types'
|
||||||
|
import EventAdapter from './EventAdapter'
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
DEBUG?: boolean
|
DEBUG?: boolean
|
||||||
|
|
@ -96,180 +96,6 @@ function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
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') {
|
|
||||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
|
||||||
RERENDER = true
|
|
||||||
}
|
|
||||||
if (ev.code === 'KeyG') {
|
|
||||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
|
||||||
RERENDER = true
|
|
||||||
}
|
|
||||||
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 async function main(
|
export async function main(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
|
|
@ -746,6 +572,12 @@ export async function main(
|
||||||
HUD.togglePlayerNames()
|
HUD.togglePlayerNames()
|
||||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
||||||
centerPuzzle()
|
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
|
||||||
|
|
@ -818,6 +650,12 @@ export async function main(
|
||||||
HUD.togglePlayerNames()
|
HUD.togglePlayerNames()
|
||||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
||||||
centerPuzzle()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,36 @@ 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 () => {
|
||||||
const res = await fetch(`/api/conf`)
|
function initClientSecret() {
|
||||||
const conf = await res.json()
|
let SECRET = settings.getStr('SECRET', '')
|
||||||
|
if (!SECRET) {
|
||||||
function initme() {
|
SECRET = Util.uniqId()
|
||||||
let ID = localStorage.getItem('ID')
|
settings.setStr('SECRET', SECRET)
|
||||||
|
}
|
||||||
|
return SECRET
|
||||||
|
}
|
||||||
|
function initClientId() {
|
||||||
|
let ID = settings.getStr('ID', '')
|
||||||
if (!ID) {
|
if (!ID) {
|
||||||
ID = Util.uniqId()
|
ID = Util.uniqId()
|
||||||
localStorage.setItem('ID', ID)
|
settings.setStr('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(),
|
||||||
|
|
@ -39,8 +56,9 @@ import Util from './../common/Util'
|
||||||
})
|
})
|
||||||
|
|
||||||
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 = initme()
|
app.config.globalProperties.$clientId = clientId
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
</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'
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
const res = await fetch('/api/index-data')
|
const res = await xhr.get('/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
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export default defineComponent({
|
||||||
this.filtersChanged()
|
this.filtersChanged()
|
||||||
},
|
},
|
||||||
async loadImages () {
|
async loadImages () {
|
||||||
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
const res = await xhr.get(`/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
|
||||||
|
|
@ -165,8 +165,7 @@ export default defineComponent({
|
||||||
return await res.json()
|
return await res.json()
|
||||||
},
|
},
|
||||||
async saveImage (data: any) {
|
async saveImage (data: any) {
|
||||||
const res = await fetch('/api/save-image', {
|
const res = await xhr.post('/api/save-image', {
|
||||||
method: 'post',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
@ -180,9 +179,13 @@ export default defineComponent({
|
||||||
return await res.json()
|
return await res.json()
|
||||||
},
|
},
|
||||||
async onSaveImageClick(data: any) {
|
async onSaveImageClick(data: any) {
|
||||||
await this.saveImage(data)
|
const res = await this.saveImage(data)
|
||||||
this.dialog = ''
|
if (res.ok) {
|
||||||
await this.loadImages()
|
this.dialog = ''
|
||||||
|
await this.loadImages()
|
||||||
|
} else {
|
||||||
|
alert(res.error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async postToGalleryClick(data: any) {
|
async postToGalleryClick(data: any) {
|
||||||
this.uploading = 'postToGallery'
|
this.uploading = 'postToGallery'
|
||||||
|
|
@ -200,8 +203,7 @@ export default defineComponent({
|
||||||
this.dialog = 'new-game'
|
this.dialog = 'new-game'
|
||||||
},
|
},
|
||||||
async onNewGame(gameSettings: GameSettings) {
|
async onNewGame(gameSettings: GameSettings) {
|
||||||
const res = await fetch('/api/newgame', {
|
const res = await xhr.post('/api/newgame', {
|
||||||
method: 'post',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ 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 { Player } from '../../common/Types'
|
import { Game, Player } from '../../common/Types'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'replay',
|
name: 'replay',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export interface Options {
|
||||||
onUploadProgress?: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => any,
|
onUploadProgress?: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let xhrClientId: string = ''
|
||||||
|
let xhrClientSecret: string = ''
|
||||||
const request = async (
|
const request = async (
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
|
@ -22,6 +24,10 @@ const request = async (
|
||||||
for (const k in options.headers || {}) {
|
for (const k in options.headers || {}) {
|
||||||
xhr.setRequestHeader(k, options.headers[k])
|
xhr.setRequestHeader(k, options.headers[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xhr.setRequestHeader('Client-Id', xhrClientId)
|
||||||
|
xhr.setRequestHeader('Client-Secret', xhrClientSecret)
|
||||||
|
|
||||||
xhr.addEventListener('load', function (ev: ProgressEvent<XMLHttpRequestEventTarget>
|
xhr.addEventListener('load', function (ev: ProgressEvent<XMLHttpRequestEventTarget>
|
||||||
) {
|
) {
|
||||||
resolve({
|
resolve({
|
||||||
|
|
@ -41,7 +47,7 @@ const request = async (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
xhr.send(options.body)
|
xhr.send(options.body || null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,4 +59,10 @@ export default {
|
||||||
post: (url: string, options: any): Promise<Response> => {
|
post: (url: string, options: any): Promise<Response> => {
|
||||||
return request('post', url, options)
|
return request('post', url, options)
|
||||||
},
|
},
|
||||||
|
setClientId: (clientId: string): void => {
|
||||||
|
xhrClientId = clientId
|
||||||
|
},
|
||||||
|
setClientSecret: (clientSecret: string): void => {
|
||||||
|
xhrClientSecret = clientSecret
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import GameCommon from './../common/GameCommon'
|
import GameCommon from './../common/GameCommon'
|
||||||
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode,ImageInfo, Timestamp } from './../common/Types'
|
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode,ImageInfo, Timestamp, GameSettings } 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 GameLog from './GameLog'
|
import GameLog from './GameLog'
|
||||||
|
|
@ -16,12 +16,14 @@ async function createGameObject(
|
||||||
ts: Timestamp,
|
ts: Timestamp,
|
||||||
scoreMode: ScoreMode,
|
scoreMode: ScoreMode,
|
||||||
shapeMode: ShapeMode,
|
shapeMode: ShapeMode,
|
||||||
snapMode: SnapMode
|
snapMode: SnapMode,
|
||||||
|
creatorUserId: number|null
|
||||||
): Promise<Game> {
|
): 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, shapeMode),
|
||||||
players: [],
|
players: [],
|
||||||
|
|
@ -32,23 +34,25 @@ async function createGameObject(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createGame(
|
async function createNewGame(
|
||||||
gameId: string,
|
gameSettings: GameSettings,
|
||||||
targetTiles: number,
|
|
||||||
image: ImageInfo,
|
|
||||||
ts: Timestamp,
|
ts: Timestamp,
|
||||||
scoreMode: ScoreMode,
|
creatorUserId: number
|
||||||
shapeMode: ShapeMode,
|
): Promise<string> {
|
||||||
snapMode: SnapMode
|
let gameId;
|
||||||
): Promise<void> {
|
do {
|
||||||
|
gameId = Util.uniqId()
|
||||||
|
} while (GameCommon.exists(gameId))
|
||||||
|
|
||||||
const gameObject = await createGameObject(
|
const gameObject = await createGameObject(
|
||||||
gameId,
|
gameId,
|
||||||
targetTiles,
|
gameSettings.tiles,
|
||||||
image,
|
gameSettings.image,
|
||||||
ts,
|
ts,
|
||||||
scoreMode,
|
gameSettings.scoreMode,
|
||||||
shapeMode,
|
gameSettings.shapeMode,
|
||||||
snapMode
|
gameSettings.snapMode,
|
||||||
|
creatorUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
GameLog.create(gameId, ts)
|
GameLog.create(gameId, ts)
|
||||||
|
|
@ -56,16 +60,19 @@ async function createGame(
|
||||||
gameId,
|
gameId,
|
||||||
Protocol.LOG_HEADER,
|
Protocol.LOG_HEADER,
|
||||||
1,
|
1,
|
||||||
targetTiles,
|
gameSettings.tiles,
|
||||||
image,
|
gameSettings.image,
|
||||||
ts,
|
ts,
|
||||||
scoreMode,
|
gameSettings.scoreMode,
|
||||||
shapeMode,
|
gameSettings.shapeMode,
|
||||||
snapMode
|
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: Timestamp): void {
|
||||||
|
|
@ -100,7 +107,7 @@ function handleInput(
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createGameObject,
|
createGameObject,
|
||||||
createGame,
|
createNewGame,
|
||||||
addPlayer,
|
addPlayer,
|
||||||
handleInput,
|
handleInput,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ const get = (
|
||||||
log[0][5] = DefaultScoreMode(log[0][5])
|
log[0][5] = DefaultScoreMode(log[0][5])
|
||||||
log[0][6] = DefaultShapeMode(log[0][6])
|
log[0][6] = DefaultShapeMode(log[0][6])
|
||||||
log[0][7] = DefaultSnapMode(log[0][7])
|
log[0][7] = DefaultSnapMode(log[0][7])
|
||||||
|
log[0][8] = log[0][8] || null // creatorUserId
|
||||||
}
|
}
|
||||||
return log
|
return log
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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')
|
||||||
|
|
||||||
|
|
@ -15,8 +16,73 @@ function setDirty(gameId: string): void {
|
||||||
function setClean(gameId: string): void {
|
function setClean(gameId: string): void {
|
||||||
delete dirtyGames[gameId]
|
delete dirtyGames[gameId]
|
||||||
}
|
}
|
||||||
|
function loadGamesFromDb(db: Db): void {
|
||||||
|
const gameRows = db.getMany('games')
|
||||||
|
for (const gameRow of gameRows) {
|
||||||
|
loadGameFromDb(db, gameRow.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadGames(): void {
|
function loadGameFromDb(db: Db, gameId: string): 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$/)
|
||||||
|
|
@ -24,11 +90,14 @@ function loadGames(): void {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const gameId = m[1]
|
const gameId = m[1]
|
||||||
loadGame(gameId)
|
loadGameFromDisk(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
|
||||||
|
|
@ -49,39 +118,29 @@ function loadGame(gameId: string): void {
|
||||||
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 = {
|
const gameObject: Game = storeDataToGame(game, null)
|
||||||
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: DefaultScoreMode(game.scoreMode),
|
|
||||||
shapeMode: DefaultShapeMode(game.shapeMode),
|
|
||||||
snapMode: DefaultSnapMode(game.snapMode),
|
|
||||||
}
|
|
||||||
GameCommon.setGame(gameObject.id, gameObject)
|
GameCommon.setGame(gameObject.id, gameObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistGames(): void {
|
function storeDataToGame(storeData: any, creatorUserId: number|null): Game {
|
||||||
for (const gameId of Object.keys(dirtyGames)) {
|
return {
|
||||||
persistGame(gameId)
|
id: storeData.id,
|
||||||
|
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 persistGame(gameId: string): void {
|
function gameToStoreData(game: Game): string {
|
||||||
const game = GameCommon.get(gameId)
|
return JSON.stringify({
|
||||||
if (!game) {
|
|
||||||
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.id in dirtyGames) {
|
|
||||||
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,
|
||||||
|
|
@ -92,14 +151,18 @@ function persistGame(gameId: string): void {
|
||||||
scoreMode: game.scoreMode,
|
scoreMode: game.scoreMode,
|
||||||
shapeMode: game.shapeMode,
|
shapeMode: game.shapeMode,
|
||||||
snapMode: game.snapMode,
|
snapMode: game.snapMode,
|
||||||
}))
|
});
|
||||||
log.info(`[INFO] persisted game ${game.id}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
loadGames,
|
// disk functions are deprecated
|
||||||
loadGame,
|
loadGamesFromDisk,
|
||||||
persistGames,
|
loadGameFromDisk,
|
||||||
persistGame,
|
|
||||||
|
loadGamesFromDb,
|
||||||
|
loadGameFromDb,
|
||||||
|
persistGamesToDb,
|
||||||
|
persistGameToDb,
|
||||||
|
|
||||||
setDirty,
|
setDirty,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ 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, { OrderBy, WhereRaw } from './Db'
|
||||||
import { Dim } from '../common/Geometry'
|
import { Dim } from '../common/Geometry'
|
||||||
import { logger } from '../common/Util'
|
import Util, { logger } from '../common/Util'
|
||||||
import { Tag, ImageInfo } from '../common/Types'
|
import { Tag, ImageInfo } from '../common/Types'
|
||||||
|
|
||||||
const log = logger('Images.ts')
|
const log = logger('Images.ts')
|
||||||
|
|
@ -85,6 +85,7 @@ const imageFromDb = (db: Db, imageId: number): ImageInfo => {
|
||||||
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,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
|
|
@ -130,6 +131,7 @@ inner join images i on i.id = ixc.image_id ${where.sql};
|
||||||
|
|
||||||
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,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
|
|
@ -151,6 +153,7 @@ const allImagesFromDisk = (
|
||||||
.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,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||||
title: f.replace(/\.[a-z]+$/, ''),
|
title: f.replace(/\.[a-z]+$/, ''),
|
||||||
|
|
@ -206,6 +209,20 @@ 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,
|
||||||
|
|
@ -213,4 +230,5 @@ export default {
|
||||||
getAllTags,
|
getAllTags,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
getDimensions,
|
getDimensions,
|
||||||
|
setTags,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/server/Users.ts
Normal file
36
src/server/Users.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import GameCommon from '../common/GameCommon'
|
||||||
import { ServerEvent, Game as GameType, GameSettings } from '../common/Types'
|
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()
|
||||||
|
|
@ -57,6 +58,14 @@ const storage = multer.diskStorage({
|
||||||
})
|
})
|
||||||
const upload = multer({storage}).single('file');
|
const upload = multer({storage}).single('file');
|
||||||
|
|
||||||
|
app.get('/api/me', (req, res): void => {
|
||||||
|
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 => {
|
app.get('/api/conf', (req, res): void => {
|
||||||
res.send({
|
res.send({
|
||||||
WS_ADDRESS: config.ws.connectstring,
|
WS_ADDRESS: config.ws.connectstring,
|
||||||
|
|
@ -92,6 +101,7 @@ app.get('/api/replay-data', async (req, res): Promise<void> => {
|
||||||
log[0][5],
|
log[0][5],
|
||||||
log[0][6],
|
log[0][6],
|
||||||
log[0][7],
|
log[0][7],
|
||||||
|
log[0][8], // creatorUserId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
res.send({ log, game: game ? Util.encodeGame(game) : null })
|
res.send({ log, game: game ? Util.encodeGame(game) : null })
|
||||||
|
|
@ -133,32 +143,27 @@ interface SaveImageRequestData {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const setImageTags = (db: Db, imageId: number, tags: string[]): void => {
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.post('/api/save-image', express.json(), (req, res): void => {
|
app.post('/api/save-image', express.json(), (req, res): void => {
|
||||||
|
const user = Users.getUser(db, req)
|
||||||
|
if (!user || !user.id) {
|
||||||
|
res.status(403).send({ ok: false, error: 'forbidden' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
})
|
})
|
||||||
|
|
||||||
db.delete('image_x_category', { image_id: data.id })
|
Images.setTags(db, data.id, data.tags || [])
|
||||||
|
|
||||||
if (data.tags) {
|
|
||||||
setImageTags(db, data.id, data.tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({ ok: true })
|
res.send({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
@ -166,20 +171,25 @@ app.post('/api/upload', (req, res): void => {
|
||||||
upload(req, res, async (err: any): Promise<void> => {
|
upload(req, res, async (err: any): Promise<void> => {
|
||||||
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(
|
const dim = await Images.getDimensions(
|
||||||
`${UPLOAD_DIR}/${req.file.filename}`
|
`${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 || '',
|
||||||
|
|
@ -190,7 +200,7 @@ app.post('/api/upload', (req, res): void => {
|
||||||
|
|
||||||
if (req.body.tags) {
|
if (req.body.tags) {
|
||||||
const tags = req.body.tags.split(',').filter((tag: string) => !!tag)
|
const tags = req.body.tags.split(',').filter((tag: string) => !!tag)
|
||||||
setImageTags(db, imageId as number, tags)
|
Images.setTags(db, imageId as number, tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(Images.imageFromDb(db, imageId as number))
|
res.send(Images.imageFromDb(db, imageId as number))
|
||||||
|
|
@ -198,21 +208,12 @@ app.post('/api/upload', (req, res): void => {
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
|
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
|
||||||
const gameSettings = req.body as GameSettings
|
const user = Users.getOrCreateUser(db, req)
|
||||||
log.log(gameSettings)
|
const gameId = await Game.createNewGame(
|
||||||
const gameId = Util.uniqId()
|
req.body as GameSettings,
|
||||||
if (!GameCommon.exists(gameId)) {
|
Time.timestamp(),
|
||||||
const ts = Time.timestamp()
|
user.id
|
||||||
await Game.createGame(
|
)
|
||||||
gameId,
|
|
||||||
gameSettings.tiles,
|
|
||||||
gameSettings.image,
|
|
||||||
ts,
|
|
||||||
gameSettings.scoreMode,
|
|
||||||
gameSettings.shapeMode,
|
|
||||||
gameSettings.snapMode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
res.send({ id: gameId })
|
res.send({ id: gameId })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -310,7 +311,7 @@ wss.on('message', async (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
GameStorage.loadGames()
|
GameStorage.loadGamesFromDb(db)
|
||||||
const server = app.listen(
|
const server = app.listen(
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
|
|
@ -333,7 +334,7 @@ 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.persistGames()
|
GameStorage.persistGamesToDb(db)
|
||||||
|
|
||||||
memoryUsageHuman()
|
memoryUsageHuman()
|
||||||
}, config.persistence.interval)
|
}, config.persistence.interval)
|
||||||
|
|
@ -345,7 +346,7 @@ const gracefulShutdown = (signal: string): void => {
|
||||||
clearInterval(persistInterval)
|
clearInterval(persistInterval)
|
||||||
|
|
||||||
log.log('persisting games...')
|
log.log('persisting games...')
|
||||||
GameStorage.persistGames()
|
GameStorage.persistGamesToDb(db)
|
||||||
|
|
||||||
log.log('shutting down webserver...')
|
log.log('shutting down webserver...')
|
||||||
server.close()
|
server.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue