Files
wasm-pong/client/www/index.js
2022-06-16 19:54:43 +02:00

384 lines
11 KiB
JavaScript

import { FieldWrapper } from "wasm-app";
const GRID_COLOR = "#CCCCCC";
let field = FieldWrapper.new();
const width = field.width();
const height = field.height();
const canvas = document.getElementById('wasm-app-canvas');
canvas.height = height
canvas.width = width
const ctx = canvas.getContext('2d');
let framesLastSecond = [];
let lastFpsUpdate = 0;
const FPS_UPDATE_THRESHOLD = 1000;
let fps = 0;
let isInitial = true;
let lastUpdate = 0;
let paused = false;
let resetRequested = false;
let debug = false;
let keysDown = new Set();
let actions = [];
let networkSession = null;
let player = null;
let websocket = null;
let isHost = true;
let events = [];
const renderLoop = () => {
if (resetRequested) {
resetRequested = false;
reset();
return;
}
actions = getInputActions().filter(it => {
if (!networkSession) {
return it
}
if (networkSession && isHost) {
return it.player === 1;
}
return it.player === 2;
});
if (paused) {
requestAnimationFrame(renderLoop);
return;
}
tick();
requestAnimationFrame(renderLoop);
}
const tick = () => {
const now = Date.now();
let update;
if (lastUpdate === 0) {
update = 0.01;
lastUpdate = now
} else {
const diff = now - lastUpdate;
lastUpdate = now;
update = diff / 1000;
}
if (debug) {
framesLastSecond.push(now)
framesLastSecond = framesLastSecond.filter(f => now - f <= 1000);
if (lastFpsUpdate === 0 || now - lastFpsUpdate >= FPS_UPDATE_THRESHOLD) {
lastFpsUpdate = now;
fps = framesLastSecond.length;
}
}
let objects;
if (!networkSession) {
renderActions(actions)
field.tick(actions, update);
objects = JSON.parse(field.objects());
} else if (isHost) {
const otherPlayerActions = getOtherPlayerInputActions();
const allActions = [
...actions, ...otherPlayerActions
];
renderActions(allActions);
field.tick(allActions, update);
objects = JSON.parse(field.objects());
sendEvents([...getInputEvents(), ...getMoveEvents(objects)])
} else {
const moveEventsByObj = events.filter(e => e.topic === "move").reduce((acc, moveEvent) => {
const {id: objId} = moveEvent.msg;
if (!acc[objId]) {
acc[objId] = [];
}
acc[objId].push(moveEvent);
return acc;
}, {});
const otherPlayerActions = getOtherPlayerInputActions();
renderActions([...actions, ...otherPlayerActions]);
const latestMoveEvents = Object.entries(moveEventsByObj)
.map(([_, moveEvents]) => moveEvents[moveEvents.length - 1]);
objects = latestMoveEvents.map(({msg}) => msg);
sendEvents(getInputEvents())
}
render(objects);
}
const getOtherPlayerInputActions = () => {
const peerInputEvents = events.filter(e => e.topic === "input").filter(it => it.msg.player !== player.id).map(it => it.msg)
const lastPeerInputEvent = peerInputEvents.length ? peerInputEvents[peerInputEvents.length - 1] : null
return lastPeerInputEvent ? lastPeerInputEvent.inputs : []
}
const getMoveEvents = objects => {
return objects.map(o => ({session_id: networkSession.hash, topic: 'move', msg: JSON.stringify({...o, session_id: networkSession.hash, ts: Date.now()})}));
}
const getInputEvents = () => {
if (actions.length) {
const inputEvent = {msg: JSON.stringify({inputs: actions, player: player.id, session_id: networkSession.hash, ts: Date.now()}), session_id: networkSession.hash, topic: 'input'}
return [inputEvent]
}
return [{msg: JSON.stringify({inputs: [], player: player.id, session_id: networkSession.hash, ts: Date.now()}), session_id: networkSession.hash, topic: 'input'}];
}
const sendEvents = events => {
const eventWrapper = {session_id: networkSession.hash, events};
if (eventWrapper.events.length) {
websocket.send(JSON.stringify(eventWrapper));
}
}
const render = objects => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawObjects(objects);
}
const renderActions = actions => {
[...document.getElementsByClassName("game_input")].forEach(el => {
el.className = "game_input game_input--inactive"
})
actions.forEach(action => {
const id = `game_inputs__player_${action.player}__${action.input.toLowerCase()}`
const el = document.getElementById(id);
el.className = "game_input game_input--active";
})
}
const reset = () => {
framesLastSecond = [];
lastFpsUpdate = 0;
fps = 0;
resetRequested = false;
isInitial = true;
lastUpdate = 0;
paused = false;
debug = false;
keysDown = new Set();
actions = [];
field = FieldWrapper.new();
networkSession = null;
isHost = true;
if (websocket) {
websocket.close();
}
websocket = null;
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
}
window.WASM_PONG = {}
window.WASM_PONG.width = width
window.WASM_PONG.height = height
window.WASM_PONG.createOnlineSession = () => {
resetRequested = true;
fetch("/pong/api/create_session", {method: 'POST'}).then(res => res.json()).then(({data: session}) => {
console.log("Created session:")
console.log(session)
networkSession = session.session
player = session.player
isHost = true;
const session_display_tag = document.getElementById("network_session");
session_display_tag.style.display = 'block';
session_display_tag.innerHTML = JSON.stringify(session)
websocket = new WebSocket(`ws://${location.host.split(':')[0]}/pong/ws?session_id=${session.session.hash}&connection_type=host`)
websocket.onmessage = (event) => {
addEvents(event);
}
waitForWebsocket(10, renderLoop)
}).catch(err => {
console.error(`Failed to create session: ${err}`)
})
}
window.WASM_PONG.joinOnlineSession = () => {
if (!isInitial) {
resetRequested = true;
setTimeout(() => {
window.WASM_PONG.joinOnlineSession()
})
return;
}
const sessionId = document.getElementById('join-online-input').value;
executeJoinSession(sessionId);
}
const executeJoinSession = (sessionId) => {
fetch(`/pong/api/join_session`, {method: 'POST', body: JSON.stringify({session_id: sessionId})}).then(res => res.json()).then(({data: session}) => {
console.log("Joined session:")
console.log(session);
networkSession = session.session
player = session.player
isHost = false
const session_display_tag = document.getElementById("network_session");
session_display_tag.style.display = 'block';
session_display_tag.innerHTML = JSON.stringify(session)
// Field will be instrumented by only drawing the objects for the peer, e.g. collision detection will happen at the host.
field = null;
websocket = new WebSocket(`ws://${location.host.split(':')[0]}/pong/ws?session_id=${session.session.hash}&connection_type=peer`)
websocket.onmessage = (event) => {
addEvents(event);
}
waitForWebsocket(10, renderLoop)
}).catch(err => {
console.error(`Failed to create session: ${err}`)
})
}
const waitForWebsocket = (retries, cb) => {
if (retries <= 0) {
console.error("Websocket not established successfully")
return
}
if(websocket.readyState !== 1) {
setTimeout(() => {
waitForWebsocket(retries - 1, cb)
}, 100)
} else {
cb()
}
}
const addEvents = wsEvent => {
const gameEvents = JSON.parse(wsEvent.data);
gameEvents.forEach(gameEvent => {
const deserialized = {...gameEvent, msg: JSON.parse(gameEvent.msg)};
events.push(deserialized);
})
events = events.slice(events.length - 100, events.length)
}
window.WASM_PONG.pauseGame = () => {
paused = true;
document.getElementById("pause-btn").disabled = true;
document.getElementById("resume-btn").disabled = false;
document.getElementById("tick-btn").disabled = false;
}
window.WASM_PONG.resumeGame = () => {
paused = false;
document.getElementById("pause-btn").disabled = false;
document.getElementById("resume-btn").disabled = true;
document.getElementById("tick-btn").disabled = true;
}
window.WASM_PONG.resetGame = () => {
reset()
document.getElementById("pause-btn").disabled = true;
document.getElementById("resume-btn").disabled = false;
document.getElementById("tick-btn").disabled = true;
}
window.WASM_PONG.oneTick = () => {
if (!paused) {
return;
}
tick()
}
window.WASM_PONG.toggleDebug = () => {
debug = !debug
}
const drawObjects = objects => {
objects.forEach(obj => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
const obj_y = height - obj.y;
const orientation_y = obj.orientation_y * -1;
const vel_y = obj.vel_y * -1;
// rect
if (obj.shape_param_2) {
ctx.moveTo(obj.x, obj_y)
ctx.arc(obj.x, obj_y, 10, 0, 2 * Math.PI);
ctx.rect(obj.x - obj.shape_param_1 / 2, obj_y - obj.shape_param_2 / 2, obj.shape_param_1, obj.shape_param_2);
}
// circle
else {
ctx.moveTo(obj.x, obj_y);
ctx.arc(obj.x, obj_y, obj.shape_param_1, 0, 2 * Math.PI);
}
ctx.stroke();
if (debug) {
// velocity
drawLine(ctx, obj.x, obj_y, obj.x + obj.vel_x * 20, obj_y + vel_y * 20, 'red')
// orientation
drawLine(ctx, obj.x, obj_y, obj.x + obj.orientation_x * 20, obj_y + orientation_y * 20, 'blue')
ctx.fillText(`[x: ${obj.x}, y: ${obj_y}]`, obj.x + 10, obj_y)
}
})
if (debug) {
const fpsPosX = width - 50;
const fpsPosY = 20;
ctx.rect(fpsPosX, fpsPosY, 100, 20)
ctx.fillText(`fps: ${fps}`, fpsPosX, fpsPosY)
}
}
const drawLine = (ctx, from_x, from_y, to_x, to_y, color) => {
ctx.beginPath();
ctx.moveTo(from_x, from_y);
ctx.strokeStyle = color;
ctx.lineTo(to_x, to_y);
ctx.stroke();
}
const listenToKeys = () => {
const relevantKeys = ['ArrowUp', 'ArrowDown', 'KeyW', 'KeyS']
document.addEventListener('keydown', (e) => {
if (!relevantKeys.includes(e.code)) {
return;
}
keysDown.add(e.code)
})
document.addEventListener('keyup', (e) => {
if (!relevantKeys.includes(e.code)) {
return;
}
keysDown.delete(e.code);
})
}
const getInputActions = () => {
return [...keysDown].map(key => {
switch(key) {
case 'KeyW':
return {input: 'UP', obj_id: 0, player: 1}
case 'KeyS':
return {input: 'DOWN', obj_id: 0, player: 1}
case 'ArrowUp':
return {input: 'UP', obj_id: 1, player: 2}
case 'ArrowDown':
return {input: 'DOWN', obj_id: 1, player: 2}
default:
return null
}
}).filter(it => !!it);
}
listenToKeys();
render(JSON.parse(field.objects()));
requestAnimationFrame(renderLoop);
const queryParams = location.search ? location.search.slice(1).split("&").map(it => it.split("=")) : [];
const joinSession = queryParams.find(([name,]) => name === "join_session");
if (joinSession) {
executeJoinSession(joinSession[1])
}