diff --git a/src/game.js b/src/game.js index 2ca84a6..a95fbcb 100644 --- a/src/game.js +++ b/src/game.js @@ -127,6 +127,7 @@ const network = { inputStateAtClient: { dir: null, bomb: false, + bombCell: null, }, lastInputSentAt: 0, activeRoster: [], @@ -712,7 +713,7 @@ function handleSocketMessage(raw) { if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") { return; } - network.remoteInputs.set(msg.playerId, msg.input || { dir: null, bomb: false }); + network.remoteInputs.set(msg.playerId, msg.input || { dir: null, bomb: false, bombCell: null }); return; } @@ -779,7 +780,7 @@ function leaveLobbyInternals() { network.activeRoster = []; state.notification.text = ""; state.notification.expiresAt = 0; - network.inputStateAtClient = { dir: null, bomb: false }; + network.inputStateAtClient = { dir: null, bomb: false, bombCell: null }; network.lastInputSentAt = 0; network.hasReceivedSnapshot = false; } @@ -975,22 +976,94 @@ function updateLobbyNetwork(dt) { } } +function sameBombCell(a, b) { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + return a.x === b.x && a.y === b.y; +} + +function resolveBombDropCell(player, preferredCell = null) { + const candidates = []; + if (player.moveProgress < 1) { + candidates.push({ x: player.moveFromX, y: player.moveFromY }); + if (player.moveToX !== player.moveFromX || player.moveToY !== player.moveFromY) { + candidates.push({ x: player.moveToX, y: player.moveToY }); + } + } else { + candidates.push({ x: player.x, y: player.y }); + } + + const canPlaceAt = (x, y) => { + if (!inBounds(x, y)) { + return false; + } + const tile = state.map[y]?.[x]; + if (!tile || tile.type !== "floor") { + return false; + } + return !getBombAt(x, y); + }; + + const isCandidateCell = (cell) => + candidates.some((candidate) => candidate.x === cell.x && candidate.y === cell.y); + + if (preferredCell && Number.isInteger(preferredCell.x) && Number.isInteger(preferredCell.y)) { + if (isCandidateCell(preferredCell) && canPlaceAt(preferredCell.x, preferredCell.y)) { + return { x: preferredCell.x, y: preferredCell.y }; + } + } + + const primary = + player.moveProgress < 1 && player.moveProgress >= 0.5 + ? { x: player.moveToX, y: player.moveToY } + : candidates[0]; + + if (primary && canPlaceAt(primary.x, primary.y)) { + return { x: primary.x, y: primary.y }; + } + + for (const candidate of candidates) { + if (canPlaceAt(candidate.x, candidate.y)) { + return { x: candidate.x, y: candidate.y }; + } + } + + return null; +} + function sendLocalMultiplayerInput(force = false) { if (state.mode !== "multiplayer" || state.screen !== "game" || network.isHost) { return; } const t = now(); + const localPlayer = state.players.find((player) => player.ownerId === network.clientId) || null; const nextInput = { dir: currentHumanDirection(), bomb: inputState.bombHeld, + bombCell: null, }; + if (nextInput.bomb && localPlayer) { + nextInput.bombCell = resolveBombDropCell(localPlayer); + } + const changed = - nextInput.dir !== network.inputStateAtClient.dir || nextInput.bomb !== network.inputStateAtClient.bomb; + nextInput.dir !== network.inputStateAtClient.dir || + nextInput.bomb !== network.inputStateAtClient.bomb || + !sameBombCell(nextInput.bombCell, network.inputStateAtClient.bombCell); const dueHeartbeat = t - network.lastInputSentAt > 0.12; if (!changed && !force && !dueHeartbeat) { return; } - network.inputStateAtClient = nextInput; + + network.inputStateAtClient = { + dir: nextInput.dir, + bomb: nextInput.bomb, + bombCell: nextInput.bombCell ? { ...nextInput.bombCell } : null, + }; network.lastInputSentAt = t; sendSocketMessage("player_input", { input: nextInput, ts: t }); } @@ -1032,18 +1105,20 @@ function queueDetonation(bombId) { state.pendingDetonations.push(bombId); } -function dropBomb(player) { +function dropBomb(player, preferredCell = null) { if (!player.alive || player.bombsPlaced >= player.bombCapacity) { return false; } - if (getBombAt(player.x, player.y)) { + + const dropCell = resolveBombDropCell(player, preferredCell); + if (!dropCell) { return false; } const bomb = { id: state.nextBombId, - x: player.x, - y: player.y, + x: dropCell.x, + y: dropCell.y, timer: CONFIG.bombFuse, range: player.flameRange, ownerId: player.id, @@ -1329,7 +1404,7 @@ function occupiedTiles(player) { } function getNetworkInput(ownerId) { - return network.remoteInputs.get(ownerId) || { dir: null, bomb: false }; + return network.remoteInputs.get(ownerId) || { dir: null, bomb: false, bombCell: null }; } function getDesiredDirection(player) { @@ -1371,8 +1446,9 @@ function updatePlayerMovement(player, dt) { player.renderY = player.y; } + const occupied = new Set(occupiedTiles(player)); for (const bomb of state.bombs) { - if (bomb.passThrough.has(player.id) && (player.x !== bomb.x || player.y !== bomb.y)) { + if (bomb.passThrough.has(player.id) && !occupied.has(tileKey(bomb.x, bomb.y))) { bomb.passThrough.delete(player.id); } } @@ -1585,7 +1661,7 @@ function updateGame(dt) { const remote = getNetworkInput(player.ownerId); const prevBomb = network.remoteBombPrev.get(player.ownerId) || false; if (remote.bomb && !prevBomb) { - dropBomb(player); + dropBomb(player, remote.bombCell || null); } network.remoteBombPrev.set(player.ownerId, remote.bomb); }