diff --git a/public/assets/portal.png b/public/assets/portal.png new file mode 100644 index 0000000..240a9a1 Binary files /dev/null and b/public/assets/portal.png differ diff --git a/src/components/Title.tsx b/src/components/Title.tsx index 1736cba..3a69dbc 100644 --- a/src/components/Title.tsx +++ b/src/components/Title.tsx @@ -17,7 +17,10 @@ export const Title = ({ setReady }: TitleProps) => {


-

WASD/arrow keys to move, space/enter to interact

+

+ WASD/arrow keys to move, space/enter to interact after highlighting with + the mouse +



diff --git a/src/engine/Game.ts b/src/engine/Game.ts index 9fe9e87..ab2ea64 100644 --- a/src/engine/Game.ts +++ b/src/engine/Game.ts @@ -20,6 +20,11 @@ export class Game { this.componentEntities = new Map(); } + public resetState() { + this.entities.clear(); + this.componentEntities.clear(); + } + public start() { this.lastTimeStamp = performance.now(); this.running = true; diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 93684ef..29bc553 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -1,14 +1,6 @@ import { Game } from "."; import { Miscellaneous, loadAssets } from "./config"; -import { - Player, - Wall, - LambdaFactory, - Key, - LockedDoor, - Curry, - FunctionApplication, -} from "./entities"; +import { LevelNames } from "./levels"; import { Grid, FacingDirection, @@ -18,6 +10,7 @@ import { GridSpawner, Life, Music, + Level, } from "./systems"; export class TheAbstractionEngine { @@ -40,6 +33,7 @@ export class TheAbstractionEngine { const facingDirectionSystem = new FacingDirection(inputSystem); [ + new Level(LevelNames.LevelSelection), inputSystem, facingDirectionSystem, new Grid( @@ -55,27 +49,6 @@ export class TheAbstractionEngine { new Music(), new Render(this.ctx), ].forEach((system) => this.game.addSystem(system)); - - const player = new Player(); - this.game.addEntity(player); - - const wall = new Wall({ x: 5, y: 3 }); - this.game.addEntity(wall); - - const factory = new LambdaFactory({ x: 3, y: 3 }, "(λ (x) . x)", 10); - this.game.addEntity(factory); - - const lockedDoor = new LockedDoor({ x: 8, y: 8 }); - this.game.addEntity(lockedDoor); - - const key = new Key({ x: 7, y: 7 }); - this.game.addEntity(key); - - const curry = new Curry({ x: 9, y: 8 }); - this.game.addEntity(curry); - - const application = new FunctionApplication({ x: 5, y: 5 }, "(_INPUT key)"); - this.game.addEntity(application); } public play() { diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts index 83bf0a0..0a4f9b5 100644 --- a/src/engine/config/sprites.ts +++ b/src/engine/config/sprites.ts @@ -9,6 +9,8 @@ export enum Sprites { LOCKED_DOOR, CURRY, BUBBLE, + PORTAL, + GRASS, } export interface SpriteSpec { @@ -106,3 +108,21 @@ const bubbleSpriteSpec = { sheet: "/assets/bubble.png", }; SPRITE_SPECS.set(Sprites.BUBBLE, bubbleSpriteSpec); + +const portalSpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/portal.png", +}; +SPRITE_SPECS.set(Sprites.PORTAL, portalSpriteSpec); + +const grassSpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/grass.png", +}; +SPRITE_SPECS.set(Sprites.GRASS, grassSpriteSpec); diff --git a/src/engine/entities/Curry.ts b/src/engine/entities/Curry.ts index bd57e19..19c5b91 100644 --- a/src/engine/entities/Curry.ts +++ b/src/engine/entities/Curry.ts @@ -3,6 +3,8 @@ import { Game } from ".."; import { BoundingBox, Colliding, Grid, Sprite } from "../components"; import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; import { Coord2D } from "../interfaces"; +import { LevelNames } from "../levels"; +import { Level, SystemNames } from "../systems"; export class Curry extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( @@ -45,9 +47,13 @@ export class Curry extends Entity { } private collisionHandler(game: Game, entity: Entity) { - if (entity.name === EntityNames.Player) { - game.removeEntity(this.id); - game.stop(); + if (entity.name !== EntityNames.Player) { + return; } + + game.removeEntity(this.id); + + const levelSystem = game.getSystem(SystemNames.Level); + levelSystem.setLevel(LevelNames.LevelSelection); } } diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts index 056db9a..b4fbbb2 100644 --- a/src/engine/entities/EntityNames.ts +++ b/src/engine/entities/EntityNames.ts @@ -8,4 +8,6 @@ export namespace EntityNames { export const Curry = "Curry"; export const FunctionApplication = "FunctionApplication"; export const Particles = "Particles"; + export const Portal = "Portal"; + export const Grass = "Grass"; } diff --git a/src/engine/entities/Grass.ts b/src/engine/entities/Grass.ts new file mode 100644 index 0000000..70fd601 --- /dev/null +++ b/src/engine/entities/Grass.ts @@ -0,0 +1,27 @@ +import { Entity, EntityNames } from "."; +import { Grid, Sprite } from "../components"; +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Coord2D } from "../interfaces"; + +export class Grass extends Entity { + private static spriteSpec = SPRITE_SPECS.get(Sprites.GRASS) as SpriteSpec; + + constructor(gridPosition: Coord2D) { + super(EntityNames.Grass); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent( + new Sprite( + IMAGES.get(Grass.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: Grass.spriteSpec.width, + height: Grass.spriteSpec.height, + }, + Grass.spriteSpec.msPerFrame, + Grass.spriteSpec.frames, + ), + ); + } +} diff --git a/src/engine/entities/Player.ts b/src/engine/entities/Player.ts index 1b98383..9e2e1cb 100644 --- a/src/engine/entities/Player.ts +++ b/src/engine/entities/Player.ts @@ -8,14 +8,14 @@ import { Control, Pushable, } from "../components"; -import { Direction } from "../interfaces/"; +import { Coord2D, Direction } from "../interfaces/"; export class Player extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( Sprites.PLAYER, ) as SpriteSpec; - constructor() { + constructor(gridPosition: Coord2D) { super(EntityNames.Player); this.addComponent( @@ -33,7 +33,7 @@ export class Player extends Entity { this.addComponent(new Control()); - this.addComponent(new Grid()); + this.addComponent(new Grid(gridPosition)); this.addFacingDirectionComponents(); } diff --git a/src/engine/entities/Portal.ts b/src/engine/entities/Portal.ts new file mode 100644 index 0000000..a747aa9 --- /dev/null +++ b/src/engine/entities/Portal.ts @@ -0,0 +1,60 @@ +import { Entity, EntityNames } from "."; +import { Game } from ".."; +import { BoundingBox, Colliding, Grid, Sprite, Text } from "../components"; +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Coord2D } from "../interfaces"; +import { Level, SystemNames } from "../systems"; + +export class Portal extends Entity { + private static spriteSpec = SPRITE_SPECS.get(Sprites.PORTAL) as SpriteSpec; + + private toLevel: string; + + constructor(toLevel: string, gridPosition: Coord2D) { + super(EntityNames.Portal); + + this.toLevel = toLevel; + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: Portal.spriteSpec.width, + height: Portal.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent( + new Sprite( + IMAGES.get(Portal.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: Portal.spriteSpec.width, + height: Portal.spriteSpec.height, + }, + Portal.spriteSpec.msPerFrame, + Portal.spriteSpec.frames, + ), + ); + + this.addComponent(new Colliding(this.handleCollision.bind(this))); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new Text(toLevel)); + } + + public handleCollision(game: Game, entity: Entity) { + if (entity.name !== EntityNames.Player) { + return; + } + + const levelSystem = game.getSystem(SystemNames.Level); + levelSystem.setLevel(this.toLevel); + } +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts index cb256ec..260db5b 100644 --- a/src/engine/entities/index.ts +++ b/src/engine/entities/index.ts @@ -9,3 +9,5 @@ export * from "./LockedDoor"; export * from "./Curry"; export * from "./FunctionApplication"; export * from "./Particles"; +export * from "./Portal"; +export * from "./Grass"; diff --git a/src/engine/levels/Level.ts b/src/engine/levels/Level.ts new file mode 100644 index 0000000..c5dc23e --- /dev/null +++ b/src/engine/levels/Level.ts @@ -0,0 +1,11 @@ +import { Game } from ".."; + +export abstract class Level { + public readonly name: string; + + constructor(name: string) { + this.name = name; + } + + abstract init(game: Game): void; +} diff --git a/src/engine/levels/LevelNames.ts b/src/engine/levels/LevelNames.ts new file mode 100644 index 0000000..e90b29a --- /dev/null +++ b/src/engine/levels/LevelNames.ts @@ -0,0 +1,4 @@ +export namespace LevelNames { + export const Tutorial = "0"; + export const LevelSelection = "LevelSelection"; +} diff --git a/src/engine/levels/LevelSelection.ts b/src/engine/levels/LevelSelection.ts new file mode 100644 index 0000000..a970d9c --- /dev/null +++ b/src/engine/levels/LevelSelection.ts @@ -0,0 +1,33 @@ +import { LEVELS, Level, LevelNames } from "."; +import { Game } from ".."; +import { Player, Portal } from "../entities"; +import { Grid, Level as LevelSystem, SystemNames } from "../systems"; + +export class LevelSelection extends Level { + constructor() { + super(LevelNames.LevelSelection); + } + + public init(game: Game): void { + const gridSystem = game.getSystem(SystemNames.Grid); + const center = gridSystem.getCenterGrid(); + + const levelSystem = game.getSystem(SystemNames.Level); + const unlocked = levelSystem.getUnlockedLevels(); + + LEVELS.forEach((level, i) => { + if ( + !unlocked.has(level.name) || + level.name === LevelNames.LevelSelection + ) { + return; + } + + const portal = new Portal(level.name, { x: i, y: 7 }); + game.addEntity(portal); + }); + + const player = new Player(center); + game.addEntity(player); + } +} diff --git a/src/engine/levels/Tutorial.ts b/src/engine/levels/Tutorial.ts new file mode 100644 index 0000000..165b10f --- /dev/null +++ b/src/engine/levels/Tutorial.ts @@ -0,0 +1,32 @@ +import { Level, LevelNames } from "."; +import { Game } from ".."; +import { + Curry, + FunctionApplication, + LambdaFactory, + LockedDoor, + Player, + Wall, +} from "../entities"; + +export class Tutorial extends Level { + constructor() { + super(LevelNames.Tutorial); + } + + public init(game: Game): void { + const entities = [ + new Player({ x: 2, y: 2 }), + new Wall({ x: 10, y: 9 }), + new Wall({ x: 10, y: 11 }), + new Wall({ x: 11, y: 10 }), + new Curry({ x: 10, y: 10 }), + new LockedDoor({ x: 9, y: 10 }), + new LambdaFactory({ x: 6, y: 3 }, "(λ (x) . x)", 3), + + new FunctionApplication({ x: 6, y: 6 }, "(_INPUT key)"), + ]; + + entities.forEach((entity) => game.addEntity(entity)); + } +} diff --git a/src/engine/levels/index.ts b/src/engine/levels/index.ts new file mode 100644 index 0000000..36291aa --- /dev/null +++ b/src/engine/levels/index.ts @@ -0,0 +1,12 @@ +export * from "./LevelNames"; +export * from "./Level"; +export * from "./LevelSelection"; +export * from "./Tutorial"; + +import { LevelNames } from "."; +import { LevelSelection, Tutorial, Level } from "."; + +export const LEVELS: Level[] = [new LevelSelection(), new Tutorial()]; +export const LEVEL_PROGRESSION = { + [LevelNames.LevelSelection]: [LevelNames.Tutorial], +}; diff --git a/src/engine/levels/utils.ts b/src/engine/levels/utils.ts new file mode 100644 index 0000000..7228f2b --- /dev/null +++ b/src/engine/levels/utils.ts @@ -0,0 +1,6 @@ +import { Entity } from "../entities"; + +// TODO +//export const levelFormatToEntityList = (lines: string[]): Entity[] => { +// +//} diff --git a/src/engine/systems/Collision.ts b/src/engine/systems/Collision.ts index 7d843cc..0bc6f5c 100644 --- a/src/engine/systems/Collision.ts +++ b/src/engine/systems/Collision.ts @@ -7,6 +7,7 @@ const collisionMap: Record> = { [EntityNames.Key]: new Set([EntityNames.LockedDoor]), [EntityNames.Curry]: new Set([EntityNames.Player]), [EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]), + [EntityNames.Portal]: new Set([EntityNames.Player]), }; export class Collision extends System { diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts index 1ec8ed9..c504bfe 100644 --- a/src/engine/systems/Grid.ts +++ b/src/engine/systems/Grid.ts @@ -377,4 +377,11 @@ export class Grid extends System { this.grid[y][x].add(id); }); } + + public getCenterGrid() { + return { + x: Math.floor(this.grid[0].length / 2), + y: Math.floor(this.grid.length / 2), + }; + } } diff --git a/src/engine/systems/Level.ts b/src/engine/systems/Level.ts new file mode 100644 index 0000000..87e12a5 --- /dev/null +++ b/src/engine/systems/Level.ts @@ -0,0 +1,57 @@ +import { SystemNames, System } from "."; +import { Game } from ".."; +import { type Level as LevelType, LEVELS, LEVEL_PROGRESSION } from "../levels"; + +export class Level extends System { + private unlockedLevels: Set; + private currentLevel: LevelType | null; + private moveToLevel: string | null; + private levelMap: Map; + + constructor(initialLevel: string) { + super(SystemNames.Level); + + this.levelMap = new Map(); + LEVELS.forEach((level) => { + this.levelMap.set(level.name, level); + }); + + this.currentLevel = null; + this.moveToLevel = initialLevel; + this.unlockedLevels = new Set(); + } + + public setLevel(level: string) { + this.moveToLevel = level; + } + + public update(_dt: number, game: Game) { + if (this.moveToLevel === this.currentLevel?.name || !this.moveToLevel) { + return; + } + + if (this.currentLevel) { + game.resetState(); + } + + const unlockedLevels = LEVEL_PROGRESSION[this.moveToLevel]; + if (unlockedLevels && unlockedLevels.length > 0) { + unlockedLevels.forEach((levelName) => { + if (!this.unlockedLevels.has(levelName)) { + this.unlockedLevels.add(levelName); + } + }); + } + + if (!this.unlockedLevels.has(this.moveToLevel)) { + this.unlockedLevels.add(this.moveToLevel); + } + this.currentLevel = this.levelMap.get(this.moveToLevel)!; + this.currentLevel.init(game); + this.moveToLevel = null; + } + + public getUnlockedLevels() { + return this.unlockedLevels; + } +} diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts index 363c72c..63f83a4 100644 --- a/src/engine/systems/SystemNames.ts +++ b/src/engine/systems/SystemNames.ts @@ -7,4 +7,5 @@ export namespace SystemNames { export const Collision = "Collision"; export const Life = "Life"; export const Music = "Music"; + export const Level = "Level"; } diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts index 46c534d..29dbc7e 100644 --- a/src/engine/systems/index.ts +++ b/src/engine/systems/index.ts @@ -8,3 +8,4 @@ export * from "./GridSpawner"; export * from "./Collision"; export * from "./Life"; export * from "./Music"; +export * from "./Level";