Implementing server wide highscore for multiplayer
This commit is contained in:
parent
e9306a2502
commit
25a8826030
197
server.js
197
server.js
@ -7,6 +7,7 @@ const { WebSocketServer } = require("ws");
|
|||||||
const PORT = Number(process.env.PORT || 8080);
|
const PORT = Number(process.env.PORT || 8080);
|
||||||
const MAX_PLAYERS = 4;
|
const MAX_PLAYERS = 4;
|
||||||
const ROOT = __dirname;
|
const ROOT = __dirname;
|
||||||
|
const HIGHSCORE_FILE = path.join(ROOT, "highscores.json");
|
||||||
|
|
||||||
const CONTENT_TYPES = {
|
const CONTENT_TYPES = {
|
||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
@ -22,6 +23,7 @@ const CONTENT_TYPES = {
|
|||||||
|
|
||||||
const clientsBySocket = new Map();
|
const clientsBySocket = new Map();
|
||||||
const clientsById = new Map();
|
const clientsById = new Map();
|
||||||
|
const highscoreByName = new Map();
|
||||||
|
|
||||||
const lobby = {
|
const lobby = {
|
||||||
phase: "lobby", // lobby | game
|
phase: "lobby", // lobby | game
|
||||||
@ -53,6 +55,159 @@ function normalizeName(value, fallback) {
|
|||||||
return normalized || fallback;
|
return normalized || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highscoreKey(name) {
|
||||||
|
const normalized = normalizeName(name, "");
|
||||||
|
return normalized ? normalized.toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHighscores(entries) {
|
||||||
|
return [...entries]
|
||||||
|
.filter((entry) => entry && typeof entry.name === "string")
|
||||||
|
.map((entry) => ({
|
||||||
|
name: normalizeName(entry.name, "Unknown"),
|
||||||
|
kills: Math.max(0, Math.floor(Number(entry.kills) || 0)),
|
||||||
|
wins: Math.max(0, Math.floor(Number(entry.wins) || 0)),
|
||||||
|
longestAlive: Math.max(0, Number(entry.longestAlive) || 0),
|
||||||
|
updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0,
|
||||||
|
}))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.wins - a.wins ||
|
||||||
|
b.kills - a.kills ||
|
||||||
|
b.longestAlive - a.longestAlive ||
|
||||||
|
b.updatedAt - a.updatedAt ||
|
||||||
|
a.name.localeCompare(b.name),
|
||||||
|
)
|
||||||
|
.slice(0, 12)
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
kills: entry.kills,
|
||||||
|
wins: entry.wins,
|
||||||
|
longestAlive: Math.round(entry.longestAlive * 10) / 10,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighscores() {
|
||||||
|
return normalizeHighscores(Array.from(highscoreByName.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHighscores() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(HIGHSCORE_FILE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = fs.readFileSync(HIGHSCORE_FILE, "utf8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const list = Array.isArray(parsed) ? normalizeHighscores(parsed) : [];
|
||||||
|
for (const row of list) {
|
||||||
|
const key = highscoreKey(row.name);
|
||||||
|
if (key) {
|
||||||
|
highscoreByName.set(key, { ...row, updatedAt: now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore load failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHighscores() {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(HIGHSCORE_FILE, `${JSON.stringify(getHighscores(), null, 2)}\n`);
|
||||||
|
} catch {
|
||||||
|
// ignore write failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlayerName(clientId) {
|
||||||
|
const active = clientsById.get(clientId);
|
||||||
|
if (active && active.name) {
|
||||||
|
return active.name;
|
||||||
|
}
|
||||||
|
const lobbyPlayer = lobby.players.find((player) => player.id === clientId);
|
||||||
|
if (lobbyPlayer && lobbyPlayer.name) {
|
||||||
|
return lobbyPlayer.name;
|
||||||
|
}
|
||||||
|
const rosterPlayer = lobby.roster.find((player) => player.id === clientId);
|
||||||
|
if (rosterPlayer && rosterPlayer.name) {
|
||||||
|
return rosterPlayer.name;
|
||||||
|
}
|
||||||
|
return defaultName(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRoundReport(report) {
|
||||||
|
if (!report || typeof report !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const killsByClientId = report.kills && typeof report.kills === "object" ? report.kills : {};
|
||||||
|
const winnerClientId = typeof report.winnerId === "string" ? report.winnerId : "";
|
||||||
|
const aliveTimesByClientId = report.aliveTimes && typeof report.aliveTimes === "object" ? report.aliveTimes : {};
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
const updateEntry = (clientId, mutator) => {
|
||||||
|
if (!clientId || typeof mutator !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = normalizeName(resolvePlayerName(clientId), "");
|
||||||
|
const key = highscoreKey(name);
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = highscoreByName.get(key) || { name, kills: 0, wins: 0, longestAlive: 0, updatedAt: 0 };
|
||||||
|
existing.name = name;
|
||||||
|
existing.kills = Math.max(0, Math.floor(Number(existing.kills) || 0));
|
||||||
|
existing.wins = Math.max(0, Math.floor(Number(existing.wins) || 0));
|
||||||
|
existing.longestAlive = Math.max(0, Number(existing.longestAlive) || 0);
|
||||||
|
|
||||||
|
if (!mutator(existing)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.updatedAt = now();
|
||||||
|
highscoreByName.set(key, existing);
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [clientId, value] of Object.entries(killsByClientId)) {
|
||||||
|
const kills = Math.floor(Number(value));
|
||||||
|
if (!clientId || !Number.isFinite(kills) || kills <= 0 || kills > 99) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEntry(clientId, (entry) => {
|
||||||
|
entry.kills += kills;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winnerClientId) {
|
||||||
|
updateEntry(winnerClientId, (entry) => {
|
||||||
|
entry.wins += 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [clientId, value] of Object.entries(aliveTimesByClientId)) {
|
||||||
|
const aliveSeconds = Number(value);
|
||||||
|
if (!clientId || !Number.isFinite(aliveSeconds) || aliveSeconds < 0 || aliveSeconds > 3600) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEntry(clientId, (entry) => {
|
||||||
|
if (aliveSeconds <= entry.longestAlive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entry.longestAlive = aliveSeconds;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
function safePath(urlPath) {
|
function safePath(urlPath) {
|
||||||
const clean = decodeURIComponent(urlPath.split("?")[0]);
|
const clean = decodeURIComponent(urlPath.split("?")[0]);
|
||||||
const target = clean === "/" ? "/index.html" : clean;
|
const target = clean === "/" ? "/index.html" : clean;
|
||||||
@ -102,10 +257,18 @@ function broadcastLobbyState(reason = null) {
|
|||||||
phase: lobby.phase,
|
phase: lobby.phase,
|
||||||
hostId: lobby.hostId,
|
hostId: lobby.hostId,
|
||||||
players: normalizePlayers(lobby.players),
|
players: normalizePlayers(lobby.players),
|
||||||
|
highscores: getHighscores(),
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function broadcastHighscoreUpdate() {
|
||||||
|
broadcast({
|
||||||
|
type: "highscore_update",
|
||||||
|
highscores: getHighscores(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function endMatch(reason = "Match ended") {
|
function endMatch(reason = "Match ended") {
|
||||||
lobby.phase = "lobby";
|
lobby.phase = "lobby";
|
||||||
lobby.roster = [];
|
lobby.roster = [];
|
||||||
@ -114,7 +277,7 @@ function endMatch(reason = "Match ended") {
|
|||||||
|
|
||||||
function startMatch() {
|
function startMatch() {
|
||||||
const ordered = normalizePlayers(lobby.players);
|
const ordered = normalizePlayers(lobby.players);
|
||||||
if (ordered.length < 1) {
|
if (ordered.length < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lobby.phase = "game";
|
lobby.phase = "game";
|
||||||
@ -123,6 +286,7 @@ function startMatch() {
|
|||||||
type: "lobby_start",
|
type: "lobby_start",
|
||||||
hostId: lobby.hostId,
|
hostId: lobby.hostId,
|
||||||
roster: lobby.roster,
|
roster: lobby.roster,
|
||||||
|
highscores: getHighscores(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +330,7 @@ function handleMessage(client, msg) {
|
|||||||
|
|
||||||
if (msg.type === "hello") {
|
if (msg.type === "hello") {
|
||||||
client.name = normalizeName(msg.name, client.name);
|
client.name = normalizeName(msg.name, client.name);
|
||||||
send(client.ws, { type: "welcome", clientId: client.id, name: client.name });
|
send(client.ws, { type: "welcome", clientId: client.id, name: client.name, highscores: getHighscores() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +348,7 @@ function handleMessage(client, msg) {
|
|||||||
}
|
}
|
||||||
broadcastLobbyState();
|
broadcastLobbyState();
|
||||||
}
|
}
|
||||||
send(client.ws, { type: "welcome", clientId: client.id, name: client.name });
|
send(client.ws, { type: "welcome", clientId: client.id, name: client.name, highscores: getHighscores() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +371,10 @@ function handleMessage(client, msg) {
|
|||||||
send(client.ws, { type: "error", message: "Only the host can start the match." });
|
send(client.ws, { type: "error", message: "Only the host can start the match." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (lobby.players.length < 2) {
|
||||||
|
send(client.ws, { type: "error", message: "At least 2 players are required to start a match." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
startMatch();
|
startMatch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -219,10 +387,29 @@ function handleMessage(client, msg) {
|
|||||||
send(client.ws, { type: "error", message: "Only the host can start the next round." });
|
send(client.ws, { type: "error", message: "Only the host can start the next round." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (lobby.players.length < 2) {
|
||||||
|
send(client.ws, { type: "error", message: "At least 2 players are required to start a match." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
startMatch();
|
startMatch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === "lobby_kill_report") {
|
||||||
|
if (lobby.phase !== "game") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client.id !== lobby.hostId) {
|
||||||
|
send(client.ws, { type: "error", message: "Only the host can submit round stats." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (applyRoundReport(msg)) {
|
||||||
|
saveHighscores();
|
||||||
|
broadcastHighscoreUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "player_input") {
|
if (msg.type === "player_input") {
|
||||||
if (lobby.phase !== "game") {
|
if (lobby.phase !== "game") {
|
||||||
return;
|
return;
|
||||||
@ -267,6 +454,8 @@ function handleMessage(client, msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadHighscores();
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
const filePath = safePath(req.url || "/");
|
const filePath = safePath(req.url || "/");
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
@ -311,7 +500,7 @@ wss.on("connection", (ws) => {
|
|||||||
clientsBySocket.set(ws, client);
|
clientsBySocket.set(ws, client);
|
||||||
clientsById.set(client.id, client);
|
clientsById.set(client.id, client);
|
||||||
|
|
||||||
send(ws, { type: "welcome", clientId: client.id, name: client.name });
|
send(ws, { type: "welcome", clientId: client.id, name: client.name, highscores: getHighscores() });
|
||||||
|
|
||||||
ws.on("message", (data) => {
|
ws.on("message", (data) => {
|
||||||
let msg;
|
let msg;
|
||||||
|
|||||||
329
src/game.js
329
src/game.js
@ -73,6 +73,9 @@ const state = {
|
|||||||
pendingDetonationSet: new Set(),
|
pendingDetonationSet: new Set(),
|
||||||
lastFrame: 0,
|
lastFrame: 0,
|
||||||
outcomePlayed: false,
|
outcomePlayed: false,
|
||||||
|
multiplayerRoundKills: {},
|
||||||
|
multiplayerRoundDeathTimes: {},
|
||||||
|
multiplayerRoundTime: 0,
|
||||||
menu: {
|
menu: {
|
||||||
open: false,
|
open: false,
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
@ -120,6 +123,7 @@ const network = {
|
|||||||
hostId: null,
|
hostId: null,
|
||||||
localName: "",
|
localName: "",
|
||||||
lobbyPlayers: [],
|
lobbyPlayers: [],
|
||||||
|
highscores: [],
|
||||||
remoteInputs: new Map(),
|
remoteInputs: new Map(),
|
||||||
remoteBombPrev: new Map(),
|
remoteBombPrev: new Map(),
|
||||||
lastSnapshotSentAt: 0,
|
lastSnapshotSentAt: 0,
|
||||||
@ -340,6 +344,9 @@ function resetRoundSingle() {
|
|||||||
state.status = "running";
|
state.status = "running";
|
||||||
state.message = "Fight";
|
state.message = "Fight";
|
||||||
state.outcomePlayed = false;
|
state.outcomePlayed = false;
|
||||||
|
state.multiplayerRoundKills = {};
|
||||||
|
state.multiplayerRoundDeathTimes = {};
|
||||||
|
state.multiplayerRoundTime = 0;
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,6 +377,9 @@ function resetRoundMultiplayer(roster) {
|
|||||||
state.status = "running";
|
state.status = "running";
|
||||||
state.message = "Multiplayer match live";
|
state.message = "Multiplayer match live";
|
||||||
state.outcomePlayed = false;
|
state.outcomePlayed = false;
|
||||||
|
state.multiplayerRoundKills = {};
|
||||||
|
state.multiplayerRoundDeathTimes = {};
|
||||||
|
state.multiplayerRoundTime = 0;
|
||||||
closeMenu();
|
closeMenu();
|
||||||
inputState.bombQueued = false;
|
inputState.bombQueued = false;
|
||||||
network.remoteBombPrev.clear();
|
network.remoteBombPrev.clear();
|
||||||
@ -508,11 +518,21 @@ function getMainMenuItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLobbyMenuItems() {
|
function getLobbyMenuItems() {
|
||||||
const canStart = network.lobbyPhase === "lobby" && network.isHost && network.lobbyPlayers.length >= 1;
|
const enoughPlayers = network.lobbyPlayers.length >= 2;
|
||||||
|
const canStart = network.lobbyPhase === "lobby" && network.isHost && enoughPlayers;
|
||||||
|
const startLabel =
|
||||||
|
network.lobbyPhase === "game"
|
||||||
|
? "Game In Progress"
|
||||||
|
: network.isHost
|
||||||
|
? enoughPlayers
|
||||||
|
? "Start Match"
|
||||||
|
: "Need 2 Players"
|
||||||
|
: "Waiting For Host";
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "start",
|
id: "start",
|
||||||
label: canStart ? "Start Match" : network.lobbyPhase === "game" ? "Game In Progress" : "Waiting For Host",
|
label: startLabel,
|
||||||
disabled: !canStart,
|
disabled: !canStart,
|
||||||
},
|
},
|
||||||
{ id: "music", label: `Music: ${audio && audio.isMusicEnabled() ? "On" : "Off"}` },
|
{ id: "music", label: `Music: ${audio && audio.isMusicEnabled() ? "On" : "Off"}` },
|
||||||
@ -570,6 +590,37 @@ function normalizeLobbyPlayers(players) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHighscores(entries) {
|
||||||
|
if (!Array.isArray(entries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...entries]
|
||||||
|
.filter((entry) => entry && typeof entry.name === "string")
|
||||||
|
.map((entry) => ({
|
||||||
|
name: normalizePlayerName(entry.name) || "Unknown",
|
||||||
|
wins: Math.max(0, Math.floor(Number(entry.wins) || 0)),
|
||||||
|
kills: Math.max(0, Math.floor(Number(entry.kills) || 0)),
|
||||||
|
longestAlive: Math.max(0, Number(entry.longestAlive) || 0),
|
||||||
|
}))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.wins - a.wins ||
|
||||||
|
b.kills - a.kills ||
|
||||||
|
b.longestAlive - a.longestAlive ||
|
||||||
|
a.name.localeCompare(b.name),
|
||||||
|
)
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
longestAlive: Math.round(entry.longestAlive * 10) / 10,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAliveSeconds(value) {
|
||||||
|
const seconds = Math.max(0, Number(value) || 0);
|
||||||
|
return String(Math.round(seconds)) + "s";
|
||||||
|
}
|
||||||
|
|
||||||
function sendSocketMessage(type, payload = {}) {
|
function sendSocketMessage(type, payload = {}) {
|
||||||
if (!network.socket || network.socket.readyState !== WebSocket.OPEN) {
|
if (!network.socket || network.socket.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@ -595,6 +646,7 @@ function ensureSocketConnection() {
|
|||||||
network.socket.addEventListener("open", () => {
|
network.socket.addEventListener("open", () => {
|
||||||
network.connected = true;
|
network.connected = true;
|
||||||
sendSocketMessage("hello", { clientId: network.clientId, name: network.localName });
|
sendSocketMessage("hello", { clientId: network.clientId, name: network.localName });
|
||||||
|
|
||||||
if (state.screen === "lobby") {
|
if (state.screen === "lobby") {
|
||||||
sendSocketMessage("lobby_join");
|
sendSocketMessage("lobby_join");
|
||||||
network.inLobby = true;
|
network.inLobby = true;
|
||||||
@ -652,6 +704,9 @@ function handleSocketMessage(raw) {
|
|||||||
} else if (!network.localName) {
|
} else if (!network.localName) {
|
||||||
network.localName = `P-${shortId(network.clientId)}`;
|
network.localName = `P-${shortId(network.clientId)}`;
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(msg.highscores)) {
|
||||||
|
network.highscores = normalizeHighscores(msg.highscores);
|
||||||
|
}
|
||||||
if (state.screen === "lobby") {
|
if (state.screen === "lobby") {
|
||||||
sendSocketMessage("lobby_join");
|
sendSocketMessage("lobby_join");
|
||||||
network.inLobby = true;
|
network.inLobby = true;
|
||||||
@ -674,6 +729,9 @@ function handleSocketMessage(raw) {
|
|||||||
network.isHost = network.hostId === network.clientId;
|
network.isHost = network.hostId === network.clientId;
|
||||||
network.lobbyPhase = msg.phase === "game" ? "game" : "lobby";
|
network.lobbyPhase = msg.phase === "game" ? "game" : "lobby";
|
||||||
network.lobbyPlayers = nextLobbyPlayers;
|
network.lobbyPlayers = nextLobbyPlayers;
|
||||||
|
if (Array.isArray(msg.highscores)) {
|
||||||
|
network.highscores = normalizeHighscores(msg.highscores);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.mode === "multiplayer" && state.screen === "game" && msg.phase === "game") {
|
if (state.mode === "multiplayer" && state.screen === "game" && msg.phase === "game") {
|
||||||
const activeMatchIds = new Set(network.activeRoster.map((player) => player.id));
|
const activeMatchIds = new Set(network.activeRoster.map((player) => player.id));
|
||||||
@ -705,10 +763,20 @@ function handleSocketMessage(raw) {
|
|||||||
network.isHost = msg.hostId === network.clientId;
|
network.isHost = msg.hostId === network.clientId;
|
||||||
network.lobbyPhase = "game";
|
network.lobbyPhase = "game";
|
||||||
network.activeRoster = normalizeLobbyPlayers(msg.roster || []);
|
network.activeRoster = normalizeLobbyPlayers(msg.roster || []);
|
||||||
|
if (Array.isArray(msg.highscores)) {
|
||||||
|
network.highscores = normalizeHighscores(msg.highscores);
|
||||||
|
}
|
||||||
startMultiplayerMatch(network.activeRoster);
|
startMultiplayerMatch(network.activeRoster);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === "highscore_update") {
|
||||||
|
if (Array.isArray(msg.highscores)) {
|
||||||
|
network.highscores = normalizeHighscores(msg.highscores);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "player_input") {
|
if (msg.type === "player_input") {
|
||||||
if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") {
|
if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") {
|
||||||
return;
|
return;
|
||||||
@ -780,6 +848,9 @@ function leaveLobbyInternals() {
|
|||||||
network.activeRoster = [];
|
network.activeRoster = [];
|
||||||
state.notification.text = "";
|
state.notification.text = "";
|
||||||
state.notification.expiresAt = 0;
|
state.notification.expiresAt = 0;
|
||||||
|
state.multiplayerRoundKills = {};
|
||||||
|
state.multiplayerRoundDeathTimes = {};
|
||||||
|
state.multiplayerRoundTime = 0;
|
||||||
network.inputStateAtClient = { dir: null, bomb: false, bombCell: null };
|
network.inputStateAtClient = { dir: null, bomb: false, bombCell: null };
|
||||||
network.lastInputSentAt = 0;
|
network.lastInputSentAt = 0;
|
||||||
network.hasReceivedSnapshot = false;
|
network.hasReceivedSnapshot = false;
|
||||||
@ -797,7 +868,7 @@ function leaveMultiplayerToMainMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hostStartMatchFromLobby() {
|
function hostStartMatchFromLobby() {
|
||||||
if (!network.isHost || network.lobbyPhase !== "lobby" || network.lobbyPlayers.length < 1) {
|
if (!network.isHost || network.lobbyPhase !== "lobby" || network.lobbyPlayers.length < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendSocketMessage("lobby_start");
|
sendSocketMessage("lobby_start");
|
||||||
@ -843,6 +914,7 @@ function serializeGameState() {
|
|||||||
})),
|
})),
|
||||||
explosions: state.explosions.map((explosion) => ({
|
explosions: state.explosions.map((explosion) => ({
|
||||||
timer: explosion.timer,
|
timer: explosion.timer,
|
||||||
|
ownerId: explosion.ownerId,
|
||||||
cells: explosion.cells.map((cell) => ({ x: cell.x, y: cell.y, type: cell.type, rot: cell.rot })),
|
cells: explosion.cells.map((cell) => ({ x: cell.x, y: cell.y, type: cell.type, rot: cell.rot })),
|
||||||
})),
|
})),
|
||||||
nextBombId: state.nextBombId,
|
nextBombId: state.nextBombId,
|
||||||
@ -941,6 +1013,7 @@ function applySnapshot(snapshot) {
|
|||||||
}));
|
}));
|
||||||
state.explosions = snapshot.explosions.map((explosion) => ({
|
state.explosions = snapshot.explosions.map((explosion) => ({
|
||||||
timer: explosion.timer,
|
timer: explosion.timer,
|
||||||
|
ownerId: explosion.ownerId,
|
||||||
cells: explosion.cells.map((cell) => ({ ...cell })),
|
cells: explosion.cells.map((cell) => ({ ...cell })),
|
||||||
}));
|
}));
|
||||||
state.nextBombId = snapshot.nextBombId;
|
state.nextBombId = snapshot.nextBombId;
|
||||||
@ -1347,9 +1420,54 @@ function updateBot(player, dt) {
|
|||||||
player.ai.desiredDir = options.length > 0 ? options[0].dir : null;
|
player.ai.desiredDir = options.length > 0 ? options[0].dir : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function killPlayer(player) {
|
function registerMultiplayerKill(killerPlayerId, victimPlayerId) {
|
||||||
|
if (state.mode !== "multiplayer" || !network.isHost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const killer = state.players.find((player) => player.id === killerPlayerId);
|
||||||
|
const victim = state.players.find((player) => player.id === victimPlayerId);
|
||||||
|
if (!killer || !victim || !killer.ownerId || !victim.ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (killer.ownerId === victim.ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous = state.multiplayerRoundKills[killer.ownerId] || 0;
|
||||||
|
state.multiplayerRoundKills[killer.ownerId] = previous + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findKillerForHit(tiles, victimPlayerId) {
|
||||||
|
for (const explosion of state.explosions) {
|
||||||
|
const ownerId = Number.isInteger(explosion.ownerId) ? explosion.ownerId : null;
|
||||||
|
if (ownerId === null || ownerId === victimPlayerId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const cell of explosion.cells) {
|
||||||
|
if (tiles.includes(tileKey(cell.x, cell.y))) {
|
||||||
|
return ownerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killPlayer(player, killerPlayerId = null) {
|
||||||
|
if (!player.alive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
player.alive = false;
|
player.alive = false;
|
||||||
player.ai.desiredDir = null;
|
player.ai.desiredDir = null;
|
||||||
|
|
||||||
|
if (state.mode === "multiplayer" && network.isHost && player.ownerId) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(state.multiplayerRoundDeathTimes, player.ownerId)) {
|
||||||
|
state.multiplayerRoundDeathTimes[player.ownerId] = state.multiplayerRoundTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isInteger(killerPlayerId) && killerPlayerId !== player.id) {
|
||||||
|
registerMultiplayerKill(killerPlayerId, player.id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectPowerup(player) {
|
function collectPowerup(player) {
|
||||||
@ -1498,7 +1616,7 @@ function detonateBomb(bomb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.explosions.push({ cells, timer: CONFIG.explosionDuration });
|
state.explosions.push({ cells, timer: CONFIG.explosionDuration, ownerId: bomb.ownerId });
|
||||||
audio.playExplosion();
|
audio.playExplosion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1529,7 +1647,8 @@ function checkDeaths() {
|
|||||||
}
|
}
|
||||||
const tiles = occupiedTiles(player);
|
const tiles = occupiedTiles(player);
|
||||||
if (tiles.some((key) => state.fireLookup.has(key))) {
|
if (tiles.some((key) => state.fireLookup.has(key))) {
|
||||||
killPlayer(player);
|
const killerPlayerId = findKillerForHit(tiles, player.id);
|
||||||
|
killPlayer(player, killerPlayerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1578,6 +1697,50 @@ function evaluateOutcome() {
|
|||||||
return endedThisFrame;
|
return endedThisFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportRoundKillsToServer() {
|
||||||
|
if (state.mode !== "multiplayer" || !network.isHost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kills = {};
|
||||||
|
let hasKills = false;
|
||||||
|
for (const [ownerId, count] of Object.entries(state.multiplayerRoundKills)) {
|
||||||
|
const cleanCount = Math.floor(Number(count));
|
||||||
|
if (!ownerId || !Number.isFinite(cleanCount) || cleanCount <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
kills[ownerId] = cleanCount;
|
||||||
|
hasKills = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundDuration = Math.max(0, state.multiplayerRoundTime);
|
||||||
|
const aliveTimes = {};
|
||||||
|
let hasAliveTimes = false;
|
||||||
|
for (const player of state.players) {
|
||||||
|
if (!player.ownerId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const deathAt = state.multiplayerRoundDeathTimes[player.ownerId];
|
||||||
|
const aliveSeconds = Number.isFinite(deathAt)
|
||||||
|
? clamp(deathAt, 0, roundDuration)
|
||||||
|
: roundDuration;
|
||||||
|
aliveTimes[player.ownerId] = Math.round(aliveSeconds * 10) / 10;
|
||||||
|
hasAliveTimes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alivePlayers = state.players.filter((player) => player.alive && player.ownerId);
|
||||||
|
const winnerId = alivePlayers.length === 1 ? alivePlayers[0].ownerId : null;
|
||||||
|
|
||||||
|
if (!hasKills && !hasAliveTimes && !winnerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSocketMessage("lobby_kill_report", { kills, winnerId, aliveTimes });
|
||||||
|
state.multiplayerRoundKills = {};
|
||||||
|
state.multiplayerRoundDeathTimes = {};
|
||||||
|
state.multiplayerRoundTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function updateNonHostClient(dt) {
|
function updateNonHostClient(dt) {
|
||||||
for (const player of state.players) {
|
for (const player of state.players) {
|
||||||
if (!player.alive) {
|
if (!player.alive) {
|
||||||
@ -1625,6 +1788,10 @@ function updateGame(dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.mode === "multiplayer") {
|
||||||
|
state.multiplayerRoundTime += dt;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.mode === "multiplayer" && !network.isHost) {
|
if (state.mode === "multiplayer" && !network.isHost) {
|
||||||
updateNonHostClient(dt);
|
updateNonHostClient(dt);
|
||||||
return;
|
return;
|
||||||
@ -1692,6 +1859,9 @@ function updateGame(dt) {
|
|||||||
const endedThisFrame = evaluateOutcome();
|
const endedThisFrame = evaluateOutcome();
|
||||||
|
|
||||||
if (state.mode === "multiplayer" && network.isHost) {
|
if (state.mode === "multiplayer" && network.isHost) {
|
||||||
|
if (endedThisFrame) {
|
||||||
|
reportRoundKillsToServer();
|
||||||
|
}
|
||||||
broadcastSnapshot(endedThisFrame);
|
broadcastSnapshot(endedThisFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1924,8 +2094,8 @@ function drawMainMenu() {
|
|||||||
|
|
||||||
function drawLobby() {
|
function drawLobby() {
|
||||||
drawStaticBackdrop();
|
drawStaticBackdrop();
|
||||||
const panelWidth = 460;
|
const panelWidth = 560;
|
||||||
const panelHeight = 400;
|
const panelHeight = 430;
|
||||||
const panelX = (canvas.width - panelWidth) / 2;
|
const panelX = (canvas.width - panelWidth) / 2;
|
||||||
const panelY = (canvas.height - panelHeight) / 2;
|
const panelY = (canvas.height - panelHeight) / 2;
|
||||||
|
|
||||||
@ -1946,24 +2116,59 @@ function drawLobby() {
|
|||||||
const lobbyHint =
|
const lobbyHint =
|
||||||
network.lobbyPhase === "game"
|
network.lobbyPhase === "game"
|
||||||
? "Match currently running. You will join next round."
|
? "Match currently running. You will join next round."
|
||||||
: "Host can start with any number of players (1-4).";
|
: "Host can start with 2-4 players.";
|
||||||
ctx.fillText(lobbyHint, canvas.width / 2, panelY + 74);
|
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";
|
||||||
|
ctx.fillStyle = "#ffdca3";
|
||||||
|
ctx.fillText("Lobby Players", panelX + 34, panelY + 102);
|
||||||
for (let i = 0; i < MAX_PLAYERS; i += 1) {
|
for (let i = 0; i < MAX_PLAYERS; i += 1) {
|
||||||
const player = network.lobbyPlayers[i];
|
const player = network.lobbyPlayers[i];
|
||||||
const y = panelY + 112 + i * 34;
|
const y = panelY + 136 + i * 34;
|
||||||
const line = player
|
const line = player
|
||||||
? `${i + 1}. ${player.name}${player.id === network.hostId ? " (Host)" : ""}`
|
? String(i + 1) + ". " + player.name + (player.id === network.hostId ? " (Host)" : "")
|
||||||
: `${i + 1}. Waiting...`;
|
: String(i + 1) + ". Waiting...";
|
||||||
ctx.fillStyle = player ? "#e9f6ff" : "#8fb6d4";
|
ctx.fillStyle = player ? "#e9f6ff" : "#8fb6d4";
|
||||||
ctx.fillText(line, panelX + 34, y);
|
ctx.fillText(line, panelX + 34, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#8acfff44";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(panelX + panelWidth / 2, panelY + 98);
|
||||||
|
ctx.lineTo(panelX + panelWidth / 2, panelY + 306);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = "#ffdca3";
|
||||||
|
ctx.font = "bold 17px Trebuchet MS";
|
||||||
|
ctx.fillText("Server Highscores", panelX + panelWidth / 2 + 24, panelY + 102);
|
||||||
|
|
||||||
|
if (network.highscores.length === 0) {
|
||||||
|
ctx.fillStyle = "#8fb6d4";
|
||||||
|
ctx.font = "bold 15px Trebuchet MS";
|
||||||
|
ctx.fillText("No rounds recorded yet.", panelX + panelWidth / 2 + 24, panelY + 136);
|
||||||
|
} else {
|
||||||
|
ctx.font = "bold 14px Trebuchet MS";
|
||||||
|
network.highscores.forEach((entry, index) => {
|
||||||
|
const y = panelY + 136 + index * 22;
|
||||||
|
const rank = String(index + 1).padStart(2, "0");
|
||||||
|
const line =
|
||||||
|
rank +
|
||||||
|
". " +
|
||||||
|
entry.name +
|
||||||
|
" - " +
|
||||||
|
entry.kills +
|
||||||
|
" (" +
|
||||||
|
formatAliveSeconds(entry.longestAlive) +
|
||||||
|
")";
|
||||||
|
ctx.fillStyle = "#e9f6ff";
|
||||||
|
ctx.fillText(line, panelX + panelWidth / 2 + 24, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const items = getLobbyMenuItems();
|
const items = getLobbyMenuItems();
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const y = panelY + 260 + index * 34;
|
const y = panelY + 324 + index * 34;
|
||||||
const selected = index === state.lobbyMenu.selectedIndex;
|
const selected = index === state.lobbyMenu.selectedIndex;
|
||||||
if (selected && !item.disabled) {
|
if (selected && !item.disabled) {
|
||||||
ctx.fillStyle = "#ffd166";
|
ctx.fillStyle = "#ffd166";
|
||||||
@ -2411,10 +2616,22 @@ class AudioSystem {
|
|||||||
this.nextStepTime = 0;
|
this.nextStepTime = 0;
|
||||||
|
|
||||||
this.music = {
|
this.music = {
|
||||||
bpm: 126,
|
bpm: 130,
|
||||||
lead: [72, null, 74, null, 76, 74, 72, null, 76, null, 79, 76, 74, null, 72, null],
|
stepsPerBar: 16,
|
||||||
bass: [48, null, 48, null, 50, null, 47, null, 45, null, 45, null, 47, null, 43, null],
|
progression: [
|
||||||
arp: [60, 64, 67, 72, 62, 65, 69, 74, 59, 62, 67, 71, 57, 60, 64, 69],
|
{ root: 45, chord: [0, 3, 7, 10] },
|
||||||
|
{ root: 50, chord: [0, 3, 7, 10] },
|
||||||
|
{ root: 43, chord: [0, 4, 7, 11] },
|
||||||
|
{ root: 48, chord: [0, 4, 7, 11] },
|
||||||
|
],
|
||||||
|
bassPattern: [0, null, 0, null, 7, null, 10, null, 0, null, 7, null, 3, null, 5, null],
|
||||||
|
arpPattern: [12, 19, 22, 19, 15, 19, 22, 19, 12, 19, 22, 24, 15, 19, 22, 19],
|
||||||
|
leadPatternA: [null, 24, null, 26, null, 27, 26, null, null, 31, null, 29, 27, null, 26, null],
|
||||||
|
leadPatternB: [null, 24, null, 24, null, 26, 27, null, null, 29, null, 31, 29, null, 27, null],
|
||||||
|
kick: [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
|
||||||
|
snare: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||||
|
hat: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
openHat: [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2445,7 +2662,7 @@ class AudioSystem {
|
|||||||
this.masterGain.connect(this.ctx.destination);
|
this.masterGain.connect(this.ctx.destination);
|
||||||
|
|
||||||
this.musicGain = this.ctx.createGain();
|
this.musicGain = this.ctx.createGain();
|
||||||
this.musicGain.gain.value = 0.26;
|
this.musicGain.gain.value = 0.24;
|
||||||
this.musicGain.connect(this.masterGain);
|
this.musicGain.connect(this.masterGain);
|
||||||
|
|
||||||
this.sfxGain = this.ctx.createGain();
|
this.sfxGain = this.ctx.createGain();
|
||||||
@ -2462,7 +2679,10 @@ class AudioSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.musicEnabled = !this.musicEnabled;
|
this.musicEnabled = !this.musicEnabled;
|
||||||
this.musicGain.gain.setTargetAtTime(this.musicEnabled ? 0.26 : 0, this.ctx.currentTime, 0.05);
|
this.musicGain.gain.setTargetAtTime(this.musicEnabled ? 0.24 : 0, this.ctx.currentTime, 0.05);
|
||||||
|
if (this.musicEnabled) {
|
||||||
|
this.nextStepTime = this.ctx.currentTime + 0.03;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
midiToFreq(note) {
|
midiToFreq(note) {
|
||||||
@ -2475,30 +2695,71 @@ class AudioSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stepDuration = (60 / this.music.bpm) / 2;
|
const stepDuration = (60 / this.music.bpm) / 2;
|
||||||
while (this.nextStepTime < this.ctx.currentTime + 0.4) {
|
const totalSteps = this.music.stepsPerBar * this.music.progression.length;
|
||||||
|
while (this.nextStepTime < this.ctx.currentTime + 0.35) {
|
||||||
this.playMusicStep(this.stepIndex, this.nextStepTime);
|
this.playMusicStep(this.stepIndex, this.nextStepTime);
|
||||||
this.stepIndex = (this.stepIndex + 1) % this.music.lead.length;
|
this.stepIndex = (this.stepIndex + 1) % totalSteps;
|
||||||
this.nextStepTime += stepDuration;
|
this.nextStepTime += stepDuration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playMusicStep(index, time) {
|
playMusicStep(index, time) {
|
||||||
const leadNote = this.music.lead[index % this.music.lead.length];
|
const step = index % this.music.stepsPerBar;
|
||||||
const bassNote = this.music.bass[index % this.music.bass.length];
|
const barIndex = Math.floor(index / this.music.stepsPerBar) % this.music.progression.length;
|
||||||
const arpNote = this.music.arp[index % this.music.arp.length];
|
const bar = this.music.progression[barIndex];
|
||||||
|
const leadPattern = barIndex % 2 === 0 ? this.music.leadPatternA : this.music.leadPatternB;
|
||||||
|
const swingTime = step % 2 === 1 ? time + 0.008 : time;
|
||||||
|
|
||||||
if (bassNote !== null) {
|
const bassInterval = this.music.bassPattern[step];
|
||||||
this.playTone(this.midiToFreq(bassNote), 0.2, "triangle", 0.22, time, this.musicGain);
|
if (bassInterval !== null) {
|
||||||
}
|
this.playTone(this.midiToFreq(bar.root + bassInterval), 0.19, "triangle", 0.17, swingTime, this.musicGain);
|
||||||
if (leadNote !== null) {
|
if (step % 4 === 0) {
|
||||||
this.playTone(this.midiToFreq(leadNote), 0.13, "square", 0.12, time, this.musicGain);
|
this.playTone(this.midiToFreq(bar.root + bassInterval - 12), 0.1, "sine", 0.08, swingTime, this.musicGain);
|
||||||
}
|
}
|
||||||
if (arpNote !== null) {
|
|
||||||
this.playTone(this.midiToFreq(arpNote), 0.08, "sawtooth", 0.06, time, this.musicGain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index % 4 === 0) {
|
const arpInterval = this.music.arpPattern[(step + barIndex * 2) % this.music.arpPattern.length];
|
||||||
this.playNoise(0.045, 0.07, time, this.musicGain);
|
if (arpInterval !== null) {
|
||||||
|
this.playTone(this.midiToFreq(bar.root + arpInterval), 0.07, "sawtooth", 0.05, swingTime, this.musicGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leadInterval = leadPattern[step];
|
||||||
|
if (leadInterval !== null) {
|
||||||
|
const accent = step % 4 === 1 ? 1.15 : 1;
|
||||||
|
this.playTone(
|
||||||
|
this.midiToFreq(bar.root + leadInterval),
|
||||||
|
0.12,
|
||||||
|
"square",
|
||||||
|
0.095 * accent,
|
||||||
|
swingTime,
|
||||||
|
this.musicGain,
|
||||||
|
0.004,
|
||||||
|
0.07,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step % 8 === 0) {
|
||||||
|
this.playTone(this.midiToFreq(bar.root + bar.chord[0] + 12), 0.42, "triangle", 0.045, time, this.musicGain);
|
||||||
|
this.playTone(this.midiToFreq(bar.root + bar.chord[1] + 12), 0.42, "triangle", 0.037, time, this.musicGain);
|
||||||
|
this.playTone(this.midiToFreq(bar.root + bar.chord[2] + 12), 0.42, "triangle", 0.04, time, this.musicGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.music.kick[step]) {
|
||||||
|
this.playTone(58, 0.05, "sine", 0.16, time, this.musicGain, 0.0015, 0.04);
|
||||||
|
this.playTone(42, 0.08, "triangle", 0.11, time, this.musicGain, 0.0015, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.music.snare[step]) {
|
||||||
|
this.playNoise(0.06, 0.04, time, this.musicGain);
|
||||||
|
this.playTone(188, 0.045, "triangle", 0.07, time, this.musicGain, 0.002, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.music.hat[step]) {
|
||||||
|
this.playNoise(0.018, 0.014, swingTime, this.musicGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.music.openHat[step]) {
|
||||||
|
this.playNoise(0.025, 0.06, swingTime, this.musicGain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user