Звук (Web Audio API) + несколько раскладок coop-карты

Клиент: audio.js синтезирует эффекты без внешних файлов (выстрел,
взрыв, разрушение кирпича, победа/поражение) — вписывается в
"чистый HTML/CSS/JS" стек. AudioContext разблокируется по клику
"готов" (браузеры блокируют автовоспроизведение без жеста).

Сервер: 3 варианта раскладки coop-карты (случайный выбор при старте
матча) вместо одной фиксированной.

Fix: сервер разрушал кирпичи в своей копии карты, но никогда не сообщал
об этом клиенту — стена оставалась видимой навсегда, хотя сквозь неё уже
можно было стрелять. Теперь game.state включает destroyed_tiles,
клиент убирает соответствующий спрайт стены.
This commit is contained in:
2026-08-06 12:42:52 +05:00
parent 79aa4a0c48
commit f2a5657740
5 changed files with 150 additions and 13 deletions
+38 -7
View File
@@ -1,5 +1,6 @@
import { ClientMessage, ServerMessage } from './protocol.js';
import { returnToLobbyContainer, leaveRoomAndReturn } from './lobby.js';
import { playShot, playWallHit, playExplosion, playWin, playLose } from './audio.js';
const TILE_PX = 32;
// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform)
@@ -13,6 +14,7 @@ let myPlayerId = null;
let stage = null;
let tankSprites = new Map();
let bulletSprites = new Map();
let wallSprites = new Map(); // "x,y" (тайл) -> спрайт, для синка разрушенных стен
let baseSprite = null;
let lastSentInput = null;
let seq = 0;
@@ -133,13 +135,16 @@ function buildMap(map) {
if (tile === 0) {
continue;
}
stage.insert(
new Q.Wall({
x: x * TILE_PX + TILE_PX / 2,
y: y * TILE_PX + TILE_PX / 2,
tile,
})
);
const sprite = new Q.Wall({
x: x * TILE_PX + TILE_PX / 2,
y: y * TILE_PX + TILE_PX / 2,
tile,
});
stage.insert(sprite);
if (tile === 1) {
// только кирпич может быть разрушен — только его отслеживаем
wallSprites.set(`${x},${y}`, sprite);
}
}
}
}
@@ -200,6 +205,8 @@ function onState(payload) {
});
stage.insert(sprite);
tankSprites.set(t.id, sprite);
} else if (!sprite.p.hidden && !t.alive) {
playExplosion();
}
sprite.p.x = t.x * TILE_PX;
sprite.p.y = t.y * TILE_PX;
@@ -221,6 +228,7 @@ function onState(payload) {
sprite = new Q.Bullet({ x: b.x * TILE_PX, y: b.y * TILE_PX });
stage.insert(sprite);
bulletSprites.set(b.id, sprite);
playShot();
}
sprite.p.x = b.x * TILE_PX;
sprite.p.y = b.y * TILE_PX;
@@ -232,6 +240,20 @@ function onState(payload) {
}
}
if (payload.destroyed_tiles) {
for (const [tx, ty] of payload.destroyed_tiles) {
const key = `${tx},${ty}`;
const sprite = wallSprites.get(key);
if (sprite) {
sprite.destroy();
wallSprites.delete(key);
}
}
if (payload.destroyed_tiles.length > 0) {
playWallHit();
}
}
if (payload.base) {
if (!baseSprite) {
baseSprite = new Q.Base({
@@ -241,6 +263,9 @@ function onState(payload) {
});
stage.insert(baseSprite);
} else {
if (baseSprite.p.alive && !payload.base.alive) {
playExplosion();
}
baseSprite.p.alive = payload.base.alive;
}
}
@@ -248,6 +273,11 @@ function onState(payload) {
function onOver(payload) {
polling = false;
if (payload.result === 'win') {
playWin();
} else {
playLose();
}
const titles = { win: 'ПОБЕДА', lose: 'ПОРАЖЕНИЕ', draw: 'НИЧЬЯ' };
const title = titles[payload.result] || payload.result.toUpperCase();
const overlay = document.getElementById('battle-result-overlay');
@@ -309,6 +339,7 @@ export function startBattle(networkInstance, payload, ownPlayerId) {
Q.stageScene('battle', 0);
tankSprites = new Map();
bulletSprites = new Map();
wallSprites = new Map();
baseSprite = null;
buildMap(payload.map);