From 8b5ba130d9bf9431d4072f667a2c0a34d79b5ef1 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 20 Sep 2026 19:06:45 +0000 Subject: [PATCH] Wire procedural world and progression into gameplay --- src/environment-physics.js | 3 +- src/game-v4.js | 356 ++++++++++++++++++++++++++++++++----- 2 files changed, 309 insertions(+), 50 deletions(-) diff --git a/src/environment-physics.js b/src/environment-physics.js index 9c76321..e30618f 100644 --- a/src/environment-physics.js +++ b/src/environment-physics.js @@ -69,6 +69,7 @@ export const environmentWindAt = ({ x = 0, y = 0, timeMs = 0, + islands = ENVIRONMENT_ISLANDS, } = {}) => { const time = timeMs / 1000; const pattern = semiRandomWindPattern(timeMs); @@ -82,7 +83,7 @@ export const environmentWindAt = ({ let windX = Math.cos(localAngle) * localStrength; let windY = Math.sin(localAngle) * localStrength; - for (const island of ENVIRONMENT_ISLANDS) { + for (const island of islands || ENVIRONMENT_ISLANDS) { const dx = x - island.x; const dy = y - island.y; const distance = Math.hypot(dx, dy); diff --git a/src/game-v4.js b/src/game-v4.js index 01a9a69..04f9674 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -23,8 +23,24 @@ import { environmentWindAt, velocityWithEnvironment, } from './environment-physics.js'; +import { + generateWorld, + nearestPort, + pirateInfluenceAt, + pirateSpawnPointsForWave, +} from './world/world-generator.js'; +import { + bestAffordableMarketUpgrade, + buyAndInstallModule, + collectSalvage, + createStarterProgression, + deriveShipStats, + installModule, + salvageForPirate, +} from './progression/ship-loadout.js'; +import { MODULE_CATALOG } from './progression/modules.js'; -const WORLD_SIZE = 2400; +const WORLD_SIZE = 3600; const PLAYER_MAX_SPEED = 180; const PLAYER_REVERSE_SPEED = -55; const PLAYER_ACCELERATION = 92; @@ -69,11 +85,20 @@ class GameScene extends Phaser.Scene { } init() { + const querySeed = new URLSearchParams(window.location.search).get('seed'); + this.world = generateWorld({ + seed: querySeed || 'SKYWARD-001', + width: WORLD_SIZE, + height: WORLD_SIZE, + }); + this.progression = createStarterProgression(); + this.playerStats = deriveShipStats(this.progression.loadout); this.playerSpeed = 0; - this.playerHull = PLAYER_MAX_HULL; + this.playerHull = this.playerStats.maxHull; this.gameState = 'playing'; this.lastPlayerShot = { port: -Infinity, starboard: -Infinity }; this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS; + this.playerBroadsideReloadMs = this.playerStats.reloadMs; this.helmVector = { x: 0, y: 0 }; this.pendingPlayerHits = []; this.playerHitSequence = 0; @@ -83,10 +108,12 @@ class GameScene extends Phaser.Scene { this.lastHudPublishAt = -Infinity; this.wave = 1; this.defeated = 0; + this.salvageSequence = 0; airshipLog('scene:init', { hull: this.playerHull, wave: this.wave, + worldSeed: this.world.seed, }); } @@ -107,12 +134,13 @@ class GameScene extends Phaser.Scene { } create() { - this.physics.world.setBounds(0, 0, WORLD_SIZE, WORLD_SIZE); + this.physics.world.setBounds(0, 0, this.world.width, this.world.height); this.drawWorld(); this.createRollingClouds(); this.createAnimations(); this.player = this.physics.add.sprite(1200, 1550, 'player-airship', 0); + this.player.setPosition(this.world.spawn.x, this.world.spawn.y); this.player.setDepth(6).setCollideWorldBounds(true).setDisplaySize(66, 126); this.player.body.setAllowGravity(false); this.player.body.setCircle(44, 20, 84); @@ -122,12 +150,15 @@ class GameScene extends Phaser.Scene { this.pirates = this.physics.add.group(); this.playerShots = this.physics.add.group(); this.pirateShots = this.physics.add.group(); + this.salvageWrecks = this.physics.add.group(); this.arcGraphics = this.add.graphics().setDepth(4); + this.createSalvageTexture(); this.createEnvironmentPhysics(); this.physics.add.overlap(this.playerShots, this.pirates, this.hitPirate, null, this); this.physics.add.overlap(this.pirateShots, this.pirates, this.hitPirate, null, this); this.physics.add.overlap(this.pirateShots, this.player, this.hitPlayer, null, this); + this.physics.add.overlap(this.player, this.salvageWrecks, this.collectSalvageWreck, null, this); registerDebugSnapshot(() => ({ gameState: this.gameState, @@ -140,6 +171,10 @@ class GameScene extends Phaser.Scene { wave: this.wave, defeated: this.defeated, environmentWind: this.environmentWind, + worldSeed: this.world?.seed, + credits: this.progression?.credits, + scrap: this.progression?.scrap, + salvageInventory: this.progression?.inventory?.length || 0, now: this.time?.now ?? null, player: this.player ? { x: Math.round(this.player.x), @@ -154,7 +189,10 @@ class GameScene extends Phaser.Scene { airshipLog('scene:create', { hull: this.playerHull, wave: this.wave, - worldSize: WORLD_SIZE, + worldSize: this.world.width, + ports: this.world.ports.length, + tradeRoutes: this.world.tradeRoutes.length, + pirateTerritories: this.world.pirateTerritories.length, }); this.cursors = this.input.keyboard.createCursorKeys(); @@ -168,9 +206,11 @@ class GameScene extends Phaser.Scene { fireStarboard: Phaser.Input.Keyboard.KeyCodes.E, stop: Phaser.Input.Keyboard.KeyCodes.SPACE, restart: Phaser.Input.Keyboard.KeyCodes.R, + buyUpgrade: Phaser.Input.Keyboard.KeyCodes.B, + fitSalvage: Phaser.Input.Keyboard.KeyCodes.U, }); - this.cameras.main.setBounds(0, 0, WORLD_SIZE, WORLD_SIZE); + this.cameras.main.setBounds(0, 0, this.world.width, this.world.height); this.cameras.main.startFollow(this.player, true, 0.08, 0.08); this.cameras.main.setZoom(1.12); @@ -274,6 +314,7 @@ class GameScene extends Phaser.Scene { x: cloud.x, y: cloud.y, timeMs: now, + islands: this.world?.islands, }); const driftScale = cloud.getData('driftScale') || 1; cloud.x += wind.x * driftScale * dt; @@ -375,6 +416,130 @@ class GameScene extends Phaser.Scene { } } + + createSalvageTexture() { + if (this.textures.exists('salvage-wreck')) return; + + const wreckTexture = this.make.graphics({ x: 0, y: 0, add: false }); + wreckTexture.fillStyle(0x70543a, 0.95); + wreckTexture.fillCircle(11, 11, 8); + wreckTexture.lineStyle(2, 0xd2aa6d, 0.9); + wreckTexture.lineBetween(4, 15, 18, 7); + wreckTexture.lineBetween(6, 5, 17, 17); + wreckTexture.generateTexture('salvage-wreck', 22, 22); + wreckTexture.destroy(); + } + + spawnSalvageWreck(x, y, classKey = 'raider') { + if (!this.salvageWrecks) return null; + + const salvage = salvageForPirate({ + seed: this.world?.seed || 'world', + classKey, + wave: this.wave, + sequence: this.salvageSequence, + }); + this.salvageSequence += 1; + + const wreck = this.salvageWrecks.create(x, y, 'salvage-wreck'); + wreck.setDepth(5).setDisplaySize(28, 28); + wreck.body.setAllowGravity(false); + wreck.body.setCircle(10); + wreck.setData('salvage', salvage); + wreck.setData('bornAt', this.time.now); + return wreck; + } + + collectSalvageWreck(first, second) { + const wreck = first === this.player ? second : first; + if (!wreck?.active || wreck === this.player) return; + + const salvage = wreck.getData?.('salvage'); + if (!salvage) return; + + this.progression = collectSalvage( + this.progression, + salvage, + this.playerStats, + ); + wreck.destroy(); + + const moduleName = salvage.moduleId + ? MODULE_CATALOG[salvage.moduleId]?.name || salvage.moduleId + : null; + const moduleCollected = this.progression.lastSalvage?.moduleCollected; + const moduleText = moduleName + ? moduleCollected + ? `\nSALVAGED: ${moduleName}` + : `\nHOLD FULL: ${moduleName}` + : ''; + + this.status.setText( + `SALVAGE +¢${salvage.credits} +${salvage.scrap} SCRAP${moduleText}`, + ); + this.time.delayedCall(1400, () => { + if (this.gameState === 'playing') this.status.setText(''); + }); + } + + applyProgressionStats(previousMaxHull = this.playerStats?.maxHull || PLAYER_MAX_HULL) { + this.playerStats = deriveShipStats(this.progression.loadout); + this.playerBroadsideReloadMs = this.playerStats.reloadMs; + + if (this.playerStats.maxHull > previousMaxHull) { + this.playerHull = Math.min( + this.playerStats.maxHull, + this.playerHull + (this.playerStats.maxHull - previousMaxHull), + ); + } else { + this.playerHull = Math.min(this.playerHull, this.playerStats.maxHull); + } + } + + updateProgressionInteraction() { + if (!this.player || !this.keys) return; + + const { port, range } = nearestPort(this.world, this.player.x, this.player.y); + const docked = Boolean(port && range <= 230); + + if (Phaser.Input.Keyboard.JustDown(this.keys.buyUpgrade)) { + if (!docked) { + this.status.setText('GET WITHIN 230m OF A PORT TO BUY UPGRADES'); + } else { + const module = bestAffordableMarketUpgrade(this.progression, port); + if (!module) { + this.status.setText(`${port.name}\nNO AFFORDABLE UPGRADES`); + } else { + const previousMaxHull = this.playerStats.maxHull; + this.progression = buyAndInstallModule(this.progression, module.id); + this.applyProgressionStats(previousMaxHull); + this.status.setText( + `${port.name}\nINSTALLED ${module.name} // ¢${this.progression.credits}`, + ); + } + } + this.time.delayedCall(1500, () => { + if (this.gameState === 'playing') this.status.setText(''); + }); + } + + if (Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) { + const moduleId = this.progression.inventory?.[0]; + if (!moduleId) { + this.status.setText('NO SALVAGED MODULES IN HOLD'); + } else { + const previousMaxHull = this.playerStats.maxHull; + this.progression = installModule(this.progression, moduleId); + this.applyProgressionStats(previousMaxHull); + const module = MODULE_CATALOG[moduleId]; + this.status.setText(`FITTED SALVAGE\n${module?.name || moduleId}`); + } + this.time.delayedCall(1400, () => { + if (this.gameState === 'playing') this.status.setText(''); + }); + } + } + createEnvironmentPhysics() { if (!this.textures.exists('environment-collider')) { const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false }); @@ -384,7 +549,8 @@ class GameScene extends Phaser.Scene { colliderTexture.destroy(); } - this.environmentObstacles = ENVIRONMENT_ISLANDS.map((island) => { + const islands = this.world?.islands || ENVIRONMENT_ISLANDS; + this.environmentObstacles = islands.map((island) => { const obstacle = this.physics.add.staticImage( island.x, island.y, @@ -396,10 +562,10 @@ class GameScene extends Phaser.Scene { .refreshBody(); obstacle.body.setCircle(island.radius); - this.physics.add.collider(this.player, obstacle, () => { - this.playerSpeed *= 0.7; + // Airships can cross islands, but lose momentum while flying through the terrain envelope. + this.physics.add.overlap(this.player, obstacle, () => { + this.playerSpeed *= 0.985; }); - this.physics.add.collider(this.pirates, obstacle); this.physics.add.overlap( this.playerShots, obstacle, @@ -456,7 +622,12 @@ class GameScene extends Phaser.Scene { if (!this.playerSmoke || !this.player?.active || !this.player.visible) return; const forwardThrust = clamp(this.playerSpeed / PLAYER_MAX_SPEED, 0, 1); - if (this.gameState !== 'playing' || forwardThrust <= 0.04) { + const effectiveForwardThrust = clamp( + this.playerSpeed / (this.playerStats?.maxSpeed || PLAYER_MAX_SPEED), + 0, + 1, + ); + if (this.gameState !== 'playing' || effectiveForwardThrust <= 0.04) { this.playerSmokeAccumulatorMs = 0; return; } @@ -470,17 +641,18 @@ class GameScene extends Phaser.Scene { const sternX = this.player.x - forwardX * PLAYER_SMOKE_REAR_OFFSET; const sternY = this.player.y - forwardY * PLAYER_SMOKE_REAR_OFFSET; - const emissionIntervalMs = Phaser.Math.Linear(150, 48, forwardThrust); + const emissionIntervalMs = Phaser.Math.Linear(150, 48, effectiveForwardThrust); this.playerSmokeAccumulatorMs += deltaMs; while (this.playerSmokeAccumulatorMs >= emissionIntervalMs) { const spread = Phaser.Math.FloatBetween(-0.16, 0.16); const trailHeading = heading + Math.PI + spread; - const trailSpeed = Phaser.Math.Linear(16, 46, forwardThrust) * Phaser.Math.FloatBetween(0.82, 1.16); + const trailSpeed = Phaser.Math.Linear(16, 46, effectiveForwardThrust) * Phaser.Math.FloatBetween(0.82, 1.16); const localWind = environmentWindAt({ x: sternX, y: sternY, timeMs: this.time.now, + islands: this.world?.islands, }); const velocityX = Math.cos(trailHeading) * trailSpeed + localWind.x * 1.4; const velocityY = Math.sin(trailHeading) * trailSpeed + localWind.y * 1.4; @@ -559,7 +731,7 @@ class GameScene extends Phaser.Scene { align: 'center', }).setOrigin(0.5, 0); - this.controlsHint = fixedText(this.scale.width / 2, this.scale.height - 22, 'ARROWS move • Q/E broadside • SPACE brake • R restart', { + this.controlsHint = fixedText(this.scale.width / 2, this.scale.height - 22, 'ARROWS move • Q/E broadside • B buy at port • U fit salvage • SPACE brake', { fontSize: '10px', color: '#91a8b7', backgroundColor: '#07101899', @@ -616,19 +788,29 @@ class GameScene extends Phaser.Scene { spawnWave() { const roster = pirateRosterForWave(this.wave, MAX_PIRATES); const count = roster.length; + const spawnPoints = pirateSpawnPointsForWave( + this.world, + this.player, + count, + this.wave, + ); + const territory = this.world.pirateTerritories + .find(({ id }) => id === spawnPoints[0]?.territoryId); + airshipLog('wave:spawn', { wave: this.wave, count, classes: roster.map(({ key }) => key), + territoryId: territory?.id || null, }); - this.status.setText(`WAVE ${this.wave}\n${count} PIRATE${count === 1 ? '' : 'S'}`); + this.status.setText( + `WAVE ${this.wave}\n${count} PIRATE${count === 1 ? '' : 'S'}${territory ? ` // ${territory.name}` : ''}`, + ); for (let i = 0; i < count; i++) { - const angle = (Math.PI * 2 * i / count) + Phaser.Math.FloatBetween(-0.25, 0.25); - const radius = Phaser.Math.Between(500, 760); - const x = clamp(this.player.x + Math.cos(angle) * radius, 120, WORLD_SIZE - 120); - const y = clamp(this.player.y + Math.sin(angle) * radius, 120, WORLD_SIZE - 120); - this.spawnPirate(x, y, angle, roster[i]); + const point = spawnPoints[i]; + const pirate = this.spawnPirate(point.x, point.y, point.angle, roster[i]); + pirate.setData('territoryId', point.territoryId); } this.time.delayedCall(900, () => { @@ -662,26 +844,64 @@ class GameScene extends Phaser.Scene { drawWorld() { const bg = this.add.graphics(); bg.fillStyle(0x132335, 1); - bg.fillRect(0, 0, WORLD_SIZE, WORLD_SIZE); + bg.fillRect(0, 0, this.world.width, this.world.height); bg.lineStyle(1, 0x8db3cc, 0.08); - for (let x = 0; x <= WORLD_SIZE; x += 100) bg.lineBetween(x, 0, x, WORLD_SIZE); - for (let y = 0; y <= WORLD_SIZE; y += 100) bg.lineBetween(0, y, WORLD_SIZE, y); + for (let x = 0; x <= this.world.width; x += 100) bg.lineBetween(x, 0, x, this.world.height); + for (let y = 0; y <= this.world.height; y += 100) bg.lineBetween(0, y, this.world.width, y); - const island = this.add.graphics(); - island.fillStyle(0x5d694e, 1); - island.fillEllipse(430, 480, 230, 150); - island.fillStyle(0xa18a62, 1); - island.fillEllipse(430, 470, 190, 105); - island.fillStyle(0x283827, 1); - island.fillCircle(390, 440, 18); - island.fillCircle(455, 450, 14); - island.fillCircle(490, 425, 12); + const territoryGraphics = this.add.graphics().setDepth(0); + for (const territory of this.world.pirateTerritories) { + territoryGraphics.fillStyle(0x7d2636, 0.035 * territory.strength); + territoryGraphics.fillCircle(territory.center.x, territory.center.y, territory.radius); + territoryGraphics.lineStyle(2, 0xd65a68, 0.16); + territoryGraphics.strokeCircle(territory.center.x, territory.center.y, territory.radius); + } - const island2 = this.add.graphics(); - island2.fillStyle(0x5d694e, 1); - island2.fillEllipse(1940, 1840, 260, 165); - island2.fillStyle(0xa18a62, 1); - island2.fillEllipse(1940, 1830, 210, 112); + const routeGraphics = this.add.graphics().setDepth(1); + for (const route of this.world.tradeRoutes) { + routeGraphics.lineStyle(route.width, 0x9bc6d8, 0.025 + route.traffic * 0.025); + routeGraphics.lineBetween(route.from.x, route.from.y, route.to.x, route.to.y); + routeGraphics.lineStyle(2, 0xb9dcea, 0.16 + route.traffic * 0.16); + routeGraphics.lineBetween(route.from.x, route.from.y, route.to.x, route.to.y); + } + + const islandGraphics = this.add.graphics().setDepth(2); + for (const island of this.world.islands) { + islandGraphics.fillStyle(0x5d694e, 1); + islandGraphics.fillEllipse( + island.x, + island.y + 8, + island.radius * 2.15, + island.radius * 1.45, + ); + islandGraphics.fillStyle(0xa18a62, 1); + islandGraphics.fillEllipse( + island.x, + island.y, + island.radius * 1.78, + island.radius * 1.08, + ); + islandGraphics.fillStyle(0x283827, 0.95); + islandGraphics.fillCircle( + island.x - island.radius * 0.2, + island.y - island.radius * 0.18, + Math.max(8, island.radius * 0.16), + ); + } + + for (const port of this.world.ports) { + const portMarker = this.add.circle(port.x, port.y, 9, 0xf4d27a, 0.95) + .setStrokeStyle(2, 0x4a3016, 0.95) + .setDepth(3); + portMarker.setData('portId', port.id); + this.add.text(port.x + 13, port.y - 8, port.name, { + fontFamily: 'monospace', + fontSize: '11px', + color: '#f5df9a', + backgroundColor: '#071018aa', + padding: { x: 3, y: 2 }, + }).setDepth(3); + } } update(_time, deltaMs) { @@ -720,6 +940,7 @@ class GameScene extends Phaser.Scene { } this.updatePlayer(dt, deltaMs); + this.updateProgressionInteraction(); this.updatePlayerSmoke(deltaMs); this.updateRollingClouds(deltaMs); this.updateCloudConcealment(); @@ -748,6 +969,7 @@ class GameScene extends Phaser.Scene { updatePlayer(dt, deltaMs) { if (!this.player?.body) return; + const stats = this.playerStats || deriveShipStats(this.progression?.loadout); 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); @@ -755,28 +977,29 @@ class GameScene extends Phaser.Scene { const throttleForward = this.cursors.up.isDown ? 1 : Math.max(0, -helmY); const throttleReverse = this.cursors.down.isDown ? 1 : Math.max(0, helmY); - if (throttleForward > 0) this.playerSpeed += PLAYER_ACCELERATION * throttleForward * dt; - if (throttleReverse > 0) this.playerSpeed -= PLAYER_BRAKE * throttleReverse * dt; + if (throttleForward > 0) this.playerSpeed += stats.acceleration * throttleForward * dt; + if (throttleReverse > 0) this.playerSpeed -= stats.brake * throttleReverse * dt; if (this.keys.stop.isDown) this.playerSpeed *= Math.pow(0.025, dt); if (throttleForward === 0 && throttleReverse === 0 && !this.keys.stop.isDown) { this.playerSpeed *= Math.pow(PLAYER_COAST_DRAG, deltaMs / 16.667); } - this.playerSpeed = clamp(this.playerSpeed, PLAYER_REVERSE_SPEED, PLAYER_MAX_SPEED); + this.playerSpeed = clamp(this.playerSpeed, stats.reverseSpeed, stats.maxSpeed); const turnStrength = clamp(Math.abs(this.playerSpeed) / 100, 0.28, 1); const directionSign = this.playerSpeed < 0 ? -1 : 1; const keyboardTurn = (this.cursors.right.isDown ? 1 : 0) - (this.cursors.left.isDown ? 1 : 0); const turnInput = keyboardTurn || helmX; - this.player.angle += PLAYER_TURN_RATE * turnStrength * directionSign * turnInput * dt; + this.player.angle += stats.turnRate * turnStrength * directionSign * turnInput * dt; const forwardThrust = clamp(this.playerSpeed / PLAYER_MAX_SPEED, 0, 1); - if (forwardThrust > 0.06) { + const effectiveForwardThrust = clamp(this.playerSpeed / stats.maxSpeed, 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 + forwardThrust * 1.35; + this.player.anims.timeScale = 0.65 + effectiveForwardThrust * 1.35; } else { if (this.player.anims.isPlaying) this.player.anims.stop(); this.player.setFrame(0); @@ -788,12 +1011,13 @@ class GameScene extends Phaser.Scene { x: this.player.x, y: this.player.y, timeMs: this.time.now, + islands: this.world?.islands, }); const playerVelocity = velocityWithEnvironment( Math.cos(h) * this.playerSpeed, Math.sin(h) * this.playerSpeed, this.environmentWind, - 1, + stats.windExposure, ); this.player.setVelocity(playerVelocity.x, playerVelocity.y); @@ -846,6 +1070,7 @@ class GameScene extends Phaser.Scene { x: pirate.x, y: pirate.y, timeMs: this.time.now, + islands: this.world?.islands, }); const pirateVelocity = velocityWithEnvironment( Math.cos(h) * speed, @@ -877,7 +1102,14 @@ class GameScene extends Phaser.Scene { playerBroadside(side) { const broadside = side < 0 ? 'port' : 'starboard'; if (this.time.now - this.lastPlayerShot[broadside] < this.playerBroadsideReloadMs) return; - this.fireBroadside(this.player, side, this.playerShots, 'cannonball', 16, 1.0); + this.fireBroadside( + this.player, + side, + this.playerShots, + 'cannonball', + this.playerStats?.cannonDamage || 16, + 1.0, + ); this.lastPlayerShot[broadside] = this.time.now; } @@ -912,6 +1144,7 @@ class GameScene extends Phaser.Scene { x, y, timeMs: this.time.now, + islands: this.world?.islands, }); const shotVelocity = velocityWithEnvironment( Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25, @@ -954,13 +1187,19 @@ class GameScene extends Phaser.Scene { }); if (hull <= 0) { + const pirateClass = pirate.getData('class') || 'raider'; + const pirateX = pirate.x; + const pirateY = pirate.y; this.spawnExplosion(pirate.x, pirate.y, 1.25); pirate.destroy(); - if (projectileOwner === 'player') this.defeated += 1; + if (projectileOwner === 'player') { + this.defeated += 1; + this.spawnSalvageWreck(pirateX, pirateY, pirateClass); + } if (this.pirates.countActive(true) === 0) { this.wave += 1; - this.playerHull = Math.min(PLAYER_MAX_HULL, this.playerHull + 15); + this.playerHull = Math.min(this.playerStats?.maxHull || PLAYER_MAX_HULL, this.playerHull + 15); this.status.setText('WAVE CLEARED\n+15 HULL'); this.time.delayedCall(1100, () => { if (this.gameState === 'playing' && this.pirates.countActive(true) === 0) this.spawnWave(); @@ -1145,6 +1384,8 @@ class GameScene extends Phaser.Scene { 0, 1, ); + const portState = nearestPort(this.world, player.x, player.y); + const influence = pirateInfluenceAt(this.world, player.x, player.y); return { gameState: this.gameState, @@ -1155,7 +1396,7 @@ class GameScene extends Phaser.Scene { kills: this.defeated, player: { hull: this.playerHull, - maxHull: PLAYER_MAX_HULL, + maxHull: this.playerStats?.maxHull || PLAYER_MAX_HULL, hitSequence: this.playerHitSequence, }, target: nearest ? { @@ -1173,6 +1414,17 @@ class GameScene extends Phaser.Scene { windHeading: this.environmentWind?.heading || 0, concealment: this.player?.getData?.('cloudConcealment') || 0, }, + progression: { + credits: this.progression?.credits || 0, + scrap: this.progression?.scrap || 0, + inventoryCount: this.progression?.inventory?.length || 0, + nearestPort: portState.port?.name || null, + nearestPortRange: portState.range, + }, + world: { + seed: this.world?.seed, + pirateInfluence: influence, + }, radar: activePirates.slice(0, 10).map((pirate) => ({ dx: (pirate.x - player.x) / 900, dy: (pirate.y - player.y) / 900, @@ -1198,6 +1450,12 @@ class GameScene extends Phaser.Scene { if (this.time.now - shot.getData('bornAt') > SHOT_LIFETIME) shot.destroy(); } } + + for (const wreck of this.salvageWrecks?.getChildren?.() || []) { + if (wreck.active && this.time.now - (wreck.getData('bornAt') || 0) > 45000) { + wreck.destroy(); + } + } } drawWeaponArcs() { @@ -1247,8 +1505,8 @@ class GameScene extends Phaser.Scene { this.headingText.setText(`${(state.heading || 0).toFixed(0).padStart(3, '0')}°`); this.waveText.setText(`WAVE ${state.wave} • ${state.hostiles} HOSTILE${state.hostiles === 1 ? '' : 'S'}`); this.combatText.setText(compact - ? `K ${state.kills}\n${rangeText}` - : `DEFEATED ${state.kills}\nRANGE ${rangeText}`); + ? `K ${state.kills}\n¢${state.progression?.credits || 0}` + : `DEFEATED ${state.kills}\nRANGE ${rangeText}\n¢${state.progression?.credits || 0} S${state.progression?.scrap || 0}`); this.playerHullLabel.setText(`SPARROW ${playerHull}/${playerMaxHull}`); this.enemyHullLabel.setText(target