import { ClientMessage, ServerMessage } from './protocol.js'; const TILE_PX = 32; // Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform) // на основе p.x/p.y/p.angle — draw() получает уже трансформированный контекст // и должен рисовать спрайт центрированным в (0,0), без своих translate/rotate. const DIR_DEGREES = { up: 0, right: 90, down: 180, left: 270 }; let Q = null; let net = null; let myPlayerId = null; let stage = null; let tankSprites = new Map(); let bulletSprites = new Map(); let lastSentInput = null; let seq = 0; let polling = false; function setupEntities() { Q.Sprite.extend('Tank', { init(p) { this._super(p, { w: 26, h: 26, renderAlways: true }); }, draw(ctx) { const p = this.p; ctx.fillStyle = p.isMe ? '#00ff41' : '#9fa3a0'; ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); ctx.fillStyle = '#0a0e0c'; ctx.fillRect(-3, -p.h / 2 - 8, 6, 12); }, }); Q.Sprite.extend('Bullet', { init(p) { this._super(p, { w: 8, h: 8, renderAlways: true }); }, draw(ctx) { ctx.fillStyle = '#c7c7c7'; ctx.beginPath(); ctx.arc(0, 0, this.p.w / 2, 0, Math.PI * 2); ctx.fill(); }, }); Q.Sprite.extend('Wall', { init(p) { this._super(p, { w: TILE_PX, h: TILE_PX }); }, draw(ctx) { const p = this.p; if (p.tile === 2) { ctx.fillStyle = '#9fa3a0'; ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); } else { ctx.fillStyle = '#0a8f2c'; ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); ctx.strokeStyle = '#0a0e0c'; ctx.strokeRect(-p.w / 2 + 1, -p.h / 2 + 1, p.w - 2, p.h - 2); } }, }); } function ensureQuintus(cols, rows) { if (Q) { return; } Q = window.Quintus().include('Sprites, Scenes, 2D, Input').setup({ width: cols * TILE_PX, height: rows * TILE_PX, }); Q.input.keyboardControls({ LEFT: 'left', RIGHT: 'right', UP: 'up', DOWN: 'down', A: 'left', D: 'right', W: 'up', S: 'down', SPACE: 'fire', }); setupEntities(); Q.scene('battle', (stageRef) => { stage = stageRef; }); document.getElementById('battlecity-canvas-container').appendChild(Q.el); } function buildMap(map) { for (let y = 0; y < map.length; y++) { for (let x = 0; x < map[y].length; x++) { const tile = map[y][x]; if (tile === 0) { continue; } stage.insert( new Q.Wall({ x: x * TILE_PX + TILE_PX / 2, y: y * TILE_PX + TILE_PX / 2, tile, }) ); } } } function pollInput() { if (!polling) { return; } const input = { seq: seq++, up: !!Q.inputs.up, down: !!Q.inputs.down, left: !!Q.inputs.left, right: !!Q.inputs.right, fire: !!Q.inputs.fire, }; const key = `${input.up}${input.down}${input.left}${input.right}${input.fire}`; if (key !== lastSentInput) { lastSentInput = key; net.send(ClientMessage.GAME_INPUT, input); } requestAnimationFrame(pollInput); } function onState(payload) { for (const t of payload.tanks) { let sprite = tankSprites.get(t.id); if (!sprite) { sprite = new Q.Tank({ x: t.x * TILE_PX, y: t.y * TILE_PX, isMe: t.id === myPlayerId }); stage.insert(sprite); tankSprites.set(t.id, sprite); } sprite.p.x = t.x * TILE_PX; sprite.p.y = t.y * TILE_PX; sprite.p.angle = DIR_DEGREES[t.direction] || 0; sprite.p.hidden = !t.alive; } const seenBullets = new Set(); for (const b of payload.bullets) { seenBullets.add(b.id); let sprite = bulletSprites.get(b.id); if (!sprite) { sprite = new Q.Bullet({ x: b.x * TILE_PX, y: b.y * TILE_PX }); stage.insert(sprite); bulletSprites.set(b.id, sprite); } sprite.p.x = b.x * TILE_PX; sprite.p.y = b.y * TILE_PX; } for (const [id, sprite] of bulletSprites) { if (!seenBullets.has(id)) { sprite.destroy(); bulletSprites.delete(id); } } } function onOver(payload) { polling = false; const resultText = { win: 'Победа', lose: 'Поражение', draw: 'Ничья' }[payload.result] || payload.result; const resultEl = document.getElementById('battle-result'); resultEl.textContent = `${resultText} — ${payload.reason}`; document.getElementById('battle-back').style.display = 'inline-block'; } export function startBattle(networkInstance, payload, ownPlayerId) { net = networkInstance; myPlayerId = ownPlayerId; const rows = payload.map.length; const cols = payload.map[0].length; document.getElementById('lobby').style.display = 'none'; const container = document.getElementById('battlecity-canvas-container'); container.style.display = 'block'; container.innerHTML = '
'; ensureQuintus(cols, rows); Q.stageScene('battle', 0); tankSprites = new Map(); bulletSprites = new Map(); buildMap(payload.map); net.on(ServerMessage.GAME_STATE, onState); net.on(ServerMessage.GAME_OVER, onOver); lastSentInput = null; seq = 0; polling = true; requestAnimationFrame(pollInput); document.getElementById('battle-back').addEventListener('click', () => { location.reload(); }); }