1444 lines
54 KiB
HTML
1444 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tank Warfare - Multiplayer Battle Arena</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
|
|
overflow: hidden;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#gameContainer {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
#gameCanvas {
|
|
display: block;
|
|
background: #0a0a15;
|
|
cursor: crosshair;
|
|
box-shadow: 0 0 50px rgba(0, 0, 0, 0.8);
|
|
}
|
|
#hud {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
right: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
}
|
|
.hud-panel {
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 15px 20px;
|
|
border-radius: 12px;
|
|
color: white;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
backdrop-filter: blur(15px);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.health-bar {
|
|
width: 200px;
|
|
height: 25px;
|
|
background: rgba(50, 50, 50, 0.8);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 2px solid #333;
|
|
margin-top: 8px;
|
|
position: relative;
|
|
}
|
|
.health-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #ff4444, #ff6b6b);
|
|
transition: width 0.3s;
|
|
box-shadow: 0 0 15px rgba(255, 68, 68, 0.8);
|
|
}
|
|
.health-text {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-weight: bold;
|
|
font-size: 12px;
|
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
|
|
}
|
|
.score-display {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
color: #ffd700;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
}
|
|
.kills-display {
|
|
font-size: 14px;
|
|
margin-top: 5px;
|
|
color: #ff6b6b;
|
|
}
|
|
#soundToggle {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 10px 20px;
|
|
border-radius: 10px;
|
|
color: white;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
backdrop-filter: blur(15px);
|
|
pointer-events: auto;
|
|
transition: all 0.2s;
|
|
z-index: 11;
|
|
}
|
|
#soundToggle:hover {
|
|
transform: translateX(-50%) scale(1.05);
|
|
border-color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
#fullscreenButton {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 320px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 10px 20px;
|
|
border-radius: 10px;
|
|
color: white;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
backdrop-filter: blur(15px);
|
|
pointer-events: auto;
|
|
transition: all 0.2s;
|
|
z-index: 11;
|
|
}
|
|
#fullscreenButton:hover {
|
|
transform: scale(1.05);
|
|
border-color: rgba(255, 255, 255, 0.4);
|
|
background: rgba(0, 0, 0, 0.95);
|
|
}
|
|
#fullscreenButton.hidden {
|
|
display: none;
|
|
}
|
|
#leaderboard {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 15px;
|
|
border-radius: 12px;
|
|
color: white;
|
|
min-width: 220px;
|
|
max-width: 280px;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
backdrop-filter: blur(15px);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
|
z-index: 10;
|
|
pointer-events: auto;
|
|
cursor: default;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
#leaderboard::-webkit-scrollbar { width: 8px; }
|
|
#leaderboard::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }
|
|
#leaderboard::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.3); border-radius: 4px; }
|
|
#leaderboard h3 {
|
|
margin-bottom: 10px;
|
|
color: #ffd700;
|
|
text-align: center;
|
|
font-size: 18px;
|
|
}
|
|
.leaderboard-entry {
|
|
padding: 8px 10px;
|
|
margin: 5px 0;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.leaderboard-entry:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
transform: translateX(2px);
|
|
}
|
|
.leaderboard-entry.current {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
border: 2px solid #ffd700;
|
|
}
|
|
.leaderboard-entry.bot { opacity: 0.7; }
|
|
#minimap {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 180px;
|
|
height: 180px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 12px;
|
|
backdrop-filter: blur(15px);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
|
z-index: 10;
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
}
|
|
#minimap:hover { border-color: rgba(255, 255, 255, 0.4); }
|
|
#connectionStatus {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 12px 18px;
|
|
border-radius: 10px;
|
|
color: white;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
backdrop-filter: blur(15px);
|
|
font-size: 14px;
|
|
z-index: 10;
|
|
}
|
|
.status-connected { color: #4CAF50; }
|
|
.status-disconnected { color: #ff6b6b; }
|
|
.status-connecting { color: #ffa500; }
|
|
.overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.95);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.overlay.hidden { display: none; }
|
|
.overlay h1 {
|
|
font-size: 64px;
|
|
color: #fff;
|
|
margin-bottom: 10px;
|
|
text-shadow: 0 0 30px rgba(126, 34, 206, 0.8);
|
|
animation: glow 2s ease-in-out infinite;
|
|
}
|
|
@keyframes glow {
|
|
0%, 100% { text-shadow: 0 0 20px rgba(126, 34, 206, 0.8); }
|
|
50% { text-shadow: 0 0 40px rgba(126, 34, 206, 1), 0 0 60px rgba(126, 34, 206, 0.8); }
|
|
}
|
|
.subtitle {
|
|
font-size: 24px;
|
|
color: #4CAF50;
|
|
margin-bottom: 30px;
|
|
font-weight: bold;
|
|
}
|
|
input {
|
|
padding: 15px 30px;
|
|
font-size: 20px;
|
|
border: 2px solid rgba(126, 34, 206, 0.8);
|
|
border-radius: 12px;
|
|
margin-bottom: 20px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
text-align: center;
|
|
width: 320px;
|
|
transition: all 0.3s;
|
|
}
|
|
input:focus {
|
|
outline: none;
|
|
border-color: rgba(126, 34, 206, 1);
|
|
box-shadow: 0 0 20px rgba(126, 34, 206, 0.5);
|
|
}
|
|
button {
|
|
padding: 15px 50px;
|
|
font-size: 20px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 5px 20px rgba(126, 34, 206, 0.5);
|
|
}
|
|
button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 30px rgba(126, 34, 206, 0.7);
|
|
}
|
|
button:disabled {
|
|
background: #666;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
.controls-info {
|
|
margin-top: 30px;
|
|
color: #aaa;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
}
|
|
.controls-info div { margin: 5px 0; }
|
|
#pauseMenu { background: rgba(0, 0, 0, 0.98); }
|
|
#deathScreen {
|
|
background: linear-gradient(135deg, rgba(20, 0, 40, 0.95), rgba(80, 0, 0, 0.95));
|
|
backdrop-filter: blur(8px);
|
|
animation: fadeInDeath 0.5s ease-out;
|
|
}
|
|
@keyframes fadeInDeath {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
#deathScreen h2 {
|
|
font-size: 72px;
|
|
color: #ff6b6b;
|
|
margin-bottom: 20px;
|
|
text-shadow: 0 0 40px rgba(255, 107, 107, 1), 0 0 80px rgba(255, 0, 0, 0.8);
|
|
animation: deathPulse 2s ease-in-out infinite;
|
|
font-weight: 900;
|
|
letter-spacing: 8px;
|
|
}
|
|
@keyframes deathPulse {
|
|
0%, 100% { transform: scale(1); text-shadow: 0 0 40px rgba(255, 107, 107, 1); }
|
|
50% { transform: scale(1.05); text-shadow: 0 0 60px rgba(255, 107, 107, 1), 0 0 100px rgba(255, 0, 0, 1); }
|
|
}
|
|
#killerFocus {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 32px;
|
|
color: #ff6b6b;
|
|
text-shadow: 0 0 20px rgba(255, 107, 107, 1);
|
|
z-index: 999;
|
|
pointer-events: none;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
|
|
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
|
|
}
|
|
#killerInfo {
|
|
font-size: 24px;
|
|
color: #ffd700;
|
|
margin-bottom: 30px;
|
|
padding: 15px 30px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 15px;
|
|
border: 2px solid rgba(255, 215, 0, 0.5);
|
|
text-shadow: 0 0 10px rgba(255, 215, 0, 0.8);
|
|
font-weight: bold;
|
|
}
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 25px;
|
|
margin: 40px 0;
|
|
max-width: 600px;
|
|
}
|
|
.stat-box {
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
|
padding: 25px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
.stat-box:hover {
|
|
transform: translateY(-5px);
|
|
border-color: rgba(255, 215, 0, 0.5);
|
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
|
|
}
|
|
.stat-value {
|
|
font-size: 48px;
|
|
font-weight: 900;
|
|
color: #ffd700;
|
|
margin: 15px 0;
|
|
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
|
|
}
|
|
.stat-label {
|
|
font-size: 16px;
|
|
color: #ccc;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
font-weight: 600;
|
|
}
|
|
.menu-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
margin-top: 30px;
|
|
}
|
|
.menu-buttons button { width: 300px; }
|
|
#deathScreen h2 {
|
|
font-size: 56px;
|
|
color: #ff6b6b;
|
|
margin-bottom: 20px;
|
|
text-shadow: 0 0 30px rgba(255, 107, 107, 0.8);
|
|
}
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 20px;
|
|
margin: 30px 0;
|
|
}
|
|
.stat-box {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
.stat-value {
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #ffd700;
|
|
margin: 10px 0;
|
|
}
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #aaa;
|
|
text-transform: uppercase;
|
|
}
|
|
#killFeed {
|
|
position: fixed;
|
|
top: 100px;
|
|
right: 20px;
|
|
max-width: 300px;
|
|
z-index: 15;
|
|
pointer-events: none;
|
|
}
|
|
.kill-message {
|
|
background: rgba(0, 0, 0, 0.9);
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
border-left: 4px solid #ff6b6b;
|
|
animation: slideIn 0.3s ease-out, fadeOut 0.5s ease-in 4.5s;
|
|
font-size: 14px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
|
}
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes fadeOut {
|
|
from { opacity: 1; }
|
|
to { opacity: 0; }
|
|
}
|
|
@media (max-width: 768px) {
|
|
.overlay h1 { font-size: 48px; }
|
|
input { width: 280px; font-size: 18px; }
|
|
#leaderboard { max-width: 200px; font-size: 12px; }
|
|
#minimap { width: 120px; height: 120px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="gameContainer">
|
|
<canvas id="gameCanvas"></canvas>
|
|
|
|
<div id="hud">
|
|
<div class="hud-panel">
|
|
<div id="playerName" style="font-weight: bold; font-size: 16px;">Spieler</div>
|
|
<div style="font-size: 12px; color: #4CAF50; margin-top: 3px;">🎮 Online</div>
|
|
<div class="health-bar">
|
|
<div class="health-fill" id="healthFill"></div>
|
|
<div class="health-text" id="healthText">100</div>
|
|
</div>
|
|
<div class="kills-display">Kills: <span id="killCount">0</span> | Deaths: <span id="deathCount">0</span></div>
|
|
</div>
|
|
<button id="soundToggle">🔊 Sound: AN</button>
|
|
<button id="fullscreenButton" class="hidden">🖥️ Fullscreen</button>
|
|
<div class="hud-panel score-display">
|
|
<div style="font-size: 14px; color: #aaa;">Score</div>
|
|
<div id="scoreDisplay">0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="leaderboard">
|
|
<h3>🏆 Rangliste</h3>
|
|
<div id="leaderboardList"></div>
|
|
</div>
|
|
|
|
<div id="killFeed"></div>
|
|
|
|
<canvas id="minimap"></canvas>
|
|
|
|
<div id="connectionStatus">
|
|
<span id="statusText" class="status-connecting">Verbinde...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="startScreen" class="overlay">
|
|
<h1>⚔️ TANK WARFARE ⚔️</h1>
|
|
<div class="subtitle">🌐 MULTIPLAYER ARENA</div>
|
|
<input type="text" id="playerNameInput" placeholder="Dein Name" maxlength="15">
|
|
<button id="startButton" disabled>Verbinde mit Server...</button>
|
|
<div class="controls-info">
|
|
<div><strong>🎮 Steuerung</strong></div>
|
|
<div>Maus bewegen → Tank folgt</div>
|
|
<div>Linke Maustaste → Schießen</div>
|
|
<div>ESC → Pause</div>
|
|
<div style="margin-top: 15px; color: #666; font-size: 12px;">
|
|
🗺️ 3000x3000 Riesen-Map | 🔊 Mit Sound | 🤖 10 Adaptive Bots | 💀 PvP
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="pauseMenu" class="overlay hidden">
|
|
<h1>⏸️ PAUSE</h1>
|
|
<div class="menu-buttons">
|
|
<button id="resumeButton">Weiterspielen</button>
|
|
<button id="quitButton">Verlassen</button>
|
|
</div>
|
|
<div class="controls-info" style="margin-top: 30px;">
|
|
<div>ESC - Weiterspielen</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="deathScreen" class="overlay hidden">
|
|
<h2 id="deathTitle">💀 ELIMINIERT 💀</h2>
|
|
<div id="killerInfo" style="font-size: 18px; color: #ff6b6b; margin-bottom: 20px;"></div>
|
|
<div class="stats-grid">
|
|
<div class="stat-box">
|
|
<div class="stat-label">Score</div>
|
|
<div class="stat-value" id="finalScore">0</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-label">Kills</div>
|
|
<div class="stat-value" id="finalKills">0</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-label">Deaths</div>
|
|
<div class="stat-value" id="finalDeaths">0</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-label">Zeit</div>
|
|
<div class="stat-value" id="survivalTime">0s</div>
|
|
</div>
|
|
</div>
|
|
<button id="respawnButton">Respawn (5s)</button>
|
|
</div>
|
|
|
|
<script src="/socket.io/socket.io.js"></script>
|
|
<script>
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const minimapCanvas = document.getElementById('minimap');
|
|
const minimapCtx = minimapCanvas.getContext('2d');
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
minimapCanvas.width = 180;
|
|
minimapCanvas.height = 180;
|
|
}
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
let WORLD_WIDTH = 3000;
|
|
let WORLD_HEIGHT = 3000;
|
|
const PLAYER_SIZE = 30;
|
|
const BULLET_SIZE = 5;
|
|
|
|
const socket = io();
|
|
let soundEnabled = true;
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
class SoundSystem {
|
|
static playNote(freq, duration, gainVal = 0.1, type = 'sine') {
|
|
if (!soundEnabled) return;
|
|
const osc = audioContext.createOscillator();
|
|
const gain = audioContext.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(audioContext.destination);
|
|
osc.frequency.value = freq;
|
|
osc.type = type;
|
|
gain.gain.setValueAtTime(gainVal, audioContext.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
|
|
osc.start(audioContext.currentTime);
|
|
osc.stop(audioContext.currentTime + duration);
|
|
}
|
|
|
|
static playShoot() {
|
|
// Quick pew sound
|
|
this.playNote(300, 0.06, 0.06, 'square');
|
|
setTimeout(() => this.playNote(250, 0.04, 0.04, 'square'), 30);
|
|
}
|
|
|
|
static playHit() {
|
|
// Soft thud sound
|
|
this.playNote(100, 0.12, 0.04, 'sine');
|
|
}
|
|
|
|
static playExplosion() {
|
|
if (!soundEnabled) return;
|
|
// Epic explosion with rumble
|
|
const osc = audioContext.createOscillator();
|
|
const gain = audioContext.createGain();
|
|
const filter = audioContext.createBiquadFilter();
|
|
osc.connect(filter);
|
|
filter.connect(gain);
|
|
gain.connect(audioContext.destination);
|
|
osc.frequency.value = 200;
|
|
osc.type = 'sawtooth';
|
|
filter.type = 'lowpass';
|
|
filter.frequency.value = 1000;
|
|
gain.gain.setValueAtTime(0.25, audioContext.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.6);
|
|
osc.frequency.exponentialRampToValueAtTime(20, audioContext.currentTime + 0.6);
|
|
filter.frequency.exponentialRampToValueAtTime(200, audioContext.currentTime + 0.6);
|
|
osc.start(audioContext.currentTime);
|
|
osc.stop(audioContext.currentTime + 0.6);
|
|
}
|
|
|
|
static playPowerUp(type) {
|
|
if (!soundEnabled) return;
|
|
// Different melodies for different power-ups
|
|
const melodies = {
|
|
health: [440, 550, 660], // A, C#, E (major chord going up)
|
|
shield: [523, 659, 784], // C, E, G (protective sound)
|
|
speed: [330, 440, 550, 660], // Fast ascending
|
|
rapid_fire: [660, 660, 880, 880], // Rapid double notes
|
|
damage_boost: [220, 330, 440, 660], // Deep to high power
|
|
regen: [392, 494, 587], // G, B, D (healing melody)
|
|
invisible: [880, 440, 880, 440] // Mysterious alternating
|
|
};
|
|
|
|
const melody = melodies[type] || melodies.health;
|
|
melody.forEach((freq, i) => {
|
|
setTimeout(() => this.playNote(freq, 0.15, 0.08, 'sine'), i * 80);
|
|
});
|
|
}
|
|
|
|
static playKill() {
|
|
if (!soundEnabled) return;
|
|
// Victory fanfare
|
|
const notes = [523, 659, 784, 1047]; // C, E, G, C (octave up)
|
|
notes.forEach((freq, i) => {
|
|
setTimeout(() => this.playNote(freq, 0.2, 0.1, 'sine'), i * 80);
|
|
});
|
|
}
|
|
|
|
static playRespawn() {
|
|
if (!soundEnabled) return;
|
|
// Spawn-in sound
|
|
const notes = [220, 330, 440];
|
|
notes.forEach((freq, i) => {
|
|
setTimeout(() => this.playNote(freq, 0.12, 0.08, 'triangle'), i * 50);
|
|
});
|
|
}
|
|
}
|
|
|
|
let myPlayerId = null;
|
|
let gameState = { players: [], bots: [], bullets: [], powerups: [] };
|
|
let camera = { x: 0, y: 0, shake: 0 };
|
|
let mouse = { x: canvas.width / 2, y: canvas.height / 2, down: false };
|
|
let gameRunning = false;
|
|
let gamePaused = false;
|
|
let gameStartTime = 0;
|
|
let respawnTimer = 0;
|
|
let respawnInterval = null;
|
|
let particles = [];
|
|
let damageNumbers = [];
|
|
let killerFocusActive = false;
|
|
let killerFocusTarget = null;
|
|
let killerFocusTime = 0;
|
|
|
|
const zones = [
|
|
{ x: 0, y: 0, w: 1500, h: 1500, color: 'rgba(30, 60, 114, 0.3)' },
|
|
{ x: 1500, y: 0, w: 1500, h: 1500, color: 'rgba(126, 34, 206, 0.3)' },
|
|
{ x: 0, y: 1500, w: 1500, h: 1500, color: 'rgba(67, 160, 71, 0.3)' },
|
|
{ x: 1500, y: 1500, w: 1500, h: 1500, color: 'rgba(251, 140, 0, 0.3)' }
|
|
];
|
|
|
|
const decorations = [];
|
|
for (let i = 0; i < 40; i++) { // 20 -> 40 for larger map
|
|
decorations.push({
|
|
x: Math.random() * WORLD_WIDTH,
|
|
y: Math.random() * WORLD_HEIGHT,
|
|
radius: 20 + Math.random() * 50,
|
|
color: `rgba(${100 + Math.random() * 100}, ${100 + Math.random() * 100}, ${150 + Math.random() * 100}, 0.1)`
|
|
});
|
|
}
|
|
|
|
const stars = [];
|
|
for (let i = 0; i < 300; i++) { // 150 -> 300 for larger map
|
|
stars.push({
|
|
x: Math.random() * WORLD_WIDTH,
|
|
y: Math.random() * WORLD_HEIGHT,
|
|
size: Math.random() * 2.5,
|
|
brightness: Math.random()
|
|
});
|
|
}
|
|
|
|
class Particle {
|
|
constructor(x, y, color, size, vx, vy, life) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.color = color;
|
|
this.size = size;
|
|
this.vx = vx;
|
|
this.vy = vy;
|
|
this.life = life;
|
|
this.maxLife = life;
|
|
}
|
|
update() {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
this.vy += 0.2;
|
|
this.life--;
|
|
return this.life > 0;
|
|
}
|
|
draw(ctx, offsetX, offsetY) {
|
|
const alpha = this.life / this.maxLife;
|
|
ctx.globalAlpha = alpha;
|
|
ctx.fillStyle = this.color;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x - offsetX, this.y - offsetY, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
}
|
|
|
|
class DamageNumber {
|
|
constructor(x, y, damage) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.damage = damage;
|
|
this.life = 60;
|
|
this.vy = -2;
|
|
}
|
|
update() {
|
|
this.y += this.vy;
|
|
this.life--;
|
|
return this.life > 0;
|
|
}
|
|
draw(ctx, offsetX, offsetY) {
|
|
const alpha = this.life / 60;
|
|
ctx.globalAlpha = alpha;
|
|
ctx.fillStyle = '#ff0000';
|
|
ctx.strokeStyle = '#000';
|
|
ctx.lineWidth = 3;
|
|
ctx.font = 'bold 20px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.strokeText('-' + this.damage, this.x - offsetX, this.y - offsetY);
|
|
ctx.fillText('-' + this.damage, this.x - offsetX, this.y - offsetY);
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
}
|
|
|
|
function createExplosion(x, y, color) {
|
|
for (let i = 0; i < 20; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = 2 + Math.random() * 4;
|
|
particles.push(new Particle(
|
|
x, y, color, 3 + Math.random() * 3,
|
|
Math.cos(angle) * speed,
|
|
Math.sin(angle) * speed,
|
|
30 + Math.random() * 20
|
|
));
|
|
}
|
|
camera.shake = 10;
|
|
}
|
|
|
|
function createHitEffect(x, y) {
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = 1 + Math.random() * 2;
|
|
particles.push(new Particle(
|
|
x, y, '#ff0000', 2 + Math.random() * 2,
|
|
Math.cos(angle) * speed,
|
|
Math.sin(angle) * speed,
|
|
15 + Math.random() * 10
|
|
));
|
|
}
|
|
}
|
|
|
|
function addKillFeed(killer, victim, killerColor, victimColor) {
|
|
const feed = document.getElementById('killFeed');
|
|
const msg = document.createElement('div');
|
|
msg.className = 'kill-message';
|
|
msg.innerHTML = `<span style="color: ${killerColor}">${killer}</span> 💀 <span style="color: ${victimColor}">${victim}</span>`;
|
|
feed.appendChild(msg);
|
|
setTimeout(() => msg.remove(), 5000);
|
|
}
|
|
|
|
socket.on('connect', () => {
|
|
console.log('Connected!');
|
|
document.getElementById('statusText').textContent = 'Verbunden';
|
|
document.getElementById('statusText').className = 'status-connected';
|
|
document.getElementById('startButton').disabled = false;
|
|
document.getElementById('startButton').textContent = 'SPIEL STARTEN';
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
document.getElementById('statusText').textContent = 'Verbindung verloren';
|
|
document.getElementById('statusText').className = 'status-disconnected';
|
|
});
|
|
|
|
socket.on('initGame', (data) => {
|
|
myPlayerId = data.playerId;
|
|
WORLD_WIDTH = data.worldSize.width;
|
|
WORLD_HEIGHT = data.worldSize.height;
|
|
gameState.players = data.players;
|
|
gameState.bots = data.bots;
|
|
gameState.bullets = data.bullets;
|
|
gameState.powerups = data.powerups;
|
|
zones[0] = { x: 0, y: 0, w: WORLD_WIDTH/2, h: WORLD_HEIGHT/2, color: 'rgba(30, 60, 114, 0.3)' };
|
|
zones[1] = { x: WORLD_WIDTH/2, y: 0, w: WORLD_WIDTH/2, h: WORLD_HEIGHT/2, color: 'rgba(126, 34, 206, 0.3)' };
|
|
zones[2] = { x: 0, y: WORLD_HEIGHT/2, w: WORLD_WIDTH/2, h: WORLD_HEIGHT/2, color: 'rgba(67, 160, 71, 0.3)' };
|
|
zones[3] = { x: WORLD_WIDTH/2, y: WORLD_HEIGHT/2, w: WORLD_WIDTH/2, h: WORLD_HEIGHT/2, color: 'rgba(251, 140, 0, 0.3)' };
|
|
});
|
|
|
|
socket.on('gameState', (state) => {
|
|
const oldPlayers = [...gameState.players, ...gameState.bots];
|
|
gameState = state;
|
|
const newPlayers = [...state.players, ...state.bots];
|
|
|
|
// Check if MY player was hit
|
|
const myPlayer = newPlayers.find(p => p.id === myPlayerId);
|
|
const oldMyPlayer = oldPlayers.find(p => p.id === myPlayerId);
|
|
|
|
if (myPlayer && oldMyPlayer && oldMyPlayer.health > myPlayer.health) {
|
|
// I got hit!
|
|
createHitEffect(myPlayer.x, myPlayer.y);
|
|
damageNumbers.push(new DamageNumber(myPlayer.x, myPlayer.y - 20, oldMyPlayer.health - myPlayer.health));
|
|
SoundSystem.playHit();
|
|
}
|
|
|
|
// Check for visual effects on other players (but no sound)
|
|
oldPlayers.forEach(oldP => {
|
|
if (oldP.id === myPlayerId) return; // Skip my player, already handled
|
|
const newP = newPlayers.find(p => p.id === oldP.id);
|
|
if (newP && oldP.health > newP.health) {
|
|
createHitEffect(newP.x, newP.y);
|
|
damageNumbers.push(new DamageNumber(newP.x, newP.y - 20, oldP.health - newP.health));
|
|
}
|
|
});
|
|
});
|
|
|
|
socket.on('playerDied', (data) => {
|
|
const player = getMyPlayer();
|
|
if (player) {
|
|
createExplosion(player.x, player.y, player.color);
|
|
}
|
|
SoundSystem.playExplosion();
|
|
gamePaused = true;
|
|
showDeathScreen(data);
|
|
});
|
|
|
|
socket.on('killFeed', (data) => {
|
|
addKillFeed(data.killer, data.victim, data.killerColor, data.victimColor);
|
|
if (data.killer === getMyPlayer()?.name) {
|
|
SoundSystem.playKill();
|
|
}
|
|
});
|
|
|
|
socket.on('powerUpCollected', (data) => {
|
|
const type = data.type || 'health';
|
|
SoundSystem.playPowerUp(type);
|
|
});
|
|
|
|
function getMyPlayer() {
|
|
return gameState.players.find(p => p.id === myPlayerId);
|
|
}
|
|
|
|
function updateCamera() {
|
|
let targetPlayer = getMyPlayer();
|
|
|
|
// Focus on killer - update killer position from gameState continuously!
|
|
if (killerFocusActive && killerFocusTarget) {
|
|
// Get fresh killer data from current gameState
|
|
const freshKiller = [...gameState.players, ...gameState.bots].find(p => p.id === killerFocusTarget.id);
|
|
if (freshKiller && !freshKiller.isDead) {
|
|
targetPlayer = freshKiller;
|
|
} else {
|
|
// Killer died or disconnected, stop following
|
|
killerFocusActive = false;
|
|
killerFocusTarget = null;
|
|
targetPlayer = getMyPlayer();
|
|
}
|
|
}
|
|
|
|
if (!targetPlayer) return;
|
|
|
|
let targetX = targetPlayer.x - canvas.width / 2;
|
|
let targetY = targetPlayer.y - canvas.height / 2;
|
|
|
|
if (camera.shake > 0) {
|
|
targetX += (Math.random() - 0.5) * camera.shake;
|
|
targetY += (Math.random() - 0.5) * camera.shake;
|
|
camera.shake *= 0.9;
|
|
if (camera.shake < 0.5) camera.shake = 0;
|
|
}
|
|
|
|
// If focusing on killer, snap to center immediately
|
|
if (killerFocusActive) {
|
|
camera.x = targetX;
|
|
camera.y = targetY;
|
|
} else {
|
|
// Smooth movement for player
|
|
const smoothness = 0.2;
|
|
camera.x += (targetX - camera.x) * smoothness;
|
|
camera.y += (targetY - camera.y) * smoothness;
|
|
}
|
|
|
|
camera.x = Math.max(0, Math.min(WORLD_WIDTH - canvas.width, camera.x));
|
|
camera.y = Math.max(0, Math.min(WORLD_HEIGHT - canvas.height, camera.y));
|
|
}
|
|
|
|
function sendPlayerUpdate() {
|
|
const player = getMyPlayer();
|
|
if (!player || player.isDead) return;
|
|
const worldMouseX = mouse.x + camera.x;
|
|
const worldMouseY = mouse.y + camera.y;
|
|
const angle = Math.atan2(worldMouseY - player.y, worldMouseX - player.x);
|
|
socket.emit('updatePlayer', { targetX: worldMouseX, targetY: worldMouseY, angle });
|
|
}
|
|
|
|
function checkPowerUpCollection() {
|
|
const player = getMyPlayer();
|
|
if (!player || player.isDead) return;
|
|
gameState.powerups.forEach((powerup, index) => {
|
|
const dist = Math.hypot(player.x - powerup.x, player.y - powerup.y);
|
|
if (dist < PLAYER_SIZE + 20) {
|
|
socket.emit('collectPowerUp', index);
|
|
}
|
|
});
|
|
}
|
|
|
|
function drawBackground(ctx, offsetX, offsetY) {
|
|
const gradient = ctx.createRadialGradient(
|
|
WORLD_WIDTH/2 - offsetX, WORLD_HEIGHT/2 - offsetY, 0,
|
|
WORLD_WIDTH/2 - offsetX, WORLD_HEIGHT/2 - offsetY, Math.max(WORLD_WIDTH, WORLD_HEIGHT)
|
|
);
|
|
gradient.addColorStop(0, '#1a1a3e');
|
|
gradient.addColorStop(1, '#0a0a15');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
zones.forEach(zone => {
|
|
if (zone.x - offsetX < canvas.width && zone.x - offsetX + zone.w > 0 &&
|
|
zone.y - offsetY < canvas.height && zone.y - offsetY + zone.h > 0) {
|
|
ctx.fillStyle = zone.color;
|
|
ctx.fillRect(zone.x - offsetX, zone.y - offsetY, zone.w, zone.h);
|
|
ctx.strokeStyle = zone.color.replace('0.3', '0.6');
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(zone.x - offsetX, zone.y - offsetY, zone.w, zone.h);
|
|
}
|
|
});
|
|
|
|
decorations.forEach(deco => {
|
|
if (deco.x - offsetX > -100 && deco.x - offsetX < canvas.width + 100 &&
|
|
deco.y - offsetY > -100 && deco.y - offsetY < canvas.height + 100) {
|
|
ctx.beginPath();
|
|
ctx.arc(deco.x - offsetX, deco.y - offsetY, deco.radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = deco.color;
|
|
ctx.fill();
|
|
}
|
|
});
|
|
|
|
ctx.strokeStyle = 'rgba(100, 150, 200, 0.15)';
|
|
ctx.lineWidth = 1;
|
|
const gridSize = 50;
|
|
const startX = Math.floor(offsetX / gridSize) * gridSize;
|
|
const startY = Math.floor(offsetY / gridSize) * gridSize;
|
|
|
|
for (let x = startX; x < offsetX + canvas.width; x += gridSize) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - offsetX, 0);
|
|
ctx.lineTo(x - offsetX, canvas.height);
|
|
ctx.stroke();
|
|
}
|
|
|
|
for (let y = startY; y < offsetY + canvas.height; y += gridSize) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y - offsetY);
|
|
ctx.lineTo(canvas.width, y - offsetY);
|
|
ctx.stroke();
|
|
}
|
|
|
|
stars.forEach(star => {
|
|
if (star.x - offsetX > 0 && star.x - offsetX < canvas.width &&
|
|
star.y - offsetY > 0 && star.y - offsetY < canvas.height) {
|
|
const twinkle = Math.sin(Date.now() * 0.001 + star.brightness * 10) * 0.3 + 0.7;
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${twinkle * 0.6})`;
|
|
ctx.beginPath();
|
|
ctx.arc(star.x - offsetX, star.y - offsetY, star.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
});
|
|
|
|
ctx.strokeStyle = '#667eea';
|
|
ctx.lineWidth = 8;
|
|
ctx.strokeRect(-offsetX, -offsetY, WORLD_WIDTH, WORLD_HEIGHT);
|
|
ctx.strokeStyle = '#764ba2';
|
|
ctx.lineWidth = 4;
|
|
ctx.strokeRect(-offsetX + 4, -offsetY + 4, WORLD_WIDTH - 8, WORLD_HEIGHT - 8);
|
|
}
|
|
|
|
function drawPlayer(player, offsetX, offsetY) {
|
|
if (player.isDead) return;
|
|
|
|
const x = player.x - offsetX;
|
|
const y = player.y - offsetY;
|
|
|
|
// Shield effect
|
|
if (player.shield) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, PLAYER_SIZE + 10, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.5)';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Rapid fire effect
|
|
if (player.rapidFire) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, PLAYER_SIZE + 15, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(255, 136, 0, 0.6)';
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
|
|
// Damage boost effect
|
|
if (player.damageBoost) {
|
|
const pulse = Math.sin(Date.now() / 100) * 5;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, PLAYER_SIZE + 12 + pulse, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(255, 0, 0, 0.7)';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Invisible effect (semi-transparent)
|
|
if (player.invisible) {
|
|
ctx.globalAlpha = 0.3;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(player.angle);
|
|
ctx.fillStyle = player.color;
|
|
ctx.fillRect(-PLAYER_SIZE/2, -PLAYER_SIZE/2, PLAYER_SIZE, PLAYER_SIZE);
|
|
ctx.strokeStyle = '#fff';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(-PLAYER_SIZE/2, -PLAYER_SIZE/2, PLAYER_SIZE, PLAYER_SIZE);
|
|
ctx.fillStyle = player.color;
|
|
ctx.fillRect(0, -5, PLAYER_SIZE, 10);
|
|
ctx.strokeStyle = '#fff';
|
|
ctx.strokeRect(0, -5, PLAYER_SIZE, 10);
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
ctx.globalAlpha = 1; // Reset alpha
|
|
|
|
const barWidth = PLAYER_SIZE * 1.5;
|
|
const barHeight = 5;
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
ctx.fillRect(x - barWidth/2, y - PLAYER_SIZE - 10, barWidth, barHeight);
|
|
const healthPercent = player.health / player.maxHealth;
|
|
ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000';
|
|
ctx.fillRect(x - barWidth/2, y - PLAYER_SIZE - 10, barWidth * healthPercent, barHeight);
|
|
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = '12px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(player.name + (player.isBot ? ' 🤖' : ''), x, y - PLAYER_SIZE - 15);
|
|
}
|
|
|
|
function drawBullet(bullet, offsetX, offsetY) {
|
|
const x = bullet.x - offsetX;
|
|
const y = bullet.y - offsetY;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, y);
|
|
ctx.lineTo(x - bullet.vx * 2, y - bullet.vy * 2);
|
|
ctx.strokeStyle = '#ffff00';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, BULLET_SIZE, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#ffff00';
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#ff0000';
|
|
ctx.stroke();
|
|
}
|
|
|
|
function drawPowerUp(powerup, offsetX, offsetY) {
|
|
const x = powerup.x - offsetX;
|
|
const y = powerup.y - offsetY;
|
|
const size = 20;
|
|
const angle = Date.now() * 0.001;
|
|
const pulseSize = Math.sin(Date.now() / 200) * 5;
|
|
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(angle);
|
|
|
|
const colors = {
|
|
health: '#00ff00',
|
|
shield: '#00ffff',
|
|
speed: '#ffff00',
|
|
rapid_fire: '#ff8800',
|
|
damage_boost: '#ff0000',
|
|
regen: '#00ff88',
|
|
invisible: '#8800ff'
|
|
};
|
|
|
|
const icons = {
|
|
health: '❤️',
|
|
shield: '🛡️',
|
|
speed: '⚡',
|
|
rapid_fire: '🔫',
|
|
damage_boost: '💥',
|
|
regen: '💚',
|
|
invisible: '👻'
|
|
};
|
|
|
|
const color = colors[powerup.type] || '#ffffff';
|
|
const icon = icons[powerup.type] || '?';
|
|
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 20;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, size + pulseSize, 0, Math.PI * 2);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, size, 0, Math.PI * 2);
|
|
ctx.fillStyle = color;
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.font = '20px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(icon, 0, 0);
|
|
ctx.restore();
|
|
}
|
|
|
|
function updateHUD() {
|
|
const player = getMyPlayer();
|
|
if (!player) return;
|
|
const healthPercent = (player.health / player.maxHealth * 100).toFixed(0);
|
|
document.getElementById('healthFill').style.width = healthPercent + '%';
|
|
document.getElementById('healthText').textContent = healthPercent;
|
|
document.getElementById('scoreDisplay').textContent = player.score;
|
|
document.getElementById('killCount').textContent = player.kills;
|
|
document.getElementById('deathCount').textContent = player.deaths;
|
|
}
|
|
|
|
function updateLeaderboard() {
|
|
const allPlayers = [...gameState.players, ...gameState.bots].filter(p => !p.isDead).sort((a, b) => b.score - a.score);
|
|
const list = document.getElementById('leaderboardList');
|
|
list.innerHTML = allPlayers.slice(0, 10).map((p, i) => `
|
|
<div class="leaderboard-entry ${p.id === myPlayerId ? 'current' : ''} ${p.isBot ? 'bot' : ''}">
|
|
<span>${i + 1}. ${p.name}${p.isBot ? ' 🤖' : ''}</span>
|
|
<span>${p.score}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function drawMinimap() {
|
|
const scale = 180 / WORLD_WIDTH;
|
|
minimapCtx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
|
minimapCtx.fillRect(0, 0, 180, 180);
|
|
|
|
zones.forEach(zone => {
|
|
minimapCtx.fillStyle = zone.color;
|
|
minimapCtx.fillRect(zone.x * scale, zone.y * scale, zone.w * scale, zone.h * scale);
|
|
});
|
|
|
|
minimapCtx.strokeStyle = '#fff';
|
|
minimapCtx.lineWidth = 2;
|
|
minimapCtx.strokeRect(0, 0, 180, 180);
|
|
|
|
const player = getMyPlayer();
|
|
if (player && !player.isDead) {
|
|
minimapCtx.fillStyle = '#00ff00';
|
|
minimapCtx.shadowColor = '#00ff00';
|
|
minimapCtx.shadowBlur = 5;
|
|
minimapCtx.fillRect(player.x * scale - 2, player.y * scale - 2, 4, 4);
|
|
minimapCtx.shadowBlur = 0;
|
|
}
|
|
|
|
[...gameState.players, ...gameState.bots].forEach(p => {
|
|
if (p.id !== myPlayerId && !p.isDead) {
|
|
minimapCtx.fillStyle = p.isBot ? '#666' : p.color;
|
|
minimapCtx.fillRect(p.x * scale - 1, p.y * scale - 1, 2, 2);
|
|
}
|
|
});
|
|
|
|
gameState.powerups.forEach(powerup => {
|
|
const colors = {
|
|
health: '#00ff00',
|
|
shield: '#00ffff',
|
|
speed: '#ffff00',
|
|
rapid_fire: '#ff8800',
|
|
damage_boost: '#ff0000',
|
|
regen: '#00ff88',
|
|
invisible: '#8800ff'
|
|
};
|
|
const color = colors[powerup.type] || '#ffffff';
|
|
minimapCtx.fillStyle = color;
|
|
minimapCtx.fillRect(powerup.x * scale - 1, powerup.y * scale - 1, 2, 2);
|
|
});
|
|
}
|
|
|
|
function gameLoop() {
|
|
if (!gameRunning) return;
|
|
|
|
// Continue updating camera even when paused to show killer
|
|
updateCamera();
|
|
|
|
if (!gamePaused || killerFocusActive) {
|
|
sendPlayerUpdate();
|
|
checkPowerUpCollection();
|
|
}
|
|
|
|
particles = particles.filter(p => p.update());
|
|
damageNumbers = damageNumbers.filter(d => d.update());
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
drawBackground(ctx, camera.x, camera.y);
|
|
|
|
particles.forEach(p => p.draw(ctx, camera.x, camera.y));
|
|
gameState.powerups.forEach(p => drawPowerUp(p, camera.x, camera.y));
|
|
[...gameState.players, ...gameState.bots].forEach(p => drawPlayer(p, camera.x, camera.y));
|
|
gameState.bullets.forEach(b => drawBullet(b, camera.x, camera.y));
|
|
damageNumbers.forEach(d => d.draw(ctx, camera.x, camera.y));
|
|
|
|
// Draw crosshair only if not paused
|
|
if (!gamePaused) {
|
|
ctx.strokeStyle = '#fff';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(mouse.x - 10, mouse.y);
|
|
ctx.lineTo(mouse.x + 10, mouse.y);
|
|
ctx.moveTo(mouse.x, mouse.y - 10);
|
|
ctx.lineTo(mouse.x, mouse.y + 10);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Highlight killer if focus is active
|
|
if (killerFocusActive && killerFocusTarget) {
|
|
// Get fresh killer position from gameState
|
|
const freshKiller = [...gameState.players, ...gameState.bots].find(p => p.id === killerFocusTarget.id);
|
|
if (freshKiller && !freshKiller.isDead) {
|
|
const x = freshKiller.x - camera.x;
|
|
const y = freshKiller.y - camera.y;
|
|
ctx.strokeStyle = '#ff0000';
|
|
ctx.lineWidth = 3;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, PLAYER_SIZE + 20, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
}
|
|
|
|
updateHUD();
|
|
updateLeaderboard();
|
|
drawMinimap();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function showDeathScreen(data) {
|
|
gamePaused = true;
|
|
const survivalTime = Math.floor((Date.now() - gameStartTime) / 1000);
|
|
|
|
if (data.killerName) {
|
|
// Find killer and focus camera on them
|
|
const killer = [...gameState.players, ...gameState.bots].find(p => p.name === data.killerName);
|
|
if (killer) {
|
|
killerFocusActive = true;
|
|
killerFocusTarget = killer;
|
|
killerFocusTime = 99999; // Stay focused until respawn!
|
|
|
|
// Show killer focus indicator
|
|
const indicator = document.createElement('div');
|
|
indicator.id = 'killerFocus';
|
|
indicator.textContent = '☠️ ' + data.killerName + ' ☠️';
|
|
document.body.appendChild(indicator);
|
|
setTimeout(() => indicator.remove(), 3000);
|
|
}
|
|
}
|
|
|
|
// Show death screen after 2 seconds (let player see killer first)
|
|
setTimeout(() => {
|
|
document.getElementById('finalScore').textContent = data.score;
|
|
document.getElementById('finalKills').textContent = data.kills;
|
|
document.getElementById('finalDeaths').textContent = data.deaths;
|
|
document.getElementById('survivalTime').textContent = survivalTime + 's';
|
|
|
|
if (data.killerName) {
|
|
document.getElementById('killerInfo').textContent = `Getötet von: ${data.killerName}`;
|
|
} else {
|
|
document.getElementById('killerInfo').textContent = '';
|
|
}
|
|
|
|
document.getElementById('deathScreen').classList.remove('hidden');
|
|
|
|
respawnTimer = 5;
|
|
document.getElementById('respawnButton').textContent = `Respawn (${respawnTimer}s)`;
|
|
document.getElementById('respawnButton').disabled = true;
|
|
|
|
respawnInterval = setInterval(() => {
|
|
respawnTimer--;
|
|
document.getElementById('respawnButton').textContent = `Respawn (${respawnTimer}s)`;
|
|
if (respawnTimer <= 0) {
|
|
clearInterval(respawnInterval);
|
|
document.getElementById('respawnButton').textContent = 'Respawn';
|
|
document.getElementById('respawnButton').disabled = false;
|
|
}
|
|
}, 1000);
|
|
}, 2000);
|
|
}
|
|
|
|
function respawn() {
|
|
socket.emit('respawn');
|
|
document.getElementById('deathScreen').classList.add('hidden');
|
|
killerFocusActive = false;
|
|
killerFocusTarget = null;
|
|
killerFocusTime = 0;
|
|
gamePaused = false;
|
|
gameStartTime = Date.now();
|
|
SoundSystem.playRespawn();
|
|
gameLoop();
|
|
}
|
|
|
|
function togglePause() {
|
|
if (document.getElementById('deathScreen').classList.contains('hidden')) {
|
|
gamePaused = !gamePaused;
|
|
document.getElementById('pauseMenu').classList.toggle('hidden');
|
|
if (!gamePaused) gameLoop();
|
|
}
|
|
}
|
|
|
|
function startGame() {
|
|
const playerName = document.getElementById('playerNameInput').value || 'Spieler';
|
|
document.getElementById('startScreen').classList.add('hidden');
|
|
document.getElementById('playerName').textContent = playerName;
|
|
socket.emit('joinGame', { name: playerName });
|
|
gameRunning = true;
|
|
gameStartTime = Date.now();
|
|
|
|
// Try to enter fullscreen automatically
|
|
enterFullscreen();
|
|
|
|
gameLoop();
|
|
}
|
|
|
|
function enterFullscreen() {
|
|
const elem = document.documentElement;
|
|
if (elem.requestFullscreen) {
|
|
elem.requestFullscreen().catch(err => {
|
|
console.log('Fullscreen failed:', err);
|
|
showFullscreenButton();
|
|
});
|
|
} else if (elem.webkitRequestFullscreen) {
|
|
elem.webkitRequestFullscreen();
|
|
} else if (elem.msRequestFullscreen) {
|
|
elem.msRequestFullscreen();
|
|
} else {
|
|
showFullscreenButton();
|
|
}
|
|
}
|
|
|
|
function showFullscreenButton() {
|
|
const btn = document.getElementById('fullscreenButton');
|
|
btn.classList.remove('hidden');
|
|
}
|
|
|
|
function hideFullscreenButton() {
|
|
const btn = document.getElementById('fullscreenButton');
|
|
btn.classList.add('hidden');
|
|
}
|
|
|
|
// Listen for fullscreen changes
|
|
document.addEventListener('fullscreenchange', () => {
|
|
if (!document.fullscreenElement) {
|
|
showFullscreenButton();
|
|
} else {
|
|
hideFullscreenButton();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('webkitfullscreenchange', () => {
|
|
if (!document.webkitFullscreenElement) {
|
|
showFullscreenButton();
|
|
} else {
|
|
hideFullscreenButton();
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', e => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
mouse.x = e.clientX - rect.left;
|
|
mouse.y = e.clientY - rect.top;
|
|
});
|
|
|
|
canvas.addEventListener('mousedown', () => {
|
|
if (!gamePaused) {
|
|
mouse.down = true;
|
|
socket.emit('shoot');
|
|
SoundSystem.playShoot();
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', () => mouse.down = false);
|
|
|
|
setInterval(() => {
|
|
if (mouse.down && gameRunning && !gamePaused) {
|
|
socket.emit('shoot');
|
|
SoundSystem.playShoot();
|
|
}
|
|
}, 250);
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
togglePause();
|
|
}
|
|
});
|
|
|
|
document.getElementById('startButton').addEventListener('click', startGame);
|
|
document.getElementById('respawnButton').addEventListener('click', respawn);
|
|
document.getElementById('resumeButton').addEventListener('click', togglePause);
|
|
document.getElementById('quitButton').addEventListener('click', () => location.reload());
|
|
|
|
document.getElementById('playerNameInput').addEventListener('keypress', e => {
|
|
if (e.key === 'Enter') startGame();
|
|
});
|
|
|
|
document.getElementById('soundToggle').addEventListener('click', () => {
|
|
soundEnabled = !soundEnabled;
|
|
document.getElementById('soundToggle').textContent = soundEnabled ? '🔊 Sound: AN' : '🔇 Sound: AUS';
|
|
if (soundEnabled) SoundSystem.playPowerUp();
|
|
});
|
|
|
|
document.getElementById('fullscreenButton').addEventListener('click', () => {
|
|
enterFullscreen();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|