RIVALS ARENA
by PrismPanda882135 lines53.0 KB๐ ๏ธ Three.js (3D graphics)
/*
RIVALS ARENA โ INTEGRATED MULTIPLAYER BUILD
Original fictional arena game.
Includes:
- 3D browser arena
- Online WebSocket multiplayer
- Registration and login
- Rooms and team modes
- Free-for-all and team matches
- Movement, jumping and sprinting
- Three fictional energy blasters
- Ammo, reload and weapon switching
- Health, respawning and scores
- Map selection and map voting
- Match timer and results
- Chat, friends and player reporting
- XP, levels, coins and saved statistics
- Shop, cosmetics and daily rewards
- Missions, achievements and battle pass
- Leaderboard and match history
- Mobile-friendly controls
Install:
npm install ws
Run:
node server.js
Open:
http://localhost:3000
For internet multiplayer, deploy this server to a host
supporting Node.js, persistent processes and WebSockets.
*/
"use strict";
const http = require("http");
const WebSocket = require("ws");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "rivals-arena-data.json");
const MATCH_SECONDS = 180;
const MAX_PLAYERS = 8;
const TICK_RATE = 20;
const WEAPONS = {
pulse: {
name: "Pulse Blaster",
damage: 25,
ammo: 20,
reload: 1.2,
cooldown: 350,
range: 35
},
burst: {
name: "Burst Blaster",
damage: 12,
ammo: 36,
reload: 1.5,
cooldown: 110,
range: 28
},
nova: {
name: "Nova Blaster",
damage: 55,
ammo: 6,
reload: 1.8,
cooldown: 750,
range: 22
}
};
const MAPS = [
"NeonCity",
"SkyArena",
"DesertBase",
"CrystalFort",
"SpaceStation"
];
const MODES = [
"Free For All",
"Team Arena",
"King Arena",
"Training"
];
let database = { users: {} };
try {
if (fs.existsSync(DATA_FILE)) {
database = JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
if (!database.users) database.users = {};
}
} catch {
database = { users: {} };
}
function saveDatabase() {
const temp = DATA_FILE + ".tmp";
fs.writeFileSync(temp, JSON.stringify(database, null, 2));
fs.renameSync(temp, DATA_FILE);
}
function cleanName(value) {
return String(value || "")
.replace(/[^a-zA-Z0-9_ -]/g, "")
.trim()
.slice(0, 18);
}
function makeId() {
return crypto.randomBytes(12).toString("hex");
}
function makeCode() {
return crypto.randomBytes(3).toString("hex").toUpperCase();
}
function passwordHash(password, salt) {
return crypto.scryptSync(password, salt, 64).toString("hex");
}
function safeUser(user) {
return {
username: user.username,
level: user.level,
xp: user.xp,
coins: user.coins,
wins: user.wins,
losses: user.losses,
eliminations: user.eliminations,
matches: user.matches,
equipped: user.equipped,
inventory: user.inventory,
friends: user.friends,
history: user.history.slice(0, 10),
achievements: user.achievements,
battlePassXP: user.battlePassXP,
season: user.season,
missions: user.missions
};
}
function getUser(username) {
return database.users[String(username || "").toLowerCase()];
}
function createUser(username, password) {
const salt = crypto.randomBytes(16).toString("hex");
return {
username,
salt,
passwordHash: passwordHash(password, salt),
level: 1,
xp: 0,
coins: 250,
wins: 0,
losses: 0,
eliminations: 0,
matches: 0,
equipped: "default",
inventory: ["default"],
friends: [],
history: [],
achievements: [],
battlePassXP: 0,
season: 1,
lastDaily: "",
missions: {
matches: 0,
eliminations: 0,
wins: 0
},
reports: []
};
}
const sessions = new Map();
const sockets = new Map();
const rooms = new Map();
const invitations = new Map();
const rateLimits = new Map();
function rateLimit(key, maximum, period) {
const now = Date.now();
let entry = rateLimits.get(key);
if (!entry || now - entry.start > period) {
entry = { start: now, count: 0 };
}
entry.count++;
rateLimits.set(key, entry);
return entry.count <= maximum;
}
function send(socket, type, data = {}) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type, ...data }));
}
}
function broadcast(room, type, data = {}) {
for (const player of room.players.values()) {
send(player.socket, type, data);
}
}
function publicPlayer(player) {
return {
id: player.id,
username: player.username,
x: player.x,
y: player.y,
z: player.z,
yaw: player.yaw,
health: player.health,
team: player.team,
score: player.score,
weapon: player.weapon,
alive: player.alive,
color: player.color
};
}
function newPlayer(socket, username) {
return {
id: makeId(),
socket,
username,
x: Math.random() * 8 - 4,
y: 1.6,
z: Math.random() * 8 - 4,
yaw: 0,
pitch: 0,
health: 100,
team: 0,
score: 0,
alive: true,
weapon: "pulse",
ammo: WEAPONS.pulse.ammo,
reloading: false,
lastShot: 0,
lastMove: Date.now(),
lastReload: 0,
color: ["#5ee7ff", "#ffcc66", "#bd88ff", "#70f0a5"][
Math.floor(Math.random() * 4)
]
};
}
function createRoom(owner, code) {
const room = {
code,
owner,
players: new Map(),
status: "lobby",
mode: "Free For All",
map: "NeonCity",
duration: MATCH_SECONDS,
timeLeft: MATCH_SECONDS,
scoreLimit: 20,
startedAt: 0,
lastTick: Date.now(),
votes: {},
king: null,
messages: [],
results: []
};
rooms.set(code, room);
return room;
}
function leaveRoom(player) {
if (!player || !player.room) return;
const room = rooms.get(player.room);
if (!room) {
player.room = null;
return;
}
room.players.delete(player.id);
player.room = null;
broadcast(room, "playerLeft", {
username: player.username
});
if (room.players.size === 0) {
rooms.delete(room.code);
return;
}
if (room.owner === player.username) {
room.owner = room.players.values().next().value.username;
}
broadcast(room, "roomUpdate", roomInfo(room));
}
function roomInfo(room) {
return {
code: room.code,
owner: room.owner,
status: room.status,
mode: room.mode,
map: room.map,
duration: room.duration,
scoreLimit: room.scoreLimit,
players: [...room.players.values()].map(publicPlayer)
};
}
function joinRoom(socket, user, code) {
code = String(code || "").toUpperCase().replace(/[^A-Z0-9]/g, "");
if (!code) code = makeCode();
let room = rooms.get(code);
if (!room) room = createRoom(user.username, code);
if (room.status !== "lobby" && room.mode !== "Training") {
send(socket, "error", { message: "This match has already started." });
return;
}
if (room.players.size >= MAX_PLAYERS) {
send(socket, "error", { message: "This room is full." });
return;
}
const player = newPlayer(socket, user.username);
player.room = code;
if (room.mode === "Team Arena") {
const counts = [0, 0];
for (const p of room.players.values()) counts[p.team]++;
player.team = counts[0] <= counts[1] ? 0 : 1;
}
room.players.set(player.id, player);
socket.playerId = player.id;
send(socket, "joined", {
self: player.id,
user: safeUser(user),
room: roomInfo(room)
});
broadcast(room, "playerJoined", {
player: publicPlayer(player)
});
broadcast(room, "roomUpdate", roomInfo(room));
}
function getPlayer(socket) {
const id = socket.playerId;
const session = sockets.get(socket);
if (!session || !id || !session.room) return null;
const room = rooms.get(session.room);
if (!room) return null;
return room.players.get(id) || null;
}
function startMatch(room) {
if (room.status === "playing") return;
room.status = "playing";
room.startedAt = Date.now();
room.lastTick = Date.now();
room.timeLeft = room.duration;
room.votes = {};
room.king = null;
for (const player of room.players.values()) {
player.health = 100;
player.score = 0;
player.alive = true;
player.x = Math.random() * 10 - 5;
player.z = Math.random() * 10 - 5;
player.weapon = "pulse";
player.ammo = WEAPONS.pulse.ammo;
player.reloading = false;
}
broadcast(room, "matchStarted", {
map: room.map,
mode: room.mode,
duration: room.duration,
players: [...room.players.values()].map(publicPlayer)
});
}
function finishMatch(room) {
if (room.status !== "playing") return;
room.status = "results";
const ranking = [...room.players.values()]
.sort((a, b) => b.score - a.score);
const topScore = ranking[0] ? ranking[0].score : 0;
const winners = ranking.filter(p => p.score === topScore);
for (const player of ranking) {
const user = getUser(player.username);
if (!user) continue;
user.matches++;
user.missions.matches++;
if (winners.some(w => w.id === player.id)) {
user.wins++;
user.missions.wins++;
user.coins += 100;
user.xp += 150;
} else {
user.losses++;
user.coins += 35;
user.xp += 50;
}
user.battlePassXP += 50;
user.history.unshift({
date: new Date().toISOString(),
map: room.map,
mode: room.mode,
score: player.score,
result: winners.some(w => w.id === player.id) ? "Win" : "Played"
});
user.history = user.history.slice(0, 20);
while (user.xp >= user.level * 250) {
user.xp -= user.level * 250;
user.level++;
user.coins += 50;
}
}
saveDatabase();
const results = ranking.map(p => ({
username: p.username,
score: p.score,
team: p.team
}));
room.results = results;
broadcast(room, "matchResults", {
results,
winner: winners.map(w => w.username)
});
setTimeout(() => {
if (!rooms.has(room.code)) return;
room.status = "lobby";
room.timeLeft = room.duration;
room.votes = {};
broadcast(room, "roomUpdate", roomInfo(room));
}, 10000);
}
function reload(player) {
const weapon = WEAPONS[player.weapon];
if (!weapon || player.reloading || player.ammo === weapon.ammo) return;
player.reloading = true;
player.lastReload = Date.now();
send(player.socket, "weaponState", {
weapon: player.weapon,
ammo: player.ammo,
maxAmmo: weapon.ammo,
reloading: true
});
setTimeout(() => {
const currentRoom = rooms.get(player.room);
const current = currentRoom && currentRoom.players.get(player.id);
if (!current || !current.alive) return;
current.ammo = WEAPONS[current.weapon].ammo;
current.reloading = false;
send(current.socket, "weaponState", {
weapon: current.weapon,
ammo: current.ammo,
maxAmmo: WEAPONS[current.weapon].ammo,
reloading: false
});
}, weapon.reload * 1000);
}
function switchWeapon(player, weaponName) {
if (!WEAPONS[weaponName]) return;
player.weapon = weaponName;
player.ammo = WEAPONS[weaponName].ammo;
player.reloading = false;
send(player.socket, "weaponState", {
weapon: weaponName,
ammo: player.ammo,
maxAmmo: WEAPONS[weaponName].ammo,
reloading: false
});
}
function fire(player, data) {
const room = rooms.get(player.room);
if (!room || room.status !== "playing" || !player.alive) return;
if (player.reloading) return;
const weapon = WEAPONS[player.weapon];
if (!weapon || player.ammo <= 0) {
send(player.socket, "emptyAmmo");
return;
}
const now = Date.now();
if (now - player.lastShot < weapon.cooldown) return;
player.lastShot = now;
player.ammo--;
const yaw = Number(data.yaw);
const pitch = Number(data.pitch);
if (!Number.isFinite(yaw) || !Number.isFinite(pitch)) return;
const direction = {
x: -Math.sin(yaw) * Math.cos(pitch),
y: Math.sin(pitch),
z: -Math.cos(yaw) * Math.cos(pitch)
};
let target = null;
let nearest = weapon.range;
for (const other of room.players.values()) {
if (other.id === player.id || !other.alive) continue;
if (
room.mode === "Team Arena" &&
other.team === player.team
) continue;
const dx = other.x - player.x;
const dy = other.y - player.y;
const dz = other.z - player.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance > nearest || distance < 0.01) continue;
const dot =
(dx / distance) * direction.x +
(dy / distance) * direction.y +
(dz / distance) * direction.z;
if (dot > 0.965) {
nearest = distance;
target = other;
}
}
if (target) {
target.health -= weapon.damage;
send(player.socket, "hitMarker", {
damage: weapon.damage
});
send(target.socket, "hitReceived", {
health: Math.max(0, target.health)
});
if (target.health <= 0) {
target.alive = false;
player.score++;
const user = getUser(player.username);
if (user) {
user.eliminations++;
user.missions.eliminations++;
user.coins += 10;
user.xp += 20;
}
if (room.mode === "King Arena") {
room.king = player.username;
}
broadcast(room, "elimination", {
attacker: player.username,
target: target.username,
scores: [...room.players.values()].map(p => ({
username: p.username,
score: p.score
}))
});
setTimeout(() => {
const activeRoom = rooms.get(room.code);
const respawning = activeRoom &&
activeRoom.players.get(target.id);
if (!respawning || activeRoom.status !== "playing") return;
respawning.health = 100;
respawning.alive = true;
respawning.x = Math.random() * 10 - 5;
respawning.z = Math.random() * 10 - 5;
send(respawning.socket, "respawned", {
x: respawning.x,
y: respawning.y,
z: respawning.z
});
broadcast(activeRoom, "playerUpdate", {
player: publicPlayer(respawning)
});
}, 2500);
saveDatabase();
if (
room.mode !== "Training" &&
player.score >= room.scoreLimit
) {
finishMatch(room);
}
}
}
send(player.socket, "weaponState", {
weapon: player.weapon,
ammo: player.ammo,
maxAmmo: weapon.ammo,
reloading: false
});
}
function handleMessage(socket, message) {
const session = sockets.get(socket);
if (!session) return;
const data = message || {};
const user = session.username ? getUser(session.username) : null;
if (!rateLimit(session.username || socket._socket?.remoteAddress || "guest", 100, 1000)) {
send(socket, "error", { message: "Please slow down." });
return;
}
if (data.type === "register") {
const username = cleanName(data.username);
const password = String(data.password || "");
if (username.length < 3 || password.length < 8) {
send(socket, "error", {
message: "Use a username of 3+ characters and a password of 8+ characters."
});
return;
}
const key = username.toLowerCase();
if (database.users[key]) {
send(socket, "error", { message: "Username already exists." });
return;
}
const newUser = createUser(username, password);
database.users[key] = newUser;
saveDatabase();
const token = makeId();
sessions.set(token, username);
session.username = username;
send(socket, "authenticated", {
token,
user: safeUser(newUser)
});
return;
}
if (data.type === "login") {
const username = cleanName(data.username);
const password = String(data.password || "");
const existing = getUser(username);
if (
!existing ||
!crypto.timingSafeEqual(
Buffer.from(passwordHash(password, existing.salt), "hex"),
Buffer.from(existing.passwordHash, "hex")
)
) {
send(socket, "error", { message: "Incorrect username or password." });
return;
}
const token = makeId();
sessions.set(token, username);
session.username = username;
send(socket, "authenticated", {
token,
user: safeUser(existing)
});
return;
}
if (data.type === "resume") {
const username = sessions.get(String(data.token || ""));
const existing = username && getUser(username);
if (!existing) {
send(socket, "error", { message: "Please log in again." });
return;
}
session.username = username;
send(socket, "authenticated", {
token: data.token,
user: safeUser(existing)
});
return;
}
if (!user) {
send(socket, "error", { message: "Please log in first." });
return;
}
if (data.type === "join") {
const old = session.room && rooms.get(session.room);
if (old) {
const oldPlayer = [...old.players.values()]
.find(p => p.socket === socket);
if (oldPlayer) leaveRoom(oldPlayer);
}
joinRoom(socket, user, data.code);
return;
}
if (data.type === "createRoom") {
const code = makeCode();
joinRoom(socket, user, code);
return;
}
if (data.type === "leave") {
const player = getPlayer(socket);
if (player) leaveRoom(player);
send(socket, "left");
return;
}
if (data.type === "start") {
const player = getPlayer(socket);
const room = player && rooms.get(player.room);
if (!room || room.owner !== user.username) return;
startMatch(room);
return;
}
if (data.type === "settings") {
const player = getPlayer(socket);
const room = player && rooms.get(player.room);
if (!room || room.owner !== user.username || room.status !== "lobby") return;
if (MODES.includes(data.mode)) room.mode = data.mode;
if (MAPS.includes(data.map)) room.map = data.map;
room.duration = Math.max(60, Math.min(600, Number(data.duration) || MATCH_SECONDS));
room.scoreLimit = Math.max(5, Math.min(100, Number(data.scoreLimit) || 20));
for (const p of room.players.values()) {
p.team = room.mode === "Team Arena"
? [...room.players.values()].indexOf(p) % 2
: 0;
}
broadcast(room, "roomUpdate", roomInfo(room));
return;
}
if (data.type === "voteMap") {
const player = getPlayer(socket);
const room = player && rooms.get(player.room);
if (!room || room.status !== "lobby" || !MAPS.includes(data.map)) return;
room.votes[player.username] = data.map;
const totals = {};
for (const map of Object.values(room.votes)) {
totals[map] = (totals[map] || 0) + 1;
}
broadcast(room, "mapVotes", { totals });
return;
}
if (data.type === "move") {
const player = getPlayer(socket);
const room = player && rooms.get(player.room);
if (!player || !room || room.status !== "playing" || !player.alive) return;
const now = Date.now();
const delta = Math.min((now - player.lastMove) / 1000, 0.1);
player.lastMove = now;
const forward = Math.max(-1, Math.min(1, Number(data.forward) || 0));
const sideways = Math.max(-1, Math.min(1, Number(data.sideways) || 0));
const sprint = data.sprint === true;
const yaw = Number(data.yaw);
if (Number.isFinite(yaw)) player.yaw = yaw;
const speed = sprint ? 8 : 5;
const dx = (-Math.sin(player.yaw) * forward +
Math.cos(player.yaw) * sideways) * speed * delta;
const dz = (-Math.cos(player.yaw) * forward -
Math.sin(player.yaw) * sideways) * speed * delta;
player.x = Math.max(-45, Math.min(45, player.x + dx));
player.z = Math.max(-45, Math.min(45, player.z + dz));
if (data.jump === true && player.y <= 1.61) {
player.y = 2.5;
} else {
player.y = Math.max(1.6, player.y - 0.12);
}
player.pitch = Math.max(-1.2, Math.min(1.2, Number(data.pitch) || 0));
broadcast(room, "playerUpdate", {
player: publicPlayer(player)
});
return;
}
if (data.type === "shoot") {
const player = getPlayer(socket);
if (player) fire(player, data);
return;
}
if (data.type === "reload") {
const player = getPlayer(socket);
if (player) reload(player);
return;
}
if (data.type === "switchWeapon") {
const player = getPlayer(socket);
if (player) switchWeapon(player, data.weapon);
return;
}
if (data.type === "chat") {
const player = getPlayer(socket);
const room = player && rooms.get(player.room);
const message = String(data.message || "").trim().slice(0, 160);
if (!room || !message) return;
const entry = {
username: user.username,
message,
time: Date.now()
};
room.messages.push(entry);
room.messages = room.messages.slice(-50);
broadcast(room, "chat", entry);
return;
}
if (data.type === "friend") {
const friendName = cleanName(data.username);
const friend = getUser(friendName);
if (!friend || friendName.toLowerCase() === user.username.toLowerCase()) {
send(socket, "error", { message: "Player not found." });
return;
}
if (!user.friends.includes(friend.username)) {
user.friends.push(friend.username);
friend.friends.push(user.username);
saveDatabase();
}
send(socket, "profile", { user: safeUser(user) });
return;
}
if (data.type === "invite") {
const targetName = cleanName(data.username);
const targetSocket = [...sockets.entries()]
.find(([s, info]) =>
info.username &&
info.username.toLowerCase() === targetName.toLowerCase()
);
if (!targetSocket) {
send(socket, "error", { message: "Player is not online." });
return;
}
const player = getPlayer(socket);
if (!player) return;
invitations.set(targetName.toLowerCase(), {
code: player.room,
from: user.username,
expires: Date.now() + 60000
});
send(targetSocket[0], "invitation", {
from: user.username,
code: player.room
});
return;
}
if (data.type === "acceptInvite") {
const invite = invitations.get(user.username.toLowerCase());
if (!invite || invite.expires < Date.now()) {
send(socket, "error", { message: "Invitation expired." });
return;
}
invitations.delete(user.username.toLowerCase());
joinRoom(socket, user, invite.code);
return;
}
if (data.type === "dailyReward") {
const today = new Date().toISOString().slice(0, 10);
if (user.lastDaily === today) {
send(socket, "error", { message: "Daily reward already collected." });
return;
}
user.lastDaily = today;
user.coins += 100;
saveDatabase();
send(socket, "profile", { user: safeUser(user) });
send(socket, "notice", { message: "Daily reward: 100 coins!" });
return;
}
if (data.type === "buy") {
const items = {
neon: 150,
galaxy: 300,
emerald: 450,
sunset: 600
};
const item = String(data.item || "");
const cost = items[item];
if (!cost || user.inventory.includes(item)) return;
if (user.coins < cost) {
send(socket, "error", { message: "Not enough coins." });
return;
}
user.coins -= cost;
user.inventory.push(item);
saveDatabase();
send(socket, "profile", { user: safeUser(user) });
return;
}
if (data.type === "equip") {
const item = String(data.item || "");
if (!user.inventory.includes(item)) return;
user.equipped = item;
saveDatabase();
send(socket, "profile", { user: safeUser(user) });
return;
}
if (data.type === "report") {
const target = cleanName(data.username);
const reason = String(data.reason || "Other").slice(0, 100);
if (
!target ||
target.toLowerCase() === user.username.toLowerCase()
) return;
user.reports.push({
target,
reason,
date: new Date().toISOString()
});
user.reports = user.reports.slice(-50);
saveDatabase();
send(socket, "notice", {
message: "Report submitted for review."
});
return;
}
if (data.type === "leaderboard") {
const leaders = Object.values(database.users)
.sort((a, b) => b.wins - a.wins)
.slice(0, 20)
.map(u => ({
username: u.username,
wins: u.wins,
level: u.level,
eliminations: u.eliminations
}));
send(socket, "leaderboard", { leaders });
return;
}
if (data.type === "profile") {
send(socket, "profile", { user: safeUser(user) });
return;
}
}
function gameTick() {
const now = Date.now();
for (const room of rooms.values()) {
if (room.status !== "playing") continue;
const elapsed = Math.floor((now - room.startedAt) / 1000);
room.timeLeft = Math.max(0, room.duration - elapsed);
if (room.mode === "King Arena" && !room.king) {
const highest = [...room.players.values()]
.sort((a, b) => b.score - a.score)[0];
if (highest && highest.score > 0) {
room.king = highest.username;
}
}
broadcast(room, "timer", {
timeLeft: room.timeLeft,
king: room.king
});
if (room.timeLeft <= 0) {
finishMatch(room);
}
}
}
const HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#10152c">
<title>RIVALS ARENA</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#0b1022;color:#fff;font-family:Arial,sans-serif}
button,input,select{font:inherit}
button{cursor:pointer;border:0;border-radius:9px;padding:11px 16px;background:#7657ff;color:white;font-weight:bold}
button:hover{filter:brightness(1.15)}
button:disabled{opacity:.5;cursor:not-allowed}
input,select{background:#192442;color:white;border:1px solid #35456d;padding:12px;border-radius:8px;width:100%;margin:5px 0}
#auth{max-width:420px;margin:9vh auto;padding:25px;background:#151f3d;border-radius:18px;text-align:center}
#auth h1{font-size:34px;color:#9b8bff}
#app{display:none}
header{background:#121b35;padding:13px 18px;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
header strong{font-size:21px;color:#a899ff}
.stats{display:flex;gap:14px;flex-wrap:wrap;color:#d8dcff}
nav{display:flex;gap:7px;padding:12px;flex-wrap:wrap;background:#101a32}
nav button{background:#253456}
.page{display:none;padding:18px;max-width:1200px;margin:auto}
.page.active{display:block}
.panel{background:#151f3d;padding:17px;border-radius:14px;margin:12px 0;border:1px solid #29385f}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
.big{font-size:26px;font-weight:bold}
.muted{color:#aab5d4}
#gameCanvas{width:100%;height:65vh;display:block;background:#070b19;border-radius:12px;touch-action:none}
#gameHud{display:none;position:relative}
.hud{position:absolute;top:12px;left:12px;right:12px;pointer-events:none;display:flex;justify-content:space-between;gap:10px;text-shadow:0 2px 5px black}
.hud span{background:#111a30cc;padding:8px;border-radius:8px}
.crosshair{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:26px;pointer-events:none}
#gameButtons{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
#gameButtons button{flex:1}
#messages{height:150px;overflow:auto;background:#0c1429;padding:8px;border-radius:8px}
.item{background:#202e50;padding:13px;border-radius:10px}
#notice{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:#7657ff;padding:12px 20px;border-radius:10px;display:none;z-index:20}
#mobileControls{display:none;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:10px}
#mobileControls button{min-width:65px}
table{width:100%;border-collapse:collapse}
td,th{text-align:left;padding:9px;border-bottom:1px solid #35415e}
@media(max-width:600px){
header{align-items:flex-start}
.stats{font-size:12px}
#gameCanvas{height:55vh}
#mobileControls{display:flex}
nav button{padding:9px;font-size:12px}
}
</style>
</head>
<body>
<div id="auth">
<h1>RIVALS ARENA</h1>
<p class="muted">Enter the arena. Build your skills. Play together.</p>
<input id="username" placeholder="Username" maxlength="18">
<input id="password" type="password" placeholder="Password">
<button onclick="authenticate('login')">LOGIN</button>
<button onclick="authenticate('register')">CREATE ACCOUNT</button>
<p id="authMessage" class="muted"></p>
</div>
<div id="app">
<header>
<strong>RIVALS ARENA</strong>
<div class="stats">
<span id="playerName">Player</span>
<span>โญ Level <b id="level">1</b></span>
<span>๐ช <b id="coins">0</b></span>
<span>๐ <b id="wins">0</b></span>
</div>
</header>
<nav>
<button onclick="showPage('home')">Home</button>
<button onclick="showPage('play')">Play</button>
<button onclick="showPage('friends')">Friends</button>
<button onclick="showPage('profile')">Profile</button>
<button onclick="showPage('shop')">Shop</button>
<button onclick="showPage('battlepass')">Battle Pass</button>
<button onclick="showPage('leaderboard')">Leaderboard</button>
<button onclick="showPage('settings')">Settings</button>
</nav>
<section id="home" class="page active">
<div class="panel">
<h1>Welcome to the Arena</h1>
<p class="muted">Join a room, invite friends, choose your equipment and compete.</p>
<button onclick="createRoom()">CREATE MATCH</button>
<button onclick="showPage('play')">JOIN MATCH</button>
<button onclick="dailyReward()">DAILY REWARD</button>
</div>
<div class="grid">
<div class="panel"><div class="big" id="homeMatches">0</div><p class="muted">Matches played</p></div>
<div class="panel"><div class="big" id="homeElims">0</div><p class="muted">Eliminations</p></div>
<div class="panel"><div class="big" id="homeXP">0</div><p class="muted">Experience</p></div>
</div>
<div class="panel">
<h2>How to play</h2>
<p>WASD: move ยท Mouse: look ยท Left click: fire ยท Space: jump ยท Shift: sprint ยท R: reload ยท 1, 2, 3: switch blaster.</p>
<p class="muted">On phones, use the movement and action buttons below the game.</p>
</div>
</section>
<section id="play" class="page">
<div class="panel">
<h2>Find a Match</h2>
<input id="roomCode" placeholder="Enter room code">
<button onclick="joinRoom()">JOIN ROOM</button>
<button onclick="createRoom()">CREATE PRIVATE ROOM</button>
</div>
<div id="lobby" class="panel">
<h2>Lobby</h2>
<p>Room: <b id="roomName">Not connected</b></p>
<p>Players: <b id="playerCount">0</b>/8</p>
<div class="grid">
<label>Game mode
<select id="mode">
<option>Free For All</option>
<option>Team Arena</option>
<option>King Arena</option>
<option>Training</option>
</select>
</label>
<label>Map
<select id="map">
<option>NeonCity</option>
<option>SkyArena</option>
<option>DesertBase</option>
<option>CrystalFort</option>
<option>SpaceStation</option>
</select>
</label>
<label>Match duration
<select id="duration">
<option value="120">2 minutes</option>
<option value="180" selected>3 minutes</option>
<option value="300">5 minutes</option>
<option value="600">10 minutes</option>
</select>
</label>
<label>Score limit
<select id="scoreLimit">
<option>10</option><option selected>20</option><option>30</option><option>50</option>
</select>
</label>
</div>
<button onclick="saveSettings()">SAVE SETTINGS</button>
<button onclick="voteMap()">VOTE FOR MAP</button>
<button onclick="startMatch()">START MATCH</button>
<button onclick="leaveRoom()">LEAVE ROOM</button>
<div id="roomPlayers"></div>
</div>
<div id="gameHud" class="panel">
<div class="hud">
<span>โค๏ธ <b id="health">100</b></span>
<span>โฑ <b id="timer">03:00</b></span>
<span>๐ <b id="score">0</b></span>
<span>๐ <b id="ammo">20</b></span>
</div>
<canvas id="gameCanvas"></canvas>
<div class="crosshair">+</div>
<div id="gameButtons">
<button onclick="shoot()">FIRE</button>
<button onclick="reloadWeapon()">RELOAD</button>
<button onclick="switchWeapon('pulse')">PULSE</button>
<button onclick="switchWeapon('burst')">BURST</button>
<button onclick="switchWeapon('nova')">NOVA</button>
<button onclick="leaveGame()">EXIT GAME</button>
</div>
<div id="mobileControls">
<button onpointerdown="mobileMove('forward',true)" onpointerup="mobileMove('forward',false)">โฒ</button>
<button onpointerdown="mobileMove('left',true)" onpointerup="mobileMove('left',false)">โ</button>
<button onpointerdown="mobileMove('back',true)" onpointerup="mobileMove('back',false)">โผ</button>
<button onpointerdown="mobileMove('right',true)" onpointerup="mobileMove('right',false)">โถ</button>
<button onclick="jump()">JUMP</button>
<button onclick="shoot()">FIRE</button>
</div>
<p id="weaponLabel">Pulse Blaster</p>
</div>
<div class="panel">
<h3>Match Chat</h3>
<div id="messages"></div>
<input id="chatInput" placeholder="Write a message">
<button onclick="sendChat()">SEND</button>
</div>
</section>
<section id="friends" class="page">
<div class="panel">
<h2>Friends</h2>
<input id="friendName" placeholder="Player username">
<button onclick="addFriend()">ADD FRIEND</button>
<div id="friendList"></div>
<h3>Invite a friend to your room</h3>
<input id="inviteName" placeholder="Friend username">
<button onclick="inviteFriend()">INVITE</button>
</div>
</section>
<section id="profile" class="page">
<div class="panel">
<h2>Player Profile</h2>
<div id="profileDetails"></div>
<h3>Recent matches</h3>
<div id="history"></div>
</div>
</section>
<section id="shop" class="page">
<div class="panel">
<h2>Cosmetic Shop</h2>
<p class="muted">Cosmetic items do not increase damage or give competitive advantages.</p>
<div class="grid" id="shopItems"></div>
<h3>Your inventory</h3>
<div id="inventory"></div>
</div>
</section>
<section id="battlepass" class="page">
<div class="panel">
<h2>Season 1 Battle Pass</h2>
<p>Earn battle pass XP by playing matches.</p>
<p>Battle pass XP: <b id="bpXP">0</b></p>
<div id="bpTiers"></div>
<p class="muted">This build tracks season progress; premium purchases are not enabled.</p>
</div>
<div class="panel">
<h3>Missions</h3>
<div id="missions"></div>
</div>
</section>
<section id="leaderboard" class="page">
<div class="panel">
<h2>Leaderboard</h2>
<button onclick="loadLeaderboard()">REFRESH</button>
<div id="leaderboardRows"></div>
</div>
</section>
<section id="settings" class="page">
<div class="panel">
<h2>Settings</h2>
<label>Mouse sensitivity
<input id="sensitivity" type="range" min="0.2" max="3" step="0.1" value="1">
</label>
<label>Sound
<select id="sound">
<option>On</option><option>Off</option>
</select>
</label>
<button onclick="saveLocalSettings()">SAVE SETTINGS</button>
<button onclick="logout()">LOG OUT</button>
</div>
<div class="panel">
<h3>Safety and support</h3>
<p>Keep chat respectful. Do not share passwords or private information.</p>
<input id="reportName" placeholder="Player username to report">
<select id="reportReason">
<option>Harassment</option>
<option>Cheating concern</option>
<option>Inappropriate chat</option>
<option>Other</option>
</select>
<button onclick="reportPlayer()">SUBMIT REPORT</button>
</div>
</section>
</div>
<div id="notice"></div>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
let ws;
let token = localStorage.getItem("ra_token") || "";
let me = null;
let room = null;
let selfId = null;
let players = {};
let keys = {};
let yaw = 0;
let pitch = 0;
let gameRunning = false;
let lastMoveSent = 0;
let healthValue = 100;
let currentWeapon = "pulse";
let ammoValue = 20;
let mobile = {};
let scene, camera, renderer, playerObjects = {};
let moveInterval;
let pointerLocked = false;
const $ = id => document.getElementById(id);
function connect() {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(protocol + "//" + location.host);
ws.onopen = () => {
if (token) send({ type: "resume", token });
};
ws.onmessage = event => {
let data;
try { data = JSON.parse(event.data); } catch { return; }
handleServer(data);
};
ws.onclose = () => {
notice("Connection lost. Reconnecting...");
setTimeout(connect, 2000);
};
}
function send(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
function notice(message) {
$("notice").textContent = message;
$("notice").style.display = "block";
clearTimeout(notice.timer);
notice.timer = setTimeout(() => {
$("notice").style.display = "none";
}, 2500);
}
function authenticate(type) {
const username = $("username").value.trim();
const password = $("password").value;
if (!username || !password) {
$("authMessage").textContent = "Enter your username and password.";
return;
}
send({ type, username, password });
}
function handleServer(data) {
if (data.type === "authenticated") {
token = data.token;
localStorage.setItem("ra_token", token);
me = data.user;
$("auth").style.display = "none";
$("app").style.display = "block";
updateProfile(me);
notice("Welcome to RIVALS ARENA!");
}
if (data.type === "error") {
$("authMessage").textContent = data.message;
notice(data.message);
}
if (data.type === "notice") notice(data.message);
if (data.type === "profile") updateProfile(data.user);
if (data.type === "joined") {
room = data.room;
selfId = data.self;
players = {};
room.players.forEach(p => players[p.id] = p);
showPage("play");
updateRoom();
notice("Joined room " + room.code);
}
if (data.type === "roomUpdate") {
room = data;
players = {};
data.players.forEach(p => players[p.id] = p);
updateRoom();
}
if (data.type === "playerJoined") {
players[data.player.id] = data.player;
updateRoom();
}
if (data.type === "playerLeft") {
notice(data.username + " left the room.");
}
if (data.type === "playerUpdate") {
players[data.player.id] = data.player;
updatePlayerObject(data.player);
updateScores();
}
if (data.type === "matchStarted") {
room.status = "playing";
$("gameHud").style.display = "block";
initGame();
notice("Match started!");
}
if (data.type === "timer") {
const m = Math.floor(data.timeLeft / 60);
const s = data.timeLeft % 60;
$("timer").textContent =
String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
}
if (data.type === "weaponState") {
currentWeapon = data.weapon;
ammoValue = data.ammo;
$("ammo").textContent = data.ammo + "/" + data.maxAmmo;
$("weaponLabel").textContent =
data.weapon.toUpperCase() + (data.reloading ? " โ RELOADING" : "");
}
if (data.type === "hitMarker") {
$("score").textContent = Number($("score").textContent) + 1;
}
if (data.type === "hitReceived") {
healthValue = data.health;
$("health").textContent = healthValue;
}
if (data.type === "respawned") {
healthValue = 100;
$("health").textContent = 100;
if (camera) camera.position.set(data.x, 1.6, data.z);
}
if (data.type === "elimination") {
notice(data.attacker + " scored an elimination!");
updateScores(data.scores);
}
if (data.type === "matchResults") {
gameRunning = false;
$("gameHud").style.display = "none";
destroyGame();
let text = "MATCH RESULTS\\n";
data.results.forEach((p, i) => {
text += (i + 1) + ". " + p.username + " โ " + p.score + "\\n";
});
alert(text);
send({ type: "profile" });
}
if (data.type === "left") {
room = null;
$("gameHud").style.display = "none";
destroyGame();
updateRoom();
}
if (data.type === "chat") {
const line = document.createElement("p");
line.textContent = data.username + ": " + data.message;
$("messages").appendChild(line);
$("messages").scrollTop = $("messages").scrollHeight;
}
if (data.type === "leaderboard") {
$("leaderboardRows").innerHTML =
"<table><tr><th>Player</th><th>Level</th><th>Wins</th><th>Eliminations</th></tr>" +
data.leaders.map(p =>
"<tr><td>" + escapeHTML(p.username) + "</td><td>" +
p.level + "</td><td>" + p.wins + "</td><td>" +
p.eliminations + "</td></tr>"
).join("") + "</table>";
}
if (data.type === "invitation") {
if (confirm(data.from + " invited you to room " + data.code + ". Join?")) {
send({ type: "acceptInvite" });
}
}
if (data.type === "mapVotes") {
notice("Map votes updated.");
}
}
function escapeHTML(text) {
return String(text).replace(/[&<>"']/g, c => ({
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'"
}[c]));
}
function updateProfile(user) {
if (!user) return;
me = user;
$("playerName").textContent = user.username;
$("level").textContent = user.level;
$("coins").textContent = user.coins;
$("wins").textContent = user.wins;
$("homeMatches").textContent = user.matches;
$("homeElims").textContent = user.eliminations;
$("homeXP").textContent = user.xp;
$("profileDetails").innerHTML =
"<p>Username: " + escapeHTML(user.username) + "</p>" +
"<p>Level: " + user.level + "</p>" +
"<p>XP: " + user.xp + "</p>" +
"<p>Wins: " + user.wins + "</p>" +
"<p>Losses: " + user.losses + "</p>" +
"<p>Eliminations: " + user.eliminations + "</p>" +
"<p>Matches: " + user.matches + "</p>";
$("friendList").innerHTML = (user.friends || [])
.map(n => "<p>" + escapeHTML(n) + "</p>").join("") || "No friends yet.";
$("history").innerHTML = (user.history || []).map(h =>
"<p>" + escapeHTML(h.map) + " โ " +
escapeHTML(h.mode) + " โ Score " + h.score +
" โ " + escapeHTML(h.result) + "</p>"
).join("") || "No matches yet.";
$("inventory").innerHTML = (user.inventory || []).map(item =>
"<button onclick=\\"equipItem('" + escapeHTML(item) + "')\\">EQUIP " +
escapeHTML(item) + "</button>"
).join(" ");
$("bpXP").textContent = user.battlePassXP || 0;
const tier = Math.floor((user.battlePassXP || 0) / 100);
$("bpTiers").innerHTML = Array.from({length: 10}, (_, i) =>
"<div class='item'>Tier " + (i + 1) + " โ " +
(tier >= i + 1 ? "Unlocked" : "Locked") + "</div>"
).join("");
$("missions").innerHTML =
"<p>Play matches: " + user.missions.matches + "/5</p>" +
"<p>Eliminations: " + user.missions.eliminations + "/10</p>" +
"<p>Wins: " + user.missions.wins + "/3</p>";
renderShop();
}
function renderShop() {
const items = [
["neon", 150, "Neon Glow"],
["galaxy", 300, "Galaxy"],
["emerald", 450, "Emerald"],
["sunset", 600, "Sunset"]
];
$("shopItems").innerHTML = items.map(item =>
"<div class='item'><h3>" + item[2] + "</h3><p>๐ช " +
item[1] + "</p><button onclick=\\"buyItem('" +
item[0] + "')\\">BUY</button></div>"
).join("");
}
function showPage(name) {
document.querySelectorAll(".page").forEach(p => p.classList.remove("active"));
$(name).classList.add("active");
}
function createRoom() {
send({ type: "createRoom" });
}
function joinRoom() {
send({ type: "join", code: $("roomCode").value });
}
function leaveRoom() {
send({ type: "leave" });
}
function leaveGame() {
leaveRoom();
}
function startMatch() {
send({ type: "start" });
}
function saveSettings() {
send({
type: "settings",
mode: $("mode").value,
map: $("map").value,
duration: Number($("duration").value),
scoreLimit: Number($("scoreLimit").value)
});
}
function voteMap() {
send({ type: "voteMap", map: $("map").value });
}
function updateRoom() {
if (!room) {
$("roomName").textContent = "Not connected";
$("playerCount").textContent = "0";
$("roomPlayers").innerHTML = "";
return;
}
$("roomName").textContent = room.code;
$("roomCode").value = room.code;
$("playerCount").textContent = room.players.length;
$("mode").value = room.mode;
$("map").value = room.map;
$("duration").value = room.duration;
$("scoreLimit").value = room.scoreLimit;
$("roomPlayers").innerHTML = "<h3>Players</h3>" +
room.players.map(p =>
"<p>" + escapeHTML(p.username) + " โ " +
(p.team === 0 ? "Blue team" : "Orange team") +
(p.username === room.owner ? " โ Host" : "") + "</p>"
).join("");
}
function updateScores(scores) {
if (!scores) return;
const meScore = scores.find(s => s.username === me.username);
if (meScore) $("score").textContent = meScore.score;
}
function dailyReward() {
send({ type: "dailyReward" });
}
function addFriend() {
send({ type: "friend", username: $("friendName").value });
}
function inviteFriend() {
send({ type: "invite", username: $("inviteName").value });
}
function buyItem(item) {
send({ type: "buy", item });
}
function equipItem(item) {
send({ type: "equip", item });
}
function loadLeaderboard() {
send({ type: "leaderboard" });
}
function sendChat() {
const message = $("chatInput").value.trim();
if (!message) return;
send({ type: "chat", message });
$("chatInput").value = "";
}
function reportPlayer() {
send({
type: "report",
username: $("reportName").value,
reason: $("reportReason").value
});
}
function saveLocalSettings() {
localStorage.setItem("ra_sensitivity", $("sensitivity").value);
localStorage.setItem("ra_sound", $("sound").value);
notice("Settings saved.");
}
function logout() {
localStorage.removeItem("ra_token");
token = "";
location.reload();
}
function initGame() {
if (gameRunning) return;
gameRunning = true;
const canvas = $("gameCanvas");
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111a35);
scene.fog = new THREE.Fog(0x111a35, 30, 100);
camera = new THREE.PerspectiveCamera(
75, canvas.clientWidth / canvas.clientHeight, 0.1, 150
);
camera.position.set(0, 1.6, 5);
camera.rotation.order = "YXZ";
renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
const ambient = new THREE.HemisphereLight(0xb9d5ff, 0x26314e, 2);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(10, 20, 10);
scene.add(sun);
const floor = new THREE.Mesh(
new THREE.BoxGeometry(100, 1, 100),
new THREE.MeshStandardMaterial({
color: 0x273455,
roughness: 0.85
})
);
floor.position.y = -0.55;
scene.add(floor);
const grid = new THREE.GridHelper(100, 50, 0x6d70c7, 0x36466c);
grid.position.y = -0.03;
scene.add(grid);
const colors = [0x4b65a0, 0x7454b8, 0x287b86, 0x8c6944];
for (let i = 0; i < 24; i++) {
const width = 2 + Math.random() * 4;
const height = 2 + Math.random() * 7;
const depth = 2 + Math.random() * 4;
const obstacle = new THREE.Mesh(
new THREE.BoxGeometry(width, height, depth),
new THREE.MeshStandardMaterial({
color: colors[i % colors.length]
})
);
obstacle.position.set(
Math.random() * 70 - 35,
height / 2,
Math.random() * 70 - 35
);
scene.add(obstacle);
}
const ring = new THREE.Mesh(
new THREE.TorusGeometry(5, 0.15, 8, 48),
new THREE.MeshStandardMaterial({
color: 0x8e7aff,
emissive: 0x3520a0
})
);
ring.rotation.x = Math.PI / 2;
ring.position.y = 0.2;
scene.add(ring);
canvas.addEventListener("click", () => {
if (document.pointerLockElement !== canvas) {
canvas.requestPointerLock?.();
}
});
document.addEventListener("pointerlockchange", () => {
pointerLocked = document.pointerLockElement === canvas;
});
document.addEventListener("mousemove", mouseLook);
for (const player of Object.values(players)) {
updatePlayerObject(player);
}
animate();
startMovement();
}
function mouseLook(event) {
if (!gameRunning || !pointerLocked) return;
const sensitivity = Number(localStorage.getItem("ra_sensitivity") || 1);
yaw -= event.movementX * 0.002 * sensitivity;
pitch -= event.movementY * 0.002 * sensitivity;
pitch = Math.max(-1.2, Math.min(1.2, pitch));
camera.rotation.set(pitch, yaw, 0);
}
function updatePlayerObject(player) {
if (!scene || player.id === selfId) return;
let object = playerObjects[player.id];
if (!object) {
const geometry = new THREE.CapsuleGeometry(0.45, 0.9, 4, 8);
const material = new THREE.MeshStandardMaterial({
color: player.team === 0 ? 0x54caff : 0xffad5a,
emissive: player.team === 0 ? 0x103b55 : 0x552b10
});
object = new THREE.Mesh(geometry, material);
scene.add(object);
playerObjects[player.id] = object;
}
object.position.set(player.x, player.y - 0.5, player.z);
object.rotation.y = player.yaw;
object.visible = player.alive;
}
function animate() {
if (!gameRunning || !renderer || !scene || !camera) return;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
function startMovement() {
clearInterval(moveInterval);
moveInterval = setInterval(() => {
if (!gameRunning) return;
let forward = 0;
let sideways = 0;
if (keys.KeyW || mobile.forward) forward++;
if (keys.KeyS || mobile.back) forward--;
if (keys.KeyD || mobile.right) sideways++;
if (keys.KeyA || mobile.left) sideways--;
if (forward || sideways) {
send({
type: "move",
forward,
sideways,
sprint: !!keys.ShiftLeft,
yaw,
pitch
});
const speed = keys.ShiftLeft ? 0.14 : 0.09;
camera.position.x +=
(-Math.sin(yaw) * forward + Math.cos(yaw) * sideways) * speed;
camera.position.z +=
(-Math.cos(yaw) * forward - Math.sin(yaw) * sideways) * speed;
}
}, 50);
}
function shoot() {
if (!gameRunning) return;
send({ type: "shoot", yaw, pitch });
}
function reloadWeapon() {
send({ type: "reload" });
}
function switchWeapon(weapon) {
send({ type: "switchWeapon", weapon });
}
function jump() {
send({
type: "move",
forward: 0,
sideways: 0,
jump: true,
yaw,
pitch
});
if (camera) camera.position.y = 2.3;
setTimeout(() => {
if (camera && gameRunning) camera.position.y = 1.6;
}, 350);
}
function mobileMove(direction, active) {
mobile[direction] = active;
}
function destroyGame() {
gameRunning = false;
clearInterval(moveInterval);
document.removeEventListener("mousemove", mouseLook);
document.exitPointerLock?.();
if (renderer) {
renderer.dispose();
renderer = null;
}
scene = null;
camera = null;
playerObjects = {};
}
document.addEventListener("keydown", event => {
keys[event.code] = true;
if (event.code === "Space") {
event.preventDefault();
jump();
}
if (event.code === "KeyR") reloadWeapon();
if (event.code === "Digit1") switchWeapon("pulse");
if (event.code === "Digit2") switchWeapon("burst");
if (event.code === "Digit3") switchWeapon("nova");
});
document.addEventListener("keyup", event => {
keys[event.code] = false;
});
document.addEventListener("mousedown", event => {
if (event.button === 0 && pointerLocked) shoot();
});
window.addEventListener("resize", () => {
if (!camera || !renderer) return;
const canvas = $("gameCanvas");
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
});
$("chatInput").addEventListener("keydown", event => {
if (event.key === "Enter") sendChat();
});
connect();
</script>
</body>
</html>`;
const server = http.createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: "online",
game: "RIVALS ARENA",
rooms: rooms.size,
players: [...rooms.values()].reduce(
(sum, room) => sum + room.players.size, 0
)
}));
return;
}
if (req.url === "/") {
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store"
});
res.end(HTML);
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
});
const wss = new WebSocket.Server({
server,
maxPayload: 16 * 1024
});
wss.on("connection", (socket, request) => {
const session = {
username: null,
room: null
};
sockets.set(socket, session);
socket.on("message", raw => {
let data;
try {
data = JSON.parse(raw.toString());
} catch {
send(socket, "error", { message: "Invalid message." });
return;
}
try {
handleMessage(socket, data);
} catch (error) {
console.error("Message error:", error.message);
send(socket, "error", { message: "Could not process that action." });
}
const player = getPlayer(socket);
if (player) session.room = player.room;
});
socket.on("close", () => {
const player = getPlayer(socket);
if (player) leaveRoom(player);
sockets.delete(socket);
});
socket.on("error", () => {
const player = getPlayer(socket);
if (player) leaveRoom(player);
sockets.delete(socket);
});
});
setInterval(gameTick, 1000 / TICK_RATE);
server.listen(PORT, "0.0.0.0", () => {
console.log("======================================");
console.log(" RIVALS ARENA SERVER");
console.log("======================================");
console.log("Website: http://localhost:" + PORT);
console.log("Health: http://localhost:" + PORT + "/health");
console.log("WebSocket multiplayer: enabled");
console.log("======================================");
});
Game Source: RIVALS ARENA
Creator: PrismPanda88
Libraries: three
Complexity: complex (2135 lines, 53.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: rivals-arena-prismpanda88" to link back to the original. Then publish at arcadelab.ai/publish.