feat: add player Names Throughout the Game UI

This commit is contained in:
2026-08-30 01:02:34 +03:00
parent 4d9d0db02d
commit dfc6b023f7
12 changed files with 307 additions and 34 deletions
+22 -12
View File
@@ -67,8 +67,8 @@
return item;
};
ui.availability.replaceChildren(
availabilityItem(`Игрок 1: ${info.player1Available ? 'свободен' : 'занят'}`),
availabilityItem(`Игрок 2: ${info.player2Available ? 'свободен' : 'занят'}`),
availabilityItem(`Игрок 1: ${info.player1Available ? 'свободен' : role === 'player1' ? 'Вы' : info.player1Name || 'занят'}`),
availabilityItem(`Игрок 2: ${info.player2Available ? 'свободен' : role === 'player2' ? 'Вы' : info.player2Name || 'занят'}`),
availabilityItem(`зрительских мест: ${info.spectatorsAvailable}`),
);
if (state?.phase === 'lobby') renderLobby();
@@ -110,6 +110,7 @@
payload.boards.every(board => typeof board === 'string' && /^[01234]{100}$/.test(board)) &&
typeof payload.version === 'number' && typeof payload.gameId === 'number' &&
(payload.winner === null || payload.winner === 0 || payload.winner === 1) && Array.isArray(payload.statistics) &&
Array.isArray(payload.players) && payload.players.length === 2 && payload.players.every(name => typeof name === 'string' && name.length <= 80) &&
payload.statistics.length === 2 && payload.statistics.every(entry => Array.isArray(entry) && entry.length === 4 &&
entry.every(value => Number.isInteger(value) && value >= 0));
}
@@ -134,7 +135,14 @@
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}`; }
function playerName(index) {
const name = state?.players?.[index];
return typeof name === 'string' && name.trim() ? name : `Игрок ${index + 1}`;
}
function playerLabel(index) {
if (index === 1 && state?.mode === 'bot') return 'ESP32';
return isPlayer() && index === ownIndex() ? 'Вы' : playerName(index);
}
async function fireTarget(target) {
if (!canFireTarget(target)) return false;
@@ -221,12 +229,12 @@
function boardDefinitions(result = false) {
if (role === 'spectator' || result) return [
{ index: 0, title: 'Поле игрока 1', key: 'player1', targetable: false },
{ index: 1, title: 'Поле игрока 2', key: 'player2', targetable: false }
{ index: 0, title: `Поле: ${playerLabel(0)}`, key: 'player1', targetable: false },
{ index: 1, title: `Поле: ${playerLabel(1)}`, key: 'player2', targetable: false }
];
return [
{ index: ownIndex(), title: 'Моё поле', key: 'own', targetable: false },
{ index: opponentIndex(), title: state.mode === 'bot' ? 'Поле ESP32' : 'Поле соперника', key: 'opponent', targetable: canShoot() }
{ index: opponentIndex(), title: state.mode === 'bot' ? 'Поле ESP32' : `Поле: ${playerName(opponentIndex())}`, key: 'opponent', targetable: canShoot() }
];
}
@@ -242,7 +250,8 @@
ui.tabs.replaceChildren();
definitions.forEach(definition => {
const tab = document.createElement('button');
tab.type = 'button'; tab.role = 'tab'; tab.textContent = definition.title;
tab.type = 'button'; tab.role = 'tab'; tab.textContent = definition.key === 'opponent' ? playerLabel(definition.index) : definition.title;
tab.title = definition.title; tab.setAttribute('aria-label', definition.title);
tab.setAttribute('aria-selected', String(definition.key === activeBoard));
tab.addEventListener('click', () => { activeBoard = definition.key; renderGame(); });
ui.tabs.append(tab);
@@ -253,7 +262,8 @@
function renderLobby() {
showScreen('lobby');
const playerOne = role === 'player1';
ui.lobbyDescription.textContent = role === 'spectator' ? 'Вы наблюдаете за подготовкой партии.' : 'Ожидайте готовности партии или настройте режим.';
const opponent = state.mode === 'bot' ? 'ESP32' : playerName(opponentIndex());
ui.lobbyDescription.textContent = role === 'spectator' ? 'Вы наблюдаете за подготовкой партии.' : state.mode === 'human' && info?.player2Available ? 'Ожидаем соперника.' : `Ожидаем готовности ${opponent}.`;
ui.modeControls.hidden = !playerOne;
if (playerOne) startLobbyInfoPolling();
else stopLobbyInfoPolling();
@@ -265,8 +275,8 @@
button.disabled = !playerOne;
});
ui.start.disabled = !canStart;
ui.modeDescription.textContent = state.mode === 'human' ? (playerTwoReady ? 'Второй игрок готов. Можно начать партию.' : 'Ожидаем подключения второго игрока.') : 'Игра против ESP32. Можно начать сразу.';
ui.lobbyHelp.textContent = playerOne ? '' : 'Игрок 1 выбирает режим и запускает партию.';
ui.modeDescription.textContent = state.mode === 'human' ? (playerTwoReady ? `${playerName(1)} готов. Можно начать партию.` : 'Ожидаем соперника.') : 'Игра против ESP32. Можно начать сразу.';
ui.lobbyHelp.textContent = playerOne ? '' : `${playerName(0)} выбирает режим и запускает партию.`;
}
function renderGame() {
@@ -275,7 +285,7 @@
showScreen('game');
if (canShoot() && !selectedTarget) activeBoard = 'opponent';
ui.turn.textContent = canShoot() ? 'Ваш ход' : `Ходит ${playerLabel(state.turn === 'player2' ? 1 : 0)}`;
ui.wins.textContent = `Победы: ${state.wins[0]} : ${state.wins[1]}`;
ui.wins.textContent = `${playerLabel(0)} ${state.wins[0]} : ${state.wins[1]} ${playerLabel(1)}`;
renderBoards(ui.boards);
const available = canShoot();
ui.shotControls.hidden = !isPlayer();
@@ -291,7 +301,7 @@
showScreen('result');
if (statisticsGameId !== state.gameId) refreshStatistics();
const waiting = state.phase === 'rematch_wait';
const winner = state.winner === 0 || state.winner === 1 ? `Победил ${playerLabel(state.winner)}. ` : '';
const winner = state.winner === ownIndex() && isPlayer() ? 'Вы победили. ' : state.winner === 0 || state.winner === 1 ? `Победил ${playerLabel(state.winner)}. ` : '';
const matchSummary = state.statistics.map((entry, index) => `${playerLabel(index)}: выстрелы ${entry[0]}, попадания ${entry[1]}, промахи ${entry[2]}, потоплено ${entry[3]}.`).join(' ');
const cumulative = cumulativeStatistics?.cumulative?.map((entry, index) => `${playerLabel(index)} всего: игр ${entry[0]}, побед ${entry[1]}, поражений ${entry[2]}, выстрелов ${entry[4]}, попаданий ${entry[5]}, промахов ${entry[6]}, потоплено ${entry[3]}.`).join(' ') || '';
ui.result.textContent = `${winner}${matchSummary} ${cumulative} ${waiting ? 'Ожидается подтверждение повторной игры.' : 'Поля раскрыты. Можно подтвердить повторную игру.'}`;
+1 -1
View File
@@ -19,7 +19,7 @@ h1, h2, h3, p { margin-top: 0; } h1 { margin-bottom: .2rem; font-size: clamp(1.7
.connection-layout { display: grid; gap: 1.25rem; } .connection-form { min-width: 0; } .connection-support { padding: 1rem; border: 1px solid #315a75; border-radius: .8rem; background: rgb(10 48 75 / 68%); } .connection-support h3 { margin-bottom: .65rem; } .join-steps { display: grid; gap: .55rem; margin: 0; padding-left: 1.35rem; color: #c8e9f8; } .join-steps li { padding-left: .15rem; } .availability-panel { margin-top: 1rem; padding-top: .9rem; border-top: 1px solid rgb(80 128 156 / 65%); } .availability-panel .eyebrow { margin-bottom: .45rem; }
#availability { display: grid; grid-template-columns: minmax(0, 1fr); gap: .45rem; margin-bottom: 0; } .availability-item { display: block; padding: .45rem .6rem; border-left: 2px solid #41708d; border-radius: .35rem; background: rgb(18 59 88 / 62%); }
.turn-status { margin-bottom: 0; color: #c8e9f8; font-weight: 650; } .score { margin: .15rem 0; padding: .5rem .75rem; border-radius: .6rem; background: #123b58; font-weight: 750; white-space: nowrap; }
.board-tabs { display: flex; gap: .5rem; margin: 1rem 0; } .board-tabs button { flex: 1; min-height: 2.5rem; background: #31536e; color: #f1f7ff; } .board-tabs button[aria-selected="true"] { background: #3ec6f0; color: #032035; }
.board-tabs { display: flex; gap: .5rem; margin: 1rem 0; } .board-tabs button { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-height: 2.5rem; background: #31536e; color: #f1f7ff; } .board-tabs button[aria-selected="true"] { background: #3ec6f0; color: #032035; }
.boards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; } .board { min-width: 0; padding: .75rem; border: 1px solid #50809c; border-radius: .8rem; background: #071f32; } .board h3 { margin: 0 0 .65rem; font-size: 1rem; }
.board-grid { display: grid; grid-template-columns: 1.15rem repeat(10, minmax(0, 1fr)); gap: 2px; width: 100%; aspect-ratio: 1.1; } .axis { display: grid; place-items: center; color: #b8d3e6; font-size: clamp(.52rem, 2.2vw, .72rem); font-weight: 700; }
.cell { position: relative; min-height: 0; padding: 0; border-radius: .12rem; border: 1px solid #4e87a6; background: #167aa6; color: #fff; font-size: clamp(.68rem, 3vw, 1.15rem); line-height: 1; } .cell.ship { background: #b2c8d6; color: #1c3442; } .cell.ship::before { content: "◆"; font-size: .8em; } .cell.miss::before { content: "•"; color: #08253a; font-size: 1.15em; } .cell.hit, .cell.sunk { background: #b83e42; color: #fff; } .cell.hit::before, .cell.sunk::before { content: "×"; font-size: 1.25em; font-weight: 900; } .cell.sunk { outline: 2px solid #ffe56a; outline-offset: -3px; } .cell.target { background: #0c648b; cursor: pointer; touch-action: manipulation; } .cell.target::after { content: ""; position: absolute; inset: 27%; border: 1px dotted #b8edff; border-radius: 50%; } .cell.own-water { background: #167aa6; } .cell.unavailable { border-style: dashed; background: #0d4666; opacity: .72; } .cell.selected { outline: 3px solid #ffe56a; outline-offset: -3px; box-shadow: inset 0 0 0 2px #061827; } .cell.selected::after { content: "◎"; inset: auto; border: 0; color: #ffe56a; font-size: 1.05em; font-weight: 900; } .cell[disabled] { cursor: default; }