feat: add game over stats
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
<div id="gameover-screen" class="hidden">
|
||||
<div class="screen-title" id="gameover-title">ЧАТ, ВЫ ПРОИГРАЛИ!</div>
|
||||
<div class="screen-subtitle">Final Score: <span id="final-score">0</span></div>
|
||||
<div id="player-stats"></div>
|
||||
<div class="screen-subtitle" id="gameover-subtitle">Введи !play чтобы играть</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,7 +449,7 @@
|
||||
const InputModule = {
|
||||
currentLine: "",
|
||||
|
||||
submitLine(text) {
|
||||
submitLine(text, username) {
|
||||
const line = text.trim().toLowerCase();
|
||||
if (!line) return;
|
||||
if (introPlaying) return;
|
||||
@@ -468,10 +469,10 @@
|
||||
const enemy = this.findEnemy(line);
|
||||
|
||||
// АНИМАЦИЯ: слово вылетает из поля ввода, потом запускается снаряд
|
||||
this.animateShoot(line, enemy);
|
||||
this.animateShoot(line, enemy, username);
|
||||
},
|
||||
|
||||
animateShoot(line, enemy) {
|
||||
animateShoot(line, enemy, username) {
|
||||
const el = document.getElementById("typed-text");
|
||||
|
||||
// Добавляем CSS-класс для анимации вылета
|
||||
@@ -484,9 +485,9 @@
|
||||
this.updateDisplay("");
|
||||
|
||||
if (enemy) {
|
||||
game.launchProjectile(line, enemy, true);
|
||||
game.launchProjectile(line, enemy, true, username);
|
||||
} else {
|
||||
game.launchProjectile(line, null, false);
|
||||
game.launchProjectile(line, null, false, username);
|
||||
}
|
||||
}, S.inputShootDelay);
|
||||
},
|
||||
@@ -656,9 +657,11 @@
|
||||
let gameLoopStarted = false;
|
||||
let introPlaying = false;
|
||||
|
||||
let playerStats = {};
|
||||
|
||||
const game = {
|
||||
launchProjectile(word, targetEnemy, isHit) {
|
||||
projectiles.push(new Projectile(word, targetEnemy, isHit));
|
||||
launchProjectile(word, targetEnemy, isHit, username) {
|
||||
projectiles.push(new Projectile(word, targetEnemy, isHit, username));
|
||||
},
|
||||
|
||||
onProjectileHit(projectile, enemy) {
|
||||
@@ -667,6 +670,10 @@
|
||||
if (dead) {
|
||||
score += enemy.isHeavy ? CONFIG.game.scoreHeavy : CONFIG.game.scoreSimple;
|
||||
document.getElementById("score").textContent = score;
|
||||
if (projectile.username) {
|
||||
if (!playerStats[projectile.username]) playerStats[projectile.username] = { kills: 0, misses: 0 };
|
||||
playerStats[projectile.username].kills++;
|
||||
}
|
||||
this.destroyEnemy(enemy);
|
||||
} else {
|
||||
spawnParticles(enemy.x, enemy.y, CONFIG.colors.heavy, S.particleArmorBreak);
|
||||
@@ -677,6 +684,10 @@
|
||||
onProjectileMiss(projectile) {
|
||||
spawnParticles(projectile.targetX, projectile.targetY, CONFIG.colors.miss, S.particleMiss);
|
||||
triggerShake(S.shakeMiss, S.shakeMissDuration);
|
||||
if (projectile.username) {
|
||||
if (!playerStats[projectile.username]) playerStats[projectile.username] = { kills: 0, misses: 0 };
|
||||
playerStats[projectile.username].misses++;
|
||||
}
|
||||
},
|
||||
|
||||
destroyEnemy(enemy) {
|
||||
@@ -696,9 +707,10 @@
|
||||
// PROJECTILE — вылетает из поля ввода (низ экрана, центр)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
class Projectile {
|
||||
constructor(word, targetEnemy, isHit) {
|
||||
constructor(word, targetEnemy, isHit, username) {
|
||||
this.word = word;
|
||||
this.isHit = isHit;
|
||||
this.username = username;
|
||||
this.x = W / 2;
|
||||
this.y = H - S.shooterOffset;
|
||||
this.speed = S.projectileSpeed;
|
||||
@@ -1040,9 +1052,50 @@
|
||||
|
||||
let gameOverTimeout = null;
|
||||
|
||||
function renderPlayerStats() {
|
||||
const container = document.getElementById("player-stats");
|
||||
container.innerHTML = "";
|
||||
|
||||
const names = Object.keys(playerStats);
|
||||
if (names.length === 0) return;
|
||||
|
||||
const byKills = names.slice().sort((a, b) => playerStats[b].kills - playerStats[a].kills);
|
||||
const top3 = byKills.slice(0, 3);
|
||||
|
||||
const sectionTitle = document.createElement("div");
|
||||
sectionTitle.className = "stats-section-title";
|
||||
sectionTitle.textContent = "УБИЙЦЫ";
|
||||
container.appendChild(sectionTitle);
|
||||
|
||||
top3.forEach((name, i) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "stats-row" + (i === 0 ? " stats-top1" : i === 1 ? " stats-top2" : " stats-top3");
|
||||
const place = i === 0 ? "1" : i === 1 ? "2" : "3";
|
||||
row.innerHTML = `<span class="stats-place">#${place}</span> <span class="stats-name">${name}</span> <span class="stats-kills">${playerStats[name].kills}</span>`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
|
||||
let maxMisses = 0;
|
||||
let mazilaName = "";
|
||||
for (const name of names) {
|
||||
if (playerStats[name].misses > maxMisses) {
|
||||
maxMisses = playerStats[name].misses;
|
||||
mazilaName = name;
|
||||
}
|
||||
}
|
||||
|
||||
if (mazilaName && maxMisses > 0) {
|
||||
const mazila = document.createElement("div");
|
||||
mazila.className = "stats-mazila";
|
||||
mazila.innerHTML = `<span class="mazila-title">Мазила:</span> ${mazilaName} <span class="mazila-count">(${maxMisses})</span>`;
|
||||
container.appendChild(mazila);
|
||||
}
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
gameState = "gameover";
|
||||
document.getElementById("final-score").textContent = score;
|
||||
renderPlayerStats();
|
||||
document.getElementById("gameover-screen").classList.remove("hidden");
|
||||
document.getElementById("gameover-subtitle").classList.add("hidden");
|
||||
|
||||
@@ -1070,6 +1123,7 @@
|
||||
enemies = [];
|
||||
particles = [];
|
||||
projectiles = [];
|
||||
playerStats = {};
|
||||
spawnTimer = CONFIG.game.spawnIntervalBase;
|
||||
spawnInterval = CONFIG.game.spawnIntervalBase;
|
||||
waveTimer = 0;
|
||||
@@ -1143,7 +1197,8 @@
|
||||
client.connect();
|
||||
|
||||
client.on("message", (channel, tags, message, self) => {
|
||||
InputModule.submitLine(message);
|
||||
const username = tags["display-name"] || tags.username || "anonymous";
|
||||
InputModule.submitLine(message, username);
|
||||
});
|
||||
|
||||
function update() {
|
||||
|
||||
@@ -111,6 +111,70 @@ body {
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
#player-stats {
|
||||
margin-bottom: 20px;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.stats-section-title {
|
||||
color: #00ff41;
|
||||
font-size: 16px;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 10px #00ff41;
|
||||
}
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-left: 2px solid #00ff41;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.stats-top1 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #00ff41;
|
||||
border-left-width: 4px;
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 12px #00ff41;
|
||||
}
|
||||
.stats-top2 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #00dd33;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.stats-top3 {
|
||||
font-size: 13px;
|
||||
color: #00bb22;
|
||||
}
|
||||
.stats-place {
|
||||
min-width: 28px;
|
||||
}
|
||||
.stats-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.stats-kills {
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
.stats-mazila {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
color: #ff6644;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mazila-title {
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 8px #ff6644;
|
||||
}
|
||||
.mazila-count {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user