Use server authority for multiplayer movement and PvP
This commit is contained in:
parent
909abb1487
commit
711d9cd884
253
src/game-v4.js
253
src/game-v4.js
@ -145,6 +145,11 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.tradeLosses = 0;
|
this.tradeLosses = 0;
|
||||||
this.playerTradeLosses = 0;
|
this.playerTradeLosses = 0;
|
||||||
this.remotePlayers = new Map();
|
this.remotePlayers = new Map();
|
||||||
|
this.multiplayerProjectiles = new Map();
|
||||||
|
this.multiplayerAuthoritativeSelf = null;
|
||||||
|
this.multiplayerHasAuthoritativeState = false;
|
||||||
|
this.multiplayerDead = false;
|
||||||
|
this.multiplayerLastHull = null;
|
||||||
this.multiplayerSendAccumulatorMs = 0;
|
this.multiplayerSendAccumulatorMs = 0;
|
||||||
|
|
||||||
airshipLog('scene:init', {
|
airshipLog('scene:init', {
|
||||||
@ -185,9 +190,22 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.createPlayerSmoke();
|
this.createPlayerSmoke();
|
||||||
|
|
||||||
this.handleMultiplayerSnapshot = (event) => {
|
this.handleMultiplayerSnapshot = (event) => {
|
||||||
this.syncRemotePlayers(event.detail?.players || []);
|
this.syncMultiplayerSnapshot(event.detail || {});
|
||||||
|
};
|
||||||
|
this.handleMultiplayerState = (event) => {
|
||||||
|
if (event.detail?.state === 'online') return;
|
||||||
|
this.clearMultiplayerEntities();
|
||||||
|
this.multiplayerAuthoritativeSelf = null;
|
||||||
|
this.multiplayerHasAuthoritativeState = false;
|
||||||
|
this.multiplayerDead = false;
|
||||||
|
this.multiplayerLastHull = null;
|
||||||
|
if (this.player && this.gameState === 'playing') {
|
||||||
|
this.player.setVisible(true);
|
||||||
|
this.playerHull = Math.max(1, this.playerHull || this.playerStats?.maxHull || PLAYER_MAX_HULL);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot);
|
window.addEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot);
|
||||||
|
window.addEventListener('airship:multiplayer-state', this.handleMultiplayerState);
|
||||||
|
|
||||||
this.pirates = this.physics.add.group();
|
this.pirates = this.physics.add.group();
|
||||||
this.merchants = this.physics.add.group();
|
this.merchants = this.physics.add.group();
|
||||||
@ -284,11 +302,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
window.removeEventListener('airship:helm-vector', this.handleHelmVector);
|
window.removeEventListener('airship:helm-vector', this.handleHelmVector);
|
||||||
window.removeEventListener('airship:action', this.handleMobileAction);
|
window.removeEventListener('airship:action', this.handleMobileAction);
|
||||||
window.removeEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot);
|
window.removeEventListener('airship:multiplayer-snapshot', this.handleMultiplayerSnapshot);
|
||||||
for (const remote of this.remotePlayers.values()) {
|
window.removeEventListener('airship:multiplayer-state', this.handleMultiplayerState);
|
||||||
remote.sprite?.destroy();
|
this.clearMultiplayerEntities();
|
||||||
remote.label?.destroy();
|
|
||||||
}
|
|
||||||
this.remotePlayers.clear();
|
|
||||||
airshipLog('scene:shutdown', {
|
airshipLog('scene:shutdown', {
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
hull: this.playerHull,
|
hull: this.playerHull,
|
||||||
@ -831,6 +846,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
// Airships can cross islands, but lose momentum while flying through the terrain envelope.
|
// Airships can cross islands, but lose momentum while flying through the terrain envelope.
|
||||||
this.physics.add.overlap(this.player, obstacle, () => {
|
this.physics.add.overlap(this.player, obstacle, () => {
|
||||||
|
if (window.airshipMultiplayer?.isConnected?.()) return;
|
||||||
this.playerSpeed *= 0.985;
|
this.playerSpeed *= 0.985;
|
||||||
});
|
});
|
||||||
this.physics.add.overlap(
|
this.physics.add.overlap(
|
||||||
@ -1451,6 +1467,71 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMultiplayerEntities() {
|
||||||
|
for (const remote of this.remotePlayers.values()) {
|
||||||
|
remote.sprite?.destroy();
|
||||||
|
remote.label?.destroy();
|
||||||
|
}
|
||||||
|
this.remotePlayers.clear();
|
||||||
|
for (const projectile of this.multiplayerProjectiles.values()) {
|
||||||
|
projectile.sprite?.destroy();
|
||||||
|
}
|
||||||
|
this.multiplayerProjectiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMultiplayerSnapshot(snapshot = {}) {
|
||||||
|
this.syncAuthoritativeSelf(snapshot.self || null);
|
||||||
|
this.syncRemotePlayers(snapshot.players || []);
|
||||||
|
this.syncMultiplayerProjectiles(snapshot.projectiles || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAuthoritativeSelf(state) {
|
||||||
|
if (!state || !this.player) return;
|
||||||
|
|
||||||
|
const wasDead = this.multiplayerDead;
|
||||||
|
const firstState = !this.multiplayerHasAuthoritativeState;
|
||||||
|
const previousHull = this.multiplayerLastHull;
|
||||||
|
this.multiplayerAuthoritativeSelf = { ...state };
|
||||||
|
this.multiplayerHasAuthoritativeState = true;
|
||||||
|
this.multiplayerDead = state.alive === false;
|
||||||
|
this.multiplayerLastHull = Math.max(0, Number(state.hull) || 0);
|
||||||
|
|
||||||
|
if (this.multiplayerDead) {
|
||||||
|
if (!wasDead) {
|
||||||
|
this.spawnExplosion(this.player.x, this.player.y, 1.2);
|
||||||
|
this.status.setText(`SHOT DOWN${state.lastKiller ? ` BY ${state.lastKiller}` : ''}\nRESPAWNING...`);
|
||||||
|
}
|
||||||
|
this.playerHull = 0;
|
||||||
|
this.playerSpeed = 0;
|
||||||
|
this.player.setVelocity(0, 0);
|
||||||
|
this.player.setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstState || wasDead) {
|
||||||
|
this.player.setVisible(true);
|
||||||
|
this.player.setPosition(Number(state.x) || this.world.spawn.x, Number(state.y) || this.world.spawn.y);
|
||||||
|
this.player.angle = Number(state.angle) || 0;
|
||||||
|
this.playerSpeed = Number(state.speed) || 0;
|
||||||
|
this.playerHull = Math.max(0, Number(state.hull) || PLAYER_MAX_HULL);
|
||||||
|
if (wasDead) {
|
||||||
|
this.status.setText('REDEPLOYED');
|
||||||
|
this.time.delayedCall(900, () => {
|
||||||
|
if (this.gameState === 'playing' && !this.multiplayerDead) this.status.setText('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousHull != null && state.hull < previousHull) {
|
||||||
|
const damage = previousHull - state.hull;
|
||||||
|
this.playerHitSequence += 1;
|
||||||
|
this.spawnExplosion(this.player.x, this.player.y, 0.32);
|
||||||
|
this.playPlayerHitReaction(damage);
|
||||||
|
}
|
||||||
|
this.playerHull = Math.max(0, Number(state.hull) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
syncRemotePlayers(players = []) {
|
syncRemotePlayers(players = []) {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
@ -1488,16 +1569,23 @@ class GameScene extends Phaser.Scene {
|
|||||||
targetX: sprite.x,
|
targetX: sprite.x,
|
||||||
targetY: sprite.y,
|
targetY: sprite.y,
|
||||||
targetAngle: Number(state.angle) || 0,
|
targetAngle: Number(state.angle) || 0,
|
||||||
hull: Number(state.hull) || 100,
|
hull: Number(state.hull) || PLAYER_MAX_HULL,
|
||||||
|
alive: state.alive !== false,
|
||||||
};
|
};
|
||||||
this.remotePlayers.set(state.id, remote);
|
this.remotePlayers.set(state.id, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wasAlive = remote.alive;
|
||||||
remote.targetX = clamp(Number(state.x) || 0, 0, this.world.width);
|
remote.targetX = clamp(Number(state.x) || 0, 0, this.world.width);
|
||||||
remote.targetY = clamp(Number(state.y) || 0, 0, this.world.height);
|
remote.targetY = clamp(Number(state.y) || 0, 0, this.world.height);
|
||||||
remote.targetAngle = normalizeDegrees(Number(state.angle) || 0);
|
remote.targetAngle = normalizeDegrees(Number(state.angle) || 0);
|
||||||
remote.hull = Math.max(0, Number(state.hull) || 0);
|
remote.hull = Math.max(0, Number(state.hull) || 0);
|
||||||
remote.label.setText(`${state.name || 'CAPTAIN'} ${Math.round(remote.hull)}`);
|
remote.alive = state.alive !== false;
|
||||||
|
if (wasAlive && !remote.alive) this.spawnExplosion(remote.sprite.x, remote.sprite.y, 1.05);
|
||||||
|
remote.sprite.setVisible(remote.alive);
|
||||||
|
remote.label.setText(remote.alive
|
||||||
|
? `${state.name || 'CAPTAIN'} ${Math.round(remote.hull)}HP K${state.kills || 0}/D${state.deaths || 0}`
|
||||||
|
: `${state.name || 'CAPTAIN'} • DOWN`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, remote] of this.remotePlayers) {
|
for (const [id, remote] of this.remotePlayers) {
|
||||||
@ -1508,16 +1596,89 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncMultiplayerProjectiles(projectiles = []) {
|
||||||
|
const seen = new Set();
|
||||||
|
const selfId = window.airshipMultiplayer?.getSelfId?.();
|
||||||
|
|
||||||
|
for (const state of projectiles) {
|
||||||
|
if (!state?.id) continue;
|
||||||
|
seen.add(state.id);
|
||||||
|
let projectile = this.multiplayerProjectiles.get(state.id);
|
||||||
|
if (!projectile) {
|
||||||
|
const sprite = this.add.image(Number(state.x) || 0, Number(state.y) || 0, 'cannonball');
|
||||||
|
sprite
|
||||||
|
.setDepth(8)
|
||||||
|
.setDisplaySize(14, 14)
|
||||||
|
.setTint(state.ownerId === selfId ? 0xffdf91 : 0x8cd3ff);
|
||||||
|
projectile = { sprite, vx: 0, vy: 0 };
|
||||||
|
this.multiplayerProjectiles.set(state.id, projectile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetX = Number(state.x) || 0;
|
||||||
|
const targetY = Number(state.y) || 0;
|
||||||
|
const error = Math.hypot(projectile.sprite.x - targetX, projectile.sprite.y - targetY);
|
||||||
|
if (error > 90) {
|
||||||
|
projectile.sprite.setPosition(targetX, targetY);
|
||||||
|
} else {
|
||||||
|
projectile.sprite.x = Phaser.Math.Linear(projectile.sprite.x, targetX, 0.48);
|
||||||
|
projectile.sprite.y = Phaser.Math.Linear(projectile.sprite.y, targetY, 0.48);
|
||||||
|
}
|
||||||
|
projectile.vx = Number(state.vx) || 0;
|
||||||
|
projectile.vy = Number(state.vy) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, projectile] of this.multiplayerProjectiles) {
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
projectile.sprite?.destroy();
|
||||||
|
this.multiplayerProjectiles.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplayerControlInput() {
|
||||||
|
if (!this.cursors || !this.keys) return { throttle: 0, steer: 0, stop: false, docked: true };
|
||||||
|
const rawHelmX = Math.abs(this.helmVector.x) > HELM_DEAD_ZONE ? this.helmVector.x : 0;
|
||||||
|
const helmX = Math.sign(rawHelmX) * Math.pow(Math.abs(rawHelmX), HELM_STEER_CURVE);
|
||||||
|
const helmY = Math.abs(this.helmVector.y) > HELM_DEAD_ZONE ? this.helmVector.y : 0;
|
||||||
|
const throttleForward = this.cursors.up.isDown ? 1 : Math.max(0, -helmY);
|
||||||
|
const throttleReverse = this.cursors.down.isDown ? 1 : Math.max(0, helmY);
|
||||||
|
const keyboardTurn = (this.cursors.right.isDown ? 1 : 0) - (this.cursors.left.isDown ? 1 : 0);
|
||||||
|
return {
|
||||||
|
throttle: clamp(throttleForward - throttleReverse, -1, 1),
|
||||||
|
steer: keyboardTurn || helmX,
|
||||||
|
stop: this.keys.stop.isDown,
|
||||||
|
docked: this.dockOpen || this.shipyardOpen || this.multiplayerDead,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
updateMultiplayer(deltaMs) {
|
updateMultiplayer(deltaMs) {
|
||||||
|
const online = Boolean(window.airshipMultiplayer?.isConnected?.());
|
||||||
|
if (!online) return;
|
||||||
|
|
||||||
this.multiplayerSendAccumulatorMs += deltaMs;
|
this.multiplayerSendAccumulatorMs += deltaMs;
|
||||||
if (this.multiplayerSendAccumulatorMs >= 100) {
|
if (this.multiplayerSendAccumulatorMs >= 50) {
|
||||||
this.multiplayerSendAccumulatorMs %= 100;
|
this.multiplayerSendAccumulatorMs %= 50;
|
||||||
window.airshipMultiplayer?.sendState?.({
|
window.airshipMultiplayer?.sendInput?.(this.multiplayerControlInput());
|
||||||
x: this.player?.x || 0,
|
}
|
||||||
y: this.player?.y || 0,
|
|
||||||
angle: this.player?.angle || 0,
|
const authoritative = this.multiplayerAuthoritativeSelf;
|
||||||
hull: this.playerHull,
|
if (authoritative && !this.multiplayerDead && this.player?.visible) {
|
||||||
});
|
const targetX = Number(authoritative.x) || 0;
|
||||||
|
const targetY = Number(authoritative.y) || 0;
|
||||||
|
const positionError = Math.hypot(this.player.x - targetX, this.player.y - targetY);
|
||||||
|
if (positionError > 180) {
|
||||||
|
this.player.setPosition(targetX, targetY);
|
||||||
|
} else {
|
||||||
|
this.player.x = Phaser.Math.Linear(this.player.x, targetX, 0.14);
|
||||||
|
this.player.y = Phaser.Math.Linear(this.player.y, targetY, 0.14);
|
||||||
|
}
|
||||||
|
this.player.angle += signedAngleDelta(Number(authoritative.angle) || 0, this.player.angle) * 0.16;
|
||||||
|
this.playerSpeed = Phaser.Math.Linear(this.playerSpeed, Number(authoritative.speed) || 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dt = Math.min(deltaMs / 1000, 0.05);
|
||||||
|
for (const projectile of this.multiplayerProjectiles.values()) {
|
||||||
|
projectile.sprite.x += projectile.vx * dt;
|
||||||
|
projectile.sprite.y += projectile.vy * dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const remote of this.remotePlayers.values()) {
|
for (const remote of this.remotePlayers.values()) {
|
||||||
@ -1530,8 +1691,52 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMultiplayerPredictedPlayer(dt, deltaMs) {
|
||||||
|
if (!this.player?.body || this.multiplayerDead) return;
|
||||||
|
const input = this.multiplayerControlInput();
|
||||||
|
|
||||||
|
if (input.throttle > 0) this.playerSpeed += PLAYER_ACCELERATION * input.throttle * dt;
|
||||||
|
if (input.throttle < 0) this.playerSpeed += PLAYER_BRAKE * input.throttle * dt;
|
||||||
|
if (input.stop) this.playerSpeed *= Math.pow(0.025, dt);
|
||||||
|
if (input.throttle === 0 && !input.stop) {
|
||||||
|
this.playerSpeed *= Math.pow(PLAYER_COAST_DRAG, deltaMs / 16.667);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerSpeed = clamp(this.playerSpeed, PLAYER_REVERSE_SPEED, PLAYER_MAX_SPEED);
|
||||||
|
const turnStrength = clamp(Math.abs(this.playerSpeed) / 100, 0.28, 1);
|
||||||
|
const directionSign = this.playerSpeed < 0 ? -1 : 1;
|
||||||
|
this.player.angle += PLAYER_TURN_RATE * turnStrength * directionSign * input.steer * dt;
|
||||||
|
|
||||||
|
const effectiveForwardThrust = clamp(this.playerSpeed / PLAYER_MAX_SPEED, 0, 1);
|
||||||
|
if (effectiveForwardThrust > 0.06) {
|
||||||
|
if (!this.player.anims.isPlaying || this.player.anims.currentAnim?.key !== 'player-forward') {
|
||||||
|
this.player.play('player-forward');
|
||||||
|
}
|
||||||
|
this.player.anims.timeScale = 0.65 + effectiveForwardThrust * 1.35;
|
||||||
|
} else {
|
||||||
|
if (this.player.anims.isPlaying) this.player.anims.stop();
|
||||||
|
this.player.setFrame(0);
|
||||||
|
this.player.anims.timeScale = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.environmentWind = environmentWindAt({
|
||||||
|
x: this.player.x,
|
||||||
|
y: this.player.y,
|
||||||
|
timeMs: this.time.now,
|
||||||
|
islands: this.world?.islands,
|
||||||
|
});
|
||||||
|
const h = headingRad(this.player);
|
||||||
|
this.player.setVelocity(Math.cos(h) * this.playerSpeed, Math.sin(h) * this.playerSpeed);
|
||||||
|
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.keys.firePort)) this.playerBroadside(-1);
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.keys.fireStarboard)) this.playerBroadside(1);
|
||||||
|
}
|
||||||
updatePlayer(dt, deltaMs) {
|
updatePlayer(dt, deltaMs) {
|
||||||
if (!this.player?.body) return;
|
if (!this.player?.body) return;
|
||||||
|
if (window.airshipMultiplayer?.isConnected?.()) {
|
||||||
|
this.updateMultiplayerPredictedPlayer(dt, deltaMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stats = this.playerStats || deriveShipStats(this.progression?.loadout, this.progression?.installedConditions);
|
const stats = this.playerStats || deriveShipStats(this.progression?.loadout, this.progression?.installedConditions);
|
||||||
|
|
||||||
const rawHelmX = Math.abs(this.helmVector.x) > HELM_DEAD_ZONE ? this.helmVector.x : 0;
|
const rawHelmX = Math.abs(this.helmVector.x) > HELM_DEAD_ZONE ? this.helmVector.x : 0;
|
||||||
@ -1673,7 +1878,10 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
playerBroadside(side) {
|
playerBroadside(side) {
|
||||||
const broadside = side < 0 ? 'port' : 'starboard';
|
const broadside = side < 0 ? 'port' : 'starboard';
|
||||||
if (this.time.now - this.lastPlayerShot[broadside] < this.playerBroadsideReloadMs) return;
|
const multiplayerOnline = Boolean(window.airshipMultiplayer?.isConnected?.());
|
||||||
|
const reloadMs = multiplayerOnline ? PLAYER_BROADSIDE_RELOAD_MS : this.playerBroadsideReloadMs;
|
||||||
|
if (this.time.now - this.lastPlayerShot[broadside] < reloadMs) return;
|
||||||
|
if (multiplayerOnline) window.airshipMultiplayer?.fire?.(broadside);
|
||||||
this.fireBroadside(
|
this.fireBroadside(
|
||||||
this.player,
|
this.player,
|
||||||
side,
|
side,
|
||||||
@ -1706,6 +1914,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
const y = ship.y + Math.sin(base) * 34;
|
const y = ship.y + Math.sin(base) * 34;
|
||||||
const shot = group.create(x, y, texture);
|
const shot = group.create(x, y, texture);
|
||||||
shot.setDepth(8).setDisplaySize(14, 14);
|
shot.setDepth(8).setDisplaySize(14, 14);
|
||||||
|
if (actor === 'player' && window.airshipMultiplayer?.isConnected?.()) {
|
||||||
|
shot.setVisible(false);
|
||||||
|
}
|
||||||
shot.body.setAllowGravity(false);
|
shot.body.setAllowGravity(false);
|
||||||
shot.setData('bornAt', this.time.now);
|
shot.setData('bornAt', this.time.now);
|
||||||
shot.setData('damage', projectileDamage);
|
shot.setData('damage', projectileDamage);
|
||||||
@ -1785,6 +1996,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
hitPlayer(first, second) {
|
hitPlayer(first, second) {
|
||||||
if (this.gameState !== 'playing') return;
|
if (this.gameState !== 'playing') return;
|
||||||
|
// Online player damage is owned by the Go server; local pirates remain client-side until AI migrates.
|
||||||
|
if (window.airshipMultiplayer?.isConnected?.()) return;
|
||||||
|
|
||||||
const shot = resolveTaggedProjectile(first, second, 'pirate');
|
const shot = resolveTaggedProjectile(first, second, 'pirate');
|
||||||
if (!shot?.active || shot === this.player) {
|
if (!shot?.active || shot === this.player) {
|
||||||
@ -1923,6 +2136,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateLivePlayerState() {
|
validateLivePlayerState() {
|
||||||
|
if (this.multiplayerDead) return;
|
||||||
if (this.gameState !== 'playing' || this.playerHull <= 0 || !this.player) return;
|
if (this.gameState !== 'playing' || this.playerHull <= 0 || !this.player) return;
|
||||||
|
|
||||||
const issues = [];
|
const issues = [];
|
||||||
@ -2037,7 +2251,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
kills: this.defeated,
|
kills: this.defeated,
|
||||||
player: {
|
player: {
|
||||||
hull: this.playerHull,
|
hull: this.playerHull,
|
||||||
maxHull: this.playerStats?.maxHull || PLAYER_MAX_HULL,
|
maxHull: window.airshipMultiplayer?.isConnected?.()
|
||||||
|
? this.multiplayerAuthoritativeSelf?.maxHull || PLAYER_MAX_HULL
|
||||||
|
: this.playerStats?.maxHull || PLAYER_MAX_HULL,
|
||||||
hitSequence: this.playerHitSequence,
|
hitSequence: this.playerHitSequence,
|
||||||
},
|
},
|
||||||
target: nearest ? {
|
target: nearest ? {
|
||||||
@ -2251,6 +2467,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkGameState() {
|
checkGameState() {
|
||||||
|
if (window.airshipMultiplayer?.isConnected?.()) return;
|
||||||
if (this.playerHull <= 0) this.finishGame();
|
if (this.playerHull <= 0) this.finishGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user