Compare commits
2 Commits
c2025e0ca2
...
e9306a2502
| Author | SHA1 | Date | |
|---|---|---|---|
| e9306a2502 | |||
| 21f9807482 |
@ -12,6 +12,7 @@ A fully original Bomberman-style browser game built from scratch.
|
|||||||
- Startup menu with mode selection (`Single Player` / `Multiplayer`) and music toggle
|
- Startup menu with mode selection (`Single Player` / `Multiplayer`) and music toggle
|
||||||
- Multiplayer name setting from menu (`Name: ...`) with local persistence
|
- Multiplayer name setting from menu (`Name: ...`) with local persistence
|
||||||
- Multiplayer lobby with host-controlled start (can start with 1-4 players)
|
- Multiplayer lobby with host-controlled start (can start with 1-4 players)
|
||||||
|
- Lobby shows when a match is currently running; waiting players auto-join the next round
|
||||||
- After each multiplayer round, host can start the next round directly (no lobby required)
|
- After each multiplayer round, host can start the next round directly (no lobby required)
|
||||||
- Host-authoritative multiplayer sync via WebSocket backend
|
- Host-authoritative multiplayer sync via WebSocket backend
|
||||||
|
|
||||||
|
|||||||
12
server.js
12
server.js
@ -211,6 +211,18 @@ function handleMessage(client, msg) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === "lobby_next_round") {
|
||||||
|
if (lobby.phase !== "game") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client.id !== lobby.hostId) {
|
||||||
|
send(client.ws, { type: "error", message: "Only the host can start the next round." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startMatch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "player_input") {
|
if (msg.type === "player_input") {
|
||||||
if (lobby.phase !== "game") {
|
if (lobby.phase !== "game") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
203
src/game.js
203
src/game.js
@ -86,6 +86,10 @@ const state = {
|
|||||||
lobbyMenu: {
|
lobbyMenu: {
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
},
|
},
|
||||||
|
notification: {
|
||||||
|
text: "",
|
||||||
|
expiresAt: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const images = {};
|
const images = {};
|
||||||
@ -123,10 +127,12 @@ const network = {
|
|||||||
inputStateAtClient: {
|
inputStateAtClient: {
|
||||||
dir: null,
|
dir: null,
|
||||||
bomb: false,
|
bomb: false,
|
||||||
|
bombCell: null,
|
||||||
},
|
},
|
||||||
lastInputSentAt: 0,
|
lastInputSentAt: 0,
|
||||||
activeRoster: [],
|
activeRoster: [],
|
||||||
lastPingAt: 0,
|
lastPingAt: 0,
|
||||||
|
lobbyPhase: "lobby",
|
||||||
hasReceivedSnapshot: false,
|
hasReceivedSnapshot: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -203,6 +209,14 @@ function now() {
|
|||||||
return performance.now() / 1000;
|
return performance.now() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showMatchNotification(text, duration = 3) {
|
||||||
|
if (typeof text !== "string" || !text.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.notification.text = text.trim();
|
||||||
|
state.notification.expiresAt = now() + duration;
|
||||||
|
}
|
||||||
|
|
||||||
function loadImage(path) {
|
function loadImage(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@ -431,19 +445,8 @@ function restartMultiplayerRoundAsHost() {
|
|||||||
if (state.mode !== "multiplayer" || state.screen !== "game" || !network.isHost) {
|
if (state.mode !== "multiplayer" || state.screen !== "game" || !network.isHost) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const roster = state.players
|
sendSocketMessage("lobby_next_round");
|
||||||
.filter((player) => typeof player.ownerId === "string" && player.ownerId)
|
state.message = "Starting next round...";
|
||||||
.map((player) => ({
|
|
||||||
id: player.ownerId,
|
|
||||||
name: player.name,
|
|
||||||
}));
|
|
||||||
if (roster.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
network.activeRoster = roster.slice(0, MAX_PLAYERS);
|
|
||||||
resetRoundMultiplayer(network.activeRoster);
|
|
||||||
network.lastSnapshotSentAt = 0;
|
|
||||||
broadcastSnapshot();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateMenuSelection() {
|
function activateMenuSelection() {
|
||||||
@ -505,11 +508,11 @@ function getMainMenuItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLobbyMenuItems() {
|
function getLobbyMenuItems() {
|
||||||
const canStart = network.isHost && network.lobbyPlayers.length >= 1;
|
const canStart = network.lobbyPhase === "lobby" && network.isHost && network.lobbyPlayers.length >= 1;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "start",
|
id: "start",
|
||||||
label: canStart ? "Start Match" : "Waiting For Host",
|
label: canStart ? "Start Match" : network.lobbyPhase === "game" ? "Game In Progress" : "Waiting For Host",
|
||||||
disabled: !canStart,
|
disabled: !canStart,
|
||||||
},
|
},
|
||||||
{ id: "music", label: `Music: ${audio && audio.isMusicEnabled() ? "On" : "Off"}` },
|
{ id: "music", label: `Music: ${audio && audio.isMusicEnabled() ? "On" : "Off"}` },
|
||||||
@ -609,6 +612,7 @@ function ensureSocketConnection() {
|
|||||||
network.isHost = false;
|
network.isHost = false;
|
||||||
network.hostId = null;
|
network.hostId = null;
|
||||||
network.lobbyPlayers = [];
|
network.lobbyPlayers = [];
|
||||||
|
network.lobbyPhase = "lobby";
|
||||||
if (state.screen === "lobby") {
|
if (state.screen === "lobby") {
|
||||||
network.inLobby = true;
|
network.inLobby = true;
|
||||||
state.message = "Disconnected. Reconnecting...";
|
state.message = "Disconnected. Reconnecting...";
|
||||||
@ -663,9 +667,30 @@ function handleSocketMessage(raw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "lobby_state") {
|
if (msg.type === "lobby_state") {
|
||||||
|
const previousLobbyPlayers = network.lobbyPlayers.slice();
|
||||||
|
const nextLobbyPlayers = normalizeLobbyPlayers(msg.players || []);
|
||||||
|
|
||||||
network.hostId = msg.hostId || null;
|
network.hostId = msg.hostId || null;
|
||||||
network.isHost = network.hostId === network.clientId;
|
network.isHost = network.hostId === network.clientId;
|
||||||
network.lobbyPlayers = normalizeLobbyPlayers(msg.players || []);
|
network.lobbyPhase = msg.phase === "game" ? "game" : "lobby";
|
||||||
|
network.lobbyPlayers = nextLobbyPlayers;
|
||||||
|
|
||||||
|
if (state.mode === "multiplayer" && state.screen === "game" && msg.phase === "game") {
|
||||||
|
const activeMatchIds = new Set(network.activeRoster.map((player) => player.id));
|
||||||
|
const previousIds = new Set(previousLobbyPlayers.map((player) => player.id));
|
||||||
|
const newlyWaiting = nextLobbyPlayers.filter(
|
||||||
|
(player) => !previousIds.has(player.id) && !activeMatchIds.has(player.id),
|
||||||
|
);
|
||||||
|
if (newlyWaiting.length === 1) {
|
||||||
|
showMatchNotification(newlyWaiting[0].name + " is waiting in lobby.");
|
||||||
|
} else if (newlyWaiting.length > 1) {
|
||||||
|
showMatchNotification(String(newlyWaiting.length) + " players are waiting in lobby.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.screen === "lobby" && network.lobbyPhase === "game") {
|
||||||
|
state.message = "Match running. You will join next round.";
|
||||||
|
}
|
||||||
if (state.mode === "multiplayer" && state.screen === "game" && msg.phase === "lobby") {
|
if (state.mode === "multiplayer" && state.screen === "game" && msg.phase === "lobby") {
|
||||||
state.screen = "lobby";
|
state.screen = "lobby";
|
||||||
state.status = "idle";
|
state.status = "idle";
|
||||||
@ -678,6 +703,7 @@ function handleSocketMessage(raw) {
|
|||||||
if (msg.type === "lobby_start") {
|
if (msg.type === "lobby_start") {
|
||||||
network.hostId = msg.hostId;
|
network.hostId = msg.hostId;
|
||||||
network.isHost = msg.hostId === network.clientId;
|
network.isHost = msg.hostId === network.clientId;
|
||||||
|
network.lobbyPhase = "game";
|
||||||
network.activeRoster = normalizeLobbyPlayers(msg.roster || []);
|
network.activeRoster = normalizeLobbyPlayers(msg.roster || []);
|
||||||
startMultiplayerMatch(network.activeRoster);
|
startMultiplayerMatch(network.activeRoster);
|
||||||
return;
|
return;
|
||||||
@ -687,7 +713,7 @@ function handleSocketMessage(raw) {
|
|||||||
if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") {
|
if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
network.remoteInputs.set(msg.playerId, msg.input || { dir: null, bomb: false });
|
network.remoteInputs.set(msg.playerId, msg.input || { dir: null, bomb: false, bombCell: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,6 +736,7 @@ function startSinglePlayerFromMenu() {
|
|||||||
function startMultiplayerMatch(roster) {
|
function startMultiplayerMatch(roster) {
|
||||||
network.activeRoster = roster.slice(0, MAX_PLAYERS);
|
network.activeRoster = roster.slice(0, MAX_PLAYERS);
|
||||||
network.inLobby = false;
|
network.inLobby = false;
|
||||||
|
network.lobbyPhase = "game";
|
||||||
network.lastSnapshotAt = now();
|
network.lastSnapshotAt = now();
|
||||||
network.lastInputSentAt = 0;
|
network.lastInputSentAt = 0;
|
||||||
network.hasReceivedSnapshot = network.isHost;
|
network.hasReceivedSnapshot = network.isHost;
|
||||||
@ -732,6 +759,7 @@ function enterLobbyScreen() {
|
|||||||
network.inLobby = true;
|
network.inLobby = true;
|
||||||
network.isHost = false;
|
network.isHost = false;
|
||||||
network.hostId = null;
|
network.hostId = null;
|
||||||
|
network.lobbyPhase = "lobby";
|
||||||
network.lobbyPlayers = [];
|
network.lobbyPlayers = [];
|
||||||
network.remoteInputs.clear();
|
network.remoteInputs.clear();
|
||||||
network.remoteBombPrev.clear();
|
network.remoteBombPrev.clear();
|
||||||
@ -745,11 +773,14 @@ function leaveLobbyInternals() {
|
|||||||
network.inLobby = false;
|
network.inLobby = false;
|
||||||
network.isHost = false;
|
network.isHost = false;
|
||||||
network.hostId = null;
|
network.hostId = null;
|
||||||
|
network.lobbyPhase = "lobby";
|
||||||
network.lobbyPlayers = [];
|
network.lobbyPlayers = [];
|
||||||
network.remoteInputs.clear();
|
network.remoteInputs.clear();
|
||||||
network.remoteBombPrev.clear();
|
network.remoteBombPrev.clear();
|
||||||
network.activeRoster = [];
|
network.activeRoster = [];
|
||||||
network.inputStateAtClient = { dir: null, bomb: false };
|
state.notification.text = "";
|
||||||
|
state.notification.expiresAt = 0;
|
||||||
|
network.inputStateAtClient = { dir: null, bomb: false, bombCell: null };
|
||||||
network.lastInputSentAt = 0;
|
network.lastInputSentAt = 0;
|
||||||
network.hasReceivedSnapshot = false;
|
network.hasReceivedSnapshot = false;
|
||||||
}
|
}
|
||||||
@ -766,7 +797,7 @@ function leaveMultiplayerToMainMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hostStartMatchFromLobby() {
|
function hostStartMatchFromLobby() {
|
||||||
if (!network.isHost || network.lobbyPlayers.length < 1) {
|
if (!network.isHost || network.lobbyPhase !== "lobby" || network.lobbyPlayers.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendSocketMessage("lobby_start");
|
sendSocketMessage("lobby_start");
|
||||||
@ -945,22 +976,94 @@ function updateLobbyNetwork(dt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameBombCell(a, b) {
|
||||||
|
if (!a && !b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!a || !b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return a.x === b.x && a.y === b.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBombDropCell(player, preferredCell = null) {
|
||||||
|
const candidates = [];
|
||||||
|
if (player.moveProgress < 1) {
|
||||||
|
candidates.push({ x: player.moveFromX, y: player.moveFromY });
|
||||||
|
if (player.moveToX !== player.moveFromX || player.moveToY !== player.moveFromY) {
|
||||||
|
candidates.push({ x: player.moveToX, y: player.moveToY });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidates.push({ x: player.x, y: player.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
const canPlaceAt = (x, y) => {
|
||||||
|
if (!inBounds(x, y)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const tile = state.map[y]?.[x];
|
||||||
|
if (!tile || tile.type !== "floor") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !getBombAt(x, y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCandidateCell = (cell) =>
|
||||||
|
candidates.some((candidate) => candidate.x === cell.x && candidate.y === cell.y);
|
||||||
|
|
||||||
|
if (preferredCell && Number.isInteger(preferredCell.x) && Number.isInteger(preferredCell.y)) {
|
||||||
|
if (isCandidateCell(preferredCell) && canPlaceAt(preferredCell.x, preferredCell.y)) {
|
||||||
|
return { x: preferredCell.x, y: preferredCell.y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primary =
|
||||||
|
player.moveProgress < 1 && player.moveProgress >= 0.5
|
||||||
|
? { x: player.moveToX, y: player.moveToY }
|
||||||
|
: candidates[0];
|
||||||
|
|
||||||
|
if (primary && canPlaceAt(primary.x, primary.y)) {
|
||||||
|
return { x: primary.x, y: primary.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (canPlaceAt(candidate.x, candidate.y)) {
|
||||||
|
return { x: candidate.x, y: candidate.y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function sendLocalMultiplayerInput(force = false) {
|
function sendLocalMultiplayerInput(force = false) {
|
||||||
if (state.mode !== "multiplayer" || state.screen !== "game" || network.isHost) {
|
if (state.mode !== "multiplayer" || state.screen !== "game" || network.isHost) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = now();
|
const t = now();
|
||||||
|
const localPlayer = state.players.find((player) => player.ownerId === network.clientId) || null;
|
||||||
const nextInput = {
|
const nextInput = {
|
||||||
dir: currentHumanDirection(),
|
dir: currentHumanDirection(),
|
||||||
bomb: inputState.bombHeld,
|
bomb: inputState.bombHeld,
|
||||||
|
bombCell: null,
|
||||||
};
|
};
|
||||||
|
if (nextInput.bomb && localPlayer) {
|
||||||
|
nextInput.bombCell = resolveBombDropCell(localPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
nextInput.dir !== network.inputStateAtClient.dir || nextInput.bomb !== network.inputStateAtClient.bomb;
|
nextInput.dir !== network.inputStateAtClient.dir ||
|
||||||
|
nextInput.bomb !== network.inputStateAtClient.bomb ||
|
||||||
|
!sameBombCell(nextInput.bombCell, network.inputStateAtClient.bombCell);
|
||||||
const dueHeartbeat = t - network.lastInputSentAt > 0.12;
|
const dueHeartbeat = t - network.lastInputSentAt > 0.12;
|
||||||
if (!changed && !force && !dueHeartbeat) {
|
if (!changed && !force && !dueHeartbeat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
network.inputStateAtClient = nextInput;
|
|
||||||
|
network.inputStateAtClient = {
|
||||||
|
dir: nextInput.dir,
|
||||||
|
bomb: nextInput.bomb,
|
||||||
|
bombCell: nextInput.bombCell ? { ...nextInput.bombCell } : null,
|
||||||
|
};
|
||||||
network.lastInputSentAt = t;
|
network.lastInputSentAt = t;
|
||||||
sendSocketMessage("player_input", { input: nextInput, ts: t });
|
sendSocketMessage("player_input", { input: nextInput, ts: t });
|
||||||
}
|
}
|
||||||
@ -1002,18 +1105,20 @@ function queueDetonation(bombId) {
|
|||||||
state.pendingDetonations.push(bombId);
|
state.pendingDetonations.push(bombId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropBomb(player) {
|
function dropBomb(player, preferredCell = null) {
|
||||||
if (!player.alive || player.bombsPlaced >= player.bombCapacity) {
|
if (!player.alive || player.bombsPlaced >= player.bombCapacity) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (getBombAt(player.x, player.y)) {
|
|
||||||
|
const dropCell = resolveBombDropCell(player, preferredCell);
|
||||||
|
if (!dropCell) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bomb = {
|
const bomb = {
|
||||||
id: state.nextBombId,
|
id: state.nextBombId,
|
||||||
x: player.x,
|
x: dropCell.x,
|
||||||
y: player.y,
|
y: dropCell.y,
|
||||||
timer: CONFIG.bombFuse,
|
timer: CONFIG.bombFuse,
|
||||||
range: player.flameRange,
|
range: player.flameRange,
|
||||||
ownerId: player.id,
|
ownerId: player.id,
|
||||||
@ -1299,7 +1404,7 @@ function occupiedTiles(player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNetworkInput(ownerId) {
|
function getNetworkInput(ownerId) {
|
||||||
return network.remoteInputs.get(ownerId) || { dir: null, bomb: false };
|
return network.remoteInputs.get(ownerId) || { dir: null, bomb: false, bombCell: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesiredDirection(player) {
|
function getDesiredDirection(player) {
|
||||||
@ -1341,8 +1446,9 @@ function updatePlayerMovement(player, dt) {
|
|||||||
player.renderY = player.y;
|
player.renderY = player.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const occupied = new Set(occupiedTiles(player));
|
||||||
for (const bomb of state.bombs) {
|
for (const bomb of state.bombs) {
|
||||||
if (bomb.passThrough.has(player.id) && (player.x !== bomb.x || player.y !== bomb.y)) {
|
if (bomb.passThrough.has(player.id) && !occupied.has(tileKey(bomb.x, bomb.y))) {
|
||||||
bomb.passThrough.delete(player.id);
|
bomb.passThrough.delete(player.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1555,7 +1661,7 @@ function updateGame(dt) {
|
|||||||
const remote = getNetworkInput(player.ownerId);
|
const remote = getNetworkInput(player.ownerId);
|
||||||
const prevBomb = network.remoteBombPrev.get(player.ownerId) || false;
|
const prevBomb = network.remoteBombPrev.get(player.ownerId) || false;
|
||||||
if (remote.bomb && !prevBomb) {
|
if (remote.bomb && !prevBomb) {
|
||||||
dropBomb(player);
|
dropBomb(player, remote.bombCell || null);
|
||||||
}
|
}
|
||||||
network.remoteBombPrev.set(player.ownerId, remote.bomb);
|
network.remoteBombPrev.set(player.ownerId, remote.bomb);
|
||||||
}
|
}
|
||||||
@ -1837,7 +1943,11 @@ function drawLobby() {
|
|||||||
ctx.fillText("Multiplayer Lobby", canvas.width / 2, panelY + 48);
|
ctx.fillText("Multiplayer Lobby", canvas.width / 2, panelY + 48);
|
||||||
ctx.fillStyle = "#b6ddff";
|
ctx.fillStyle = "#b6ddff";
|
||||||
ctx.font = "bold 14px Trebuchet MS";
|
ctx.font = "bold 14px Trebuchet MS";
|
||||||
ctx.fillText("Host can start with any number of players (1-4).", canvas.width / 2, panelY + 74);
|
const lobbyHint =
|
||||||
|
network.lobbyPhase === "game"
|
||||||
|
? "Match currently running. You will join next round."
|
||||||
|
: "Host can start with any number of players (1-4).";
|
||||||
|
ctx.fillText(lobbyHint, canvas.width / 2, panelY + 74);
|
||||||
|
|
||||||
ctx.textAlign = "left";
|
ctx.textAlign = "left";
|
||||||
ctx.font = "bold 17px Trebuchet MS";
|
ctx.font = "bold 17px Trebuchet MS";
|
||||||
@ -1866,6 +1976,40 @@ function drawLobby() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawMatchNotification() {
|
||||||
|
if (state.screen !== "game" || !state.notification.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = state.notification.expiresAt - now();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
state.notification.text = "";
|
||||||
|
state.notification.expiresAt = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fade = clamp(remaining < 0.35 ? remaining / 0.35 : 1, 0, 1);
|
||||||
|
const panelWidth = Math.min(canvas.width - 36, 520);
|
||||||
|
const panelHeight = 34;
|
||||||
|
const panelX = (canvas.width - panelWidth) / 2;
|
||||||
|
const panelY = 14;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.96 * fade;
|
||||||
|
ctx.fillStyle = "#071421db";
|
||||||
|
ctx.strokeStyle = "#8acfff99";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.fillRect(panelX, panelY, panelWidth, panelHeight);
|
||||||
|
ctx.strokeRect(panelX, panelY, panelWidth, panelHeight);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#e7f4ff";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.font = "bold 16px Trebuchet MS";
|
||||||
|
ctx.fillText(state.notification.text, canvas.width / 2, panelY + panelHeight / 2 + 1);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
function drawFrame() {
|
function drawFrame() {
|
||||||
if (state.screen === "mainMenu") {
|
if (state.screen === "mainMenu") {
|
||||||
drawMainMenu();
|
drawMainMenu();
|
||||||
@ -1883,6 +2027,7 @@ function drawFrame() {
|
|||||||
drawBombs();
|
drawBombs();
|
||||||
drawExplosions();
|
drawExplosions();
|
||||||
drawPlayers();
|
drawPlayers();
|
||||||
|
drawMatchNotification();
|
||||||
drawMenu();
|
drawMenu();
|
||||||
|
|
||||||
ctx.strokeStyle = "#86cef91f";
|
ctx.strokeStyle = "#86cef91f";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user