import Phaser from 'phaser'; import './style.css'; const WORLD_SIZE = 2400; const MAX_FORWARD_SPEED = 180; const MAX_REVERSE_SPEED = -55; const ACCELERATION = 85; const BRAKE = 110; const TURN_RATE = 72; const BROADSIDE_SPEED = 520; class GameScene extends Phaser.Scene { constructor() { super('game'); this.speed = 0; } create() { this.physics.world.setBounds(0, 0, WORLD_SIZE, WORLD_SIZE); this.drawWorld(); this.createTextures(); this.ship = this.physics.add.image(WORLD_SIZE / 2, WORLD_SIZE / 2, 'ship'); this.ship.setDepth(5); this.ship.setCollideWorldBounds(true); this.ship.body.setAllowGravity(false); this.projectiles = this.physics.add.group(); this.arcGraphics = this.add.graphics().setDepth(4); this.cursors = this.input.keyboard.createCursorKeys(); this.keys = this.input.keyboard.addKeys({ firePort: Phaser.Input.Keyboard.KeyCodes.Q, fireStarboard: Phaser.Input.Keyboard.KeyCodes.E, stop: Phaser.Input.Keyboard.KeyCodes.SPACE, }); this.cameras.main.setBounds(0, 0, WORLD_SIZE, WORLD_SIZE); this.cameras.main.startFollow(this.ship, true, 0.08, 0.08); this.cameras.main.setZoom(1.15); this.hud = this.add.text(18, 18, '', { fontFamily: 'monospace', fontSize: '15px', color: '#e8f2ff', backgroundColor: '#071018cc', padding: { x: 12, y: 10 }, lineSpacing: 4, }).setScrollFactor(0).setDepth(20); this.title = this.add.text(18, 18, 'ARROWSTACK AIRSHIP', { fontFamily: 'monospace', fontSize: '12px', color: '#8cd3ff', }).setScrollFactor(0).setDepth(21); this.hud.y = 42; } drawWorld() { const background = this.add.graphics(); background.fillStyle(0x132335, 1); background.fillRect(0, 0, WORLD_SIZE, WORLD_SIZE); background.lineStyle(1, 0x8db3cc, 0.08); for (let x = 0; x <= WORLD_SIZE; x += 100) background.lineBetween(x, 0, x, WORLD_SIZE); for (let y = 0; y <= WORLD_SIZE; y += 100) background.lineBetween(0, y, WORLD_SIZE, y); const clouds = this.add.graphics(); for (let i = 0; i < 42; i++) { const x = Phaser.Math.Between(80, WORLD_SIZE - 80); const y = Phaser.Math.Between(80, WORLD_SIZE - 80); const radius = Phaser.Math.Between(25, 65); clouds.fillStyle(0xbcd2df, Phaser.Math.FloatBetween(0.025, 0.09)); clouds.fillCircle(x, y, radius); clouds.fillCircle(x + radius * 0.6, y + 8, radius * 0.7); clouds.fillCircle(x - radius * 0.5, y + 5, radius * 0.6); } 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); } createTextures() { const ship = this.make.graphics({ x: 0, y: 0, add: false }); ship.fillStyle(0xd8a65f, 1); ship.fillTriangle(32, 2, 8, 62, 56, 62); ship.fillStyle(0x715438, 1); ship.fillRect(17, 31, 30, 20); ship.fillStyle(0xe8dfc7, 1); ship.fillEllipse(32, 24, 42, 16); ship.lineStyle(2, 0x201914, 0.8); ship.strokeTriangle(32, 2, 8, 62, 56, 62); ship.generateTexture('ship', 64, 68); ship.destroy(); const shot = this.make.graphics({ x: 0, y: 0, add: false }); shot.fillStyle(0xffd36a, 1); shot.fillCircle(5, 5, 5); shot.generateTexture('shot', 10, 10); shot.destroy(); } update(_time, deltaMs) { const dt = deltaMs / 1000; if (this.cursors.up.isDown) this.speed += ACCELERATION * dt; if (this.cursors.down.isDown) this.speed -= BRAKE * dt; if (this.keys.stop.isDown) this.speed *= Math.pow(0.025, dt); if (!this.cursors.up.isDown && !this.cursors.down.isDown && !this.keys.stop.isDown) { this.speed *= Math.pow(0.985, deltaMs / 16.667); } this.speed = Phaser.Math.Clamp(this.speed, MAX_REVERSE_SPEED, MAX_FORWARD_SPEED); const turnStrength = Phaser.Math.Clamp(Math.abs(this.speed) / 100, 0.28, 1); const directionSign = this.speed < 0 ? -1 : 1; if (this.cursors.left.isDown) this.ship.angle -= TURN_RATE * turnStrength * directionSign * dt; if (this.cursors.right.isDown) this.ship.angle += TURN_RATE * turnStrength * directionSign * dt; const heading = Phaser.Math.DegToRad(this.ship.angle - 90); this.ship.setVelocity(Math.cos(heading) * this.speed, Math.sin(heading) * this.speed); if (Phaser.Input.Keyboard.JustDown(this.keys.firePort)) this.fireBroadside(-1); if (Phaser.Input.Keyboard.JustDown(this.keys.fireStarboard)) this.fireBroadside(1); this.drawWeaponArcs(); this.cleanupProjectiles(); const normalisedHeading = Phaser.Math.Angle.NormalizeDegrees(this.ship.angle); this.hud.setText([ `speed ${Math.abs(this.speed).toFixed(0).padStart(3, ' ')} ${this.speed < 0 ? 'REV' : 'FWD'}`, `heading ${normalisedHeading.toFixed(0).padStart(3, ' ')}°`, '', '↑ / ↓ throttle / reverse', '← / → steer', 'Q port broadside', 'E starboard broadside', 'SPACE brake', ]); } fireBroadside(side) { const broadsideAngle = Phaser.Math.DegToRad(this.ship.angle - 90 + side * 90); const muzzleOffset = 34; const x = this.ship.x + Math.cos(broadsideAngle) * muzzleOffset; const y = this.ship.y + Math.sin(broadsideAngle) * muzzleOffset; const shot = this.projectiles.create(x, y, 'shot'); shot.setDepth(6); shot.body.setAllowGravity(false); shot.setData('bornAt', this.time.now); shot.setVelocity( Math.cos(broadsideAngle) * BROADSIDE_SPEED + this.ship.body.velocity.x * 0.3, Math.sin(broadsideAngle) * BROADSIDE_SPEED + this.ship.body.velocity.y * 0.3, ); this.tweens.add({ targets: shot, alpha: 0.15, duration: 700, ease: 'Quad.easeIn', }); } cleanupProjectiles() { for (const shot of this.projectiles.getChildren()) { if (!shot.active) continue; if (this.time.now - shot.getData('bornAt') > 800) shot.destroy(); } } drawWeaponArcs() { const g = this.arcGraphics; g.clear(); const radius = 190; const base = Phaser.Math.DegToRad(this.ship.angle - 90); g.lineStyle(2, 0xffc857, 0.28); g.beginPath(); g.arc(this.ship.x, this.ship.y, radius, base + Phaser.Math.DegToRad(-135), base + Phaser.Math.DegToRad(-45)); g.strokePath(); g.beginPath(); g.arc(this.ship.x, this.ship.y, radius, base + Phaser.Math.DegToRad(45), base + Phaser.Math.DegToRad(135)); g.strokePath(); } } new Phaser.Game({ type: Phaser.AUTO, parent: 'game', width: window.innerWidth, height: window.innerHeight, backgroundColor: '#132335', physics: { default: 'arcade', arcade: { debug: false }, }, scale: { mode: Phaser.Scale.RESIZE, autoCenter: Phaser.Scale.CENTER_BOTH, }, scene: GameScene, });