feature/authoritative-server #1
15 changed files with 967 additions and 770 deletions
412
common/GameCommon.js
Normal file
412
common/GameCommon.js
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
import Geometry from './Geometry.js'
|
||||||
|
|
||||||
|
const GAMES = {}
|
||||||
|
|
||||||
|
function exists(gameId) {
|
||||||
|
return (!!GAMES[gameId]) || false
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGame(gameId, game) {
|
||||||
|
GAMES[gameId] = game
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPlayer(gameId, playerId) {
|
||||||
|
GAMES[gameId].players[playerId] = {
|
||||||
|
id: playerId,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
down: false
|
||||||
|
}
|
||||||
|
GAMES[gameId].evtInfos[playerId] = {
|
||||||
|
_last_mouse: null,
|
||||||
|
_last_mouse_down: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSocket(gameId, socket) {
|
||||||
|
const sockets = GAMES[gameId].sockets
|
||||||
|
|
||||||
|
if (!sockets.includes(socket)) {
|
||||||
|
sockets.push(socket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(gameId) {
|
||||||
|
return GAMES[gameId]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSockets(gameId) {
|
||||||
|
return GAMES[gameId].sockets
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePlayer(gameId, playerId, change) {
|
||||||
|
for (let k of Object.keys(change)) {
|
||||||
|
GAMES[gameId].players[playerId][k] = change[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeData(gameId, change) {
|
||||||
|
for (let k of Object.keys(change)) {
|
||||||
|
GAMES[gameId].puzzle.data[k] = change[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTile(gameId, tileIdx, change) {
|
||||||
|
for (let k of Object.keys(change)) {
|
||||||
|
GAMES[gameId].puzzle.tiles[tileIdx][k] = change[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTile = (gameId, tileIdx) => {
|
||||||
|
return GAMES[gameId].puzzle.tiles[tileIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTileGroup = (gameId, tileIdx) => {
|
||||||
|
const tile = getTile(gameId, tileIdx)
|
||||||
|
return tile.group
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFinalTilePos = (gameId, tileIdx) => {
|
||||||
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
const boardPos = {
|
||||||
|
x: (info.table.width - info.width) / 2,
|
||||||
|
y: (info.table.height - info.height) / 2
|
||||||
|
}
|
||||||
|
const srcPos = srcPosByTileIdx(gameId, tileIdx)
|
||||||
|
return Geometry.pointAdd(boardPos, srcPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTilePos = (gameId, tileIdx) => {
|
||||||
|
const tile = getTile(gameId, tileIdx)
|
||||||
|
return tile.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTileZIndex = (gameId, tileIdx) => {
|
||||||
|
const tile = getTile(gameId, tileIdx)
|
||||||
|
return tile.z
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFirstOwnedTileIdx = (gameId, userId) => {
|
||||||
|
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||||
|
if (t.owner === userId) {
|
||||||
|
return t.idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxGroup = (gameId) => {
|
||||||
|
return GAMES[gameId].puzzle.data.maxGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxZIndex = (gameId) => {
|
||||||
|
return GAMES[gameId].puzzle.data.maxZ
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
|
||||||
|
let maxZ = 0
|
||||||
|
for (let tileIdx of tileIdxs) {
|
||||||
|
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
||||||
|
if (tileZIndex > maxZ) {
|
||||||
|
maxZ = tileZIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxZ
|
||||||
|
}
|
||||||
|
|
||||||
|
function srcPosByTileIdx(gameId, tileIdx) {
|
||||||
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
|
const c = info.coords[tileIdx]
|
||||||
|
const cx = c.x * info.tileSize
|
||||||
|
const cy = c.y * info.tileSize
|
||||||
|
|
||||||
|
return { x: cx, y: cy }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSurroundingTilesByIdx(gameId, tileIdx) {
|
||||||
|
const info = GAMES[gameId].puzzle.info
|
||||||
|
|
||||||
|
const _X = info.coords[tileIdx].x
|
||||||
|
const _Y = info.coords[tileIdx].y
|
||||||
|
|
||||||
|
return [
|
||||||
|
// top
|
||||||
|
(_Y > 0) ? (tileIdx - info.tilesX) : -1,
|
||||||
|
// right
|
||||||
|
(_X < info.tilesX - 1) ? (tileIdx + 1) : -1,
|
||||||
|
// bottom
|
||||||
|
(_Y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -1,
|
||||||
|
// left
|
||||||
|
(_X > 0) ? (tileIdx - 1) : -1,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTilesZIndex = (gameId, tileIdxs, zIndex) => {
|
||||||
|
for (let tilesIdx of tileIdxs) {
|
||||||
|
changeTile(gameId, tilesIdx, { z: zIndex })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveTileDiff = (gameId, tileIdx, diff) => {
|
||||||
|
const oldPos = getTilePos(gameId, tileIdx)
|
||||||
|
const pos = Geometry.pointAdd(oldPos, diff)
|
||||||
|
changeTile(gameId, tileIdx, { pos })
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveTilesDiff = (gameId, tileIdxs, diff) => {
|
||||||
|
for (let tileIdx of tileIdxs) {
|
||||||
|
moveTileDiff(gameId, tileIdx, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishTiles = (gameId, tileIdxs) => {
|
||||||
|
for (let tileIdx of tileIdxs) {
|
||||||
|
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTilesOwner = (gameId, tileIdxs, owner) => {
|
||||||
|
for (let tileIdx of tileIdxs) {
|
||||||
|
changeTile(gameId, tileIdx, { owner })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all grouped tiles for a tile
|
||||||
|
function getGroupedTileIdxs(gameId, tileIdx) {
|
||||||
|
const tiles = GAMES[gameId].puzzle.tiles
|
||||||
|
const tile = tiles[tileIdx]
|
||||||
|
|
||||||
|
const grouped = []
|
||||||
|
if (tile.group) {
|
||||||
|
for (let other of tiles) {
|
||||||
|
if (other.group === tile.group) {
|
||||||
|
grouped.push(other.idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grouped.push(tile.idx)
|
||||||
|
}
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the puzzle tile with the highest z index
|
||||||
|
// that is not finished yet and that matches the position
|
||||||
|
const freeTileIdxByPos = (gameId, pos) => {
|
||||||
|
let info = GAMES[gameId].puzzle.info
|
||||||
|
let tiles = GAMES[gameId].puzzle.tiles
|
||||||
|
|
||||||
|
let maxZ = -1
|
||||||
|
let tileIdx = -1
|
||||||
|
for (let idx = 0; idx < tiles.length; idx++) {
|
||||||
|
const tile = tiles[idx]
|
||||||
|
if (tile.owner !== 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const collisionRect = {
|
||||||
|
x: tile.pos.x,
|
||||||
|
y: tile.pos.y,
|
||||||
|
w: info.tileSize,
|
||||||
|
h: info.tileSize,
|
||||||
|
}
|
||||||
|
if (Geometry.pointInBounds(pos, collisionRect)) {
|
||||||
|
if (maxZ === -1 || tile.z > maxZ) {
|
||||||
|
maxZ = tile.z
|
||||||
|
tileIdx = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tileIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if two tiles are grouped together
|
||||||
|
const areGrouped = (gameId, tileIdx1, tileIdx2) => {
|
||||||
|
const g1 = getTileGroup(gameId, tileIdx1)
|
||||||
|
const g2 = getTileGroup(gameId, tileIdx2)
|
||||||
|
return g1 && g1 === g2
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(gameId, playerId, input) {
|
||||||
|
let puzzle = GAMES[gameId].puzzle
|
||||||
|
let players = GAMES[gameId].players
|
||||||
|
let evtInfo = GAMES[gameId].evtInfos[playerId]
|
||||||
|
|
||||||
|
let changes = []
|
||||||
|
|
||||||
|
const _dataChange = () => {
|
||||||
|
changes.push(['data', puzzle.data])
|
||||||
|
}
|
||||||
|
|
||||||
|
const _tileChange = (tileIdx) => {
|
||||||
|
changes.push(['tile', getTile(gameId, tileIdx)])
|
||||||
|
}
|
||||||
|
|
||||||
|
const _tileChanges = (tileIdxs) => {
|
||||||
|
for (let tileIdx of tileIdxs) {
|
||||||
|
_tileChange(tileIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _playerChange = () => {
|
||||||
|
changes.push(['player', players[playerId]])
|
||||||
|
}
|
||||||
|
|
||||||
|
// put both tiles (and their grouped tiles) in the same group
|
||||||
|
const groupTiles = (gameId, tileIdx1, tileIdx2) => {
|
||||||
|
let tiles = GAMES[gameId].puzzle.tiles
|
||||||
|
let group1 = getTileGroup(gameId, tileIdx1)
|
||||||
|
let group2 = getTileGroup(gameId, tileIdx2)
|
||||||
|
|
||||||
|
let group
|
||||||
|
let searchGroups = []
|
||||||
|
if (group1) {
|
||||||
|
searchGroups.push(group1)
|
||||||
|
}
|
||||||
|
if (group2) {
|
||||||
|
searchGroups.push(group2)
|
||||||
|
}
|
||||||
|
if (group1) {
|
||||||
|
group = group1
|
||||||
|
} else if (group2) {
|
||||||
|
group = group2
|
||||||
|
} else {
|
||||||
|
let maxGroup = getMaxGroup(gameId) + 1
|
||||||
|
changeData(gameId, { maxGroup })
|
||||||
|
_dataChange()
|
||||||
|
group = getMaxGroup(gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTile(gameId, tileIdx1, { group })
|
||||||
|
_tileChange(tileIdx1)
|
||||||
|
changeTile(gameId, tileIdx2, { group })
|
||||||
|
_tileChange(tileIdx2)
|
||||||
|
|
||||||
|
// TODO: strange
|
||||||
|
if (searchGroups.length > 0) {
|
||||||
|
for (let tile of tiles) {
|
||||||
|
if (searchGroups.includes(tile.group)) {
|
||||||
|
changeTile(gameId, tile.idx, { group })
|
||||||
|
_tileChange(tile.idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let [type, x, y] = input
|
||||||
|
let pos = {x, y}
|
||||||
|
if (type === 'down') {
|
||||||
|
changePlayer(gameId, playerId, { down: true })
|
||||||
|
_playerChange()
|
||||||
|
evtInfo._last_mouse_down = pos
|
||||||
|
|
||||||
|
const tileIdxAtPos = freeTileIdxByPos(gameId, pos)
|
||||||
|
if (tileIdxAtPos >= 0) {
|
||||||
|
console.log('tile: ', tileIdxAtPos)
|
||||||
|
let maxZ = getMaxZIndex(gameId) + 1
|
||||||
|
changeData(gameId, { maxZ })
|
||||||
|
_dataChange()
|
||||||
|
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos)
|
||||||
|
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
|
||||||
|
setTilesOwner(gameId, tileIdxs, playerId)
|
||||||
|
_tileChanges(tileIdxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (type === 'move') {
|
||||||
|
changePlayer(gameId, playerId, pos)
|
||||||
|
_playerChange()
|
||||||
|
|
||||||
|
if (evtInfo._last_mouse_down !== null) {
|
||||||
|
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
|
if (tileIdx >= 0) {
|
||||||
|
const diffX = x - evtInfo._last_mouse_down.x
|
||||||
|
const diffY = y - evtInfo._last_mouse_down.y
|
||||||
|
const diff = { x: diffX, y: diffY }
|
||||||
|
const tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
|
_tileChanges(tileIdxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
evtInfo._last_mouse_down = pos
|
||||||
|
}
|
||||||
|
} else if (type === 'up') {
|
||||||
|
changePlayer(gameId, playerId, { down: false })
|
||||||
|
_playerChange()
|
||||||
|
evtInfo._last_mouse_down = null
|
||||||
|
|
||||||
|
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||||
|
if (tileIdx >= 0) {
|
||||||
|
// drop the tile(s)
|
||||||
|
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
|
setTilesOwner(gameId, tileIdxs, 0)
|
||||||
|
_tileChanges(tileIdxs)
|
||||||
|
|
||||||
|
// Check if the tile was dropped near the final location
|
||||||
|
let tilePos = getTilePos(gameId, tileIdx)
|
||||||
|
let finalPos = getFinalTilePos(gameId, tileIdx)
|
||||||
|
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
|
||||||
|
let diff = Geometry.pointSub(finalPos, tilePos)
|
||||||
|
// Snap the tile to the final destination
|
||||||
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
|
finishTiles(gameId, tileIdxs)
|
||||||
|
_tileChanges(tileIdxs)
|
||||||
|
} else {
|
||||||
|
// Snap to other tiles
|
||||||
|
const check = (gameId, tileIdx, otherTileIdx, off) => {
|
||||||
|
let info = GAMES[gameId].puzzle.info
|
||||||
|
if (otherTileIdx < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const tilePos = getTilePos(gameId, tileIdx)
|
||||||
|
const dstPos = Geometry.pointAdd(
|
||||||
|
getTilePos(gameId, otherTileIdx),
|
||||||
|
{x: off[0] * info.tileSize, y: off[1] * info.tileSize}
|
||||||
|
)
|
||||||
|
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
|
||||||
|
let diff = Geometry.pointSub(dstPos, tilePos)
|
||||||
|
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
|
moveTilesDiff(gameId, tileIdxs, diff)
|
||||||
|
groupTiles(gameId, tileIdx, otherTileIdx)
|
||||||
|
tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||||
|
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs)
|
||||||
|
console.log('z:' , zIndex, tileIdxs)
|
||||||
|
setTilesZIndex(gameId, tileIdxs, zIndex)
|
||||||
|
_tileChanges(tileIdxs)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
|
||||||
|
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp)
|
||||||
|
if (
|
||||||
|
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
|
||||||
|
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right
|
||||||
|
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom
|
||||||
|
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(mouse)
|
||||||
|
evtInfo._last_mouse = pos
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setGame,
|
||||||
|
exists,
|
||||||
|
addPlayer,
|
||||||
|
addSocket,
|
||||||
|
get,
|
||||||
|
getSockets,
|
||||||
|
handleInput,
|
||||||
|
}
|
||||||
50
common/Geometry.js
Normal file
50
common/Geometry.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
function pointSub(a, b) {
|
||||||
|
return { x: a.x - b.x, y: a.y - b.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointAdd(a, b) {
|
||||||
|
return { x: a.x + b.x, y: a.y + b.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointDistance(a, b) {
|
||||||
|
const diffX = a.x - b.x
|
||||||
|
const diffY = a.y - b.y
|
||||||
|
return Math.sqrt(diffX * diffX + diffY * diffY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointInBounds(pt, rect) {
|
||||||
|
return pt.x >= rect.x
|
||||||
|
&& pt.x <= rect.x + rect.w
|
||||||
|
&& pt.y >= rect.y
|
||||||
|
&& pt.y <= rect.y + rect.h
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectCenter(rect) {
|
||||||
|
return {
|
||||||
|
x: rect.x + (rect.w / 2),
|
||||||
|
y: rect.y + (rect.h / 2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectMoved(rect, x, y) {
|
||||||
|
return {
|
||||||
|
x: rect.x + x,
|
||||||
|
y: rect.y + y,
|
||||||
|
w: rect.w,
|
||||||
|
h: rect.h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectCenterDistance(rectA, rectB) {
|
||||||
|
return pointDistance(rectCenter(rectA), rectCenter(rectB))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
pointSub,
|
||||||
|
pointAdd,
|
||||||
|
pointDistance,
|
||||||
|
pointInBounds,
|
||||||
|
rectCenter,
|
||||||
|
rectMoved,
|
||||||
|
rectCenterDistance,
|
||||||
|
}
|
||||||
51
common/Protocol.js
Normal file
51
common/Protocol.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
SERVER_CLIENT_MESSAGE_PROTOCOL
|
||||||
|
NOTE: clients always send game id and their id
|
||||||
|
when creating sockets (via socket.protocol), so
|
||||||
|
this doesn't need to be set in each message data
|
||||||
|
|
||||||
|
NOTE: first value in the array is always the type of event/message
|
||||||
|
when describing them below, the value each has is used
|
||||||
|
instead of writing EVENT_TYPE or something ismilar
|
||||||
|
|
||||||
|
|
||||||
|
EV_CLIENT_EVENT: event triggered by clients and sent to server
|
||||||
|
[
|
||||||
|
EV_CLIENT_EVENT, // constant value, type of event
|
||||||
|
CLIENT_SEQ, // sequence number sent by client.
|
||||||
|
EV_DATA, // (eg. mouse input info)
|
||||||
|
]
|
||||||
|
|
||||||
|
EV_SERVER_EVENT: event sent to clients after recieving a client
|
||||||
|
event and processing it
|
||||||
|
[
|
||||||
|
EV_SERVER_EVENT, // constant value, type of event
|
||||||
|
CLIENT_ID, // user who sent the client event
|
||||||
|
CLIENT_SEQ, // sequence number of the client event msg
|
||||||
|
CHANGES_TRIGGERED_BY_CLIENT_EVENT,
|
||||||
|
]
|
||||||
|
|
||||||
|
EV_CLIENT_INIT: event sent by client to enter a game
|
||||||
|
[
|
||||||
|
EV_CLIENT_INIT, // constant value, type of event
|
||||||
|
]
|
||||||
|
|
||||||
|
EV_SERVER_INIT: event sent to one client after that client
|
||||||
|
connects to a game
|
||||||
|
[
|
||||||
|
EV_SERVER_INIT, // constant value, type of event
|
||||||
|
GAME, // complete game instance required by
|
||||||
|
// client to build client side of the game
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
const EV_SERVER_EVENT = 1
|
||||||
|
const EV_SERVER_INIT = 4
|
||||||
|
const EV_CLIENT_EVENT = 2
|
||||||
|
const EV_CLIENT_INIT = 3
|
||||||
|
|
||||||
|
export default {
|
||||||
|
EV_SERVER_EVENT,
|
||||||
|
EV_SERVER_INIT,
|
||||||
|
EV_CLIENT_EVENT,
|
||||||
|
EV_CLIENT_INIT,
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// get a unique id
|
// get a unique id
|
||||||
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
|
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
|
||||||
|
|
||||||
3
common/package.json
Normal file
3
common/package.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
|
@ -28,30 +28,27 @@ export default class Camera {
|
||||||
this.y += y / this.zoom
|
this.y += y / this.zoom
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomOut() {
|
setZoom(newzoom) {
|
||||||
const newzoom = Math.max(this.zoom - this.zoomStep, this.minZoom)
|
const zoom = Math.min(Math.max(newzoom, this.minZoom), this.maxZoom)
|
||||||
if (newzoom !== this.zoom) {
|
if (zoom == this.zoom) {
|
||||||
// centered zoom
|
|
||||||
this.x -= ((this.width / this.zoom) - (this.width / newzoom)) / 2
|
|
||||||
this.y -= ((this.height / this.zoom) - (this.height / newzoom)) / 2
|
|
||||||
|
|
||||||
this.zoom = newzoom
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// centered zoom
|
||||||
|
// TODO: mouse-centered-zoom
|
||||||
|
this.x -= Math.round(((this.width / this.zoom) - (this.width / zoom)) / 2)
|
||||||
|
this.y -= Math.round(((this.height / this.zoom) - (this.height / zoom)) / 2)
|
||||||
|
|
||||||
|
this.zoom = zoom
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomOut() {
|
||||||
|
return this.setZoom(this.zoom - this.zoomStep)
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn() {
|
zoomIn() {
|
||||||
const newzoom = Math.min(this.zoom + this.zoomStep, this.maxZoom)
|
return this.setZoom(this.zoom + this.zoomStep)
|
||||||
if (newzoom !== this.zoom) {
|
|
||||||
// centered zoom
|
|
||||||
this.x -= ((this.width / this.zoom) - (this.width / newzoom)) / 2
|
|
||||||
this.y -= ((this.height / this.zoom) - (this.height / newzoom)) / 2
|
|
||||||
|
|
||||||
this.zoom = newzoom
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,8 +58,8 @@ export default class Camera {
|
||||||
*/
|
*/
|
||||||
viewportToWorld(coord) {
|
viewportToWorld(coord) {
|
||||||
return {
|
return {
|
||||||
x: (coord.x / this.zoom) - this.x,
|
x: Math.round((coord.x / this.zoom) - this.x),
|
||||||
y: (coord.y / this.zoom) - this.y,
|
y: Math.round((coord.y / this.zoom) - this.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,15 +70,15 @@ export default class Camera {
|
||||||
*/
|
*/
|
||||||
worldToViewport(coord) {
|
worldToViewport(coord) {
|
||||||
return {
|
return {
|
||||||
x: (coord.x + this.x) * this.zoom,
|
x: Math.round((coord.x + this.x) * this.zoom),
|
||||||
y: (coord.y + this.y) * this.zoom,
|
y: Math.round((coord.y + this.y) * this.zoom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
worldDimToViewport(dim) {
|
worldDimToViewport(dim) {
|
||||||
return {
|
return {
|
||||||
w: dim.w * this.zoom,
|
w: Math.round(dim.w * this.zoom),
|
||||||
h: dim.h * this.zoom,
|
h: Math.round(dim.h * this.zoom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,55 @@
|
||||||
import WsClient from './WsClient.js'
|
import WsClient from './WsClient.js'
|
||||||
|
import Protocol from './../common/Protocol.js'
|
||||||
|
|
||||||
|
/** @type WsClient */
|
||||||
let conn
|
let conn
|
||||||
let changesCallback = () => {}
|
let changesCallback = () => {}
|
||||||
|
|
||||||
function onChanges(callback) {
|
function onServerChange(callback) {
|
||||||
changesCallback = callback
|
changesCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect(gameId, playerId) {
|
function send(message) {
|
||||||
conn = new WsClient(WS_ADDRESS, playerId + '|' + gameId)
|
conn.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientSeq
|
||||||
|
let events
|
||||||
|
function connect(gameId, clientId) {
|
||||||
|
clientSeq = 0
|
||||||
|
events = {}
|
||||||
|
conn = new WsClient(WS_ADDRESS, clientId + '|' + gameId)
|
||||||
return new Promise(r => {
|
return new Promise(r => {
|
||||||
conn.connect()
|
conn.connect()
|
||||||
conn.send(JSON.stringify({ type: 'init' }))
|
send([Protocol.EV_CLIENT_INIT])
|
||||||
conn.onSocket('message', async ({ data }) => {
|
conn.onSocket('message', async ({ data }) => {
|
||||||
const d = JSON.parse(data)
|
const msg = JSON.parse(data)
|
||||||
if (d.type === 'init') {
|
const msgType = msg[0]
|
||||||
r(d.game)
|
if (msgType === Protocol.EV_SERVER_INIT) {
|
||||||
} else if (d.type === 'state_changed' && d.origin !== playerId) {
|
const game = msg[1]
|
||||||
changesCallback(d.changes)
|
r(game)
|
||||||
|
} else if (msgType === Protocol.EV_SERVER_EVENT) {
|
||||||
|
const msgClientId = msg[1]
|
||||||
|
const msgClientSeq = msg[2]
|
||||||
|
if (msgClientId === clientId && events[msgClientSeq]) {
|
||||||
|
delete events[msgClientSeq]
|
||||||
|
}
|
||||||
|
changesCallback(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const _STATE = {
|
function sendClientEvent(mouse) {
|
||||||
changed: false,
|
// when sending event, increase number of sent events
|
||||||
changes: [],
|
// and add the event locally
|
||||||
}
|
clientSeq++;
|
||||||
|
events[clientSeq] = mouse
|
||||||
function addChange(change) {
|
send([Protocol.EV_CLIENT_EVENT, clientSeq, events[clientSeq]])
|
||||||
_STATE.changes.push(change)
|
|
||||||
_STATE.changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendChanges() {
|
|
||||||
if (_STATE.changed) {
|
|
||||||
conn.send(JSON.stringify({ type: 'state', state: _STATE }))
|
|
||||||
_STATE.changes = []
|
|
||||||
_STATE.changed = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
connect,
|
connect,
|
||||||
onChanges,
|
onServerChange,
|
||||||
addChange,
|
sendClientEvent,
|
||||||
sendChanges,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
export default class EventAdapter {
|
|
||||||
constructor(canvas) {
|
|
||||||
this._mouseEvts = []
|
|
||||||
canvas.addEventListener('mousedown', this._mouseDown.bind(this))
|
|
||||||
canvas.addEventListener('mouseup', this._mouseUp.bind(this))
|
|
||||||
canvas.addEventListener('mousemove', this._mouseMove.bind(this))
|
|
||||||
canvas.addEventListener('wheel', this._wheel.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
consumeAll() {
|
|
||||||
if (this._mouseEvts.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const all = this._mouseEvts.slice()
|
|
||||||
this._mouseEvts = []
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
_mouseDown(e) {
|
|
||||||
if (e.button === 0) {
|
|
||||||
this._mouseEvts.push({type: 'down', x: e.offsetX, y: e.offsetY})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mouseUp(e) {
|
|
||||||
if (e.button === 0) {
|
|
||||||
this._mouseEvts.push({type: 'up', x: e.offsetX, y: e.offsetY})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mouseMove(e) {
|
|
||||||
this._mouseEvts.push({type: 'move', x: e.offsetX, y: e.offsetY})
|
|
||||||
}
|
|
||||||
|
|
||||||
_wheel(e) {
|
|
||||||
this._mouseEvts.push({type: 'wheel', deltaY: e.deltaY, x: e.offsetX, y: e.offsetY})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
game/Game.js
Normal file
6
game/Game.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import GameCommon from './../common/GameCommon.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createGame: GameCommon.setGame,
|
||||||
|
handleInput: GameCommon.handleInput,
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// import Bitmap from './Bitmap.js'
|
|
||||||
|
|
||||||
function createCanvas(width = 0, height = 0) {
|
function createCanvas(width = 0, height = 0) {
|
||||||
const c = document.createElement('canvas')
|
const c = document.createElement('canvas')
|
||||||
c.width = width === 0 ? window.innerWidth : width
|
c.width = width === 0 ? window.innerWidth : width
|
||||||
|
|
|
||||||
119
game/PuzzleGraphics.js
Normal file
119
game/PuzzleGraphics.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import Geometry from '../common/Geometry.js'
|
||||||
|
import Graphics from './Graphics.js'
|
||||||
|
|
||||||
|
async function createPuzzleTileBitmaps(img, tiles, info) {
|
||||||
|
var tileSize = info.tileSize
|
||||||
|
var tileMarginWidth = info.tileMarginWidth
|
||||||
|
var tileDrawSize = info.tileDrawSize
|
||||||
|
var tileRatio = tileSize / 100.0
|
||||||
|
|
||||||
|
var curvyCoords = [
|
||||||
|
0, 0, 40, 15, 37, 5,
|
||||||
|
37, 5, 40, 0, 38, -5,
|
||||||
|
38, -5, 20, -20, 50, -20,
|
||||||
|
50, -20, 80, -20, 62, -5,
|
||||||
|
62, -5, 60, 0, 63, 5,
|
||||||
|
63, 5, 65, 15, 100, 0
|
||||||
|
];
|
||||||
|
|
||||||
|
const bitmaps = new Array(tiles.length)
|
||||||
|
|
||||||
|
const paths = {}
|
||||||
|
function pathForShape(shape) {
|
||||||
|
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
|
||||||
|
if (paths[key]) {
|
||||||
|
return paths[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = new Path2D()
|
||||||
|
const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth }
|
||||||
|
path.moveTo(topLeftEdge.x, topLeftEdge.y)
|
||||||
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
|
const p1 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio })
|
||||||
|
const p2 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio })
|
||||||
|
const p3 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio })
|
||||||
|
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
||||||
|
}
|
||||||
|
const topRightEdge = Geometry.pointAdd(topLeftEdge, { x: tileSize, y: 0 })
|
||||||
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
|
const p1 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
||||||
|
const p2 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
|
||||||
|
const p3 = Geometry.pointAdd(topRightEdge, { x: -shape.right * 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);
|
||||||
|
}
|
||||||
|
const bottomRightEdge = Geometry.pointAdd(topRightEdge, { x: 0, y: tileSize })
|
||||||
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
|
let p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
|
||||||
|
let p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * 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);
|
||||||
|
}
|
||||||
|
const bottomLeftEdge = Geometry.pointSub(bottomRightEdge, { x: tileSize, y: 0 })
|
||||||
|
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
||||||
|
let p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
||||||
|
let p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * 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);
|
||||||
|
}
|
||||||
|
paths[key] = path
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tile of tiles) {
|
||||||
|
const srcRect = srcRectByIdx(info, tile.idx)
|
||||||
|
const path = pathForShape(info.shapes[tile.idx])
|
||||||
|
|
||||||
|
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
||||||
|
const ctx = c.getContext('2d')
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke(path)
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
ctx.save();
|
||||||
|
ctx.clip(path)
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
srcRect.x - tileMarginWidth,
|
||||||
|
srcRect.y - tileMarginWidth,
|
||||||
|
tileDrawSize,
|
||||||
|
tileDrawSize,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
tileDrawSize,
|
||||||
|
tileDrawSize,
|
||||||
|
)
|
||||||
|
ctx.stroke(path)
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
bitmaps[tile.idx] = await createImageBitmap(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmaps
|
||||||
|
}
|
||||||
|
|
||||||
|
function srcRectByIdx(puzzleInfo, idx) {
|
||||||
|
const c = puzzleInfo.coords[idx]
|
||||||
|
return {
|
||||||
|
x: c.x * puzzleInfo.tileSize,
|
||||||
|
y: c.y * puzzleInfo.tileSize,
|
||||||
|
w: puzzleInfo.tileSize,
|
||||||
|
h: puzzleInfo.tileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPuzzleBitmaps(puzzle) {
|
||||||
|
// load bitmap, to determine the original size of the image
|
||||||
|
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
|
||||||
|
|
||||||
|
// creation of tile bitmaps
|
||||||
|
// then create the final puzzle bitmap
|
||||||
|
// NOTE: this can decrease OR increase in size!
|
||||||
|
const bmpResized = await Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height)
|
||||||
|
return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
loadPuzzleBitmaps,
|
||||||
|
}
|
||||||
723
game/index.js
723
game/index.js
|
|
@ -1,10 +1,12 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
import {run} from './gameloop.js'
|
import {run} from './gameloop.js'
|
||||||
import Camera from './Camera.js'
|
import Camera from './Camera.js'
|
||||||
import EventAdapter from './EventAdapter.js'
|
|
||||||
import Graphics from './Graphics.js'
|
import Graphics from './Graphics.js'
|
||||||
import Debug from './Debug.js'
|
import Debug from './Debug.js'
|
||||||
import Communication from './Communication.js'
|
import Communication from './Communication.js'
|
||||||
|
import Util from './../common/Util.js'
|
||||||
|
import PuzzleGraphics from './PuzzleGraphics.js'
|
||||||
|
import Game from './Game.js'
|
||||||
|
|
||||||
if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]'
|
if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]'
|
||||||
if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]'
|
if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]'
|
||||||
|
|
@ -16,622 +18,199 @@ function addCanvasToDom(canvas) {
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointDistance(a, b) {
|
|
||||||
const diffX = a.x - b.x
|
|
||||||
const diffY = a.y - b.y
|
|
||||||
return Math.sqrt(diffX * diffX + diffY * diffY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function rectMoved(rect, x, y) {
|
|
||||||
return {
|
|
||||||
x: rect.x + x,
|
|
||||||
y: rect.y + y,
|
|
||||||
w: rect.w,
|
|
||||||
h: rect.h,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rectCenter = (rect) => {
|
|
||||||
return {
|
|
||||||
x: rect.x + (rect.w / 2),
|
|
||||||
y: rect.y + (rect.h / 2),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rectCenterDistance(a, b) {
|
|
||||||
return pointDistance(rectCenter(a), rectCenter(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointInBounds(pt, rect) {
|
|
||||||
return pt.x >= rect.x
|
|
||||||
&& pt.x <= rect.x + rect.w
|
|
||||||
&& pt.y >= rect.y
|
|
||||||
&& pt.y <= rect.y + rect.h
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSurroundingTilesByIdx(puzzle, idx) {
|
|
||||||
var _X = puzzle.info.coords[idx].x
|
|
||||||
var _Y = puzzle.info.coords[idx].y
|
|
||||||
|
|
||||||
return [
|
|
||||||
// top
|
|
||||||
_Y === 0 ? null : puzzle.tiles[idx - puzzle.info.tiles_x],
|
|
||||||
// right
|
|
||||||
(_X === puzzle.info.tiles_x - 1) ? null : puzzle.tiles[idx + 1],
|
|
||||||
// bottom
|
|
||||||
(_Y === puzzle.info.tiles_y - 1) ? null : puzzle.tiles[idx + puzzle.info.tiles_x],
|
|
||||||
// left
|
|
||||||
_X === 0 ? null : puzzle.tiles[idx - 1]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createPuzzleTileBitmaps(img, tiles, info) {
|
|
||||||
var tileSize = info.tileSize
|
|
||||||
var tileMarginWidth = info.tileMarginWidth
|
|
||||||
var tileDrawSize = info.tileDrawSize
|
|
||||||
var tileRatio = tileSize / 100.0
|
|
||||||
|
|
||||||
var curvyCoords = [
|
|
||||||
0, 0, 40, 15, 37, 5,
|
|
||||||
37, 5, 40, 0, 38, -5,
|
|
||||||
38, -5, 20, -20, 50, -20,
|
|
||||||
50, -20, 80, -20, 62, -5,
|
|
||||||
62, -5, 60, 0, 63, 5,
|
|
||||||
63, 5, 65, 15, 100, 0
|
|
||||||
];
|
|
||||||
|
|
||||||
const bitmaps = new Array(tiles.length)
|
|
||||||
|
|
||||||
const paths = {}
|
|
||||||
function pathForShape(shape) {
|
|
||||||
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
|
|
||||||
if (paths[key]) {
|
|
||||||
return paths[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = new Path2D()
|
|
||||||
const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth }
|
|
||||||
path.moveTo(topLeftEdge.x, topLeftEdge.y)
|
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
|
||||||
const p1 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio })
|
|
||||||
const p2 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio })
|
|
||||||
const p3 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio })
|
|
||||||
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
|
|
||||||
}
|
|
||||||
const topRightEdge = pointAdd(topLeftEdge, { x: tileSize, y: 0 })
|
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
|
||||||
const p1 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
|
||||||
const p2 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
|
|
||||||
const p3 = pointAdd(topRightEdge, { x: -shape.right * 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);
|
|
||||||
}
|
|
||||||
const bottomRightEdge = pointAdd(topRightEdge, { x: 0, y: tileSize })
|
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
|
||||||
let p1 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
|
|
||||||
let p2 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
|
|
||||||
let p3 = 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);
|
|
||||||
}
|
|
||||||
const bottomLeftEdge = pointSub(bottomRightEdge, { x: tileSize, y: 0 })
|
|
||||||
for (let i = 0; i < curvyCoords.length / 6; i++) {
|
|
||||||
let p1 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
|
|
||||||
let p2 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
|
|
||||||
let p3 = 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);
|
|
||||||
}
|
|
||||||
paths[key] = path
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let tile of tiles) {
|
|
||||||
const srcRect = srcRectByIdx(info, tile.idx)
|
|
||||||
const path = pathForShape(info.shapes[tile.idx])
|
|
||||||
|
|
||||||
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
|
|
||||||
const ctx = c.getContext('2d')
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
ctx.lineWidth = 2
|
|
||||||
ctx.stroke(path)
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
ctx.save();
|
|
||||||
ctx.clip(path)
|
|
||||||
ctx.drawImage(
|
|
||||||
img,
|
|
||||||
srcRect.x - tileMarginWidth,
|
|
||||||
srcRect.y - tileMarginWidth,
|
|
||||||
tileDrawSize,
|
|
||||||
tileDrawSize,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
tileDrawSize,
|
|
||||||
tileDrawSize,
|
|
||||||
)
|
|
||||||
ctx.stroke(path)
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
bitmaps[tile.idx] = await createImageBitmap(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitmaps
|
|
||||||
}
|
|
||||||
|
|
||||||
function srcRectByIdx(puzzleInfo, idx) {
|
|
||||||
let c = puzzleInfo.coords[idx]
|
|
||||||
let cx = c.x * puzzleInfo.tileSize
|
|
||||||
let cy = c.y * puzzleInfo.tileSize
|
|
||||||
return {
|
|
||||||
x: cx,
|
|
||||||
y: cy,
|
|
||||||
w: puzzleInfo.tileSize,
|
|
||||||
h: puzzleInfo.tileSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pointSub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y })
|
|
||||||
|
|
||||||
const pointAdd = (a, b) => ({x: a.x + b.x, y: a.y + b.y})
|
|
||||||
|
|
||||||
// Returns the index of the puzzle tile with the highest z index
|
|
||||||
// that is not finished yet and that matches the position
|
|
||||||
const unfinishedTileByPos = (puzzle, pos) => {
|
|
||||||
let maxZ = -1
|
|
||||||
let tileIdx = -1
|
|
||||||
for (let idx = 0; idx < puzzle.tiles.length; idx++) {
|
|
||||||
const tile = puzzle.tiles[idx]
|
|
||||||
if (tile.owner === -1) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const collisionRect = {
|
|
||||||
x: tile.pos.x,
|
|
||||||
y: tile.pos.y,
|
|
||||||
w: puzzle.info.tileSize,
|
|
||||||
h: puzzle.info.tileSize,
|
|
||||||
}
|
|
||||||
if (pointInBounds(pos, collisionRect)) {
|
|
||||||
if (maxZ === -1 || tile.z > maxZ) {
|
|
||||||
maxZ = tile.z
|
|
||||||
tileIdx = idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tileIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
class DirtyRect {
|
|
||||||
constructor() {
|
|
||||||
this.reset()
|
|
||||||
}
|
|
||||||
get () {
|
|
||||||
return this.x0 === null ? null : [
|
|
||||||
{x0: this.x0, x1: this.x1, y0: this.y0, y1: this.y1}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
add (pos, offset) {
|
|
||||||
const x0 = pos.x - offset
|
|
||||||
const x1 = pos.x + offset
|
|
||||||
const y0 = pos.y - offset
|
|
||||||
const y1 = pos.y + offset
|
|
||||||
this.x0 = this.x0 === null ? x0 : Math.min(this.x0, x0)
|
|
||||||
this.x1 = this.x1 === null ? x1 : Math.max(this.x1, x1)
|
|
||||||
this.y0 = this.y0 === null ? y0 : Math.min(this.y0, y0)
|
|
||||||
this.y1 = this.y1 === null ? y1 : Math.max(this.y1, y1)
|
|
||||||
}
|
|
||||||
reset () {
|
|
||||||
this.x0 = null
|
|
||||||
this.x1 = null
|
|
||||||
this.y0 = null
|
|
||||||
this.y1 = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPuzzleBitmaps(puzzle) {
|
|
||||||
// load bitmap, to determine the original size of the image
|
|
||||||
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
|
|
||||||
|
|
||||||
// creation of tile bitmaps
|
|
||||||
// then create the final puzzle bitmap
|
|
||||||
// NOTE: this can decrease OR increase in size!
|
|
||||||
const bmpResized = await Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height)
|
|
||||||
return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqId() {
|
|
||||||
return Date.now().toString(36) + Math.random().toString(36).substring(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
function initme() {
|
function initme() {
|
||||||
// return uniqId()
|
// return uniqId()
|
||||||
let ID = localStorage.getItem("ID")
|
let ID = localStorage.getItem("ID")
|
||||||
if (!ID) {
|
if (!ID) {
|
||||||
ID = uniqId()
|
ID = Util.uniqId()
|
||||||
localStorage.setItem("ID", ID)
|
localStorage.setItem("ID", ID)
|
||||||
}
|
}
|
||||||
return ID
|
return ID
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main () {
|
const getFirstOwnedTile = (puzzle, userId) => {
|
||||||
|
for (let t of puzzle.tiles) {
|
||||||
|
if (t.owner === userId) {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class EventAdapter {
|
||||||
|
constructor(canvas, viewport) {
|
||||||
|
this._mouseEvts = []
|
||||||
|
this._viewport = viewport
|
||||||
|
canvas.addEventListener('mousedown', this._mouseDown.bind(this))
|
||||||
|
canvas.addEventListener('mouseup', this._mouseUp.bind(this))
|
||||||
|
canvas.addEventListener('mousemove', this._mouseMove.bind(this))
|
||||||
|
canvas.addEventListener('wheel', this._wheel.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeAll() {
|
||||||
|
if (this._mouseEvts.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const all = this._mouseEvts.slice()
|
||||||
|
this._mouseEvts = []
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseDown(e) {
|
||||||
|
if (e.button === 0) {
|
||||||
|
const pos = this._viewport.viewportToWorld({
|
||||||
|
x: e.offsetX,
|
||||||
|
y: e.offsetY,
|
||||||
|
})
|
||||||
|
this._mouseEvts.push(['down', pos.x, pos.y])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseUp(e) {
|
||||||
|
if (e.button === 0) {
|
||||||
|
const pos = this._viewport.viewportToWorld({
|
||||||
|
x: e.offsetX,
|
||||||
|
y: e.offsetY,
|
||||||
|
})
|
||||||
|
this._mouseEvts.push(['up', pos.x, pos.y])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseMove(e) {
|
||||||
|
const pos = this._viewport.viewportToWorld({
|
||||||
|
x: e.offsetX,
|
||||||
|
y: e.offsetY,
|
||||||
|
})
|
||||||
|
this._mouseEvts.push(['move', pos.x, pos.y])
|
||||||
|
}
|
||||||
|
|
||||||
|
_wheel(e) {
|
||||||
|
const pos = this._viewport.viewportToWorld({
|
||||||
|
x: e.offsetX,
|
||||||
|
y: e.offsetY,
|
||||||
|
})
|
||||||
|
const evt = e.deltaY < 0 ? 'zoomin' : 'zoomout'
|
||||||
|
this._mouseEvts.push([evt, pos.x, pos.y])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
let gameId = GAME_ID
|
let gameId = GAME_ID
|
||||||
let me = initme()
|
let CLIENT_ID = initme()
|
||||||
|
|
||||||
let cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
|
let cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
|
||||||
let cursorHand = await Graphics.loadImageToBitmap('/hand.png')
|
let cursorHand = await Graphics.loadImageToBitmap('/hand.png')
|
||||||
|
|
||||||
const game = await Communication.connect(gameId, me)
|
const game = await Communication.connect(gameId, CLIENT_ID)
|
||||||
|
Game.createGame(GAME_ID, game);
|
||||||
|
|
||||||
const bitmaps = await loadPuzzleBitmaps(game.puzzle)
|
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle)
|
||||||
const puzzle = game.puzzle
|
const puzzle = game.puzzle
|
||||||
const players = game.players
|
const players = game.players
|
||||||
|
|
||||||
// information for next render cycle
|
|
||||||
let rectPlayer = new DirtyRect()
|
|
||||||
let rerenderPlayer = true
|
|
||||||
let rectTable = new DirtyRect()
|
|
||||||
let rerenderTable = true
|
|
||||||
let rerender = true
|
let rerender = true
|
||||||
|
|
||||||
const changePlayer = (change) => {
|
const changePlayer = (change) => {
|
||||||
for (let k of Object.keys(change)) {
|
for (let k of Object.keys(change)) {
|
||||||
players[me][k] = change[k]
|
players[CLIENT_ID][k] = change[k]
|
||||||
}
|
}
|
||||||
Communication.addChange({type: 'change_player', player: players[me]})
|
|
||||||
}
|
|
||||||
const changeData = (change) => {
|
|
||||||
for (let k of Object.keys(change)) {
|
|
||||||
puzzle.data[k] = change[k]
|
|
||||||
}
|
|
||||||
Communication.addChange({type: 'change_data', data: puzzle.data})
|
|
||||||
}
|
|
||||||
const changeTile = (t, change) => {
|
|
||||||
for (let k of Object.keys(change)) {
|
|
||||||
t[k] = change[k]
|
|
||||||
}
|
|
||||||
Communication.addChange({type: 'change_tile', tile: t})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a dom and attach adapters to it so we can work with it
|
// Create a dom and attach adapters to it so we can work with it
|
||||||
const canvas = addCanvasToDom(Graphics.createCanvas())
|
const canvas = addCanvasToDom(Graphics.createCanvas())
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
const evts = new EventAdapter(canvas)
|
|
||||||
|
|
||||||
// initialize some view data
|
// initialize some view data
|
||||||
// this global data will change according to input events
|
// this global data will change according to input events
|
||||||
const viewport = new Camera(canvas)
|
const viewport = new Camera(canvas)
|
||||||
|
// center viewport
|
||||||
|
viewport.move(
|
||||||
|
-(puzzle.info.table.width - viewport.width) /2,
|
||||||
|
-(puzzle.info.table.height - viewport.height) /2
|
||||||
|
)
|
||||||
|
|
||||||
Communication.onChanges((changes) => {
|
const evts = new EventAdapter(canvas, viewport)
|
||||||
for (let change of changes) {
|
|
||||||
switch (change.type) {
|
Communication.onServerChange((msg) => {
|
||||||
case 'change_player': {
|
const msgType = msg[0]
|
||||||
if (players[change.player.id]) {
|
const evClientId = msg[1]
|
||||||
rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width)
|
const evClientSeq = msg[2]
|
||||||
|
const evChanges = msg[3]
|
||||||
|
for(let [changeType, changeData] of evChanges) {
|
||||||
|
switch (changeType) {
|
||||||
|
case 'player': {
|
||||||
|
if (changeData.id !== CLIENT_ID) {
|
||||||
|
players[changeData.id] = changeData
|
||||||
|
rerender = true
|
||||||
}
|
}
|
||||||
|
|
||||||
players[change.player.id] = change.player
|
|
||||||
|
|
||||||
rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width)
|
|
||||||
} break;
|
} break;
|
||||||
|
case 'tile': {
|
||||||
case 'change_tile': {
|
puzzle.tiles[changeData.idx] = changeData
|
||||||
rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize)
|
rerender = true
|
||||||
|
|
||||||
puzzle.tiles[change.tile.idx] = change.tile
|
|
||||||
|
|
||||||
rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize)
|
|
||||||
} break;
|
} break;
|
||||||
case 'change_data': {
|
case 'data': {
|
||||||
puzzle.data = change.data
|
puzzle.data = changeData
|
||||||
|
rerender = true
|
||||||
} break;
|
} break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Information about what tile is the player currently grabbing
|
|
||||||
let grabbingTileIdx = -1
|
|
||||||
|
|
||||||
// The actual place for the puzzle. The tiles may
|
|
||||||
// not be moved around infinitely, just on the (invisible)
|
|
||||||
// puzzle table. however, the camera may move away from the table
|
|
||||||
const puzzleTableColor = '#222'
|
|
||||||
const puzzleTable = await Graphics.createBitmap(
|
|
||||||
puzzle.info.table.width,
|
|
||||||
puzzle.info.table.height,
|
|
||||||
puzzleTableColor
|
|
||||||
)
|
|
||||||
|
|
||||||
// In the middle of the table, there is a board. this is to
|
// In the middle of the table, there is a board. this is to
|
||||||
// tell the player where to place the final puzzle
|
// tell the player where to place the final puzzle
|
||||||
const boardColor = '#505050'
|
const boardColor = '#505050'
|
||||||
const board = await Graphics.createBitmap(
|
|
||||||
puzzle.info.width,
|
|
||||||
puzzle.info.height,
|
|
||||||
boardColor
|
|
||||||
)
|
|
||||||
const boardPos = {
|
|
||||||
x: (puzzleTable.width - board.width) / 2,
|
|
||||||
y: (puzzleTable.height - board.height) / 2
|
|
||||||
} // relative to table.
|
|
||||||
|
|
||||||
|
|
||||||
// Some helper functions for working with the grabbing and snapping
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
// get all grouped tiles for a tile
|
|
||||||
function getGroupedTiles(tile) {
|
|
||||||
let grouped = []
|
|
||||||
if (tile.group) {
|
|
||||||
for (let other of puzzle.tiles) {
|
|
||||||
if (other.group === tile.group) {
|
|
||||||
grouped.push(other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
grouped.push(tile)
|
|
||||||
}
|
|
||||||
return grouped
|
|
||||||
}
|
|
||||||
|
|
||||||
// put both tiles (and their grouped tiles) in the same group
|
|
||||||
const groupTiles = (tile, other) => {
|
|
||||||
let targetGroup
|
|
||||||
let searchGroups = []
|
|
||||||
if (tile.group) {
|
|
||||||
searchGroups.push(tile.group)
|
|
||||||
}
|
|
||||||
if (other.group) {
|
|
||||||
searchGroups.push(other.group)
|
|
||||||
}
|
|
||||||
if (tile.group) {
|
|
||||||
targetGroup = tile.group
|
|
||||||
} else if (other.group) {
|
|
||||||
targetGroup = other.group
|
|
||||||
} else {
|
|
||||||
changeData({ maxGroup: puzzle.data.maxGroup + 1 })
|
|
||||||
targetGroup = puzzle.data.maxGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTile(tile, { group: targetGroup })
|
|
||||||
changeTile(other, { group: targetGroup })
|
|
||||||
|
|
||||||
if (searchGroups.length > 0) {
|
|
||||||
for (let tmp of puzzle.tiles) {
|
|
||||||
if (searchGroups.includes(tmp.group)) {
|
|
||||||
changeTile(tmp, { group: targetGroup })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine if two tiles are grouped together
|
|
||||||
const areGrouped = (t1, t2) => {
|
|
||||||
return t1.group && t1.group === t2.group
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the center position of a tile
|
|
||||||
const tileCenterPos = (tile) => {
|
|
||||||
|
|
||||||
return rectCenter(tileRectByTile(tile))
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the would-be visible bounding rect if a tile was
|
|
||||||
// in given position
|
|
||||||
const tileRectByPos = (pos) => {
|
|
||||||
return {
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y,
|
|
||||||
w: puzzle.info.tileSize,
|
|
||||||
h: puzzle.info.tileSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the current visible bounding rect for a tile
|
|
||||||
const tileRectByTile = (tile) => {
|
|
||||||
return tileRectByPos(tile.pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tilesSortedByZIndex = () => {
|
const tilesSortedByZIndex = () => {
|
||||||
const sorted = puzzle.tiles.slice()
|
const sorted = puzzle.tiles.slice()
|
||||||
return sorted.sort((t1, t2) => t1.z - t2.z)
|
return sorted.sort((t1, t2) => t1.z - t2.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setGroupedZIndex = (tile, zIndex) => {
|
|
||||||
for (let t of getGroupedTiles(tile)) {
|
|
||||||
changeTile(t, { z: zIndex })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setGroupedOwner = (tile, owner) => {
|
|
||||||
for (let t of getGroupedTiles(tile)) {
|
|
||||||
// may only change own tiles or untaken tiles
|
|
||||||
if (t.owner === me || t.owner === 0) {
|
|
||||||
changeTile(t, { owner: owner })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveGroupedTilesDiff = (tile, diffX, diffY) => {
|
|
||||||
for (let t of getGroupedTiles(tile)) {
|
|
||||||
changeTile(t, { pos: pointAdd(t.pos, { x: diffX, y: diffY }) })
|
|
||||||
|
|
||||||
// TODO: instead there could be a function to
|
|
||||||
// get min/max x/y of a group
|
|
||||||
rectTable.add(tileCenterPos(t), puzzle.info.tileDrawSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const moveGroupedTiles = (tile, dst) => {
|
|
||||||
let diff = pointSub(tile.pos, dst)
|
|
||||||
moveGroupedTilesDiff(tile, -diff.x, -diff.y)
|
|
||||||
}
|
|
||||||
const finishGroupedTiles = (tile) => {
|
|
||||||
for (let t of getGroupedTiles(tile)) {
|
|
||||||
changeTile(t, { owner: -1, z: 1 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let _last_mouse = null
|
|
||||||
let _last_mouse_down = null
|
let _last_mouse_down = null
|
||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
let last_x = null
|
for (let evt of evts.consumeAll()) {
|
||||||
let last_y = null
|
|
||||||
|
|
||||||
if (_last_mouse_down !== null) {
|
// LOCAL ONLY CHANGES
|
||||||
last_x = _last_mouse_down.x
|
// -------------------------------------------------------------
|
||||||
last_y = _last_mouse_down.y
|
const type = evt[0]
|
||||||
}
|
const pos = {x: evt[1], y: evt[2]}
|
||||||
for (let mouse of evts.consumeAll()) {
|
if (type === 'move') {
|
||||||
const tp = viewport.viewportToWorld(mouse)
|
rerender = true
|
||||||
if (mouse.type === 'move') {
|
changePlayer(pos)
|
||||||
changePlayer({ x: tp.x, y: tp.y })
|
|
||||||
if (_last_mouse) {
|
|
||||||
rectPlayer.add(_last_mouse, cursorGrab.width)
|
|
||||||
}
|
|
||||||
rectPlayer.add(mouse, cursorGrab.width)
|
|
||||||
|
|
||||||
if (_last_mouse_down !== null) {
|
if (_last_mouse_down && !getFirstOwnedTile(puzzle, CLIENT_ID)) {
|
||||||
_last_mouse_down = mouse
|
|
||||||
|
|
||||||
if (last_x === null || last_y === null) {
|
|
||||||
last_x = mouse.x
|
|
||||||
last_y = mouse.y
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grabbingTileIdx >= 0) {
|
|
||||||
const tp_last = viewport.viewportToWorld({ x: last_x, y: last_y })
|
|
||||||
const diffX = tp.x - tp_last.x
|
|
||||||
const diffY = tp.y - tp_last.y
|
|
||||||
|
|
||||||
const t = puzzle.tiles[grabbingTileIdx]
|
|
||||||
moveGroupedTilesDiff(t, diffX, diffY)
|
|
||||||
|
|
||||||
// todo: dont +- tileDrawSize, we can work with less?
|
|
||||||
rectTable.add(tp, puzzle.info.tileDrawSize)
|
|
||||||
rectTable.add(tp_last, puzzle.info.tileDrawSize)
|
|
||||||
} else {
|
|
||||||
// move the cam
|
// move the cam
|
||||||
const diffX = Math.round(mouse.x - last_x)
|
const mouse = viewport.worldToViewport(pos)
|
||||||
const diffY = Math.round(mouse.y - last_y)
|
const diffX = Math.round(mouse.x - _last_mouse_down.x)
|
||||||
|
const diffY = Math.round(mouse.y - _last_mouse_down.y)
|
||||||
viewport.move(diffX, diffY)
|
viewport.move(diffX, diffY)
|
||||||
rerender = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (mouse.type === 'down') {
|
|
||||||
changePlayer({ down: true })
|
|
||||||
rectPlayer.add(mouse, cursorGrab.width)
|
|
||||||
|
|
||||||
_last_mouse_down = mouse
|
_last_mouse_down = mouse
|
||||||
if (last_x === null || last_y === null) {
|
|
||||||
last_x = mouse.x
|
|
||||||
last_y = mouse.y
|
|
||||||
}
|
}
|
||||||
|
} else if (type === 'down') {
|
||||||
grabbingTileIdx = unfinishedTileByPos(puzzle, tp)
|
_last_mouse_down = viewport.worldToViewport(pos)
|
||||||
console.log(grabbingTileIdx)
|
} else if (type === 'up') {
|
||||||
if (grabbingTileIdx >= 0) {
|
|
||||||
changeData({ maxZ: puzzle.data.maxZ + 1 })
|
|
||||||
setGroupedZIndex(puzzle.tiles[grabbingTileIdx], puzzle.data.maxZ)
|
|
||||||
setGroupedOwner(puzzle.tiles[grabbingTileIdx], me)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (mouse.type === 'up') {
|
|
||||||
changePlayer({ down: false })
|
|
||||||
if (_last_mouse) {
|
|
||||||
rectPlayer.add(_last_mouse, cursorGrab.width)
|
|
||||||
}
|
|
||||||
rectPlayer.add(mouse, cursorGrab.width)
|
|
||||||
|
|
||||||
_last_mouse_down = null
|
_last_mouse_down = null
|
||||||
last_x = null
|
} else if (type === 'zoomin') {
|
||||||
last_y === null
|
if (viewport.zoomIn()) {
|
||||||
|
|
||||||
if (grabbingTileIdx >= 0) {
|
|
||||||
// Check if the tile was dropped at the correct
|
|
||||||
// location
|
|
||||||
|
|
||||||
let tile = puzzle.tiles[grabbingTileIdx]
|
|
||||||
setGroupedOwner(tile, 0)
|
|
||||||
let pt = pointSub(tile.pos, boardPos)
|
|
||||||
let dst = tileRectByPos(pt)
|
|
||||||
let srcRect = srcRectByIdx(puzzle.info, grabbingTileIdx)
|
|
||||||
if (rectCenterDistance(srcRect, dst) < puzzle.info.snapDistance) {
|
|
||||||
// Snap the tile to the final destination
|
|
||||||
moveGroupedTiles(tile, {
|
|
||||||
x: srcRect.x + boardPos.x,
|
|
||||||
y: srcRect.y + boardPos.y,
|
|
||||||
})
|
|
||||||
finishGroupedTiles(tile)
|
|
||||||
rectTable.add(tp, puzzle.info.tileDrawSize)
|
|
||||||
} else {
|
|
||||||
// Snap to other tiles
|
|
||||||
const check = (t, off, other) => {
|
|
||||||
if (!other || (other.owner === -1) || areGrouped(t, other)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const trec_ = tileRectByTile(t)
|
|
||||||
const otrec = rectMoved(
|
|
||||||
tileRectByTile(other),
|
|
||||||
off[0] * puzzle.info.tileSize,
|
|
||||||
off[1] * puzzle.info.tileSize
|
|
||||||
)
|
|
||||||
if (rectCenterDistance(trec_, otrec) < puzzle.info.snapDistance) {
|
|
||||||
moveGroupedTiles(t, { x: otrec.x, y: otrec.y })
|
|
||||||
groupTiles(t, other)
|
|
||||||
setGroupedZIndex(t, t.z)
|
|
||||||
rectTable.add(tileCenterPos(t), puzzle.info.tileDrawSize)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let t of getGroupedTiles(tile)) {
|
|
||||||
let others = getSurroundingTilesByIdx(puzzle, t.idx)
|
|
||||||
if (
|
|
||||||
check(t, [0, 1], others[0]) // top
|
|
||||||
|| check(t, [-1, 0], others[1]) // right
|
|
||||||
|| check(t, [0, -1], others[2]) // bottom
|
|
||||||
|| check(t, [1, 0], others[3]) // left
|
|
||||||
) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
grabbingTileIdx = -1
|
|
||||||
} else if (mouse.type === 'wheel') {
|
|
||||||
if (
|
|
||||||
mouse.deltaY < 0 && viewport.zoomIn()
|
|
||||||
|| mouse.deltaY > 0 && viewport.zoomOut()
|
|
||||||
) {
|
|
||||||
rerender = true
|
rerender = true
|
||||||
changePlayer({ x: tp.x, y: tp.y })
|
changePlayer(pos)
|
||||||
if (_last_mouse) {
|
}
|
||||||
rectPlayer.add(_last_mouse, cursorGrab.width)
|
} else if (type === 'zoomout') {
|
||||||
}
|
if (viewport.zoomOut()) {
|
||||||
rectPlayer.add(mouse, cursorGrab.width)
|
rerender = true
|
||||||
|
changePlayer(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(mouse)
|
|
||||||
_last_mouse = mouse
|
|
||||||
}
|
|
||||||
if (rectTable.get()) {
|
|
||||||
rerenderTable = true
|
|
||||||
}
|
|
||||||
if (rectPlayer.get()) {
|
|
||||||
rerenderPlayer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Communication.sendChanges()
|
// LOCAL + SERVER CHANGES
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
Game.handleInput(GAME_ID, CLIENT_ID, evt)
|
||||||
|
Communication.sendClientEvent(evt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRender = () => {
|
const onRender = () => {
|
||||||
if (!rerenderTable && !rerenderPlayer && !rerender) {
|
if (!rerender) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -640,24 +219,33 @@ async function main () {
|
||||||
|
|
||||||
if (DEBUG) Debug.checkpoint_start(0)
|
if (DEBUG) Debug.checkpoint_start(0)
|
||||||
|
|
||||||
ctx.fillStyle = puzzleTableColor
|
// CLEAR CTX
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
// ---------------------------------------------------------------
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
if (DEBUG) Debug.checkpoint('clear done')
|
if (DEBUG) Debug.checkpoint('clear done')
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// DRAW BOARD
|
// DRAW BOARD
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
pos = viewport.worldToViewport(boardPos)
|
pos = viewport.worldToViewport({
|
||||||
dim = viewport.worldDimToViewport({w: board.width, h: board.height})
|
x: (puzzle.info.table.width - puzzle.info.width) / 2,
|
||||||
ctx.drawImage(board,
|
y: (puzzle.info.table.height - puzzle.info.height) / 2
|
||||||
0, 0, board.width, board.height,
|
})
|
||||||
pos.x, pos.y, dim.w, dim.h
|
dim = viewport.worldDimToViewport({
|
||||||
)
|
w: puzzle.info.width,
|
||||||
|
h: puzzle.info.height,
|
||||||
|
})
|
||||||
|
ctx.fillStyle = boardColor
|
||||||
|
ctx.fillRect(pos.x, pos.y, dim.w, dim.h)
|
||||||
if (DEBUG) Debug.checkpoint('board done')
|
if (DEBUG) Debug.checkpoint('board done')
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// DRAW TILES
|
// DRAW TILES
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
for (let tile of tilesSortedByZIndex()) {
|
for (let tile of tilesSortedByZIndex()) {
|
||||||
let bmp = bitmaps[tile.idx]
|
const bmp = bitmaps[tile.idx]
|
||||||
pos = viewport.worldToViewport({
|
pos = viewport.worldToViewport({
|
||||||
x: puzzle.info.tileDrawOffset + tile.pos.x,
|
x: puzzle.info.tileDrawOffset + tile.pos.x,
|
||||||
y: puzzle.info.tileDrawOffset + tile.pos.y,
|
y: puzzle.info.tileDrawOffset + tile.pos.y,
|
||||||
|
|
@ -672,6 +260,8 @@ async function main () {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (DEBUG) Debug.checkpoint('tiles done')
|
if (DEBUG) Debug.checkpoint('tiles done')
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// DRAW PLAYERS
|
// DRAW PLAYERS
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -679,19 +269,16 @@ async function main () {
|
||||||
const p = players[id]
|
const p = players[id]
|
||||||
const cursor = p.down ? cursorGrab : cursorHand
|
const cursor = p.down ? cursorGrab : cursorHand
|
||||||
const pos = viewport.worldToViewport(p)
|
const pos = viewport.worldToViewport(p)
|
||||||
ctx.drawImage(cursor, pos.x, pos.y)
|
ctx.drawImage(cursor,
|
||||||
|
Math.round(pos.x - cursor.width/2),
|
||||||
|
Math.round(pos.y - cursor.height/2)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
//
|
|
||||||
if (DEBUG) Debug.checkpoint('players done')
|
if (DEBUG) Debug.checkpoint('players done')
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
if (DEBUG) Debug.checkpoint('all done')
|
|
||||||
|
|
||||||
rerenderTable = false
|
|
||||||
rerenderPlayer = false
|
|
||||||
rerender = false
|
rerender = false
|
||||||
rectTable.reset()
|
|
||||||
rectPlayer.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run({
|
run({
|
||||||
|
|
|
||||||
23
server/Game.js
Normal file
23
server/Game.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createPuzzle } from './Puzzle.js'
|
||||||
|
import GameCommon from './../common/GameCommon.js'
|
||||||
|
|
||||||
|
async function createGame(gameId, targetTiles, image) {
|
||||||
|
const game = {
|
||||||
|
puzzle: await createPuzzle(targetTiles, image),
|
||||||
|
players: {},
|
||||||
|
|
||||||
|
sockets: [],
|
||||||
|
evtInfos: {},
|
||||||
|
}
|
||||||
|
GameCommon.setGame(gameId, game)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createGame,
|
||||||
|
exists: GameCommon.exists,
|
||||||
|
addPlayer: GameCommon.addPlayer,
|
||||||
|
addSocket: GameCommon.addSocket,
|
||||||
|
get: GameCommon.get,
|
||||||
|
getSockets: GameCommon.getSockets,
|
||||||
|
handleInput: GameCommon.handleInput,
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,23 @@
|
||||||
import sizeOf from 'image-size'
|
import sizeOf from 'image-size'
|
||||||
import { choice, shuffle } from './util.js'
|
import Util from './../common/Util.js'
|
||||||
|
|
||||||
// cut size of each puzzle tile in the
|
// cut size of each puzzle tile in the
|
||||||
// final resized version of the puzzle image
|
// final resized version of the puzzle image
|
||||||
const TILE_SIZE = 64
|
const TILE_SIZE = 64
|
||||||
|
|
||||||
async function createPuzzle(targetTiles, image) {
|
async function createPuzzle(targetTiles, image) {
|
||||||
const imgPath = './../game' + image
|
const imagePath = './../game' + image
|
||||||
const imgUrl = image
|
const imageUrl = image
|
||||||
|
|
||||||
// load bitmap, to determine the original size of the image
|
// load bitmap, to determine the original size of the image
|
||||||
let dim = sizeOf(imgPath)
|
const dim = sizeOf(imagePath)
|
||||||
|
|
||||||
// determine puzzle information from the bitmap
|
// determine puzzle information from the bitmap
|
||||||
let info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
|
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
|
||||||
|
|
||||||
let tiles = new Array(info.tiles)
|
let tiles = new Array(info.tiles)
|
||||||
for (let i = 0; i < tiles.length; i++) {
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
tiles[i] = {
|
tiles[i] = { idx: i }
|
||||||
idx: i,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const shapes = determinePuzzleTileShapes(info)
|
const shapes = determinePuzzleTileShapes(info)
|
||||||
|
|
||||||
|
|
@ -33,43 +31,46 @@ async function createPuzzle(targetTiles, image) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tableWidth = info.width * 3
|
const tableWidth = info.width * 3
|
||||||
let tableHeight = info.height * 3
|
const tableHeight = info.height * 3
|
||||||
|
|
||||||
let off = (info.tileSize * 1.5)
|
const off = info.tileSize * 1.5
|
||||||
let last = {x: info.width - (1 * off), y: info.height - (2 * off) }
|
let last = {
|
||||||
let count_x = Math.ceil(info.width / off) + 2
|
x: info.width - (1 * off),
|
||||||
let count_y = Math.ceil(info.height / off) + 2
|
y: info.height - (2 * off),
|
||||||
|
}
|
||||||
|
let countX = Math.ceil(info.width / off) + 2
|
||||||
|
let countY = Math.ceil(info.height / off) + 2
|
||||||
|
|
||||||
let diff_x = off
|
let diffX = off
|
||||||
let diff_y = 0
|
let diffY = 0
|
||||||
let index = 0
|
let index = 0
|
||||||
for (let 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+=diff_x
|
last.x+=diffX
|
||||||
last.y+=diff_y
|
last.y+=diffY
|
||||||
index++
|
index++
|
||||||
// did we move horizontally?
|
// did we move horizontally?
|
||||||
if (diff_x !== 0) {
|
if (diffX !== 0) {
|
||||||
if (index === count_x) {
|
if (index === countX) {
|
||||||
diff_y = diff_x
|
diffY = diffX
|
||||||
count_y ++
|
countY++
|
||||||
diff_x = 0
|
diffX = 0
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (index === count_y) {
|
if (index === countY) {
|
||||||
diff_x = -diff_y
|
diffX = -diffY
|
||||||
count_x ++
|
countX++
|
||||||
diff_y = 0
|
diffY = 0
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// then shuffle the positions
|
// then shuffle the positions
|
||||||
positions = shuffle(positions)
|
positions = Util.shuffle(positions)
|
||||||
|
|
||||||
tiles = tiles.map(tile => {
|
tiles = tiles.map(tile => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -91,7 +92,7 @@ async function createPuzzle(targetTiles, image) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Complete puzzle object
|
// Complete puzzle object
|
||||||
const p = {
|
return {
|
||||||
// tiles array
|
// tiles array
|
||||||
tiles,
|
tiles,
|
||||||
// game data for puzzle, data changes during the game
|
// game data for puzzle, data changes during the game
|
||||||
|
|
@ -109,7 +110,7 @@ async function createPuzzle(targetTiles, image) {
|
||||||
},
|
},
|
||||||
// information that was used to create the puzzle
|
// information that was used to create the puzzle
|
||||||
targetTiles: targetTiles,
|
targetTiles: targetTiles,
|
||||||
imageUrl: imgUrl,
|
imageUrl,
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -122,8 +123,8 @@ async function createPuzzle(targetTiles, image) {
|
||||||
// makes the tile snap to destination
|
// makes the tile snap to destination
|
||||||
snapDistance: info.tileSize / 2,
|
snapDistance: info.tileSize / 2,
|
||||||
tiles: info.tiles, // the final number of tiles in the puzzle
|
tiles: info.tiles, // the final number of tiles in the puzzle
|
||||||
tiles_x: info.tiles_x, // number of tiles each row
|
tilesX: info.tilesX, // number of tiles each row
|
||||||
tiles_y: info.tiles_y, // number of tiles each col
|
tilesY: info.tilesY, // number of tiles each col
|
||||||
coords: info.coords, // map of tile index to its coordinates
|
coords: info.coords, // map of tile index to its coordinates
|
||||||
// ( index => {x, y} )
|
// ( index => {x, y} )
|
||||||
// this is not the physical coordinate, but
|
// this is not the physical coordinate, but
|
||||||
|
|
@ -133,7 +134,6 @@ async function createPuzzle(targetTiles, image) {
|
||||||
shapes: shapes, // tile shapes
|
shapes: shapes, // tile shapes
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function determinePuzzleTileShapes(info) {
|
function determinePuzzleTileShapes(info) {
|
||||||
|
|
@ -142,34 +142,38 @@ function determinePuzzleTileShapes(info) {
|
||||||
const shapes = new Array(info.tiles)
|
const shapes = new Array(info.tiles)
|
||||||
for (let i = 0; i < info.tiles; i++) {
|
for (let i = 0; i < info.tiles; i++) {
|
||||||
shapes[i] = {
|
shapes[i] = {
|
||||||
top: info.coords[i].y === 0 ? 0 : shapes[i - info.tiles_x].bottom * -1,
|
top: info.coords[i].y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
|
||||||
right: info.coords[i].x === info.tiles_x - 1 ? 0 : choice(tabs),
|
right: info.coords[i].x === info.tilesX - 1 ? 0 : Util.choice(tabs),
|
||||||
left: info.coords[i].x === 0 ? 0 : shapes[i - 1].right * -1,
|
left: info.coords[i].x === 0 ? 0 : shapes[i - 1].right * -1,
|
||||||
bottom: info.coords[i].y === info.tiles_y - 1 ? 0 : choice(tabs),
|
bottom: info.coords[i].y === info.tilesY - 1 ? 0 : Util.choice(tabs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shapes
|
return shapes
|
||||||
}
|
}
|
||||||
|
|
||||||
const determinePuzzleInfo = (w, h, targetTiles) => {
|
const determineTilesXY = (w, h, targetTiles) => {
|
||||||
let tileSize = 0
|
const w_ = w < h ? (w * h) : (w * w)
|
||||||
|
const h_ = w < h ? (h * h) : (w * h)
|
||||||
|
let size = 0
|
||||||
let tiles = 0
|
let tiles = 0
|
||||||
do {
|
do {
|
||||||
tileSize++
|
size++
|
||||||
tiles = tilesFit(w, h, tileSize)
|
tiles = Math.floor(w_ / size) * Math.floor(h_ / size)
|
||||||
} while (tiles >= targetTiles)
|
} while (tiles >= targetTiles)
|
||||||
tileSize--
|
size--
|
||||||
|
return {
|
||||||
|
tilesX: Math.round(w_ / size),
|
||||||
|
tilesY: Math.round(h_ / size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tiles = tilesFit(w, h, tileSize)
|
const determinePuzzleInfo = (w, h, targetTiles) => {
|
||||||
const tiles_x = Math.round(w / tileSize)
|
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
|
||||||
const tiles_y = Math.round(h / tileSize)
|
const tiles = tilesX * tilesY
|
||||||
tiles = tiles_x * tiles_y
|
const tileSize = TILE_SIZE
|
||||||
|
const width = tilesX * tileSize
|
||||||
// then resize to final TILE_SIZE (which is always the same)
|
const height = tilesY * tileSize
|
||||||
tileSize = TILE_SIZE
|
const coords = buildCoords({ width, height, tileSize, tiles })
|
||||||
const width = tiles_x * tileSize
|
|
||||||
const height = tiles_y * tileSize
|
|
||||||
const coords = coordsByNum({ width, height, tileSize, tiles })
|
|
||||||
|
|
||||||
const tileMarginWidth = tileSize * .5;
|
const tileMarginWidth = tileSize * .5;
|
||||||
const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2)
|
const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2)
|
||||||
|
|
@ -181,25 +185,23 @@ const determinePuzzleInfo = (w, h, targetTiles) => {
|
||||||
tileMarginWidth,
|
tileMarginWidth,
|
||||||
tileDrawSize,
|
tileDrawSize,
|
||||||
tiles,
|
tiles,
|
||||||
tiles_x,
|
tilesX,
|
||||||
tiles_y,
|
tilesY,
|
||||||
coords,
|
coords,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tilesFit = (w, h, size) => Math.floor(w / size) * Math.floor(h / size)
|
const buildCoords = (puzzleInfo) => {
|
||||||
|
const wTiles = puzzleInfo.width / puzzleInfo.tileSize
|
||||||
const coordsByNum = (puzzleInfo) => {
|
|
||||||
const w_tiles = puzzleInfo.width / puzzleInfo.tileSize
|
|
||||||
const coords = new Array(puzzleInfo.tiles)
|
const coords = new Array(puzzleInfo.tiles)
|
||||||
for (let i = 0; i < puzzleInfo.tiles; i++) {
|
for (let i = 0; i < puzzleInfo.tiles; i++) {
|
||||||
const y = Math.floor(i / w_tiles)
|
const y = Math.floor(i / wTiles)
|
||||||
const x = i % w_tiles
|
const x = i % wTiles
|
||||||
coords[i] = { x, y }
|
coords[i] = { x, y }
|
||||||
}
|
}
|
||||||
return coords
|
return coords
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createPuzzle
|
createPuzzle,
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import WebSocketServer from './WebSocketServer.js'
|
import WebSocketServer from './WebSocketServer.js'
|
||||||
|
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { createPuzzle } from './puzzle.js'
|
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
import { uniqId, choice } from './util.js'
|
import Protocol from './../common/Protocol.js'
|
||||||
|
import Util from './../common/Util.js'
|
||||||
|
import Game from './Game.js'
|
||||||
|
|
||||||
// desired number of tiles
|
// desired number of tiles
|
||||||
// actual calculated number can be higher
|
// actual calculated number can be higher
|
||||||
const TARGET_TILES = 500
|
const TARGET_TILES = 1000
|
||||||
|
|
||||||
const IMAGES = [
|
const IMAGES = [
|
||||||
'/example-images/ima_86ec3fa.jpeg',
|
'/example-images/ima_86ec3fa.jpeg',
|
||||||
|
|
@ -37,6 +38,7 @@ app.use('/g/:gid', (req, res, next) => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use('/common/', express.static('./../common/'))
|
||||||
app.use('/', (req, res, next) => {
|
app.use('/', (req, res, next) => {
|
||||||
if (req.path === '/') {
|
if (req.path === '/') {
|
||||||
res.send(`
|
res.send(`
|
||||||
|
|
@ -44,7 +46,7 @@ app.use('/', (req, res, next) => {
|
||||||
html,body {margin: 0; overflow: hidden;}
|
html,body {margin: 0; overflow: hidden;}
|
||||||
html, body, #main { background: #222 }
|
html, body, #main { background: #222 }
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<a href="/g/${uniqId()}">New game :P</a>
|
<a href="/g/${Util.uniqId()}">New game :P</a>
|
||||||
${Object.keys(games).map(k => {
|
${Object.keys(games).map(k => {
|
||||||
return `<a href="/g/${k}">Game ${k}</a>`
|
return `<a href="/g/${k}">Game ${k}</a>`
|
||||||
})}
|
})}
|
||||||
|
|
@ -59,61 +61,41 @@ app.use('/', (req, res, next) => {
|
||||||
const wss = new WebSocketServer(config.ws);
|
const wss = new WebSocketServer(config.ws);
|
||||||
|
|
||||||
const notify = (data, sockets) => {
|
const notify = (data, sockets) => {
|
||||||
// TODO: throttle
|
// TODO: throttle?
|
||||||
for (let socket of sockets) {
|
for (let socket of sockets) {
|
||||||
wss.notifyOne(data, socket)
|
wss.notifyOne(data, socket)
|
||||||
}
|
}
|
||||||
console.log('notify', data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.on('message', async ({socket, data}) => {
|
wss.on('message', async ({socket, data}) => {
|
||||||
try {
|
try {
|
||||||
const proto = socket.protocol.split('|')
|
const proto = socket.protocol.split('|')
|
||||||
const uid = proto[0]
|
const clientId = proto[0]
|
||||||
const gid = proto[1]
|
const gameId = proto[1]
|
||||||
const parsed = JSON.parse(data)
|
const msg = JSON.parse(data)
|
||||||
switch (parsed.type) {
|
const msgType = msg[0]
|
||||||
case 'init': {
|
switch (msgType) {
|
||||||
// a new player (or previous player) joined
|
case Protocol.EV_CLIENT_INIT: {
|
||||||
games[gid] = games[gid] || {
|
if (!Game.exists(gameId)) {
|
||||||
puzzle: await createPuzzle(TARGET_TILES, choice(IMAGES)),
|
await Game.createGame(gameId, TARGET_TILES, Util.choice(IMAGES))
|
||||||
players: {},
|
|
||||||
sockets: []
|
|
||||||
}
|
}
|
||||||
if (!games[gid].sockets.includes(socket)) {
|
Game.addPlayer(gameId, clientId)
|
||||||
games[gid].sockets.push(socket)
|
Game.addSocket(gameId, socket)
|
||||||
}
|
|
||||||
games[gid].players[uid] = {id: uid, x: 0, y: 0, down: false}
|
|
||||||
|
|
||||||
wss.notifyOne({
|
notify(
|
||||||
type: 'init',
|
[Protocol.EV_SERVER_INIT, Game.get(gameId)],
|
||||||
game: {
|
[socket]
|
||||||
puzzle: games[gid].puzzle,
|
)
|
||||||
players: games[gid].players,
|
|
||||||
},
|
|
||||||
}, socket)
|
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
// somebody has changed the state
|
case Protocol.EV_CLIENT_EVENT: {
|
||||||
case 'state': {
|
const clientSeq = msg[1]
|
||||||
for (let change of parsed.state.changes) {
|
const clientEvtData = msg[2]
|
||||||
switch (change.type) {
|
const changes = Game.handleInput(gameId, clientId, clientEvtData)
|
||||||
case 'change_player': {
|
notify(
|
||||||
games[gid].players[uid] = change.player
|
[Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes],
|
||||||
} break;
|
Game.getSockets(gameId)
|
||||||
case 'change_tile': {
|
)
|
||||||
games[gid].puzzle.tiles[change.tile.idx] = change.tile
|
|
||||||
} break;
|
|
||||||
case 'change_data': {
|
|
||||||
games[gid].puzzle.data = change.data
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notify({
|
|
||||||
type:'state_changed',
|
|
||||||
origin: uid,
|
|
||||||
changes: parsed.state.changes,
|
|
||||||
}, games[gid].sockets)
|
|
||||||
} break;
|
} break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue