From ebae24f5a3a251654a1e41be58f52ba2a777d9d7 Mon Sep 17 00:00:00 2001
From: Elizabeth Hunt
Date: Thu, 7 Mar 2024 22:49:43 -0700
Subject: [PATCH] level system!
---
public/assets/portal.png | Bin 0 -> 1320 bytes
src/components/Title.tsx | 5 ++-
src/engine/Game.ts | 5 +++
src/engine/TheAbstractionEngine.ts | 33 ++-------------
src/engine/config/sprites.ts | 20 ++++++++++
src/engine/entities/Curry.ts | 12 ++++--
src/engine/entities/EntityNames.ts | 2 +
src/engine/entities/Grass.ts | 27 +++++++++++++
src/engine/entities/Player.ts | 6 +--
src/engine/entities/Portal.ts | 60 ++++++++++++++++++++++++++++
src/engine/entities/index.ts | 2 +
src/engine/levels/Level.ts | 11 +++++
src/engine/levels/LevelNames.ts | 4 ++
src/engine/levels/LevelSelection.ts | 33 +++++++++++++++
src/engine/levels/Tutorial.ts | 32 +++++++++++++++
src/engine/levels/index.ts | 12 ++++++
src/engine/levels/utils.ts | 6 +++
src/engine/systems/Collision.ts | 1 +
src/engine/systems/Grid.ts | 7 ++++
src/engine/systems/Level.ts | 57 ++++++++++++++++++++++++++
src/engine/systems/SystemNames.ts | 1 +
src/engine/systems/index.ts | 1 +
22 files changed, 300 insertions(+), 37 deletions(-)
create mode 100644 public/assets/portal.png
create mode 100644 src/engine/entities/Grass.ts
create mode 100644 src/engine/entities/Portal.ts
create mode 100644 src/engine/levels/Level.ts
create mode 100644 src/engine/levels/LevelNames.ts
create mode 100644 src/engine/levels/LevelSelection.ts
create mode 100644 src/engine/levels/Tutorial.ts
create mode 100644 src/engine/levels/index.ts
create mode 100644 src/engine/levels/utils.ts
create mode 100644 src/engine/systems/Level.ts
diff --git a/public/assets/portal.png b/public/assets/portal.png
new file mode 100644
index 0000000000000000000000000000000000000000..240a9a1258d815257cd8c18bbb3a1ab45c2c65df
GIT binary patch
literal 1320
zcmV+@1=sqCP)YuN>rXeplcS~6Ou7!cSorPgX)!WHi!gdhY7~>!
z^2*VZFVz52vJyd=u_XD}YnVk`RST^K+WnH6rZqr@`=La@-Wqn(vg2vEN%k`FGGn#a
zX$G(YX%$yeM0AY>W{a38Kgm8eUUs~bLwW<8CxPdOk)*(0lmD8>stGiF@Ag{Zw1`VF
zfCaF-55W~oV5x;=Y?cYEF|y+?V}KP-0(Nmcr|T*PVE4gN24ItC75$9kVSvmUc=2ay
zPzE5mV8!B-sQ(MIq6TUMc#`&>v@;Fjt%G;Ikrokn7&*%Tqsj_?&M1T+&ATNLIiD~_
z;9=xk(g4gm;B4b%`Y>fQz{>m?eV{TxbOECCM_I;jtZ|#TLx$pK(OCxyV@XR$+9t77`28j9?MueX!taPmx4zD~?
z-kO)7GQh0LH|w)BgclC4JhP=iaFqdORW1Xh`0X$2JE#n>kpVoefXI|3NGuA0V4F4o
zl0<}_xX2r$cfKd7tg%?f*R%nGtqh=1{O?{wctYCS5Y||%<7;$5u$2KcivQk=h^|ce
zxZN6$b$pFB2(~hSM)5L$Qa$eWGC;tU1QDHE_K(plFVjaa6F{jtBDyl=aJwZY
z%h*F=g9Qk-GJsNbM091!;dV<*ma&J#$^bHnJd5O+OUB;z!#pG3Ll+cRodD9V>a0VB
zSEdbn3ChT~4A8WLXOTQ}Nlasn$2z`*GS|=U#bcaG{vCGXN^YjM$8$7tQyy&x^ZP9;rqeJY@i>%AIu@
z@s+7RZ1=+Al}DnF7E2jGqVmHUZ2^?&-);BA;+f0x)GGrhJ8+jqn3Z7{-jD$@zJ5f!
z0Av-5HdDVJibL1eu+L8B7qtJ0hpWYZPpN428E4WHkdsHsTJsw=qC;F%IKI
z{7)&-ydyX>hwl~!h-`wWZx3b@AgA6gyj2FsP=pN4AJGh1czfeu=UdVMhX`lWMIFIb
zuM(p-?^on)IbhTV@Hz#uHofPEID7hOu0Ec;Eqjb&084;p<7b-mqbbeCx{3x?2DOTN
z)RAU@(*)9~(rc3Iq>p-8k#gv*V-}1VOUp~LpKVrlyp{vi01|<&_qn_E?CCj!iMbXj
zTx {
- 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";