diff --git a/src/game-v4.js b/src/game-v4.js index 281b175..24645f4 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -144,6 +144,8 @@ class GameScene extends Phaser.Scene { this.tradeDeliveries = 0; this.tradeLosses = 0; this.playerTradeLosses = 0; + this.remotePlayers = new Map(); + this.multiplayerSendAccumulatorMs = 0; airshipLog('scene:init', { hull: this.playerHull, @@ -182,6 +184,11 @@ class GameScene extends Phaser.Scene { this.player.setFrame(0); this.createPlayerSmoke(); + this.handleMultiplayerSnapshot = (event) => { + this.syncRemotePlayers(event.detail?.players || []); + }; + window.addEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot); + this.pirates = this.physics.add.group(); this.merchants = this.physics.add.group(); this.playerShots = this.physics.add.group(); @@ -276,6 +283,12 @@ class GameScene extends Phaser.Scene { registerDebugSnapshot(null); window.removeEventListener('airship:helm-vector', this.handleHelmVector); window.removeEventListener('airship:action', this.handleMobileAction); + window.removeEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot); + for (const remote of this.remotePlayers.values()) { + remote.sprite?.destroy(); + remote.label?.destroy(); + } + this.remotePlayers.clear(); airshipLog('scene:shutdown', { gameState: this.gameState, hull: this.playerHull, @@ -1401,6 +1414,7 @@ class GameScene extends Phaser.Scene { } this.updateProgressionInteraction(); + this.updateMultiplayer(deltaMs); if (this.dockOpen) { this.player.setVelocity(0, 0); const hudState = this.buildHudState(); @@ -1437,6 +1451,85 @@ class GameScene extends Phaser.Scene { } } + syncRemotePlayers(players = []) { + const seen = new Set(); + + for (const state of players) { + if (!state?.id) continue; + seen.add(state.id); + + let remote = this.remotePlayers.get(state.id); + if (!remote) { + const sprite = this.add.sprite( + Number(state.x) || this.world.spawn.x, + Number(state.y) || this.world.spawn.y, + 'player-airship', + 0, + ); + sprite + .setDepth(5.5) + .setDisplaySize(60, 114) + .setTint(0x8cd3ff) + .setAlpha(0.86); + sprite.play('player-forward'); + sprite.anims.timeScale = 0.72; + + const label = this.add.text(sprite.x, sprite.y - 70, state.name || 'CAPTAIN', { + fontFamily: 'monospace', + fontSize: '10px', + color: '#bfe6ff', + backgroundColor: '#071018aa', + padding: { x: 4, y: 2 }, + }).setOrigin(0.5).setDepth(12); + + remote = { + sprite, + label, + targetX: sprite.x, + targetY: sprite.y, + targetAngle: Number(state.angle) || 0, + hull: Number(state.hull) || 100, + }; + this.remotePlayers.set(state.id, remote); + } + + remote.targetX = clamp(Number(state.x) || 0, 0, this.world.width); + remote.targetY = clamp(Number(state.y) || 0, 0, this.world.height); + remote.targetAngle = normalizeDegrees(Number(state.angle) || 0); + remote.hull = Math.max(0, Number(state.hull) || 0); + remote.label.setText(`${state.name || 'CAPTAIN'} ${Math.round(remote.hull)}`); + } + + for (const [id, remote] of this.remotePlayers) { + if (seen.has(id)) continue; + remote.sprite?.destroy(); + remote.label?.destroy(); + this.remotePlayers.delete(id); + } + } + + updateMultiplayer(deltaMs) { + this.multiplayerSendAccumulatorMs += deltaMs; + if (this.multiplayerSendAccumulatorMs >= 100) { + this.multiplayerSendAccumulatorMs %= 100; + window.airshipMultiplayer?.sendState?.({ + x: this.player?.x || 0, + y: this.player?.y || 0, + angle: this.player?.angle || 0, + hull: this.playerHull, + }); + } + + for (const remote of this.remotePlayers.values()) { + const sprite = remote.sprite; + if (!sprite?.active) continue; + sprite.x = Phaser.Math.Linear(sprite.x, remote.targetX, 0.32); + sprite.y = Phaser.Math.Linear(sprite.y, remote.targetY, 0.32); + sprite.angle += signedAngleDelta(remote.targetAngle, sprite.angle) * 0.32; + remote.label.setPosition(sprite.x, sprite.y - 70); + } + } + updatePlayer(dt, deltaMs) { if (!this.player?.body) return; const stats = this.playerStats || deriveShipStats(this.progression?.loadout, this.progression?.installedConditions);