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";