import {
Engine,
Rectangle,
Vector2,
Text,
Sprite,
Scene,
Circle,
VisibilityComponent,
PhysicsComponent,
Input,
GameObject,
Loader as ResourceLoader
} from 'dino-ge';
class MenuScene extends Scene {
constructor(game, onStart) {
super();
this.game = game;
this.onStart = onStart;
}
onLoad() {
this.game.cursor = 'pointer';
this.title = new Text({
tag: 'title',
text: 'DINO SURVIVAL',
fontSize: 64,
colour: '#43aa8b',
position: new Vector2(this.game.width / 2 - 250, 100),
width: 500,
zIndex: 10
});
this.startBtn = new Text({
tag: 'start-btn',
text: 'CLICK TO START',
fontSize: 32,
colour: 'white',
position: new Vector2(this.game.width / 2 - 150, 300),
width: 300,
zIndex: 10,
onClick: () => this.onStart()
});
this.controls = new Text({
tag: 'controls',
text: 'WASD to Move - SPACE to Shoot',
fontSize: 20,
colour: '#a0a0a0',
position: new Vector2(this.game.width / 2 - 150, 410),
width: 300,
zIndex: 10
});
}
onResize(width) {
if (this.title && this.startBtn && this.controls) {
this.title.transform.position.x = width / 2 - 250;
this.startBtn.transform.position.x = width / 2 - 150;
this.controls.transform.position.x = width / 2 - 150;
}
}
}
class PlayScene extends Scene {
static WORLD_WIDTH = 2000;
static WORLD_HEIGHT = 2000;
constructor(game, onGameOver) {
super();
this.game = game;
this.onGameOver = onGameOver;
this.score = 0;
this.lives = 3;
this.lastShotTime = 0;
this.shotCooldown = 250;
this.lastMeteorSpawn = 0;
this.isInvulnerable = false;
this.invulnerabilityDuration = 2000;
this.lastHitTime = 0;
this.highScore = parseInt(localStorage.getItem('dinoHighScore') || '0', 10);
this.startTime = Date.now();
this.fireballs = [];
this.meteors = [];
this.particles = [];
this.starsMid = [];
this.starsNear = [];
this.shakeIntensity = 0;
this.shakeDecay = 0.9;
}
onLoad() {
this.game.cursor = 'none';
this.startTime = Date.now();
// Background Layers (Hierarchical)
const background = new GameObject('background', -10);
this.add(background);
const midStarsLayer = new GameObject('stars-mid', -5);
background.transform.addChild(midStarsLayer.transform);
const nearStarsLayer = new GameObject('stars-near', -2);
background.transform.addChild(nearStarsLayer.transform);
// Mid Stars (Slow, small)
for (let i = 0; i < 100; i++) {
const star = new Rectangle({
position: new Vector2(
Math.random() * PlayScene.WORLD_WIDTH,
Math.random() * PlayScene.WORLD_HEIGHT
),
width: 2,
height: 2,
colour: 'rgba(255, 255, 255, 0.5)',
zIndex: 1
});
star.speed = 1 + Math.random() * 2;
midStarsLayer.transform.addChild(star.transform);
this.starsMid.push(star);
}
// Near Stars (Very fast, large, bright)
for (let i = 0; i < 40; i++) {
const star = new Rectangle({
position: new Vector2(
Math.random() * PlayScene.WORLD_WIDTH,
Math.random() * PlayScene.WORLD_HEIGHT
),
width: 3,
height: 3,
colour: 'rgba(255, 255, 255, 0.9)',
zIndex: 2
});
star.speed = 3 + Math.random() * 4;
nearStarsLayer.transform.addChild(star.transform);
this.starsNear.push(star);
}
this.player = new Sprite({
img: 'dino',
rows: 1,
cols: 24,
position: new Vector2(
PlayScene.WORLD_WIDTH / 2,
PlayScene.WORLD_HEIGHT / 2
),
startCol: 4,
endCol: 10,
tag: 'player',
zIndex: 5,
scale: 3
});
this.player.addComponent(new VisibilityComponent());
this.player.addComponent(new PhysicsComponent());
this.player.play();
// Auto-collision handling for player
this.player.on('collision', (e) => {
const { other } = e.detail;
if (other.metadata.tag === 'meteor' && !this.isInvulnerable) {
this.lives -= 1;
console.log(`Player hit! Lives remaining: ${this.lives}`);
this.livesText.text = `Lives: ${this.lives}`;
this.shakeIntensity = 20;
if (this.lives <= 0) {
const finalScore =
this.score + Math.floor((Date.now() - this.startTime) / 1000);
this.onGameOver(finalScore);
} else {
this.isInvulnerable = true;
this.player.getComponent(PhysicsComponent).isSensor = true;
this.lastHitTime = Date.now();
this.spawnExplosion(this.player.transform.position, 20, '#F94144');
other.destroySelf();
this.meteors = this.meteors.filter((m) => m !== other);
}
}
});
// Scene Graph Parenting
// Create a name tag for the dino
this.nameTag = new Text({
tag: 'name-tag',
text: 'Dino',
fontSize: '12',
colour: 'white',
position: new Vector2(-5, 20), // Compensated for parent scale: 3
width: 100,
zIndex: 6
});
this.nameTag.transform.scale = new Vector2(0.333, 0.333); // Keep size absolute
this.player.transform.addChild(this.nameTag.transform);
// Group UI elements under a container
this.uiContainer = new GameObject('ui-root', 10);
this.add(this.uiContainer);
this.scoreText = new Text({
tag: 'score',
text: 'Score: 0',
fontSize: '24',
colour: 'white',
position: new Vector2(this.game.width / 2 - 75, 20),
width: 150,
zIndex: 11
});
this.uiContainer.transform.addChild(this.scoreText.transform);
this.livesText = new Text({
tag: 'lives',
text: `Lives: ${this.lives}`,
fontSize: '24',
colour: '#F94144',
position: new Vector2(20, 20),
width: 150,
zIndex: 11,
horizontalAlign: 'left'
});
this.uiContainer.transform.addChild(this.livesText.transform);
}
update() {
const playerTransform = this.player.transform;
const now = Date.now();
// Movement Logic
const moveSpeed = 400;
let targetVX = 0;
let targetVY = 0;
if (Input.isKeyDown('w') || Input.isKeyDown('arrowup'))
targetVY = -moveSpeed;
else if (Input.isKeyDown('s') || Input.isKeyDown('arrowdown'))
targetVY = moveSpeed;
if (Input.isKeyDown('a') || Input.isKeyDown('arrowleft'))
targetVX = -moveSpeed;
else if (Input.isKeyDown('d') || Input.isKeyDown('arrowright'))
targetVX = moveSpeed;
if (targetVX !== 0 && targetVY !== 0) {
targetVX *= 0.707;
targetVY *= 0.707;
}
const playerPhysics = this.player.getComponent(PhysicsComponent);
playerPhysics.velocity.x = targetVX;
playerPhysics.velocity.y = targetVY;
// Bounds Checking
const pw = this.player.bounds ? this.player.bounds.width : 0;
const ph = this.player.bounds ? this.player.bounds.height : 0;
if (playerTransform.position.x < 0) playerTransform.position.x = 0;
if (playerTransform.position.x > PlayScene.WORLD_WIDTH - pw)
playerTransform.position.x = PlayScene.WORLD_WIDTH - pw;
if (playerTransform.position.y < 0) playerTransform.position.y = 0;
if (playerTransform.position.y > PlayScene.WORLD_HEIGHT - ph)
playerTransform.position.y = PlayScene.WORLD_HEIGHT - ph;
if (targetVX < 0) this.player.flip = true;
else if (targetVX > 0) this.player.flip = false;
// Shooting System
const isShooting = Input.isKeyDown(' ') || Input.isKeyDown('mouse0');
if (now - this.lastShotTime > this.shotCooldown && isShooting) {
const fireball = new Circle({
position: new Vector2(
playerTransform.position.x + pw / 2 - 5,
playerTransform.position.y
),
radius: 5,
colour: '#FFB703',
zIndex: 6,
tag: 'fireball'
});
fireball.addComponent(new PhysicsComponent());
fireball.getComponent(PhysicsComponent).velocity.y = -600;
fireball.on('collision', (e) => {
const { other } = e.detail;
if (other.metadata.tag === 'meteor') {
this.spawnExplosion(
other.transform.position,
other.radius,
other.colour
);
other.destroySelf();
fireball.destroySelf();
this.score += 10;
this.shakeIntensity = Math.max(this.shakeIntensity, 5);
this.meteors = this.meteors.filter((m) => m !== other);
this.fireballs = this.fireballs.filter((fb) => fb !== fireball);
}
});
this.fireballs.push(fireball);
this.lastShotTime = now;
}
// Cleanup off-screen fireballs
this.fireballs = this.fireballs.filter((fb) => {
if (fb.transform.position.y < Engine.camera.position.y - 50) {
fb.destroySelf();
return false;
}
return true;
});
// Update and Cleanup Particles
this.particles = this.particles.filter((p) => {
p.life -= 0.02;
if (p.life <= 0) {
p.destroySelf();
return false;
}
return true;
});
// Camera follows player
Engine.camera.follow(this.player, this.game.width, this.game.height);
// Apply Screen Shake
if (this.shakeIntensity > 0.1) {
Engine.camera.position.x += (Math.random() - 0.5) * this.shakeIntensity;
Engine.camera.position.y += (Math.random() - 0.5) * this.shakeIntensity;
this.shakeIntensity *= this.shakeDecay;
} else {
this.shakeIntensity = 0;
}
// Update Invulnerability
if (this.isInvulnerable) {
const visibility = this.player.getComponent(VisibilityComponent);
if (visibility) {
visibility.visible = Math.floor(now / 100) % 2 === 0;
}
if (now - this.lastHitTime > this.invulnerabilityDuration) {
this.isInvulnerable = false;
this.player.getComponent(PhysicsComponent).isSensor = false;
if (visibility) visibility.visible = true;
}
}
// Fixed UI container
this.uiContainer.transform.position.x = Engine.camera.position.x;
this.uiContainer.transform.position.y = Engine.camera.position.y;
// Parallax
this.starsMid.forEach((s) => {
s.transform.position.y += s.speed;
if (s.transform.position.y > PlayScene.WORLD_HEIGHT)
s.transform.position.y = 0;
});
this.starsNear.forEach((s) => {
s.transform.position.y += s.speed;
if (s.transform.position.y > PlayScene.WORLD_HEIGHT)
s.transform.position.y = 0;
});
// Spawn Meteors
if (Math.random() < 0.05) {
const radius = 15 + Math.random() * 20;
const spawnX = Engine.camera.position.x + Math.random() * this.game.width;
const meteor = new Circle({
position: new Vector2(spawnX, Engine.camera.position.y - 100),
radius: radius,
colour: '#F94144',
zIndex: 4,
tag: 'meteor'
});
meteor.addComponent(new PhysicsComponent());
const meteorPhys = meteor.getComponent(PhysicsComponent);
meteorPhys.velocity.y = 100 + Math.random() * 200;
meteorPhys.acceleration.y = 50;
this.meteors.push(meteor);
}
this.meteors = this.meteors.filter((m) => {
if (
m.transform.position.y >
Engine.camera.position.y + this.game.height + 100
) {
m.destroySelf();
return false;
}
return true;
});
const timeScore = Math.floor((now - this.startTime) / 1000);
this.scoreText.text = `Score: ${this.score + timeScore}`;
}
spawnExplosion(pos, radius, colour) {
for (let i = 0; i < 8; i++) {
const p = new Circle({
position: new Vector2(pos.x + radius, pos.y + radius),
radius: 2 + Math.random() * 3,
colour: colour,
zIndex: 3,
tag: 'particle'
});
p.addComponent(new PhysicsComponent());
const pPhys = p.getComponent(PhysicsComponent);
pPhys.velocity.x = (Math.random() - 0.5) * 400;
pPhys.velocity.y = (Math.random() - 0.5) * 400;
p.life = 1.0;
this.particles.push(p);
}
}
}
class GameOverScene extends Scene {
constructor(game, score, onRestart) {
super();
this.game = game;
this.score = score;
this.onRestart = onRestart;
this.highScore = parseInt(localStorage.getItem('dinoHighScore') || '0', 10);
}
onLoad() {
this.game.cursor = 'pointer';
this.gameOverText = new Text({
tag: 'gameOver',
text: 'GAME OVER',
fontSize: '60',
colour: '#F94144',
position: new Vector2(this.game.width / 2 - 200, 150),
width: 400,
zIndex: 10
});
this.finalScoreText = new Text({
tag: 'finalScore',
text: `Score: ${this.score}`,
fontSize: '30',
colour: 'white',
position: new Vector2(this.game.width / 2 - 100, 250),
width: 200,
zIndex: 10
});
if (this.score > this.highScore) {
localStorage.setItem('dinoHighScore', this.score.toString());
this.highScoreText = new Text({
tag: 'newHigh',
text: 'NEW HIGH SCORE!',
fontSize: '24',
colour: '#FFB703',
position: new Vector2(this.game.width / 2 - 100, 300),
width: 200,
zIndex: 10
});
} else {
this.highScoreText = new Text({
tag: 'highScore',
text: `High Score: ${this.highScore}`,
fontSize: '24',
colour: '#a0a0a0',
position: new Vector2(this.game.width / 2 - 100, 300),
width: 200,
zIndex: 10
});
}
this.restartBtn = new Text({
tag: 'restart-btn',
text: 'PLAY AGAIN',
fontSize: '32',
colour: '#43aa8b',
position: new Vector2(this.game.width / 2 - 100, 400),
width: 200,
zIndex: 10,
onClick: () => this.onRestart()
});
}
onResize(width, height) {
if (
this.gameOverText &&
this.finalScoreText &&
this.highScoreText &&
this.restartBtn
) {
this.gameOverText.transform.position.x = width / 2 - 200;
this.finalScoreText.transform.position.x = width / 2 - 100;
this.highScoreText.transform.position.x = width / 2 - 100;
this.restartBtn.transform.position.x = width / 2 - 100;
}
}
}
class DinoSurvival {
constructor() {
this.game = new Engine(
{
onLoad: async () => {
ResourceLoader.queueImage('dino', './sprites/DinoSprites - doux.png');
await ResourceLoader.loadAll((percent) => {
console.log(`Loading: ${Math.round(percent)}%`);
});
// Use Event Bus for Game State management
Engine.on('GAME_OVER', (e) => this.showGameOver(e.detail));
Engine.on('START_GAME', () => this.startGame());
Engine.on('SHOW_MENU', () => this.showMenu());
this.showMenu();
},
update: () => {}
},
{
title: 'Dino Survival',
backgroundColour: '#264653',
containerId: 'playground-canvas-container'
}
);
}
showMenu() {
Engine.currentScene = new MenuScene(this.game, () =>
Engine.emit('START_GAME')
);
}
startGame() {
Engine.currentScene = new PlayScene(this.game, (score) =>
Engine.emit('GAME_OVER', score)
);
}
showGameOver(score) {
Engine.currentScene = new GameOverScene(this.game, score, () =>
Engine.emit('SHOW_MENU')
);
}
}
new DinoSurvival();