Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions js/Game.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ class Game {
this.systems.weather.reset();
this.screenEffects.reset();

// Spawn asteroids across the map
this.systems.spawner.spawnAsteroids(50);

// Hide menu, show game
this.systems.ui.showScreen('game');

Expand Down Expand Up @@ -389,8 +392,8 @@ class Game {
const maxHealth = shipData.baseStats.maxHealth + metaHealth;

this.player.addComponent('position', Components.Position(
this.canvas.width / 2,
this.canvas.height / 2
WORLD_WIDTH / 2,
WORLD_HEIGHT / 2
));

this.player.addComponent('velocity', Components.Velocity(0, 0));
Expand Down
4 changes: 4 additions & 0 deletions js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* Centralized constants to avoid redeclaration errors
*/

// World size (playable area - 2x canvas size)
const WORLD_WIDTH = 2560;
const WORLD_HEIGHT = 1440;

// Enemy size threshold for boss detection
const BOSS_SIZE_THRESHOLD = 35;

Expand Down
5 changes: 5 additions & 0 deletions js/core/ECS.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,10 @@ const Components = {
patterns,
phaseTime: 0,
nextPhaseHealth: 0.5
}),
Asteroid: (sizeTier) => ({
sizeTier,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 2
})
};
95 changes: 95 additions & 0 deletions js/systems/CollisionSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class CollisionSystem {
// Check projectile-enemy collisions
this.checkProjectileEnemyCollisions();

// Check projectile-asteroid collisions
this.checkProjectileAsteroidCollisions();

// Check player-enemy collisions
this.checkPlayerEnemyCollisions();

Expand Down Expand Up @@ -97,6 +100,53 @@ class CollisionSystem {
}
}

checkProjectileAsteroidCollisions() {
const projectiles = this.world.getEntitiesByType('projectile');
const asteroids = this.world.getEntitiesByType('asteroid');

for (const projectile of projectiles) {
const projPos = projectile.getComponent('position');
const projCol = projectile.getComponent('collision');
const projComp = projectile.getComponent('projectile');

if (!projPos || !projCol || !projComp) continue;

// Check if projectile is from player
const ownerEntity = this.world.getEntity(projComp.owner);
if (!ownerEntity || ownerEntity.type !== 'player') continue;

for (const asteroid of asteroids) {
const asteroidPos = asteroid.getComponent('position');
const asteroidCol = asteroid.getComponent('collision');
const asteroidHealth = asteroid.getComponent('health');

if (!asteroidPos || !asteroidCol || !asteroidHealth) continue;

if (MathUtils.circleCollision(
projPos.x, projPos.y, projCol.radius,
asteroidPos.x, asteroidPos.y, asteroidCol.radius
)) {
// Deal damage to asteroid
this.damageAsteroid(asteroid, projComp.damage);

// Don't remove orbital projectiles
if (projComp.orbital) {
continue;
}

// Remove projectile if not piercing
if (projComp.piercing <= 0) {
this.world.removeEntity(projectile.id);
} else {
projComp.piercing--;
}

break;
}
}
}
}

checkPlayerEnemyCollisions() {
const players = this.world.getEntitiesByType('player');
const enemies = this.world.getEntitiesByType('enemy');
Expand Down Expand Up @@ -312,6 +362,51 @@ class CollisionSystem {
}
}

damageAsteroid(asteroid, damage) {
const health = asteroid.getComponent('health');
const pos = asteroid.getComponent('position');
const renderable = asteroid.getComponent('renderable');

if (!health) return;

health.current -= damage;

// Play hit sound
if (this.audioManager && this.audioManager.initialized) {
this.audioManager.playSFX('hit', MathUtils.randomFloat(1.0, 1.3));
}

if (health.current <= 0) {
this.destroyAsteroid(asteroid);
}
}

