diff --git a/.env b/.env index 576f078..1666370 100644 --- a/.env +++ b/.env @@ -1,4 +1,8 @@ # The port for the Node.js / Socket.io server PUBLIC_SERVER_PORT=8001 # The full URL for the client to connect to +<<<<<<< HEAD +PUBLIC_SERVER_URL=http://localhost:8000 +======= PUBLIC_SERVER_URL=http://localhost:8001 +>>>>>>> master diff --git a/package-lock.json b/package-lock.json index baba06a..49f594b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "fff", "version": "0.0.1", "dependencies": { - "dotenv": "^17.3.1", + "dotenv": "^17.4.2", "express": "^5.2.1", "express.js": "^1.0.0", "honeycomb-grid": "^4.1.5", @@ -21,10 +21,10 @@ "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "svelte": "^5.55.1", + "svelte": "^5.55.5", "svelte-check": "^4.3.3", "typescript": "^5.9.3", - "vite": "^7.3.1", + "vite": "^7.3.2", "vitest": "^4.0.15" } }, diff --git a/package.json b/package.json index b9fe00f..0e45b97 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "svelte": "^5.55.1", + "svelte": "^5.55.5", "svelte-check": "^4.3.3", "typescript": "^5.9.3", - "vite": "^7.3.1", + "vite": "^7.3.2", "vitest": "^4.0.15" }, "dependencies": { - "dotenv": "^17.3.1", + "dotenv": "^17.4.2", "express": "^5.2.1", "express.js": "^1.0.0", "honeycomb-grid": "^4.1.5", diff --git a/server/server.js b/server/server.js index b26a3fd..a252d17 100644 --- a/server/server.js +++ b/server/server.js @@ -112,13 +112,19 @@ app.post('/save-map', (req, res) => { app.post('/create-lobby', (req, res) => { const { map } = req.body; const gameId = uuidv4().substring(0, 6); // Shorter ID for easier sharing + const mode = req.body?.mode || 'multi'; + const isPublic = req.body?.isPublic || false; lobbies[gameId] = { players: {}, // Using object keyed by socket.id status: 'waiting', + full: false, turn: 1, // Tracks the current round number (starts at 1) activePlayer: null, // Tracks whose turn it is + isPublic: isPublic, fleets: {}, // Secret fleet positions { socketId: { alpha: {q,r}, beta: {q,r} } } assets: {}, // Tracks assets like fuel, special weapons, etc. + botMemory: { knownHits: [], firedShots: [] }, + mode: mode, history: [], // Stores a log of all moves/strikes for replay or reconnection fleetPlaced: {}, map: map || null @@ -126,6 +132,35 @@ app.post('/create-lobby', (req, res) => { res.json({ gameId, message: 'Lobby created!' }); }); +/** + * Matchmaking: Find a lobby that is 'waiting' and has exactly 1 player. + */ +app.get('/find-lobby', (req, res) => { + const gameId = Object.keys(lobbies).find(id => { + const lobby = lobbies[id]; + return ( + lobby.isPublic && + lobby.status === 'waiting' && + !lobby.full && + Object.keys(lobby.players).length === 1 && + lobby.mode === 'multi' + ); + }); + res.json({ gameId: gameId || null }); +}); + +/** + * Dequeue/Delete: Remove a lobby if a player cancels matchmaking or leaves. + */ +app.post('/delete-lobby', (req, res) => { + const { gameId } = req.body; + if (lobbies[gameId]) { + delete lobbies[gameId]; + return res.json({ success: true }); + } + res.status(404).json({ success: false, error: 'Lobby not found' }); +}); + // --- SOCKET.IO: GAME LOGIC --- diff --git a/server/socketHandler.js b/server/socketHandler.js index 4c54fdc..1a9b853 100644 --- a/server/socketHandler.js +++ b/server/socketHandler.js @@ -98,6 +98,27 @@ module.exports = (io, lobbies) => { } + function checkForActionConditions(lobby, socket){ + let result = true; + + if (!lobby ) { + socket.emit('error', 'Game does not exist'); + result = false; + } + + else if (lobby.status !== 'active') { + socket.emit('error', 'Game is not active'); + result = false; + } + + else if (lobby.activePlayer !== socket.id) { + socket.emit('error', 'It is not your turn.'); + result = false; + } + + return result; + } + const switchTurn = (gameId) => { const lobby = lobbies[gameId]; if (!lobby) return; @@ -405,6 +426,10 @@ module.exports = (io, lobbies) => { delete lobby.fleets[socketId]; if (lobby.assets) delete lobby.assets[socketId]; + if (Object.keys(lobby.players).length < 2) { + lobby.full = false; + } + if (lobby.status === 'active' && Object.keys(lobby.players).length > 0) { const winnerId = Object.keys(lobby.players)[0]; lobby.status = 'game_over'; @@ -449,6 +474,10 @@ module.exports = (io, lobbies) => { return; } + if (Object.keys(lobby.players).length == 1) { + lobby.full = true; + } + socket.join(gameId); lobby.players[socket.id] = { @@ -554,13 +583,16 @@ module.exports = (io, lobbies) => { // Handle the "Finish" - Strike Logic - socket.on('execute_strike', ({ gameId, targetHex, dieResult1, dieResult2 }) => { + socket.on('execute_strike', ({ gameId, sourceFleet, targetHex, dieResult1, dieResult2 }) => { const lobby = lobbies[gameId]; // Calculate the shortest distance from a living friendly fleet to the target hex const attackerFleets = lobby.fleets[socket.id]; + const firingFleet = attackerFleets[sourceFleet]; + console.log("fire" + firingFleet) const distances = []; + if (attackerFleets.alpha && attackerFleets.alpha.hp > 0) { distances.push(calculateHexDistance(attackerFleets.alpha, targetHex)); } @@ -570,6 +602,7 @@ module.exports = (io, lobbies) => { } const shortestDistance = distances.length > 0 ? Math.min(...distances) : Infinity; + lobby.lastAttackerPos = { q: firingFleet.q, r: firingFleet.r }; // Find the opponent const opponentId = Object.keys(lobby.players).find(id => id !== socket.id); @@ -659,13 +692,13 @@ module.exports = (io, lobbies) => { } const isSuccess = counterResult >= 3; + console.log("sad" + lobby.lastAttackerPos); io.to(gameId).emit('counter_result', { - success: isSuccess + success: isSuccess, + attackerPos : isSuccess ? lobby.lastAttackerPos : null }); - console.log("ASTFASTF"); - switchTurn(gameId); }); diff --git a/src/lib/+map.svelte b/src/lib/+map.svelte index cbcd715..f7dab7a 100644 --- a/src/lib/+map.svelte +++ b/src/lib/+map.svelte @@ -280,12 +280,12 @@ } }); - $socket.on("counter_result", ({success}) => { + $socket.on("counter_result", ({success, attackerPos}) => { console.log("COUNTTEERRRR RESULTTTTTT"); if(isMyTurn){ if (success) { - enemySearchedHexes = [...enemySearchedHexes, { q: sourceFleet.q, r: sourceFleet.r }]; + enemySearchedHexes = [...enemySearchedHexes, { q: attackerPos.q, r: attackerPos.r }]; triggerOverlay("WARNING: LOCATION COMPROMISED!", "fail"); addLog("WARNING: Enemy traced your firing signal!", "enemy"); setTimeout(() => handleTurnEnd(), 2000); @@ -294,10 +294,8 @@ } } else{ - console.log("not my turn"); if (success) { - const attacker = enemyFleets[Math.floor(Math.random() * enemyFleets.length)]; - friendlySearchedHexes = [...friendlySearchedHexes, { q: attacker.q, r: attacker.r }]; + friendlySearchedHexes = [...friendlySearchedHexes, { q: attackerPos.q, r: attackerPos.r }]; triggerOverlay("ENEMY SIGNAL TRACED!", "success"); } else { triggerOverlay("SIGNAL TRACE FAILED!", "fail"); @@ -408,7 +406,6 @@ const coordStr = `${String.fromCharCode(65 + hex.col)}-${hex.row + 1}`; // e.g., "A-1" - console.log("hiii" + targetEnemy); if(targetEnemy){ const friendlyFleet = fleetSelections.find(f => f.q === hex.q && f.r === hex.r); @@ -531,6 +528,7 @@ addLog("All fleets are positioned!", "system"); isPlacementLocked = true; if ($isMultiplayer) { + console.log("wooooo"); const fleetPositions = { alpha: { q: fleetSelections[0].q, r: fleetSelections[0].r }, beta: { q: fleetSelections[1].q, r: fleetSelections[1].r } }; $socket.emit('place_fleets', { gameId: $gameId, fleetPositions }); $socket.once('fleets_placed_confirmation', () => { $socket.emit('ready_check', {gameId: $gameId}); }); @@ -626,6 +624,9 @@ function resolveAttack() { if (!sourceFleet || !targetEnemy) return; + const fleetIndex = fleetSelections.findIndex(f => f.id === sourceFleet.id); + const fleetKey = fleetIndex === 0 ? 'alpha' : 'beta'; + const targetCoord = `${String.fromCharCode(65 + targetEnemy.col)}-${targetEnemy.row + 1}`; addLog(`[${sourceFleet.name}] firing on coordinates ${targetCoord}...`, "player"); @@ -646,6 +647,7 @@ if ($isMultiplayer) { $socket.emit('execute_strike', { gameId: $gameId, + sourceFleet: fleetKey, targetHex: { q: cachedTarget.q, r: cachedTarget.r }, dieResult1: roll1, dieResult2: roll2 diff --git a/src/lib/Sidebar.svelte b/src/lib/Sidebar.svelte index c589476..f0fc4e6 100644 --- a/src/lib/Sidebar.svelte +++ b/src/lib/Sidebar.svelte @@ -188,10 +188,10 @@ .panel-header { font-family: 'Chakra Petch', sans-serif; color: #abbbd1; - font-size: clamp(1.2rem, 3vh, 1.8rem); + font-size: clamp(0.6rem, 3vh, 1rem); border-bottom: 1px solid rgba(171, 187, 209, 0.3); padding-bottom: 10px; - letter-spacing: 2px; + letter-spacing: 1px; } /*Focus, Directional, Area, Activate*/ diff --git a/src/lib/StatusBar.svelte b/src/lib/StatusBar.svelte index ebb04e9..5e23d02 100644 --- a/src/lib/StatusBar.svelte +++ b/src/lib/StatusBar.svelte @@ -209,7 +209,7 @@ /*HEALTH and FUEL*/ .stat-label { font-family: 'Chakra Petch', sans-serif; - font-size: clamp(0.9rem, 3vh, 1.2rem); + font-size: clamp(0.4rem, 3vh, 0.8rem); color: #abbbd1; letter-spacing: 2px; font-weight: 700; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5cc8bf2..bcab043 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -11,6 +11,35 @@ let showSingleplayerModal = $state(false); let nickname = $state(''); let lobbyCode = $state(''); + let statusMessage = $state(''); // New state variable for messages + + // --- AUDIO STATE --- + let bgAudio = $state(null); + let isMuted = $state(false); + + function initBackgroundMusic() { + if (!bgAudio) { + bgAudio = new Audio('pirates.mp3'); // Path to your file in the static/ folder + bgAudio.loop = true; + bgAudio.volume = 0.4; + } + if (!isMuted) bgAudio.play().catch(err => console.log("Autoplay blocked until interaction", err)); + } + + function stopBackgroundMusic() { + if (bgAudio) { + bgAudio.pause(); + bgAudio.currentTime = 0; + bgAudio = null; + } + } + + function toggleMute() { + isMuted = !isMuted; + if (bgAudio) bgAudio.muted = isMuted; + } + + let isMatchmaking = $state(false); // Track if we are in the find-lobby queue let dotCount = $state(0); let serverError = $state(''); let dots = $derived('.'.repeat(dotCount)); @@ -52,6 +81,8 @@ modalStep = 0; nickname = ''; lobbyCode = ''; + isMatchmaking = false; + statusMessage = ''; // Reset status message selectedMap = ''; }, 200); return @@ -63,6 +94,8 @@ const handleRoomUpdate = ({ players, map }) => { if (Object.keys(players).length === 2) { + stopBackgroundMusic(); + goto("/multiplayer"); let url = "/multiplayer"; if (map) url += `?map=${encodeURIComponent(map)}`; goto(url); @@ -112,25 +145,26 @@ /** Confirms the nickname for the player*/ function confirmName() { - if (nickname.trim().length > 0){ - modalStep = 1; + if (nickname.trim().length > 0) { + modalStep = 1; + initBackgroundMusic(); playerName.set(nickname); - } + } + } /*****************BACKEND METHODS******************/ /** * Sends a POST request to the server to create a multiplayer lobby. */ - async function goToCreate() { + async function goToCreate(publicMode = false) { try { - isMultiplayer.set(true); + statusMessage = publicMode ? 'WAITING FOR OPPONENT...' : 'New lobby created. Share this ID:'; modalStep = 2; - const payload = selectedMap ? { map: selectedMap.replace('.json', '') } : {}; const res = await fetch(`${PUBLIC_SERVER_URL}/create-lobby`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: JSON.stringify({ mode: 'multi', isPublic: publicMode }) }); const data = await res.json(); lobbyCode = data.gameId; @@ -138,12 +172,16 @@ gameId.set(lobbyCode); $socket.emit('join_game', {gameId: lobbyCode, playerName: nickname}); } catch (e) { + statusMessage = 'Failed to create lobby.'; // Error message + modalStep = 2; // Still show modal with error console.error("Failed to create lobby", e); } } - async function fetchMaps() { + async function fetchMaps(multiplayer) { try { + isMultiplayer.set(multiplayer); + console.log("sdfasdf" + $isMultiplayer); const res = await fetch(`http://${window.location.hostname}:${PORT}/list-maps`); if (res.ok) { mapList = await res.json(); @@ -159,6 +197,8 @@ * ease of singleplayer testing. However server may not be needed for singleplayer. */ function goToGame() { + isMultiplayer.set(false); + stopBackgroundMusic(); let url = '/singleplayer'; if (selectedMap) { url += `?map=${encodeURIComponent(selectedMap.replace('.json', ''))}`; @@ -167,8 +207,37 @@ } /** - * Changes the modal to 3 in order to support multiplayer join lobby functionality. + * Automatically finds an available lobby or creates one if none exist. + * This connects two users without requiring manual code entry. */ + async function autoJoin() { + try { + isMultiplayer.set(true); + isMatchmaking = true; + statusMessage = 'ESTABLISHING SECURE CONNECTION...'; + modalStep = 2; + + // Attempt to find a lobby with an available slot (e.g., 1/2 players) + const res = await fetch(`${PUBLIC_SERVER_URL}/find-lobby`); + const data = await res.json(); + + if (data.gameId) { + lobbyCode = data.gameId; + connect(); // Autojoin the found lobby + } else { + // No lobby found, we become the first person in the "queue" + statusMessage = 'SEARCHING FOR COMMANDERS...'; + await goToCreate(true); + } + } catch (e) { + console.error("Failed to find or create lobby", e); + statusMessage = 'Failed to connect. Please try again.'; // Generic error message + setTimeout(() => { + modalStep = 1; + }, 1500); + } + } + function goToJoin() { modalStep = 3; serverError = ''; @@ -185,6 +254,8 @@ $socket.emit('join_game', {gameId: lobbyCode, playerName: nickname}); //$socket.onAny((eventName, ...args) => { //alert(`[SOCKET INBOUND] Event: ${eventName} and ${args}`); + statusMessage = `Attempting to join lobby: ${lobbyCode}`; + modalStep = 2; // Show the lobby code and status } @@ -193,6 +264,15 @@ * Revert Modal Step by one. */ function goBack() { + if (modalStep === 2 && isMatchmaking) { + // Dequeue: remove the lobby we created for matchmaking + fetch(`${PUBLIC_SERVER_URL}/delete-lobby`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gameId: lobbyCode }) + }); + isMatchmaking = false; + } if (modalStep > 1) modalStep = 1; else modalStep = 0; } @@ -332,7 +412,7 @@ {:else if modalStep === 2} -
{lobbyCode}
@@ -560,11 +646,38 @@
+
+
+