Ping測定用システム
let pingIntervalToken = null, lastPingSentTime = 0; function generateUniqueRoomBase() { return `${window.location.hostname.replace(/[^a-zA-Z0-9]/g, "") || "localplayer"}_fps_room_`; } const ROOM_BASE_NAME = generateUniqueRoomBase(); let currentRoomNumber = 1; const PEER_CONFIG = { config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }] } };②マッチングシステム
function startMatching() { if (isConnecting) return; isConnecting = true; const targetRoomId = ROOM_BASE_NAME + currentRoomNumber; statusEl.innerText = `ACCESSING ROOM ${currentRoomNumber}...`; if (peer) peer.destroy(); peer = new Peer(PEER_CONFIG); peer.on('open', () => { const conn = peer.connect(targetRoomId); const connectTimeout = setTimeout(() => { if (!activeConn) { conn.close(); peer.destroy(); initHost(targetRoomId); } }, 1500); conn.on('open', () => { clearTimeout(connectTimeout); setTimeout(() => { if (!activeConn) { isConnecting = false; playerRole = "GUEST"; goToLobby(conn, "GUEST"); } }, 500); }); conn.on('data', (data) => { if (data.type === "FULL") { clearTimeout(connectTimeout); conn.close(); currentRoomNumber++; isConnecting = false; startMatching(); } else { handleNetworkData(data); } }); peer.on('error', (err) => { clearTimeout(connectTimeout); if (err.type === 'peer-unavailable') { peer.destroy(); initHost(targetRoomId); } else { isConnecting = false; setTimeout(startMatching, 1000); } }); }); } function initHost(targetRoomId) { peer = new Peer(targetRoomId, PEER_CONFIG); peer.on('open', () => { statusEl.innerText = "AWAITING OPPONENT..."; }); peer.on('connection', (conn) => { if (activeConn) { conn.on('open', () => { conn.send({ type: "FULL" }); setTimeout(() => conn.close(), 500); }); return; } isConnecting = false; playerRole = "HOST"; goToLobby(conn, "HOST"); }); peer.on('error', (err) => { isConnecting = false; currentRoomNumber++; startMatching(); }); } function goToLobby(conn, role) { activeConn = conn; screenMatching.classList.add("hidden"); screenLobby.classList.remove("hidden"); matchPerfectEnded = false; document.getElementById("lobbyRoomName").innerText = `ROOM 0${currentRoomNumber} // ${role}`; document.getElementById("lobbyMyName").innerText = myPlayerName; setUIScreen("LOBBY"); if (activeConn) activeConn.send({ type: "NAME_EXCHANGE", name: myPlayerName }); conn.on('data', (data) => { handleNetworkData(data); }); conn.on('close', () => { handleOpponentDisconnect(); }); startLobbyTimer(); // Ping定期計測開始 clearInterval(pingIntervalToken); pingIntervalToken = setInterval(() => { if(activeConn && gameStarted) { lastPingSentTime = performance.now(); activeConn.send({ type: "NET_PING" }); } }, 2000); } function handleOpponentDisconnect() { if (matchPerfectEnded) return; clearInterval(lobbyTimerInterval); clearInterval(matchCountdownInterval); clearInterval(pingIntervalToken); cleanup3DGame(); screenLobby.classList.add("hidden"); blocker.classList.add("hidden"); crosshair.classList.add("hidden"); hudContainer.classList.add("hidden"); scoreBoard.classList.add("hidden"); roundOverlay.innerText = ""; vsOverlay.classList.add("hidden"); document.getElementById("resultTitle").innerText = "VICTORY"; document.getElementById("resultTitle").style.color = "#00ffaa"; document.getElementById("resultReason").innerText = "対戦相手が抜けました。不戦勝です。"; screenResult.classList.remove("hidden"); setUIScreen("RESULT"); } function resetToMenu() { clearInterval(pingIntervalToken); if (activeConn) { activeConn.close(); activeConn = null; } if (peer) { peer.destroy(); peer = null; } amIReady = false; isEnemyReady = false; gameStarted = false; isConnecting = false; lobbyTimeoutCount = 10; currentRoomNumber = 1; matchPerfectEnded = false; enemyPlayerName = "OPPONENT"; myStatusInd.innerText = "WAITING"; myStatusInd.classList.remove("ready"); enemyStatusInd.innerText = "WAITING"; enemyStatusInd.classList.remove("ready"); readyBtn.innerText = "READY"; readyBtn.classList.remove("is-ready"); readyBtn.classList.remove("hidden"); lobbyTimerEl.innerText = "AUTO START IN: 10s"; lobbyTimerEl.classList.remove("hidden"); countdownEl.classList.add("hidden"); document.getElementById("lobbyEnemyName").innerText = "ENEMY"; cleanup3DGame(); screenResult.classList.add("hidden"); blocker.classList.add("hidden"); crosshair.classList.add("hidden"); hudContainer.classList.add("hidden"); scoreBoard.classList.add("hidden"); roundOverlay.innerText = ""; vsOverlay.classList.add("hidden"); screenMenu.classList.remove("hidden"); setUIScreen("MENU"); } function startLobbyTimer() { lobbyTimeoutCount = 10; clearInterval(lobbyTimerInterval); lobbyTimerInterval = setInterval(() => { lobbyTimeoutCount--; lobbyTimerEl.innerText = `AUTO START IN: ${lobbyTimeoutCount}s`; if (lobbyTimeoutCount <= 0) { clearInterval(lobbyTimerInterval); startMatchCountdown(); } }, 1000); } readyBtn.addEventListener('click', () => { amIReady = !amIReady; readyBtn.innerText = amIReady ? "CANCEL READY" : "READY"; if (amIReady) { readyBtn.classList.add("is-ready"); myStatusInd.innerText = "READY"; myStatusInd.classList.add("ready"); } else { readyBtn.classList.remove("is-ready"); myStatusInd.innerText = "WAITING"; myStatusInd.classList.remove("ready"); } if (activeConn) activeConn.send({ type: "READY_CHANGE", isReady: amIReady }); checkDirectStart(); }); function handleNetworkData(data) { if (data.type === "NAME_EXCHANGE") { enemyPlayerName = data.name; document.getElementById("lobbyEnemyName").innerText = enemyPlayerName; } else if (data.type === "READY_CHANGE") { isEnemyReady = data.isReady; if (isEnemyReady) { enemyStatusInd.innerText = "READY"; enemyStatusInd.classList.add("ready"); } else { enemyStatusInd.innerText = "WAITING"; enemyStatusInd.classList.remove("ready"); } checkDirectStart(); } else if (data.type === "GAME_SYNC") { if (enemyPlayerMesh) { enemyPlayerMesh.position.set(data.pos.x, data.pos.y - 2.0, data.pos.z); enemyPlayerMesh.rotation.y = data.rotY; } } else if (data.type === "GAME_SHOOT") { spawnBulletLaser(new THREE.Vector3().copy(data.from), new THREE.Vector3().copy(data.to)); } else if (data.type === "GAME_GRENADE_SPAWN") { spawnNetworkGrenade(data.id, new THREE.Vector3().copy(data.pos), new THREE.Vector3().copy(data.vel)); } else if (data.type === "GAME_GRENADE_EXPLODE") { triggerGrenadeExplosionEffects(new THREE.Vector3().copy(data.pos)); } else if (data.type === "GAME_DAMAGE") { let currentPos = new THREE.Vector3(camera.position.x, camera.position.y - (isCrouching ? 0.9 : 0.3), camera.position.z); let hitOffset = new THREE.Vector3().copy(data.hitOffset); let predictedHitPoint = new THREE.Vector3().copy(currentPos).add(hitOffset); let distance = currentPos.distanceTo(predictedHitPoint); if (distance < 3.2) { processReceivedDamage(data.amount); } } else if (data.type === "GAME_EXPLOSION_DAMAGE") { let currentPos = new THREE.Vector3(camera.position.x, camera.position.y - (isCrouching ? 0.9 : 0.3), camera.position.z); let dist = currentPos.distanceTo(new THREE.Vector3().copy(data.pos)); if (dist < 12.0) { let damageCalc = Math.floor(65 * (1.0 - (dist / 12.0))); if (damageCalc > 5) processReceivedDamage(damageCalc); } } else if (data.type === "GAME_POPUP") { triggerFortnitePopup(data.amount, data.isShield, new THREE.Vector3().copy(data.pos)); } else if (data.type === "ROUND_WINNER") { processRoundEnd(data.winner); } else if (data.type === "GAME_OVER") { matchPerfectEnded = true; endGame(data.winner === playerRole); } else if (data.type === "NET_PING") { if(activeConn) activeConn.send({ type: "NET_PONG" }); } else if (data.type === "NET_PONG") { let rtt = performance.now() - lastPingSentTime; pingDisplayEl.innerText = `PING: ${Math.round(rtt)}ms`; } } function checkDirectStart() { if (amIReady && isEnemyReady) { clearInterval(lobbyTimerInterval); startMatchCountdown(); } } function startMatchCountdown() { clearInterval(lobbyTimerInterval); clearInterval(matchCountdownInterval); lobbyTimerEl.classList.add("hidden"); readyBtn.classList.add("hidden"); countdownEl.classList.remove("hidden"); let count = 3; countdownEl.innerText = count; matchCountdownInterval = setInterval(() => { count--; if (count > 0) { countdownEl.innerText = count; } else { clearInterval(matchCountdownInterval); screenLobby.classList.add("hidden"); triggerVSEffect(); } }, 1000); } function triggerVSEffect() { document.getElementById("vsPlayer1").innerText = myPlayerName; document.getElementById("vsPlayer2").innerText = enemyPlayerName; vsOverlay.classList.remove("hidden"); setTimeout(() => { vsOverlay.classList.add("hidden"); init3DGame(); }, 3000); }