destroyAsteroid(asteroid) {
const pos = asteroid.getComponent('position');
const renderable = asteroid.getComponent('renderable');

if (pos) {
// Create particle explosion
if (this.particleSystem) {
const color = renderable ? renderable.color : '#8B7355';
this.particleSystem.createExplosion(pos.x, pos.y, renderable ? renderable.size : 12, color, 15);
}

// Play destruction sound
if (this.audioManager && this.audioManager.initialized) {
this.audioManager.playSFX('explosion', MathUtils.randomFloat(0.9, 1.1));
}

// Small chance to drop XP
if (Math.random() < 0.3) {
this.spawnPickup(pos.x, pos.y, 'xp', 2);
}
}

// Remove asteroid (asteroids don't count as kills)
this.world.removeEntity(asteroid.id);
}

killEnemy(enemy) {
const enemyComp = enemy.getComponent('enemy');
const pos = enemy.getComponent('position');
Expand Down
23 changes: 16 additions & 7 deletions js/systems/MovementSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ class MovementSystem {
this.updatePlayerMovement(player, deltaTime);
}

// Update asteroid rotation
const asteroids = this.world.getEntitiesByType('asteroid');
for (const asteroid of asteroids) {
const asteroidComp = asteroid.getComponent('asteroid');
if (asteroidComp) {
asteroidComp.rotation += asteroidComp.rotationSpeed * deltaTime;
}
}

// Update orbital and laser projectiles
const projectiles = this.world.getEntitiesByType('projectile');
for (const projectile of projectiles) {
Expand Down Expand Up @@ -133,12 +142,12 @@ class MovementSystem {
if (Math.abs(vel.vy) < velocityThreshold) vel.vy = 0;
}

// Keep player in bounds
// Keep player in world bounds
const collision = player.getComponent('collision');
const radius = collision ? collision.radius : 15;

pos.x = MathUtils.clamp(pos.x, radius, this.canvas.width - radius);
pos.y = MathUtils.clamp(pos.y, radius, this.canvas.height - radius);
pos.x = MathUtils.clamp(pos.x, radius, WORLD_WIDTH - radius);
pos.y = MathUtils.clamp(pos.y, radius, WORLD_HEIGHT - radius);
}

updateEntityPosition(entity, deltaTime) {
Expand All @@ -150,10 +159,10 @@ class MovementSystem {
pos.x += vel.vx * deltaTime;
pos.y += vel.vy * deltaTime;

// Remove entities that are off-screen (with buffer)
const buffer = 100;
if (pos.x < -buffer || pos.x > this.canvas.width + buffer ||
pos.y < -buffer || pos.y > this.canvas.height + buffer) {
// Remove entities that are far off-world (with buffer)
const buffer = 200;
if (pos.x < -buffer || pos.x > WORLD_WIDTH + buffer ||
pos.y < -buffer || pos.y > WORLD_HEIGHT + buffer) {

// Don't remove player or pickups
if (entity.type !== 'player' && entity.type !== 'pickup') {
Expand Down
116 changes: 108 additions & 8 deletions js/systems/RenderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ class RenderSystem {
this.world = world;
this.gameState = gameState;

// Camera system for larger world
this.cameraX = 0;
this.cameraY = 0;

// Zoom level (0.5 = 2x zoom out to see more)
this.zoomLevel = 0.5;

// Screen effects reference (set from Game.js)
this.screenEffects = null;

Expand Down Expand Up @@ -41,8 +48,8 @@ class RenderSystem {
layers.forEach(layer => {
for (let i = 0; i < layer.count; i++) {
this.stars.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
x: Math.random() * WORLD_WIDTH,
y: Math.random() * WORLD_HEIGHT,
speed: layer.speed,
size: layer.size,
alpha: layer.alpha,
Expand All @@ -53,6 +60,30 @@ class RenderSystem {
});
}

/**
* Update camera position to follow player
*/
updateCamera() {
const players = this.world.getEntitiesByType('player');
if (players.length > 0) {
const player = players[0];
const pos = player.getComponent('position');
if (pos) {
// Calculate visible area based on zoom level
const visibleWidth = this.canvas.width / this.zoomLevel;
const visibleHeight = this.canvas.height / this.zoomLevel;

// Center camera on player
this.cameraX = pos.x - visibleWidth / 2;
this.cameraY = pos.y - visibleHeight / 2;

// Clamp camera to world bounds
this.cameraX = MathUtils.clamp(this.cameraX, 0, WORLD_WIDTH - visibleWidth);
this.cameraY = MathUtils.clamp(this.cameraY, 0, WORLD_HEIGHT - visibleHeight);
}
}
}

/**
* Main render loop
* @param {number} deltaTime - Time since last frame in seconds
Expand All @@ -61,6 +92,9 @@ class RenderSystem {
this.lastFrameTime = deltaTime;
this.fps = deltaTime > 0 ? 1 / deltaTime : 60;

// Update camera position based on player
this.updateCamera();

// Clear canvas
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
Expand All @@ -70,18 +104,25 @@ class RenderSystem {
this.gameState.isState(GameStates.LEVEL_UP) ||
this.gameState.isState(GameStates.PAUSED)) {

// Save context for screen shake
// Save context for camera, zoom, and screen shake
this.ctx.save();

// Apply screen shake if available
if (this.screenEffects) {
this.screenEffects.applyShake(this.ctx);
}

// Apply zoom scale (0.5 = 2x zoom out)
// IMPORTANT: Scale must be applied before translate to maintain correct camera positioning
this.ctx.scale(this.zoomLevel, this.zoomLevel);

// Apply camera translation (adjusted for zoom)
this.ctx.translate(-this.cameraX, -this.cameraY);

this.renderStarfield(deltaTime);
this.renderEntities();

// Restore context after shake
// Restore context after camera, zoom, and shake
this.ctx.restore();

this.renderBossHealthBar();
Expand All @@ -101,11 +142,11 @@ class RenderSystem {
this.ctx.save();

this.stars.forEach(star => {
// Parallax movement
// Parallax movement (vertical scrolling)
star.y += star.speed * 60 * deltaTime;
if (star.y > this.canvas.height) {
if (star.y > WORLD_HEIGHT) {
star.y = 0;
star.x = Math.random() * this.canvas.width;
star.x = Math.random() * WORLD_WIDTH;
}

// Twinkling effect
Expand All @@ -124,8 +165,9 @@ class RenderSystem {
* Render all entities in the game world
*/
renderEntities() {
// Render order: particles -> pickups -> projectiles -> enemies -> weather -> player
// Render order: particles -> asteroids -> pickups -> projectiles -> enemies -> weather -> player
this.renderParticles();
this.renderAsteroids();
this.renderPickups();
this.renderProjectiles();
this.renderEnemies();
Expand Down Expand Up @@ -162,6 +204,64 @@ class RenderSystem {
});
}

/**
* Render asteroids
*/
renderAsteroids() {
const asteroids = this.world.getEntitiesByType('asteroid');

asteroids.forEach(asteroid => {
const pos = asteroid.getComponent('position');
const render = asteroid.getComponent('renderable');
const asteroidComp = asteroid.getComponent('asteroid');
const health = asteroid.getComponent('health');

if (!pos || !render || !asteroidComp) return;

this.ctx.save();
this.ctx.translate(pos.x, pos.y);
this.ctx.rotate(asteroidComp.rotation);

// Draw asteroid as rocky brown circle with darker patches
const gradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, render.size);
gradient.addColorStop(0, '#8B7355');
gradient.addColorStop(0.5, '#6B5345');
gradient.addColorStop(1, '#4B3325');

this.ctx.fillStyle = gradient;
this.ctx.shadowBlur = 5;
this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';

this.ctx.beginPath();
this.ctx.arc(0, 0, render.size, 0, Math.PI * 2);
this.ctx.fill();

// Add some crater-like details
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
for (let i = 0; i < 3; i++) {
const angle = (i / 3) * Math.PI * 2;
const dist = render.size * 0.4;
const craterSize = render.size * 0.15;
this.ctx.beginPath();
this.ctx.arc(
Math.cos(angle) * dist,
Math.sin(angle) * dist,
craterSize,
0,
Math.PI * 2
);
this.ctx.fill();
}

this.ctx.restore();

// Health bar for larger asteroids
if (health && render.size > 15) {
this.drawHealthBar(pos.x, pos.y - render.size - 8, health.current, health.max, false);
}
});
}

/**
* Render pickups (XP, health, etc.)
*/
Expand Down
Loading