Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fc7af9ba0 | ||
|
|
f2a5657740 |
@@ -0,0 +1,76 @@
|
|||||||
|
// Простой синтез звука через Web Audio API — без внешних аудиофайлов.
|
||||||
|
let ctx = null;
|
||||||
|
|
||||||
|
function ensureCtx() {
|
||||||
|
if (!ctx) {
|
||||||
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||||
|
ctx = new AudioCtx();
|
||||||
|
}
|
||||||
|
if (ctx.state === 'suspended') {
|
||||||
|
ctx.resume();
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызывать из обработчика реального пользовательского клика/нажатия —
|
||||||
|
// браузеры блокируют автозапуск звука без явного жеста.
|
||||||
|
export function unlockAudio() {
|
||||||
|
ensureCtx();
|
||||||
|
}
|
||||||
|
|
||||||
|
function beep({ freq, duration = 0.08, type = 'square', gain = 0.1, freqEnd = null }) {
|
||||||
|
const c = ensureCtx();
|
||||||
|
const osc = c.createOscillator();
|
||||||
|
const g = c.createGain();
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(freq, c.currentTime);
|
||||||
|
if (freqEnd !== null) {
|
||||||
|
osc.frequency.linearRampToValueAtTime(freqEnd, c.currentTime + duration);
|
||||||
|
}
|
||||||
|
g.gain.setValueAtTime(gain, c.currentTime);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, c.currentTime + duration);
|
||||||
|
osc.connect(g).connect(c.destination);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(c.currentTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function noiseBurst({ duration = 0.2, gain = 0.18 } = {}) {
|
||||||
|
const c = ensureCtx();
|
||||||
|
const size = Math.max(1, Math.floor(c.sampleRate * duration));
|
||||||
|
const buffer = c.createBuffer(1, size, c.sampleRate);
|
||||||
|
const data = buffer.getChannelData(0);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
data[i] = (Math.random() * 2 - 1) * (1 - i / size);
|
||||||
|
}
|
||||||
|
const source = c.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
const g = c.createGain();
|
||||||
|
g.gain.setValueAtTime(gain, c.currentTime);
|
||||||
|
source.connect(g).connect(c.destination);
|
||||||
|
source.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playShot() {
|
||||||
|
beep({ freq: 660, freqEnd: 220, duration: 0.06, type: 'square', gain: 0.08 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playWallHit() {
|
||||||
|
noiseBurst({ duration: 0.08, gain: 0.1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playExplosion() {
|
||||||
|
noiseBurst({ duration: 0.25, gain: 0.18 });
|
||||||
|
beep({ freq: 120, freqEnd: 40, duration: 0.25, type: 'sawtooth', gain: 0.1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playWin() {
|
||||||
|
[523, 659, 784, 1047].forEach((freq, i) =>
|
||||||
|
setTimeout(() => beep({ freq, duration: 0.15, type: 'square', gain: 0.1 }), i * 110)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playLose() {
|
||||||
|
[392, 330, 262, 196].forEach((freq, i) =>
|
||||||
|
setTimeout(() => beep({ freq, duration: 0.2, type: 'sawtooth', gain: 0.1 }), i * 130)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ClientMessage, ServerMessage } from './protocol.js';
|
import { ClientMessage, ServerMessage } from './protocol.js';
|
||||||
import { returnToLobbyContainer, leaveRoomAndReturn } from './lobby.js';
|
import { returnToLobbyContainer, leaveRoomAndReturn } from './lobby.js';
|
||||||
|
import { playShot, playWallHit, playExplosion, playWin, playLose } from './audio.js';
|
||||||
|
|
||||||
const TILE_PX = 32;
|
const TILE_PX = 32;
|
||||||
// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform)
|
// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform)
|
||||||
@@ -13,6 +14,7 @@ let myPlayerId = null;
|
|||||||
let stage = null;
|
let stage = null;
|
||||||
let tankSprites = new Map();
|
let tankSprites = new Map();
|
||||||
let bulletSprites = new Map();
|
let bulletSprites = new Map();
|
||||||
|
let wallSprites = new Map(); // "x,y" (тайл) -> спрайт, для синка разрушенных стен
|
||||||
let baseSprite = null;
|
let baseSprite = null;
|
||||||
let lastSentInput = null;
|
let lastSentInput = null;
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
@@ -133,13 +135,16 @@ function buildMap(map) {
|
|||||||
if (tile === 0) {
|
if (tile === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
stage.insert(
|
const sprite = new Q.Wall({
|
||||||
new Q.Wall({
|
x: x * TILE_PX + TILE_PX / 2,
|
||||||
x: x * TILE_PX + TILE_PX / 2,
|
y: y * TILE_PX + TILE_PX / 2,
|
||||||
y: y * TILE_PX + TILE_PX / 2,
|
tile,
|
||||||
tile,
|
});
|
||||||
})
|
stage.insert(sprite);
|
||||||
);
|
if (tile === 1) {
|
||||||
|
// только кирпич может быть разрушен — только его отслеживаем
|
||||||
|
wallSprites.set(`${x},${y}`, sprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,6 +205,8 @@ function onState(payload) {
|
|||||||
});
|
});
|
||||||
stage.insert(sprite);
|
stage.insert(sprite);
|
||||||
tankSprites.set(t.id, sprite);
|
tankSprites.set(t.id, sprite);
|
||||||
|
} else if (!sprite.p.hidden && !t.alive) {
|
||||||
|
playExplosion();
|
||||||
}
|
}
|
||||||
sprite.p.x = t.x * TILE_PX;
|
sprite.p.x = t.x * TILE_PX;
|
||||||
sprite.p.y = t.y * 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 });
|
sprite = new Q.Bullet({ x: b.x * TILE_PX, y: b.y * TILE_PX });
|
||||||
stage.insert(sprite);
|
stage.insert(sprite);
|
||||||
bulletSprites.set(b.id, sprite);
|
bulletSprites.set(b.id, sprite);
|
||||||
|
playShot();
|
||||||
}
|
}
|
||||||
sprite.p.x = b.x * TILE_PX;
|
sprite.p.x = b.x * TILE_PX;
|
||||||
sprite.p.y = b.y * 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 (payload.base) {
|
||||||
if (!baseSprite) {
|
if (!baseSprite) {
|
||||||
baseSprite = new Q.Base({
|
baseSprite = new Q.Base({
|
||||||
@@ -241,6 +263,9 @@ function onState(payload) {
|
|||||||
});
|
});
|
||||||
stage.insert(baseSprite);
|
stage.insert(baseSprite);
|
||||||
} else {
|
} else {
|
||||||
|
if (baseSprite.p.alive && !payload.base.alive) {
|
||||||
|
playExplosion();
|
||||||
|
}
|
||||||
baseSprite.p.alive = payload.base.alive;
|
baseSprite.p.alive = payload.base.alive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +273,11 @@ function onState(payload) {
|
|||||||
|
|
||||||
function onOver(payload) {
|
function onOver(payload) {
|
||||||
polling = false;
|
polling = false;
|
||||||
|
if (payload.result === 'win') {
|
||||||
|
playWin();
|
||||||
|
} else {
|
||||||
|
playLose();
|
||||||
|
}
|
||||||
const titles = { win: 'ПОБЕДА', lose: 'ПОРАЖЕНИЕ', draw: 'НИЧЬЯ' };
|
const titles = { win: 'ПОБЕДА', lose: 'ПОРАЖЕНИЕ', draw: 'НИЧЬЯ' };
|
||||||
const title = titles[payload.result] || payload.result.toUpperCase();
|
const title = titles[payload.result] || payload.result.toUpperCase();
|
||||||
const overlay = document.getElementById('battle-result-overlay');
|
const overlay = document.getElementById('battle-result-overlay');
|
||||||
@@ -309,6 +339,7 @@ export function startBattle(networkInstance, payload, ownPlayerId) {
|
|||||||
Q.stageScene('battle', 0);
|
Q.stageScene('battle', 0);
|
||||||
tankSprites = new Map();
|
tankSprites = new Map();
|
||||||
bulletSprites = new Map();
|
bulletSprites = new Map();
|
||||||
|
wallSprites = new Map();
|
||||||
baseSprite = null;
|
baseSprite = null;
|
||||||
|
|
||||||
buildMap(payload.map);
|
buildMap(payload.map);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Network, ServerMessage } from './network.js';
|
import { Network, ServerMessage } from './network.js';
|
||||||
import { ClientMessage } from './protocol.js';
|
import { ClientMessage } from './protocol.js';
|
||||||
import { startBattle } from './battle.js';
|
import { startBattle } from './battle.js';
|
||||||
|
import { unlockAudio } from './audio.js';
|
||||||
|
|
||||||
const container = document.getElementById('lobby');
|
const container = document.getElementById('lobby');
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ container?.addEventListener('click', (e) => {
|
|||||||
net.joinRoom(btn.dataset.roomId);
|
net.joinRoom(btn.dataset.roomId);
|
||||||
break;
|
break;
|
||||||
case 'ready':
|
case 'ready':
|
||||||
|
unlockAudio();
|
||||||
net.send(ClientMessage.GAME_READY);
|
net.send(ClientMessage.GAME_READY);
|
||||||
break;
|
break;
|
||||||
case 'leave':
|
case 'leave':
|
||||||
|
|||||||
+33
-6
@@ -88,7 +88,7 @@ void Game::buildCoopMap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// немного укрытий по полю
|
// немного укрытий по полю — несколько вариантов раскладки для разнообразия
|
||||||
auto block = [this](int x, int y, int w, int h, int tile) {
|
auto block = [this](int x, int y, int w, int h, int tile) {
|
||||||
for (int dy = 0; dy < h; dy++) {
|
for (int dy = 0; dy < h; dy++) {
|
||||||
for (int dx = 0; dx < w; dx++) {
|
for (int dx = 0; dx < w; dx++) {
|
||||||
@@ -96,11 +96,29 @@ void Game::buildCoopMap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
block(2, 5, 2, 2, kBrick);
|
switch (std::rand() % 3) {
|
||||||
block(11, 5, 2, 2, kBrick);
|
case 0:
|
||||||
block(6, 4, 3, 1, kSteel);
|
block(2, 5, 2, 2, kBrick);
|
||||||
block(4, 8, 2, 2, kBrick);
|
block(11, 5, 2, 2, kBrick);
|
||||||
block(9, 8, 2, 2, kBrick);
|
block(6, 4, 3, 1, kSteel);
|
||||||
|
block(4, 8, 2, 2, kBrick);
|
||||||
|
block(9, 8, 2, 2, kBrick);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
block(1, 4, 3, 1, kSteel);
|
||||||
|
block(11, 4, 3, 1, kSteel);
|
||||||
|
block(6, 6, 3, 3, kBrick);
|
||||||
|
block(2, 9, 2, 2, kBrick);
|
||||||
|
block(11, 9, 2, 2, kBrick);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
block(4, 3, 1, 4, kBrick);
|
||||||
|
block(10, 3, 1, 4, kBrick);
|
||||||
|
block(6, 5, 3, 2, kSteel);
|
||||||
|
block(3, 9, 3, 1, kBrick);
|
||||||
|
block(9, 9, 3, 1, kBrick);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// база внизу по центру, укрыта кирпичом с трёх сторон
|
// база внизу по центру, укрыта кирпичом с трёх сторон
|
||||||
int bx = kMapWidth / 2;
|
int bx = kMapWidth / 2;
|
||||||
@@ -320,6 +338,7 @@ void Game::fireBullet(Tank &tank) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Game::updateBullets() {
|
void Game::updateBullets() {
|
||||||
|
destroyed_tiles_.clear();
|
||||||
double step = kBulletSpeed * kDt;
|
double step = kBulletSpeed * kDt;
|
||||||
|
|
||||||
for (auto &b : bullets_) {
|
for (auto &b : bullets_) {
|
||||||
@@ -361,6 +380,7 @@ void Game::updateBullets() {
|
|||||||
}
|
}
|
||||||
if (map_[ty][tx] == kBrick) {
|
if (map_[ty][tx] == kBrick) {
|
||||||
map_[ty][tx] = kEmpty;
|
map_[ty][tx] = kEmpty;
|
||||||
|
destroyed_tiles_.push_back({tx, ty});
|
||||||
dead[i] = true;
|
dead[i] = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -518,6 +538,13 @@ void Game::broadcastState() {
|
|||||||
if (base_) {
|
if (base_) {
|
||||||
payload["base"] = {{"x", base_->x}, {"y", base_->y}, {"alive", base_->alive}};
|
payload["base"] = {{"x", base_->x}, {"y", base_->y}, {"alive", base_->alive}};
|
||||||
}
|
}
|
||||||
|
if (!destroyed_tiles_.empty()) {
|
||||||
|
json destroyed = json::array();
|
||||||
|
for (const auto &[tx, ty] : destroyed_tiles_) {
|
||||||
|
destroyed.push_back({tx, ty});
|
||||||
|
}
|
||||||
|
payload["destroyed_tiles"] = destroyed;
|
||||||
|
}
|
||||||
for (const auto &t : tanks_) {
|
for (const auto &t : tanks_) {
|
||||||
if (!t.is_ai) {
|
if (!t.is_ai) {
|
||||||
sendTo(t.ws, "game.state", payload);
|
sendTo(t.ws, "game.state", payload);
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ private:
|
|||||||
std::vector<Bullet> bullets_;
|
std::vector<Bullet> bullets_;
|
||||||
std::optional<Base> base_;
|
std::optional<Base> base_;
|
||||||
std::vector<std::pair<double, double>> enemy_spawn_points_;
|
std::vector<std::pair<double, double>> enemy_spawn_points_;
|
||||||
|
std::vector<std::pair<int, int>> destroyed_tiles_; // за последний тик — для звука/синка стен на клиенте
|
||||||
int enemies_spawned_total_ = 0;
|
int enemies_spawned_total_ = 0;
|
||||||
int enemy_spawn_cooldown_ = 0;
|
int enemy_spawn_cooldown_ = 0;
|
||||||
int next_bullet_id_ = 1;
|
int next_bullet_id_ = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user