diff options
Diffstat (limited to 'src/engine/entities')
-rw-r--r-- | src/engine/entities/Curry.ts | 10 | ||||
-rw-r--r-- | src/engine/entities/Entity.ts | 2 | ||||
-rw-r--r-- | src/engine/entities/EntityNames.ts | 1 | ||||
-rw-r--r-- | src/engine/entities/LockedDoor.ts | 47 | ||||
-rw-r--r-- | src/engine/entities/Particles.ts | 194 | ||||
-rw-r--r-- | src/engine/entities/index.ts | 2 |
6 files changed, 251 insertions, 5 deletions
diff --git a/src/engine/entities/Curry.ts b/src/engine/entities/Curry.ts index 85bc7ef..bd57e19 100644 --- a/src/engine/entities/Curry.ts +++ b/src/engine/entities/Curry.ts @@ -1,4 +1,5 @@ import { Entity, EntityNames } from "."; +import { Game } from ".."; import { BoundingBox, Colliding, Grid, Sprite } from "../components"; import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; import { Coord2D } from "../interfaces"; @@ -13,7 +14,7 @@ export class Curry extends Entity { this.addComponent(new Grid(gridPosition)); - this.addComponent(new Colliding()); + this.addComponent(new Colliding(this.collisionHandler)); this.addComponent( new BoundingBox( @@ -42,4 +43,11 @@ export class Curry extends Entity { ), ); } + + private collisionHandler(game: Game, entity: Entity) { + if (entity.name === EntityNames.Player) { + game.removeEntity(this.id); + game.stop(); + } + } } diff --git a/src/engine/entities/Entity.ts b/src/engine/entities/Entity.ts index d5a8e6e..a9c0d4b 100644 --- a/src/engine/entities/Entity.ts +++ b/src/engine/entities/Entity.ts @@ -29,7 +29,7 @@ export abstract class Entity { this.hooks.get(name)?.remove(); } - public getComponent<T extends Component>(name: string): T { + public getComponent<T>(name: string): T { if (!this.hasComponent(name)) { throw new Error("Entity does not have component " + name); } diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts index 3f6d26f..056db9a 100644 --- a/src/engine/entities/EntityNames.ts +++ b/src/engine/entities/EntityNames.ts @@ -7,4 +7,5 @@ export namespace EntityNames { export const LockedDoor = "LockedDoor"; export const Curry = "Curry"; export const FunctionApplication = "FunctionApplication"; + export const Particles = "Particles"; } diff --git a/src/engine/entities/LockedDoor.ts b/src/engine/entities/LockedDoor.ts index 5e364b8..b4887d6 100644 --- a/src/engine/entities/LockedDoor.ts +++ b/src/engine/entities/LockedDoor.ts @@ -1,7 +1,16 @@ -import { Entity, EntityNames } from "."; -import { BoundingBox, Colliding, Grid, Sprite } from "../components"; +import { Entity, EntityNames, Particles } from "."; +import { Game } from ".."; +import { + BoundingBox, + Colliding, + Grid, + Sprite, + ComponentNames, +} from "../components"; import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; import { Coord2D } from "../interfaces"; +import { Grid as GridSystem, SystemNames } from "../systems"; +import { colors } from "../utils"; export class LockedDoor extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( @@ -13,7 +22,7 @@ export class LockedDoor extends Entity { this.addComponent(new Grid(gridPosition)); - this.addComponent(new Colliding()); + this.addComponent(new Colliding(this.handleCollision.bind(this))); this.addComponent( new BoundingBox( @@ -42,4 +51,36 @@ export class LockedDoor extends Entity { ), ); } + + private handleCollision(game: Game, entity: Entity) { + if (entity.name !== EntityNames.Key) { + return; + } + + game.removeEntity(this.id); + game.removeEntity(entity.id); + + const grid = this.getComponent<Grid>(ComponentNames.Grid); + const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid); + const { dimension } = gridSystem; + const particles = new Particles({ + center: gridSystem.gridToScreenPosition(grid.gridPosition), + spawnerDimensions: { + width: dimension.width / 2, + height: dimension.height / 2, + }, + particleCount: 20, + spawnerShape: "rectangle", + particleShape: "rectangle", + particleMeanSpeed: 0.35, + particleSpeedVariance: 0.15, + particleMeanLife: 80, + particleMeanSize: 3, + particleSizeVariance: 1, + particleLifeVariance: 20, + particleColors: [colors.yellow, colors.lightYellow], + }); + + game.addEntity(particles); + } } diff --git a/src/engine/entities/Particles.ts b/src/engine/entities/Particles.ts new file mode 100644 index 0000000..34b475c --- /dev/null +++ b/src/engine/entities/Particles.ts @@ -0,0 +1,194 @@ +import { Entity, EntityNames } from "."; +import { + BoundingBox, + Component, + ComponentNames, + Life, + Renderable, +} from "../components"; +import { Coord2D, Dimension2D, DrawArgs } from "../interfaces"; +import { colors } from "../utils"; +import { normalRandom } from "../utils/random"; + +export interface ParticleSpawnOptions { + spawnerDimensions: Dimension2D; + center: Coord2D; + spawnerShape: "circle" | "rectangle"; + particleShape: "circle" | "rectangle"; + particleCount: number; + particleMeanLife: number; + particleLifeVariance: number; + particleMeanSize: number; + particleSizeVariance: number; + particleMeanSpeed: number; + particleSpeedVariance: number; + particleColors: Array<string>; +} + +const DEFAULT_PARTICLE_SPAWN_OPTIONS: ParticleSpawnOptions = { + spawnerDimensions: { width: 0, height: 0 }, + center: { x: 0, y: 0 }, + spawnerShape: "circle", + particleShape: "circle", + particleCount: 50, + particleMeanLife: 200, + particleLifeVariance: 50, + particleMeanSize: 12, + particleSizeVariance: 1, + particleMeanSpeed: 2, + particleSpeedVariance: 1, + particleColors: [colors.gray, colors.aqua, colors.lightAqua], +}; + +interface Particle { + position: Coord2D; + velocity: Coord2D; + dimension: Dimension2D; + color: string; + life: number; + shape: "circle" | "rectangle"; +} + +class ParticleRenderer extends Component implements Renderable { + private particles: Array<Particle>; + private onDeath?: () => void; + + constructor(particles: Array<Particle> = [], onDeath?: () => void) { + super(ComponentNames.Sprite); + + this.particles = particles; + this.onDeath = onDeath; + } + + public update(dt: number) { + this.particles = this.particles.filter((particle) => { + particle.position.x += particle.velocity.x * dt; + particle.position.y += particle.velocity.y * dt; + particle.life -= dt; + return particle.life > 0; + }); + + if (this.particles.length === 0 && this.onDeath) { + this.onDeath(); + } + } + + public draw(ctx: CanvasRenderingContext2D, _drawArgs: DrawArgs) { + for (const particle of this.particles) { + ctx.fillStyle = particle.color; + if (particle.shape === "circle") { + ctx.beginPath(); + + ctx.ellipse( + particle.position.x, + particle.position.y, + particle.dimension.width / 2, + particle.dimension.height / 2, + 0, + 0, + Math.PI * 2, + ); + ctx.fill(); + } else { + ctx.fillRect( + particle.position.x - particle.dimension.width / 2, + particle.position.y - particle.dimension.height / 2, + particle.dimension.width, + particle.dimension.height, + ); + } + } + } +} + +export class Particles extends Entity { + constructor(options: Partial<ParticleSpawnOptions>) { + super(EntityNames.Particles); + + const spawnOptions = { + ...DEFAULT_PARTICLE_SPAWN_OPTIONS, + ...options, + }; + const particles = Array(options.particleCount) + .fill(0) + .map(() => Particles.spawnParticle(spawnOptions)); + + this.addComponent(new Life(true)); + this.addComponent( + new ParticleRenderer(particles, () => { + const life = this.getComponent<Life>(ComponentNames.Life); + life.alive = false; + this.addComponent(life); + }), + ); + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: spawnOptions.spawnerDimensions.width, + height: spawnOptions.spawnerDimensions.height, + }, + 0, + ), + ); + } + + static spawnParticle(options: ParticleSpawnOptions) { + const angle = Math.random() * Math.PI * 2; + const speed = normalRandom( + options.particleMeanSpeed, + options.particleSpeedVariance, + ); + const life = normalRandom( + options.particleMeanLife, + options.particleLifeVariance, + ); + const size = normalRandom( + options.particleMeanSize, + options.particleSizeVariance, + ); + const color = + options.particleColors[ + Math.floor(Math.random() * options.particleColors.length) + ]; + const position = { + x: options.center.x + Math.cos(angle) * options.spawnerDimensions.width, + y: options.center.y + Math.sin(angle) * options.spawnerDimensions.height, + }; + if (options.spawnerShape === "rectangle") { + // determine a random position on the edge of the spawner based on the angle + const halfWidth = options.spawnerDimensions.width / 2; + const halfHeight = options.spawnerDimensions.height / 2; + + if (angle < Math.PI / 4 || angle > (Math.PI * 7) / 4) { + position.x += halfWidth; + position.y += Math.tan(angle) * halfWidth; + } else if (angle < (Math.PI * 3) / 4) { + position.y += halfHeight; + position.x += halfHeight / Math.tan(angle); + } else if (angle < (Math.PI * 5) / 4) { + position.x -= halfWidth; + position.y -= Math.tan(angle) * halfWidth; + } else { + position.y -= halfHeight; + position.x -= halfHeight / Math.tan(angle); + } + } + + return { + position, + velocity: { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed, + }, + color, + life, + dimension: { width: size, height: size }, + shape: options.particleShape, + }; + } +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts index 45c95c0..cb256ec 100644 --- a/src/engine/entities/index.ts +++ b/src/engine/entities/index.ts @@ -7,3 +7,5 @@ export * from "./LambdaFactory"; export * from "./Key"; export * from "./LockedDoor"; export * from "./Curry"; +export * from "./FunctionApplication"; +export * from "./Particles"; |