Sync verified procedural gameplay build
This commit is contained in:
parent
580994daee
commit
c726f9333e
@ -69,6 +69,7 @@ export const environmentWindAt = ({
|
|||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
timeMs = 0,
|
timeMs = 0,
|
||||||
|
islands = ENVIRONMENT_ISLANDS,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const time = timeMs / 1000;
|
const time = timeMs / 1000;
|
||||||
const pattern = semiRandomWindPattern(timeMs);
|
const pattern = semiRandomWindPattern(timeMs);
|
||||||
@ -82,7 +83,7 @@ export const environmentWindAt = ({
|
|||||||
let windX = Math.cos(localAngle) * localStrength;
|
let windX = Math.cos(localAngle) * localStrength;
|
||||||
let windY = Math.sin(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 dx = x - island.x;
|
||||||
const dy = y - island.y;
|
const dy = y - island.y;
|
||||||
const distance = Math.hypot(dx, dy);
|
const distance = Math.hypot(dx, dy);
|
||||||
|
|||||||
356
src/game-v4.js
356
src/game-v4.js
@ -23,8 +23,24 @@ import {
|
|||||||
environmentWindAt,
|
environmentWindAt,
|
||||||
velocityWithEnvironment,
|
velocityWithEnvironment,
|
||||||
} from './environment-physics.js';
|
} 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_MAX_SPEED = 180;
|
||||||
const PLAYER_REVERSE_SPEED = -55;
|
const PLAYER_REVERSE_SPEED = -55;
|
||||||
const PLAYER_ACCELERATION = 92;
|
const PLAYER_ACCELERATION = 92;
|
||||||
@ -69,11 +85,20 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
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.playerSpeed = 0;
|
||||||
this.playerHull = PLAYER_MAX_HULL;
|
this.playerHull = this.playerStats.maxHull;
|
||||||
this.gameState = 'playing';
|
this.gameState = 'playing';
|
||||||
this.lastPlayerShot = { port: -Infinity, starboard: -Infinity };
|
this.lastPlayerShot = { port: -Infinity, starboard: -Infinity };
|
||||||
this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS;
|
this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS;
|
||||||
|
this.playerBroadsideReloadMs = this.playerStats.reloadMs;
|
||||||
this.helmVector = { x: 0, y: 0 };
|
this.helmVector = { x: 0, y: 0 };
|
||||||
this.pendingPlayerHits = [];
|
this.pendingPlayerHits = [];
|
||||||
this.playerHitSequence = 0;
|
this.playerHitSequence = 0;
|
||||||
@ -83,10 +108,12 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.lastHudPublishAt = -Infinity;
|
this.lastHudPublishAt = -Infinity;
|
||||||
this.wave = 1;
|
this.wave = 1;
|
||||||
this.defeated = 0;
|
this.defeated = 0;
|
||||||
|
this.salvageSequence = 0;
|
||||||
|
|
||||||
airshipLog('scene:init', {
|
airshipLog('scene:init', {
|
||||||
hull: this.playerHull,
|
hull: this.playerHull,
|
||||||
wave: this.wave,
|
wave: this.wave,
|
||||||
|
worldSeed: this.world.seed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,12 +134,13 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
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.drawWorld();
|
||||||
this.createRollingClouds();
|
this.createRollingClouds();
|
||||||
this.createAnimations();
|
this.createAnimations();
|
||||||
|
|
||||||
this.player = this.physics.add.sprite(1200, 1550, 'player-airship', 0);
|
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.setDepth(6).setCollideWorldBounds(true).setDisplaySize(66, 126);
|
||||||
this.player.body.setAllowGravity(false);
|
this.player.body.setAllowGravity(false);
|
||||||
this.player.body.setCircle(44, 20, 84);
|
this.player.body.setCircle(44, 20, 84);
|
||||||
@ -122,12 +150,15 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.pirates = this.physics.add.group();
|
this.pirates = this.physics.add.group();
|
||||||
this.playerShots = this.physics.add.group();
|
this.playerShots = this.physics.add.group();
|
||||||
this.pirateShots = this.physics.add.group();
|
this.pirateShots = this.physics.add.group();
|
||||||
|
this.salvageWrecks = this.physics.add.group();
|
||||||
this.arcGraphics = this.add.graphics().setDepth(4);
|
this.arcGraphics = this.add.graphics().setDepth(4);
|
||||||
|
this.createSalvageTexture();
|
||||||
this.createEnvironmentPhysics();
|
this.createEnvironmentPhysics();
|
||||||
|
|
||||||
this.physics.add.overlap(this.playerShots, this.pirates, this.hitPirate, null, this);
|
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.pirates, this.hitPirate, null, this);
|
||||||
this.physics.add.overlap(this.pirateShots, this.player, this.hitPlayer, 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(() => ({
|
registerDebugSnapshot(() => ({
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
@ -140,6 +171,10 @@ class GameScene extends Phaser.Scene {
|
|||||||
wave: this.wave,
|
wave: this.wave,
|
||||||
defeated: this.defeated,
|
defeated: this.defeated,
|
||||||
environmentWind: this.environmentWind,
|
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,
|
now: this.time?.now ?? null,
|
||||||
player: this.player ? {
|
player: this.player ? {
|
||||||
x: Math.round(this.player.x),
|
x: Math.round(this.player.x),
|
||||||
@ -154,7 +189,10 @@ class GameScene extends Phaser.Scene {
|
|||||||
airshipLog('scene:create', {
|
airshipLog('scene:create', {
|
||||||
hull: this.playerHull,
|
hull: this.playerHull,
|
||||||
wave: this.wave,
|
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();
|
this.cursors = this.input.keyboard.createCursorKeys();
|
||||||
@ -168,9 +206,11 @@ class GameScene extends Phaser.Scene {
|
|||||||
fireStarboard: Phaser.Input.Keyboard.KeyCodes.E,
|
fireStarboard: Phaser.Input.Keyboard.KeyCodes.E,
|
||||||
stop: Phaser.Input.Keyboard.KeyCodes.SPACE,
|
stop: Phaser.Input.Keyboard.KeyCodes.SPACE,
|
||||||
restart: Phaser.Input.Keyboard.KeyCodes.R,
|
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.startFollow(this.player, true, 0.08, 0.08);
|
||||||
this.cameras.main.setZoom(1.12);
|
this.cameras.main.setZoom(1.12);
|
||||||
|
|
||||||
@ -274,6 +314,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
x: cloud.x,
|
x: cloud.x,
|
||||||
y: cloud.y,
|
y: cloud.y,
|
||||||
timeMs: now,
|
timeMs: now,
|
||||||
|
islands: this.world?.islands,
|
||||||
});
|
});
|
||||||
const driftScale = cloud.getData('driftScale') || 1;
|
const driftScale = cloud.getData('driftScale') || 1;
|
||||||
cloud.x += wind.x * driftScale * dt;
|
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() {
|
createEnvironmentPhysics() {
|
||||||
if (!this.textures.exists('environment-collider')) {
|
if (!this.textures.exists('environment-collider')) {
|
||||||
const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false });
|
const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false });
|
||||||
@ -384,7 +549,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
colliderTexture.destroy();
|
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(
|
const obstacle = this.physics.add.staticImage(
|
||||||
island.x,
|
island.x,
|
||||||
island.y,
|
island.y,
|
||||||
@ -396,10 +562,10 @@ class GameScene extends Phaser.Scene {
|
|||||||
.refreshBody();
|
.refreshBody();
|
||||||
obstacle.body.setCircle(island.radius);
|
obstacle.body.setCircle(island.radius);
|
||||||
|
|
||||||
this.physics.add.collider(this.player, obstacle, () => {
|
// Airships can cross islands, but lose momentum while flying through the terrain envelope.
|
||||||
this.playerSpeed *= 0.7;
|
this.physics.add.overlap(this.player, obstacle, () => {
|
||||||
|
this.playerSpeed *= 0.985;
|
||||||
});
|
});
|
||||||
this.physics.add.collider(this.pirates, obstacle);
|
|
||||||
this.physics.add.overlap(
|
this.physics.add.overlap(
|
||||||
this.playerShots,
|
this.playerShots,
|
||||||
obstacle,
|
obstacle,
|
||||||
@ -456,7 +622,12 @@ class GameScene extends Phaser.Scene {
|
|||||||
if (!this.playerSmoke || !this.player?.active || !this.player.visible) return;
|
if (!this.playerSmoke || !this.player?.active || !this.player.visible) return;
|
||||||
|
|
||||||
const forwardThrust = clamp(this.playerSpeed / PLAYER_MAX_SPEED, 0, 1);
|
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;
|
this.playerSmokeAccumulatorMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -470,17 +641,18 @@ class GameScene extends Phaser.Scene {
|
|||||||
const sternX = this.player.x - forwardX * PLAYER_SMOKE_REAR_OFFSET;
|
const sternX = this.player.x - forwardX * PLAYER_SMOKE_REAR_OFFSET;
|
||||||
const sternY = this.player.y - forwardY * 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;
|
this.playerSmokeAccumulatorMs += deltaMs;
|
||||||
|
|
||||||
while (this.playerSmokeAccumulatorMs >= emissionIntervalMs) {
|
while (this.playerSmokeAccumulatorMs >= emissionIntervalMs) {
|
||||||
const spread = Phaser.Math.FloatBetween(-0.16, 0.16);
|
const spread = Phaser.Math.FloatBetween(-0.16, 0.16);
|
||||||
const trailHeading = heading + Math.PI + spread;
|
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({
|
const localWind = environmentWindAt({
|
||||||
x: sternX,
|
x: sternX,
|
||||||
y: sternY,
|
y: sternY,
|
||||||
timeMs: this.time.now,
|
timeMs: this.time.now,
|
||||||
|
islands: this.world?.islands,
|
||||||
});
|
});
|
||||||
const velocityX = Math.cos(trailHeading) * trailSpeed + localWind.x * 1.4;
|
const velocityX = Math.cos(trailHeading) * trailSpeed + localWind.x * 1.4;
|
||||||
const velocityY = Math.sin(trailHeading) * trailSpeed + localWind.y * 1.4;
|
const velocityY = Math.sin(trailHeading) * trailSpeed + localWind.y * 1.4;
|
||||||
@ -559,7 +731,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
}).setOrigin(0.5, 0);
|
}).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',
|
fontSize: '10px',
|
||||||
color: '#91a8b7',
|
color: '#91a8b7',
|
||||||
backgroundColor: '#07101899',
|
backgroundColor: '#07101899',
|
||||||
@ -616,19 +788,29 @@ class GameScene extends Phaser.Scene {
|
|||||||
spawnWave() {
|
spawnWave() {
|
||||||
const roster = pirateRosterForWave(this.wave, MAX_PIRATES);
|
const roster = pirateRosterForWave(this.wave, MAX_PIRATES);
|
||||||
const count = roster.length;
|
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', {
|
airshipLog('wave:spawn', {
|
||||||
wave: this.wave,
|
wave: this.wave,
|
||||||
count,
|
count,
|
||||||
classes: roster.map(({ key }) => key),
|
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++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const angle = (Math.PI * 2 * i / count) + Phaser.Math.FloatBetween(-0.25, 0.25);
|
const point = spawnPoints[i];
|
||||||
const radius = Phaser.Math.Between(500, 760);
|
const pirate = this.spawnPirate(point.x, point.y, point.angle, roster[i]);
|
||||||
const x = clamp(this.player.x + Math.cos(angle) * radius, 120, WORLD_SIZE - 120);
|
pirate.setData('territoryId', point.territoryId);
|
||||||
const y = clamp(this.player.y + Math.sin(angle) * radius, 120, WORLD_SIZE - 120);
|
|
||||||
this.spawnPirate(x, y, angle, roster[i]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.time.delayedCall(900, () => {
|
this.time.delayedCall(900, () => {
|
||||||
@ -662,26 +844,64 @@ class GameScene extends Phaser.Scene {
|
|||||||
drawWorld() {
|
drawWorld() {
|
||||||
const bg = this.add.graphics();
|
const bg = this.add.graphics();
|
||||||
bg.fillStyle(0x132335, 1);
|
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);
|
bg.lineStyle(1, 0x8db3cc, 0.08);
|
||||||
for (let x = 0; x <= WORLD_SIZE; x += 100) bg.lineBetween(x, 0, x, WORLD_SIZE);
|
for (let x = 0; x <= this.world.width; x += 100) bg.lineBetween(x, 0, x, this.world.height);
|
||||||
for (let y = 0; y <= WORLD_SIZE; y += 100) bg.lineBetween(0, y, WORLD_SIZE, y);
|
for (let y = 0; y <= this.world.height; y += 100) bg.lineBetween(0, y, this.world.width, y);
|
||||||
|
|
||||||
const island = this.add.graphics();
|
const territoryGraphics = this.add.graphics().setDepth(0);
|
||||||
island.fillStyle(0x5d694e, 1);
|
for (const territory of this.world.pirateTerritories) {
|
||||||
island.fillEllipse(430, 480, 230, 150);
|
territoryGraphics.fillStyle(0x7d2636, 0.035 * territory.strength);
|
||||||
island.fillStyle(0xa18a62, 1);
|
territoryGraphics.fillCircle(territory.center.x, territory.center.y, territory.radius);
|
||||||
island.fillEllipse(430, 470, 190, 105);
|
territoryGraphics.lineStyle(2, 0xd65a68, 0.16);
|
||||||
island.fillStyle(0x283827, 1);
|
territoryGraphics.strokeCircle(territory.center.x, territory.center.y, territory.radius);
|
||||||
island.fillCircle(390, 440, 18);
|
}
|
||||||
island.fillCircle(455, 450, 14);
|
|
||||||
island.fillCircle(490, 425, 12);
|
|
||||||
|
|
||||||
const island2 = this.add.graphics();
|
const routeGraphics = this.add.graphics().setDepth(1);
|
||||||
island2.fillStyle(0x5d694e, 1);
|
for (const route of this.world.tradeRoutes) {
|
||||||
island2.fillEllipse(1940, 1840, 260, 165);
|
routeGraphics.lineStyle(route.width, 0x9bc6d8, 0.025 + route.traffic * 0.025);
|
||||||
island2.fillStyle(0xa18a62, 1);
|
routeGraphics.lineBetween(route.from.x, route.from.y, route.to.x, route.to.y);
|
||||||
island2.fillEllipse(1940, 1830, 210, 112);
|
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) {
|
update(_time, deltaMs) {
|
||||||
@ -720,6 +940,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updatePlayer(dt, deltaMs);
|
this.updatePlayer(dt, deltaMs);
|
||||||
|
this.updateProgressionInteraction();
|
||||||
this.updatePlayerSmoke(deltaMs);
|
this.updatePlayerSmoke(deltaMs);
|
||||||
this.updateRollingClouds(deltaMs);
|
this.updateRollingClouds(deltaMs);
|
||||||
this.updateCloudConcealment();
|
this.updateCloudConcealment();
|
||||||
@ -748,6 +969,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
updatePlayer(dt, deltaMs) {
|
updatePlayer(dt, deltaMs) {
|
||||||
if (!this.player?.body) return;
|
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 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 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 throttleForward = this.cursors.up.isDown ? 1 : Math.max(0, -helmY);
|
||||||
const throttleReverse = this.cursors.down.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 (throttleForward > 0) this.playerSpeed += stats.acceleration * throttleForward * dt;
|
||||||
if (throttleReverse > 0) this.playerSpeed -= PLAYER_BRAKE * throttleReverse * dt;
|
if (throttleReverse > 0) this.playerSpeed -= stats.brake * throttleReverse * dt;
|
||||||
if (this.keys.stop.isDown) this.playerSpeed *= Math.pow(0.025, dt);
|
if (this.keys.stop.isDown) this.playerSpeed *= Math.pow(0.025, dt);
|
||||||
|
|
||||||
if (throttleForward === 0 && throttleReverse === 0 && !this.keys.stop.isDown) {
|
if (throttleForward === 0 && throttleReverse === 0 && !this.keys.stop.isDown) {
|
||||||
this.playerSpeed *= Math.pow(PLAYER_COAST_DRAG, deltaMs / 16.667);
|
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 turnStrength = clamp(Math.abs(this.playerSpeed) / 100, 0.28, 1);
|
||||||
const directionSign = this.playerSpeed < 0 ? -1 : 1;
|
const directionSign = this.playerSpeed < 0 ? -1 : 1;
|
||||||
const keyboardTurn = (this.cursors.right.isDown ? 1 : 0) - (this.cursors.left.isDown ? 1 : 0);
|
const keyboardTurn = (this.cursors.right.isDown ? 1 : 0) - (this.cursors.left.isDown ? 1 : 0);
|
||||||
const turnInput = keyboardTurn || helmX;
|
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);
|
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') {
|
if (!this.player.anims.isPlaying || this.player.anims.currentAnim?.key !== 'player-forward') {
|
||||||
this.player.play('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 {
|
} else {
|
||||||
if (this.player.anims.isPlaying) this.player.anims.stop();
|
if (this.player.anims.isPlaying) this.player.anims.stop();
|
||||||
this.player.setFrame(0);
|
this.player.setFrame(0);
|
||||||
@ -788,12 +1011,13 @@ class GameScene extends Phaser.Scene {
|
|||||||
x: this.player.x,
|
x: this.player.x,
|
||||||
y: this.player.y,
|
y: this.player.y,
|
||||||
timeMs: this.time.now,
|
timeMs: this.time.now,
|
||||||
|
islands: this.world?.islands,
|
||||||
});
|
});
|
||||||
const playerVelocity = velocityWithEnvironment(
|
const playerVelocity = velocityWithEnvironment(
|
||||||
Math.cos(h) * this.playerSpeed,
|
Math.cos(h) * this.playerSpeed,
|
||||||
Math.sin(h) * this.playerSpeed,
|
Math.sin(h) * this.playerSpeed,
|
||||||
this.environmentWind,
|
this.environmentWind,
|
||||||
1,
|
stats.windExposure,
|
||||||
);
|
);
|
||||||
this.player.setVelocity(playerVelocity.x, playerVelocity.y);
|
this.player.setVelocity(playerVelocity.x, playerVelocity.y);
|
||||||
|
|
||||||
@ -846,6 +1070,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
x: pirate.x,
|
x: pirate.x,
|
||||||
y: pirate.y,
|
y: pirate.y,
|
||||||
timeMs: this.time.now,
|
timeMs: this.time.now,
|
||||||
|
islands: this.world?.islands,
|
||||||
});
|
});
|
||||||
const pirateVelocity = velocityWithEnvironment(
|
const pirateVelocity = velocityWithEnvironment(
|
||||||
Math.cos(h) * speed,
|
Math.cos(h) * speed,
|
||||||
@ -877,7 +1102,14 @@ 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;
|
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;
|
this.lastPlayerShot[broadside] = this.time.now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -912,6 +1144,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
timeMs: this.time.now,
|
timeMs: this.time.now,
|
||||||
|
islands: this.world?.islands,
|
||||||
});
|
});
|
||||||
const shotVelocity = velocityWithEnvironment(
|
const shotVelocity = velocityWithEnvironment(
|
||||||
Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25,
|
Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25,
|
||||||
@ -954,13 +1187,19 @@ class GameScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hull <= 0) {
|
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);
|
this.spawnExplosion(pirate.x, pirate.y, 1.25);
|
||||||
pirate.destroy();
|
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) {
|
if (this.pirates.countActive(true) === 0) {
|
||||||
this.wave += 1;
|
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.status.setText('WAVE CLEARED\n+15 HULL');
|
||||||
this.time.delayedCall(1100, () => {
|
this.time.delayedCall(1100, () => {
|
||||||
if (this.gameState === 'playing' && this.pirates.countActive(true) === 0) this.spawnWave();
|
if (this.gameState === 'playing' && this.pirates.countActive(true) === 0) this.spawnWave();
|
||||||
@ -1145,6 +1384,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
const portState = nearestPort(this.world, player.x, player.y);
|
||||||
|
const influence = pirateInfluenceAt(this.world, player.x, player.y);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
@ -1155,7 +1396,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
kills: this.defeated,
|
kills: this.defeated,
|
||||||
player: {
|
player: {
|
||||||
hull: this.playerHull,
|
hull: this.playerHull,
|
||||||
maxHull: PLAYER_MAX_HULL,
|
maxHull: this.playerStats?.maxHull || PLAYER_MAX_HULL,
|
||||||
hitSequence: this.playerHitSequence,
|
hitSequence: this.playerHitSequence,
|
||||||
},
|
},
|
||||||
target: nearest ? {
|
target: nearest ? {
|
||||||
@ -1173,6 +1414,17 @@ class GameScene extends Phaser.Scene {
|
|||||||
windHeading: this.environmentWind?.heading || 0,
|
windHeading: this.environmentWind?.heading || 0,
|
||||||
concealment: this.player?.getData?.('cloudConcealment') || 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) => ({
|
radar: activePirates.slice(0, 10).map((pirate) => ({
|
||||||
dx: (pirate.x - player.x) / 900,
|
dx: (pirate.x - player.x) / 900,
|
||||||
dy: (pirate.y - player.y) / 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();
|
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() {
|
drawWeaponArcs() {
|
||||||
@ -1247,8 +1505,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.headingText.setText(`${(state.heading || 0).toFixed(0).padStart(3, '0')}°`);
|
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.waveText.setText(`WAVE ${state.wave} • ${state.hostiles} HOSTILE${state.hostiles === 1 ? '' : 'S'}`);
|
||||||
this.combatText.setText(compact
|
this.combatText.setText(compact
|
||||||
? `K ${state.kills}\n${rangeText}`
|
? `K ${state.kills}\n¢${state.progression?.credits || 0}`
|
||||||
: `DEFEATED ${state.kills}\nRANGE ${rangeText}`);
|
: `DEFEATED ${state.kills}\nRANGE ${rangeText}\n¢${state.progression?.credits || 0} S${state.progression?.scrap || 0}`);
|
||||||
|
|
||||||
this.playerHullLabel.setText(`SPARROW ${playerHull}/${playerMaxHull}`);
|
this.playerHullLabel.setText(`SPARROW ${playerHull}/${playerMaxHull}`);
|
||||||
this.enemyHullLabel.setText(target
|
this.enemyHullLabel.setText(target
|
||||||
|
|||||||
@ -187,7 +187,7 @@ test('player and pirate broadsides use the cannonball image asset', async () =>
|
|||||||
|
|
||||||
assert.match(source, /import cannonballUrl from '.\/assets\/cannonball\.png\?url'/);
|
assert.match(source, /import cannonballUrl from '.\/assets\/cannonball\.png\?url'/);
|
||||||
assert.match(source, /this\.load\.image\('cannonball', cannonballUrl\)/);
|
assert.match(source, /this\.load\.image\('cannonball', cannonballUrl\)/);
|
||||||
assert.match(source, /this\.fireBroadside\(this\.player, side, this\.playerShots, 'cannonball'/);
|
assert.match(source, /this\.fireBroadside\([\s\S]*?this\.player,[\s\S]*?this\.playerShots,[\s\S]*?'cannonball',[\s\S]*?this\.playerStats\?\.cannonDamage/);
|
||||||
assert.match(source, /this\.fireBroadside\(pirate, side, this\.pirateShots, 'cannonball'/);
|
assert.match(source, /this\.fireBroadside\(pirate, side, this\.pirateShots, 'cannonball'/);
|
||||||
assert.match(source, /shot\.setDepth\(8\)\.setDisplaySize\(14, 14\)/);
|
assert.match(source, /shot\.setDepth\(8\)\.setDisplaySize\(14, 14\)/);
|
||||||
assert.doesNotMatch(source, /generateTexture\('player-shot'/);
|
assert.doesNotMatch(source, /generateTexture\('player-shot'/);
|
||||||
@ -202,7 +202,7 @@ test('pirate cannonballs damage other pirates but ignore their source ship', asy
|
|||||||
assert.match(source, /const pirateShot = playerShot \? null : resolveTaggedProjectile\(first, second, 'pirate'\)/);
|
assert.match(source, /const pirateShot = playerShot \? null : resolveTaggedProjectile\(first, second, 'pirate'\)/);
|
||||||
assert.match(source, /if \(projectileOwner === 'pirate' && sourceShip === pirate\) return/);
|
assert.match(source, /if \(projectileOwner === 'pirate' && sourceShip === pirate\) return/);
|
||||||
assert.match(source, /friendlyFire: projectileOwner === 'pirate'/);
|
assert.match(source, /friendlyFire: projectileOwner === 'pirate'/);
|
||||||
assert.match(source, /if \(projectileOwner === 'player'\) this\.defeated \+= 1/);
|
assert.match(source, /if \(projectileOwner === 'player'\) \{[\s\S]*?this\.defeated \+= 1;[\s\S]*?this\.spawnSalvageWreck\(/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('player uses the four-frame north-facing sheet as a forward propulsion animation', async () => {
|
test('player uses the four-frame north-facing sheet as a forward propulsion animation', async () => {
|
||||||
@ -216,7 +216,8 @@ test('player uses the four-frame north-facing sheet as a forward propulsion anim
|
|||||||
assert.match(source, /key: 'player-forward'/);
|
assert.match(source, /key: 'player-forward'/);
|
||||||
assert.match(source, /generateFrameNumbers\('player-airship', \{ start: 0, end: 3 \}\)/);
|
assert.match(source, /generateFrameNumbers\('player-airship', \{ start: 0, end: 3 \}\)/);
|
||||||
assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/);
|
assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/);
|
||||||
assert.match(source, /this\.player\.anims\.timeScale = 0\.65 \+ forwardThrust \* 1\.35/);
|
assert.match(source, /const effectiveForwardThrust = clamp\(this\.playerSpeed \/ stats\.maxSpeed, 0, 1\)/);
|
||||||
|
assert.match(source, /this\.player\.anims\.timeScale = 0\.65 \+ effectiveForwardThrust \* 1\.35/);
|
||||||
assert.match(source, /this\.player\.setFrame\(0\)/);
|
assert.match(source, /this\.player\.setFrame\(0\)/);
|
||||||
assert.doesNotMatch(source, /key: 'player-idle'/);
|
assert.doesNotMatch(source, /key: 'player-idle'/);
|
||||||
});
|
});
|
||||||
@ -229,14 +230,15 @@ test('player propulsion smoke emits at world engine positions with per-particle
|
|||||||
assert.match(source, /frequency: -1/);
|
assert.match(source, /frequency: -1/);
|
||||||
assert.match(source, /this\.updatePlayerSmoke\(deltaMs\)/);
|
assert.match(source, /this\.updatePlayerSmoke\(deltaMs\)/);
|
||||||
assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/);
|
assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/);
|
||||||
assert.match(source, /if \(this\.gameState !== 'playing' \|\| forwardThrust <= 0\.04\)/);
|
assert.match(source, /const effectiveForwardThrust = clamp\([\s\S]*?this\.playerStats\?\.maxSpeed \|\| PLAYER_MAX_SPEED[\s\S]*?\)/);
|
||||||
|
assert.match(source, /if \(this\.gameState !== 'playing' \|\| effectiveForwardThrust <= 0\.04\)/);
|
||||||
assert.match(source, /const sternX = this\.player\.x - forwardX \* PLAYER_SMOKE_REAR_OFFSET/);
|
assert.match(source, /const sternX = this\.player\.x - forwardX \* PLAYER_SMOKE_REAR_OFFSET/);
|
||||||
assert.match(source, /const sternY = this\.player\.y - forwardY \* PLAYER_SMOKE_REAR_OFFSET/);
|
assert.match(source, /const sternY = this\.player\.y - forwardY \* PLAYER_SMOKE_REAR_OFFSET/);
|
||||||
assert.match(source, /PLAYER_SMOKE_ENGINE_OFFSET \* engineSide/);
|
assert.match(source, /PLAYER_SMOKE_ENGINE_OFFSET \* engineSide/);
|
||||||
assert.match(source, /const particle = this\.playerSmoke\.emitParticleAt\(engineX, engineY, 1\)/);
|
assert.match(source, /const particle = this\.playerSmoke\.emitParticleAt\(engineX, engineY, 1\)/);
|
||||||
assert.match(source, /particle\.velocityX = velocityX/);
|
assert.match(source, /particle\.velocityX = velocityX/);
|
||||||
assert.match(source, /particle\.velocityY = velocityY/);
|
assert.match(source, /particle\.velocityY = velocityY/);
|
||||||
assert.match(source, /Phaser\.Math\.Linear\(150, 48, forwardThrust\)/);
|
assert.match(source, /Phaser\.Math\.Linear\(150, 48, effectiveForwardThrust\)/);
|
||||||
assert.doesNotMatch(source, /this\.playerSmoke\.setPosition\(sternX, sternY\)/);
|
assert.doesNotMatch(source, /this\.playerSmoke\.setPosition\(sternX, sternY\)/);
|
||||||
assert.doesNotMatch(source, /this\.playerSmoke\.setParticleSpeed\(/);
|
assert.doesNotMatch(source, /this\.playerSmoke\.setParticleSpeed\(/);
|
||||||
});
|
});
|
||||||
@ -250,8 +252,9 @@ test('environment physics adds wind, soft island collision, turbulence, and proj
|
|||||||
assert.match(source, /this\.createEnvironmentPhysics\(\)/);
|
assert.match(source, /this\.createEnvironmentPhysics\(\)/);
|
||||||
assert.match(source, /this\.physics\.add\.staticImage/);
|
assert.match(source, /this\.physics\.add\.staticImage/);
|
||||||
assert.match(source, /obstacle\.body\.setCircle\(island\.radius\)/);
|
assert.match(source, /obstacle\.body\.setCircle\(island\.radius\)/);
|
||||||
assert.match(source, /this\.physics\.add\.collider\(this\.player, obstacle, \(\) => \{[\s\S]*?this\.playerSpeed \*= 0\.7;/);
|
assert.match(source, /const islands = this\.world\?\.islands \|\| ENVIRONMENT_ISLANDS/);
|
||||||
assert.match(source, /this\.physics\.add\.collider\(this\.pirates, obstacle\)/);
|
assert.match(source, /this\.physics\.add\.overlap\(this\.player, obstacle, \(\) => \{[\s\S]*?this\.playerSpeed \*= 0\.985;/);
|
||||||
|
assert.doesNotMatch(source, /this\.physics\.add\.collider\(this\.pirates, obstacle\)/);
|
||||||
assert.match(source, /this\.physics\.add\.overlap\(\s*this\.playerShots,[\s\S]*?this\.hitEnvironmentProjectile/);
|
assert.match(source, /this\.physics\.add\.overlap\(\s*this\.playerShots,[\s\S]*?this\.hitEnvironmentProjectile/);
|
||||||
assert.match(source, /this\.environmentWind = environmentWindAt\(\{/);
|
assert.match(source, /this\.environmentWind = environmentWindAt\(\{/);
|
||||||
assert.match(source, /const pirateWind = environmentWindAt\(\{/);
|
assert.match(source, /const pirateWind = environmentWindAt\(\{/);
|
||||||
@ -260,6 +263,23 @@ test('environment physics adds wind, soft island collision, turbulence, and proj
|
|||||||
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
|
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('procedural world and ship progression are wired into live gameplay', async () => {
|
||||||
|
const source = await read('src/game-v4.js');
|
||||||
|
|
||||||
|
assert.match(source, /generateWorld\(\{/);
|
||||||
|
assert.match(source, /this\.player\.setPosition\(this\.world\.spawn\.x, this\.world\.spawn\.y\)/);
|
||||||
|
assert.match(source, /pirateSpawnPointsForWave\(/);
|
||||||
|
assert.match(source, /for \(const route of this\.world\.tradeRoutes\)/);
|
||||||
|
assert.match(source, /for \(const territory of this\.world\.pirateTerritories\)/);
|
||||||
|
assert.match(source, /this\.salvageWrecks = this\.physics\.add\.group\(\)/);
|
||||||
|
assert.match(source, /collectSalvageWreck\(/);
|
||||||
|
assert.match(source, /bestAffordableMarketUpgrade\(/);
|
||||||
|
assert.match(source, /buyAndInstallModule\(/);
|
||||||
|
assert.match(source, /installModule\(this\.progression, moduleId\)/);
|
||||||
|
assert.match(source, /this\.playerBroadsideReloadMs = this\.playerStats\.reloadMs/);
|
||||||
|
assert.match(source, /stats\.windExposure/);
|
||||||
|
});
|
||||||
|
|
||||||
test('rolling clouds include large dense banks that phase in and out with the wind', async () => {
|
test('rolling clouds include large dense banks that phase in and out with the wind', async () => {
|
||||||
const source = await read('src/game-v4.js');
|
const source = await read('src/game-v4.js');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user