feat: add active-game screen and firing interactions
This commit is contained in:
+58
-8
@@ -24,6 +24,8 @@
|
||||
let pollTimer;
|
||||
let heartbeatTimer;
|
||||
let lobbyInfoTimer;
|
||||
let firePending = false;
|
||||
let targetActivator;
|
||||
let retryIndex = 0;
|
||||
|
||||
function showScreen(name) {
|
||||
@@ -127,8 +129,40 @@
|
||||
function ownIndex() { return role === 'player2' ? 1 : 0; }
|
||||
function opponentIndex() { return ownIndex() ^ 1; }
|
||||
function canShoot() { return state?.phase === 'in_progress' && state.turn === role && isPlayer(); }
|
||||
function canFireTarget(target) {
|
||||
return !firePending && canShoot() && Number.isInteger(target?.x) && Number.isInteger(target?.y) &&
|
||||
target.x >= 0 && target.x < 10 && target.y >= 0 && target.y < 10 &&
|
||||
state.boards[opponentIndex()][target.y * 10 + target.x] === '0';
|
||||
}
|
||||
function playerLabel(index) { return index === 1 && state?.mode === 'bot' ? 'ESP32' : `Игрок ${index + 1}`; }
|
||||
|
||||
async function fireTarget(target) {
|
||||
if (!canFireTarget(target)) return false;
|
||||
firePending = true;
|
||||
selectedTarget = undefined;
|
||||
renderGame();
|
||||
try {
|
||||
await post('/api/game/shot', { token, gameId: state.gameId, ...target });
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
firePending = false;
|
||||
if (state?.phase === 'in_progress') renderGame();
|
||||
}
|
||||
}
|
||||
|
||||
function activateTarget(target, keyboard = false) {
|
||||
if (!targetActivator) {
|
||||
targetActivator = globalThis.BattleshipTargetInteraction.createTargetActivator({
|
||||
canFire: canFireTarget,
|
||||
select: nextTarget => { selectedTarget = nextTarget; renderGame(); },
|
||||
fire: fireTarget,
|
||||
});
|
||||
}
|
||||
return keyboard ? targetActivator.keyboard(target) : targetActivator.tap(target);
|
||||
}
|
||||
|
||||
function cellInfo(value) {
|
||||
if (value === '1') return ['ship', 'Корабль'];
|
||||
if (value === '2') return ['miss', 'Промах'];
|
||||
@@ -156,12 +190,26 @@
|
||||
const value = state.boards[index][y * 10 + x];
|
||||
const [kind, label] = cellInfo(value);
|
||||
const cell = document.createElement('button');
|
||||
cell.type = 'button'; cell.className = `cell ${kind}`.trim(); cell.disabled = !targetable || value !== '0';
|
||||
cell.type = 'button'; cell.className = `cell ${kind}`.trim(); cell.disabled = !targetable || value !== '0' || firePending;
|
||||
cell.setAttribute('aria-label', `${columns[x]}${y + 1}: ${label}`);
|
||||
if (targetable && value === '0') {
|
||||
cell.classList.add('target');
|
||||
if (selectedTarget?.x === x && selectedTarget?.y === y) cell.classList.add('selected');
|
||||
cell.addEventListener('click', () => { selectedTarget = { x, y }; renderGame(); });
|
||||
const target = { x, y };
|
||||
cell.addEventListener('pointerup', event => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
activateTarget(target);
|
||||
});
|
||||
cell.addEventListener('click', event => {
|
||||
if (event.detail === 0) activateTarget(target, true);
|
||||
});
|
||||
cell.addEventListener('dblclick', event => {
|
||||
event.preventDefault();
|
||||
if (targetActivator) targetActivator.doubleActivate(target);
|
||||
});
|
||||
} else if (value === '0') {
|
||||
cell.classList.add(index === ownIndex() && isPlayer() ? 'own-water' : 'unavailable');
|
||||
}
|
||||
grid.append(cell);
|
||||
}
|
||||
@@ -225,14 +273,16 @@
|
||||
if (!state) return;
|
||||
stopLobbyInfoPolling();
|
||||
showScreen('game');
|
||||
ui.turn.textContent = state.turn === role ? 'Ваш ход. Выберите клетку соперника.' : `Ходит ${playerLabel(state.turn === 'player2' ? 1 : 0)}.`;
|
||||
if (canShoot() && !selectedTarget) activeBoard = 'opponent';
|
||||
ui.turn.textContent = canShoot() ? 'Ваш ход' : `Ходит ${playerLabel(state.turn === 'player2' ? 1 : 0)}`;
|
||||
ui.wins.textContent = `Победы: ${state.wins[0]} : ${state.wins[1]}`;
|
||||
renderBoards(ui.boards);
|
||||
const available = canShoot();
|
||||
ui.shotControls.hidden = !isPlayer();
|
||||
ui.target.textContent = available ? (selectedTarget ? `Цель: ${columns[selectedTarget.x]}${selectedTarget.y + 1}. Подтвердите выстрел.` : 'Выберите клетку на поле соперника.') : 'Ожидайте своего хода.';
|
||||
ui.fire.disabled = !available || !selectedTarget;
|
||||
ui.cancel.disabled = !selectedTarget;
|
||||
ui.target.textContent = firePending ? 'Выстрел отправляется…' : available ? (selectedTarget ? `Цель: ${columns[selectedTarget.x]}${selectedTarget.y + 1}` : 'Выберите клетку на поле соперника.') : 'Ожидайте своего хода.';
|
||||
ui.fire.disabled = !selectedTarget || !canFireTarget(selectedTarget);
|
||||
ui.fire.textContent = firePending ? 'Выстрел…' : 'Огонь';
|
||||
ui.cancel.disabled = !selectedTarget || firePending;
|
||||
ui.abort.hidden = !(role === 'player1' && state.mode === 'human' && state.phase === 'in_progress');
|
||||
}
|
||||
|
||||
@@ -292,7 +342,7 @@
|
||||
const joined = await request('/api/session/join', { method: 'POST', headers: headers(), body: JSON.stringify({ name, requestedRole }) });
|
||||
token = joined.token; role = joined.role;
|
||||
localStorage.setItem(storage.token, token); localStorage.setItem(storage.name, name);
|
||||
notify('Подключено.');
|
||||
notify(''); setConnection('Подключено', 'online');
|
||||
await pollState();
|
||||
connectSocket();
|
||||
} catch (error) { notify(error.message, true); }
|
||||
@@ -329,7 +379,7 @@
|
||||
el('join-spectator').addEventListener('click', () => join('spectator'));
|
||||
document.querySelectorAll('.mode-button').forEach(button => button.addEventListener('click', () => post('/api/game/config', { token, gameId: state.gameId, mode: button.dataset.mode })));
|
||||
ui.start.addEventListener('click', () => post('/api/game/start', { token, gameId: state.gameId }));
|
||||
ui.fire.addEventListener('click', async () => { if (!selectedTarget) return; await post('/api/game/shot', { token, gameId: state.gameId, ...selectedTarget }); selectedTarget = undefined; });
|
||||
ui.fire.addEventListener('click', () => { if (selectedTarget) fireTarget(selectedTarget); });
|
||||
ui.cancel.addEventListener('click', () => { selectedTarget = undefined; renderGame(); });
|
||||
ui.abort.addEventListener('click', () => post('/api/game/abort', { token, gameId: state.gameId }));
|
||||
ui.rematch.addEventListener('click', () => post('/api/game/rematch', { token, gameId: state.gameId }));
|
||||
|
||||
Reference in New Issue
Block a user