diff --git a/public/assets/function_factory.png b/public/assets/function_factory.png index 2c46758..e070be6 100644 Binary files a/public/assets/function_factory.png and b/public/assets/function_factory.png differ diff --git a/src/App.tsx b/src/App.tsx index aadff37..0ae052f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,7 @@ export const App = () => { simponic {" "} | inspired by{" "} - + baba is you diff --git a/src/css/modal.css b/src/css/modal.css index a609ef9..c10070e 100644 --- a/src/css/modal.css +++ b/src/css/modal.css @@ -1,3 +1,5 @@ +@import url("./colors.css"); + .modal { display: none; position: fixed; @@ -15,11 +17,11 @@ .modal-content { display: flex; - background-color: #282828; - color: #ebdbb2; + background-color: var(--bg); + color: var(--text); margin: auto; padding: 20px; - border: 1px solid #928374; + border: 1px solid var(--yellow); width: 40%; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); animation: scaleUp 0.25s; diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 2db599b..7f71fd5 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -1,7 +1,13 @@ import { Game } from "."; import { Miscellaneous, loadAssets } from "./config"; -import { Player, FunctionBox, Wall } from "./entities"; -import { Grid, FacingDirection, Input, Render } from "./systems"; +import { Player, FunctionBox, Wall, LambdaFactory } from "./entities"; +import { + Grid, + FacingDirection, + Input, + Render, + LambdaFactory as LambdaFactorySpawnSystem, +} from "./systems"; export class TheAbstractionEngine { private game: Game; @@ -32,6 +38,7 @@ export class TheAbstractionEngine { height: Miscellaneous.GRID_CELL_HEIGHT, }, ), + new LambdaFactorySpawnSystem(), new Render(this.ctx), ].forEach((system) => this.game.addSystem(system)); @@ -45,6 +52,9 @@ export class TheAbstractionEngine { const wall = new Wall({ x: 5, y: 3 }); this.game.addEntity(wall); + + const factory = new LambdaFactory({ x: 6, y: 6 }, "λ x . (x)", 10); + this.game.addEntity(factory); } public play() { diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts index 5e9b589..1f41d18 100644 --- a/src/engine/components/ComponentNames.ts +++ b/src/engine/components/ComponentNames.ts @@ -8,4 +8,6 @@ export namespace ComponentNames { export const Interactable = "Interactable"; export const Pushable = "Pushable"; export const Colliding = "Colliding"; + export const LambdaSpawn = "LambdaSpawn"; + export const Text = "Text"; } diff --git a/src/engine/components/Highlight.ts b/src/engine/components/Highlight.ts index 5875057..66ec74b 100644 --- a/src/engine/components/Highlight.ts +++ b/src/engine/components/Highlight.ts @@ -1,4 +1,5 @@ import { Component, ComponentNames } from "."; +import { Direction } from "../interfaces"; export class Highlight extends Component { public isHighlighted: boolean; @@ -6,8 +7,8 @@ export class Highlight extends Component { private onUnhighlight: Function; constructor( - onHighlight: Function, - onUnhighlight: Function, + onHighlight: (direction: Direction) => void, + onUnhighlight: () => void, isHighlighted: boolean = false, ) { super(ComponentNames.Highlight); @@ -17,10 +18,10 @@ export class Highlight extends Component { this.onUnhighlight = onUnhighlight; } - public highlight() { + public highlight(direction: Direction) { if (!this.isHighlighted) { this.isHighlighted = true; - this.onHighlight(); + this.onHighlight(direction); } } diff --git a/src/engine/components/LambdaSpawn.ts b/src/engine/components/LambdaSpawn.ts new file mode 100644 index 0000000..c45092a --- /dev/null +++ b/src/engine/components/LambdaSpawn.ts @@ -0,0 +1,29 @@ +import { Component, ComponentNames } from "."; +import { Direction } from "../interfaces"; + +export class LambdaSpawn extends Component { + public direction: Direction | null; + public spawnsLeft: number; + public code: string = ""; + + constructor( + spawnsLeft: number, + code: string, + direction: Direction | null = null, + ) { + super(ComponentNames.LambdaSpawn); + + this.spawnsLeft = spawnsLeft; + this.direction = direction; + this.code = code; + } + + public spawn(direction: Direction) { + if (this.spawnsLeft <= 0) { + return; + } + + this.direction = direction; + this.spawnsLeft -= 1; + } +} diff --git a/src/engine/components/Sprite.ts b/src/engine/components/Sprite.ts index 82d7011..c623bac 100644 --- a/src/engine/components/Sprite.ts +++ b/src/engine/components/Sprite.ts @@ -46,7 +46,7 @@ export class Sprite extends Component { } public draw(ctx: CanvasRenderingContext2D, drawArgs: DrawArgs) { - const { center, rotation, tint, opacity } = drawArgs; + const { center, rotation, tint, opacity, backgroundText } = drawArgs; ctx.save(); ctx.translate(center.x, center.y); @@ -59,6 +59,17 @@ export class Sprite extends Component { ctx.globalAlpha = opacity; } + if (backgroundText) { + // draw text + const { fillStyle, font, textAlign, text } = backgroundText; + ctx.fillStyle = fillStyle; + ctx.font = font; + ctx.textAlign = textAlign; + + const height = ctx.measureText("M").width; + ctx.fillText(text, center.x, center.y + height / 2); + } + ctx.drawImage( this.sheet, ...this.getSpriteArgs(), diff --git a/src/engine/components/Text.ts b/src/engine/components/Text.ts new file mode 100644 index 0000000..94dc7a7 --- /dev/null +++ b/src/engine/components/Text.ts @@ -0,0 +1,22 @@ +import { Component, ComponentNames } from "."; + +export class Text extends Component { + public text: string = ""; + public fillStyle: string; + public font: string; + public textAlign: CanvasTextAlign; + + constructor( + text: string, + fillStyle = "white", + font = "25px scientifica", + textAlign: CanvasTextAlign = "center", + ) { + super(ComponentNames.Text); + + this.text = text; + this.fillStyle = fillStyle; + this.font = font; + this.textAlign = textAlign; + } +} diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts index 4ae886a..104ba2d 100644 --- a/src/engine/components/index.ts +++ b/src/engine/components/index.ts @@ -9,3 +9,5 @@ export * from "./Highlight"; export * from "./Interactable"; export * from "./Pushable"; export * from "./Colliding"; +export * from "./LambdaSpawn"; +export * from "./Text"; diff --git a/src/engine/config/assets.ts b/src/engine/config/assets.ts index bf41461..5ce13e8 100644 --- a/src/engine/config/assets.ts +++ b/src/engine/config/assets.ts @@ -1,5 +1,10 @@ import { type SpriteSpec, SPRITE_SPECS } from "."; +export const FONT = new FontFace("scientifica", "url(/fonts/scientifica.ttf)"); +FONT.load().then((font) => { + document.fonts.add(font); +}); + export const IMAGES = new Map(); export const loadSpritesIntoImageElements = ( @@ -37,5 +42,6 @@ export const loadAssets = () => (key) => SPRITE_SPECS.get(key) as SpriteSpec, ), ), + FONT.load(), // TODO: Sound ]); diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts index 7cb8adf..39ad260 100644 --- a/src/engine/config/sprites.ts +++ b/src/engine/config/sprites.ts @@ -4,6 +4,7 @@ export enum Sprites { PLAYER, FUNCTION_BOX, WALL, + LAMBDA_FACTORY, } export interface SpriteSpec { @@ -56,3 +57,12 @@ const wallSpriteSpec = { sheet: "/assets/wall.png", }; SPRITE_SPECS.set(Sprites.WALL, wallSpriteSpec); + +const lambdaFactorySpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/function_factory.png", +}; +SPRITE_SPECS.set(Sprites.LAMBDA_FACTORY, lambdaFactorySpriteSpec); diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts index ffcc937..3ad31d0 100644 --- a/src/engine/entities/EntityNames.ts +++ b/src/engine/entities/EntityNames.ts @@ -2,4 +2,5 @@ export namespace EntityNames { export const Player = "Player"; export const FunctionBox = "FunctionBox"; export const Wall = "Wall"; + export const LambdaFactory = "LambdaFactory"; } diff --git a/src/engine/entities/FunctionBox.ts b/src/engine/entities/FunctionBox.ts index e5d031a..b7015f2 100644 --- a/src/engine/entities/FunctionBox.ts +++ b/src/engine/entities/FunctionBox.ts @@ -57,7 +57,7 @@ export class FunctionBox extends Entity { this.addComponent( new Highlight( - () => this.onHighlight(), + (_direction) => this.onHighlight(), () => this.onUnhighlight(), ), ); diff --git a/src/engine/entities/LambdaFactory.ts b/src/engine/entities/LambdaFactory.ts new file mode 100644 index 0000000..1483b9d --- /dev/null +++ b/src/engine/entities/LambdaFactory.ts @@ -0,0 +1,109 @@ +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Entity, EntityNames } from "."; +import { + BoundingBox, + Colliding, + ComponentNames, + Grid, + Highlight, + Interactable, + LambdaSpawn, + Sprite, + Text, +} from "../components"; +import { Coord2D, Direction } from "../interfaces"; +import { openModal, closeModal } from "../utils"; + +export class LambdaFactory extends Entity { + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.LAMBDA_FACTORY, + ) as SpriteSpec; + + private code: string; + private spawns: number; + + constructor(gridPosition: Coord2D, code: string, spawns: number) { + super(EntityNames.LambdaFactory); + + this.code = code; + this.spawns = spawns; + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: LambdaFactory.spriteSpec.width, + height: LambdaFactory.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent(new Text(spawns.toString())); + + this.addComponent(new Colliding()); + + this.addComponent(new LambdaSpawn(this.spawns, this.code)); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent( + new Sprite( + IMAGES.get(LambdaFactory.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: LambdaFactory.spriteSpec.width, + height: LambdaFactory.spriteSpec.height, + }, + LambdaFactory.spriteSpec.msPerFrame, + LambdaFactory.spriteSpec.frames, + ), + ); + + this.addComponent( + new Highlight( + (direction) => this.onHighlight(direction), + () => this.onUnhighlight(), + ), + ); + } + + private onUnhighlight() { + closeModal(); + this.removeComponent(ComponentNames.Interactable); + } + + private onHighlight(direction: Direction) { + if (direction === Direction.LEFT || direction === Direction.RIGHT) { + const interaction = () => { + const spawner = this.getComponent( + ComponentNames.LambdaSpawn, + ); + spawner.spawn(direction); + + const text = this.getComponent(ComponentNames.Text); + text.text = spawner.spawnsLeft.toString(); + this.addComponent(text); + }; + + this.addComponent(new Interactable(interaction)); + return; + } + + let modalOpen = false; + const interaction = () => { + if (modalOpen) { + modalOpen = false; + closeModal(); + return; + } + modalOpen = true; + openModal(this.code); + }; + + this.addComponent(new Interactable(interaction)); + } +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts index e63b272..a049350 100644 --- a/src/engine/entities/index.ts +++ b/src/engine/entities/index.ts @@ -3,3 +3,4 @@ export * from "./EntityNames"; export * from "./Player"; export * from "./FunctionBox"; export * from "./Wall"; +export * from "./LambdaFactory"; diff --git a/src/engine/interfaces/Draw.ts b/src/engine/interfaces/Draw.ts index 6561a01..78de7ed 100644 --- a/src/engine/interfaces/Draw.ts +++ b/src/engine/interfaces/Draw.ts @@ -6,4 +6,10 @@ export interface DrawArgs { tint?: string; opacity?: number; rotation?: number; + backgroundText?: { + text: string; + font: string; + fillStyle: string; + textAlign: CanvasTextAlign; + }; } diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts index 28ca5ea..8756320 100644 --- a/src/engine/systems/Grid.ts +++ b/src/engine/systems/Grid.ts @@ -39,7 +39,7 @@ export class Grid extends System { } private highlightEntitiesLookedAt(game: Game) { - const highlightableEntities = new Set(); + const highlightableEntities: [string, Direction][] = []; game.forEachEntityWithComponent( ComponentNames.FacingDirection, @@ -64,23 +64,23 @@ export class Grid extends System { } this.grid[lookingAt.y][lookingAt.x].forEach((id) => { - highlightableEntities.add(id); + highlightableEntities.push([id, facingDirection.currentDirection]); }); }, ); - highlightableEntities.forEach((id) => { + highlightableEntities.forEach(([id, direction]) => { const entity = game.getEntity(id)!; if (entity.hasComponent(ComponentNames.Highlight)) { const highlight = entity.getComponent( ComponentNames.Highlight, )!; - highlight.highlight(); + highlight.highlight(direction); } }); game.forEachEntityWithComponent(ComponentNames.Highlight, (entity) => { - if (!highlightableEntities.has(entity.id)) { + if (!highlightableEntities.find(([id]) => id === entity.id)) { const highlight = entity.getComponent( ComponentNames.Highlight, )!; diff --git a/src/engine/systems/LambdaFactory.ts b/src/engine/systems/LambdaFactory.ts new file mode 100644 index 0000000..1263eae --- /dev/null +++ b/src/engine/systems/LambdaFactory.ts @@ -0,0 +1,34 @@ +import { System, SystemNames } from "."; +import { Game } from ".."; +import { ComponentNames, Grid, LambdaSpawn } from "../components"; +import { FunctionBox } from "../entities"; + +export class LambdaFactory extends System { + constructor() { + super(SystemNames.LambdaFactory); + } + + public update(_dt: number, game: Game) { + game.forEachEntityWithComponent(ComponentNames.LambdaSpawn, (entity) => { + const lambdaSpawn = entity.getComponent( + ComponentNames.LambdaSpawn, + )!; + const hasGrid = entity.hasComponent(SystemNames.Grid); + + if (!lambdaSpawn.direction || !hasGrid) { + return; + } + + const grid = entity.getComponent(SystemNames.Grid)!; + + const lambda = new FunctionBox(grid.gridPosition, lambdaSpawn.code); + const lambdaGrid = lambda.getComponent(SystemNames.Grid)!; + lambdaGrid.movingDirection = lambdaSpawn.direction; + lambda.addComponent(lambdaGrid); + game.addEntity(lambda); + + lambdaSpawn.direction = null; + entity.addComponent(lambdaSpawn); + }); + } +} diff --git a/src/engine/systems/Render.ts b/src/engine/systems/Render.ts index 83daa52..f273deb 100644 --- a/src/engine/systems/Render.ts +++ b/src/engine/systems/Render.ts @@ -1,5 +1,11 @@ import { System, SystemNames } from "."; -import { BoundingBox, ComponentNames, Highlight, Sprite } from "../components"; +import { + BoundingBox, + Text, + ComponentNames, + Highlight, + Sprite, +} from "../components"; import { Game } from ".."; import { clamp } from "../utils"; import { DrawArgs } from "../interfaces"; @@ -50,6 +56,15 @@ export class Render extends System { ); drawArgs.tint = highlight.isHighlighted ? "red" : undefined; } + if (entity.hasComponent(ComponentNames.Text)) { + const text = entity.getComponent(ComponentNames.Text); + drawArgs.backgroundText = { + text: text.text, + font: text.font, + fillStyle: text.fillStyle, + textAlign: text.textAlign, + }; + } sprite.draw(this.ctx, drawArgs); }); diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts index 0de5857..85d1539 100644 --- a/src/engine/systems/SystemNames.ts +++ b/src/engine/systems/SystemNames.ts @@ -3,4 +3,5 @@ export namespace SystemNames { export const Input = "Input"; export const FacingDirection = "FacingDirection"; export const Grid = "Grid"; + export const LambdaFactory = "LambdaFactory"; } diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts index 18ffa2e..4490ee2 100644 --- a/src/engine/systems/index.ts +++ b/src/engine/systems/index.ts @@ -4,3 +4,4 @@ export * from "./Render"; export * from "./Input"; export * from "./FacingDirection"; export * from "./Grid"; +export * from "./LambdaFactory";