diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..f460a6c --- /dev/null +++ b/src/main.js @@ -0,0 +1,216 @@ +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, +});