Build top-down airship prototype
This commit is contained in:
parent
f852ebe07a
commit
1776313df7
216
src/main.js
Normal file
216
src/main.js
Normal file
@ -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,
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user