level system!

This commit is contained in:
Elizabeth Hunt 2024-03-07 22:49:43 -07:00
parent 808a44e854
commit ebae24f5a3
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
22 changed files with 300 additions and 37 deletions

BIN
public/assets/portal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -17,7 +17,10 @@ export const Title = ({ setReady }: TitleProps) => {
</a> </a>
</p> </p>
<br /> <br />
<p>WASD/arrow keys to move, space/enter to interact</p> <p>
WASD/arrow keys to move, space/enter to interact after highlighting with
the mouse
</p>
<br /> <br />
<hr /> <hr />

View File

@ -20,6 +20,11 @@ export class Game {
this.componentEntities = new Map(); this.componentEntities = new Map();
} }
public resetState() {
this.entities.clear();
this.componentEntities.clear();
}
public start() { public start() {
this.lastTimeStamp = performance.now(); this.lastTimeStamp = performance.now();
this.running = true; this.running = true;

View File

@ -1,14 +1,6 @@
import { Game } from "."; import { Game } from ".";
import { Miscellaneous, loadAssets } from "./config"; import { Miscellaneous, loadAssets } from "./config";
import { import { LevelNames } from "./levels";
Player,
Wall,
LambdaFactory,
Key,
LockedDoor,
Curry,
FunctionApplication,
} from "./entities";
import { import {
Grid, Grid,
FacingDirection, FacingDirection,
@ -18,6 +10,7 @@ import {
GridSpawner, GridSpawner,
Life, Life,
Music, Music,
Level,
} from "./systems"; } from "./systems";
export class TheAbstractionEngine { export class TheAbstractionEngine {
@ -40,6 +33,7 @@ export class TheAbstractionEngine {
const facingDirectionSystem = new FacingDirection(inputSystem); const facingDirectionSystem = new FacingDirection(inputSystem);
[ [
new Level(LevelNames.LevelSelection),
inputSystem, inputSystem,
facingDirectionSystem, facingDirectionSystem,
new Grid( new Grid(
@ -55,27 +49,6 @@ export class TheAbstractionEngine {
new Music(), new Music(),
new Render(this.ctx), new Render(this.ctx),
].forEach((system) => this.game.addSystem(system)); ].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() { public play() {

View File

@ -9,6 +9,8 @@ export enum Sprites {
LOCKED_DOOR, LOCKED_DOOR,
CURRY, CURRY,
BUBBLE, BUBBLE,
PORTAL,
GRASS,
} }
export interface SpriteSpec { export interface SpriteSpec {
@ -106,3 +108,21 @@ const bubbleSpriteSpec = {
sheet: "/assets/bubble.png", sheet: "/assets/bubble.png",
}; };
SPRITE_SPECS.set(Sprites.BUBBLE, bubbleSpriteSpec); 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);

View File

@ -3,6 +3,8 @@ import { Game } from "..";
import { BoundingBox, Colliding, Grid, Sprite } from "../components"; import { BoundingBox, Colliding, Grid, Sprite } from "../components";
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Coord2D } from "../interfaces"; import { Coord2D } from "../interfaces";
import { LevelNames } from "../levels";
import { Level, SystemNames } from "../systems";
export class Curry extends Entity { export class Curry extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
@ -45,9 +47,13 @@ export class Curry extends Entity {
} }
private collisionHandler(game: Game, entity: Entity) { private collisionHandler(game: Game, entity: Entity) {
if (entity.name === EntityNames.Player) { if (entity.name !== EntityNames.Player) {
game.removeEntity(this.id); return;
game.stop();
} }
game.removeEntity(this.id);
const levelSystem = game.getSystem<Level>(SystemNames.Level);
levelSystem.setLevel(LevelNames.LevelSelection);
} }
} }

View File

@ -8,4 +8,6 @@ export namespace EntityNames {
export const Curry = "Curry"; export const Curry = "Curry";
export const FunctionApplication = "FunctionApplication"; export const FunctionApplication = "FunctionApplication";
export const Particles = "Particles"; export const Particles = "Particles";
export const Portal = "Portal";
export const Grass = "Grass";
} }

View File

@ -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,
),
);
}
}

View File

@ -8,14 +8,14 @@ import {
Control, Control,
Pushable, Pushable,
} from "../components"; } from "../components";
import { Direction } from "../interfaces/"; import { Coord2D, Direction } from "../interfaces/";
export class Player extends Entity { export class Player extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
Sprites.PLAYER, Sprites.PLAYER,
) as SpriteSpec; ) as SpriteSpec;
constructor() { constructor(gridPosition: Coord2D) {
super(EntityNames.Player); super(EntityNames.Player);
this.addComponent( this.addComponent(
@ -33,7 +33,7 @@ export class Player extends Entity {
this.addComponent(new Control()); this.addComponent(new Control());
this.addComponent(new Grid()); this.addComponent(new Grid(gridPosition));
this.addFacingDirectionComponents(); this.addFacingDirectionComponents();
} }

View File

@ -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<Level>(SystemNames.Level);
levelSystem.setLevel(this.toLevel);
}
}

View File

@ -9,3 +9,5 @@ export * from "./LockedDoor";
export * from "./Curry"; export * from "./Curry";
export * from "./FunctionApplication"; export * from "./FunctionApplication";
export * from "./Particles"; export * from "./Particles";
export * from "./Portal";
export * from "./Grass";

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
export namespace LevelNames {
export const Tutorial = "0";
export const LevelSelection = "LevelSelection";
}

View File

@ -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<Grid>(SystemNames.Grid);
const center = gridSystem.getCenterGrid();
const levelSystem = game.getSystem<LevelSystem>(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);
}
}

View File

@ -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));
}
}

View File

@ -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],
};

View File

@ -0,0 +1,6 @@
import { Entity } from "../entities";
// TODO
//export const levelFormatToEntityList = (lines: string[]): Entity[] => {
//
//}

View File

@ -7,6 +7,7 @@ const collisionMap: Record<string, Set<string>> = {
[EntityNames.Key]: new Set([EntityNames.LockedDoor]), [EntityNames.Key]: new Set([EntityNames.LockedDoor]),
[EntityNames.Curry]: new Set([EntityNames.Player]), [EntityNames.Curry]: new Set([EntityNames.Player]),
[EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]), [EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]),
[EntityNames.Portal]: new Set([EntityNames.Player]),
}; };
export class Collision extends System { export class Collision extends System {

View File

@ -377,4 +377,11 @@ export class Grid extends System {
this.grid[y][x].add(id); this.grid[y][x].add(id);
}); });
} }
public getCenterGrid() {
return {
x: Math.floor(this.grid[0].length / 2),
y: Math.floor(this.grid.length / 2),
};
}
} }

View File

@ -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<string>;
private currentLevel: LevelType | null;
private moveToLevel: string | null;
private levelMap: Map<string, LevelType>;
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;
}
}

View File

@ -7,4 +7,5 @@ export namespace SystemNames {
export const Collision = "Collision"; export const Collision = "Collision";
export const Life = "Life"; export const Life = "Life";
export const Music = "Music"; export const Music = "Music";
export const Level = "Level";
} }

View File

@ -8,3 +8,4 @@ export * from "./GridSpawner";
export * from "./Collision"; export * from "./Collision";
export * from "./Life"; export * from "./Life";
export * from "./Music"; export * from "./Music";
export * from "./Level";