diff --git a/ASSETS.md b/assets/ASSETS.md similarity index 100% rename from ASSETS.md rename to assets/ASSETS.md diff --git a/client/about.ts b/client/about.ts new file mode 100644 index 0000000..13f595c --- /dev/null +++ b/client/about.ts @@ -0,0 +1,97 @@ +import { marked } from "marked"; +import { AssetManager } from "./assets.ts"; + +export async function open_about() { + const popup = self.open( + "", + "popupWindow", + "width=500,height=400", + ); + + if (!popup) { + alert("oh no"); + return; + } + + popup.document.body.innerHTML = await marked.parse(AssetManager.instance.get("bworld:assets_text")); + popup.document.head.innerHTML = ``; +} diff --git a/client/assets.ts b/client/assets.ts index b04cbfd..40cb99b 100644 --- a/client/assets.ts +++ b/client/assets.ts @@ -1,4 +1,4 @@ -type Asset = HTMLImageElement | HTMLAudioElement; +type Asset = HTMLImageElement | HTMLAudioElement | string; export class AssetManager { static instance = new AssetManager(); @@ -41,6 +41,12 @@ export class AssetManager { audio.load(); this.assets[key] = audio; this.loading_promises.push(promise); + } else if (src.endsWith(".txt") || src.endsWith(".md")) { + const promise = async () => { + const resp = await fetch(src); + this.assets[key] = await resp.text(); + }; + this.loading_promises.push(promise()); } } diff --git a/client/client_world.ts b/client/client_world.ts index 7c87a3a..a140ae8 100644 --- a/client/client_world.ts +++ b/client/client_world.ts @@ -9,11 +9,17 @@ import { Tilemap } from "$/client/components/tilemap.ts"; import { AssetManager } from "$/client/assets.ts"; import { TileEditorSystem } from "$/client/systems/tile_editor_system.ts"; import { InventorySystem } from "$/client/systems/inventory_system.ts"; +import { UIInteractionSystem } from "$/client/systems/ui_interaction_system.ts"; +import { UIRenderSystem } from "$/client/systems/ui_render_system.ts"; +import { UIButton } from "./components/ui_components.ts"; +import { Position } from "../common/components/position.ts"; +import { open_about } from "./about.ts"; export class ClientWorld extends World { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; + paused = false; debugging = false; constructor(canvas: HTMLCanvasElement) { @@ -61,6 +67,19 @@ export class ClientWorld extends World { const player = create_player(); this.add_entity(player); + // UI ! + const unpause_button = new Entity("unpausebutton"); + unpause_button.add(new Position(canvas.width / 2 - 150, canvas.height / 2 - 80)); + unpause_button.add(new UIButton("Unpause", 320, 64, () => this.paused = false)); + this.add_entity(unpause_button); + + const about_button = new Entity("aboutbutton"); + about_button.add(new Position(canvas.width / 2 - 150, canvas.height / 2 + 80)); + about_button.add(new UIButton("About", 320, 64, () => open_about())); + this.add_entity(about_button); + + // Logic systems + this.add_system(new UIInteractionSystem()); this.add_system(new InventorySystem()); this.add_system(new PlayerControlsSystem(player)); this.add_system(new MovementSystem()); @@ -69,5 +88,6 @@ export class ClientWorld extends World { this.add_system(new RenderSystem(this.ctx)); this.add_system(new DebugSystem()); this.add_system(new TileEditorSystem()); + this.add_system(new UIRenderSystem()); } } diff --git a/client/components/ui_components.ts b/client/components/ui_components.ts new file mode 100644 index 0000000..807b1e4 --- /dev/null +++ b/client/components/ui_components.ts @@ -0,0 +1,18 @@ +import { Component } from "$/common/ecs/mod.ts"; + +export class UIButton extends Component { + text: string; + width: number; + height: number; + on_click: () => void; + + hovered = false; + + constructor(text: string, width: number, height: number, on_click: () => void) { + super(); + this.text = text; + this.width = width; + this.height = height; + this.on_click = on_click; + } +} diff --git a/client/main.ts b/client/main.ts index 7d63f5b..6f005a4 100644 --- a/client/main.ts +++ b/client/main.ts @@ -61,6 +61,7 @@ if (!canvas) { InputManager.initialize(canvas); +AssetManager.instance.load("bworld:assets_text", "/assets/ASSETS.md"); AssetManager.instance.load("bworld:player", "/assets/sprites/player.png"); AssetManager.instance.load("bworld:roguelike", "/assets/sprites/roguelike.png"); AssetManager.instance.load("bworld:tiny_town", "/assets/sprites/tiny_town.png"); diff --git a/client/systems/inventory_system.ts b/client/systems/inventory_system.ts index 8130038..ba70689 100644 --- a/client/systems/inventory_system.ts +++ b/client/systems/inventory_system.ts @@ -1,11 +1,16 @@ -import { System, World } from "$/common/ecs/mod.ts"; +import { System } from "$/common/ecs/mod.ts"; import { point_inside_rec } from "$/common/utils.ts"; import { SLOT_SIZE } from "$/common/constants.ts"; import { Container, PlayerInventory } from "$/client/components/inventory.ts"; import { InputManager } from "$/client/input_manager.ts"; +import { ClientWorld } from "$/client/client_world.ts"; export class InventorySystem extends System { - update(world: World, _delta: number): void { + update(world: ClientWorld, _delta: number): void { + if (world.paused) { + return; + } + for (const entity of world.get_entities()) { const player_inventory = entity.get(PlayerInventory); diff --git a/client/systems/player_controls.ts b/client/systems/player_controls.ts index 1c0b949..cda5f4f 100644 --- a/client/systems/player_controls.ts +++ b/client/systems/player_controls.ts @@ -17,6 +17,10 @@ export class PlayerControlsSystem extends System { } update(world: ClientWorld, _delta: number): void { + if (world.paused) { + return; + } + const velocity = this.player.get(Velocity)!; const controls = this.player.get(PlayerControls)!; diff --git a/client/systems/ui_interaction_system.ts b/client/systems/ui_interaction_system.ts new file mode 100644 index 0000000..4ee71a7 --- /dev/null +++ b/client/systems/ui_interaction_system.ts @@ -0,0 +1,35 @@ +import { System } from "$/common/ecs/mod.ts"; +import { Position } from "$/common/components/position.ts"; +import { point_inside_rec } from "$/common/utils.ts"; +import { ClientWorld } from "$/client/client_world.ts"; +import { UIButton } from "$/client/components/ui_components.ts"; +import { InputManager } from "$/client/input_manager.ts"; + +export class UIInteractionSystem implements System { + update(world: ClientWorld, _delta: number) { + // TODO: dont like + if (InputManager.is_key_pressed("Escape")) { + world.paused = !world.paused; + } + + if (!world.paused) { + return; + } + + const mouse = InputManager.get_mouse_position(); + + for (const entity of world.get_entities()) { + const position = entity.get(Position); + const button = entity.get(UIButton); + + if (position && button) { + const hovered = point_inside_rec(mouse.x, mouse.y, position.x, position.y, button.width, button.height); + button.hovered = hovered; + if (hovered && InputManager.is_mouse_pressed(0)) { + InputManager.consume_mouse(0); + button.on_click(); + } + } + } + } +} diff --git a/client/systems/ui_render_system.ts b/client/systems/ui_render_system.ts new file mode 100644 index 0000000..a30d331 --- /dev/null +++ b/client/systems/ui_render_system.ts @@ -0,0 +1,67 @@ +import { System } from "$/common/ecs/mod.ts"; +import { Position } from "$/common/components/position.ts"; +import { ClientWorld } from "$/client/client_world.ts"; +import { UIButton } from "$/client/components/ui_components.ts"; +import { draw_text, measure_text } from "../text_rendering.ts"; +import { draw_nine_slice } from "./rendering/render_utils.ts"; +import { AssetManager } from "../assets.ts"; + +const SCALE = 4; + +export class UIRenderSystem implements System { + constructor() {} + + update(world: ClientWorld, _delta: number) { + if (!world.paused) { + return; + } + + const ctx = world.ctx; + + ctx.save(); + ctx.resetTransform(); + + ctx.scale(SCALE, SCALE); + + // dark background + ctx.fillStyle = "rgba(0,0,0,0.6)"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const ui = AssetManager.instance.get("bworld:ui"); + + for (const entity of world.get_entities()) { + const position = entity.get(Position); + const button = entity.get(UIButton); + + if (position && button) { + draw_nine_slice( + ctx, + ui, + 16 * (button.hovered ? 14 : 11), + 16 * (button.hovered ? 1 : 0), + 16, + 16, + 3, + 3, + 3, + 3, + position.x / SCALE, + position.y / SCALE, + button.width / SCALE, + button.height / SCALE, + ); + const measure = measure_text(ctx, button.text, 1.25); + draw_text( + ctx, + button.text, + (position.x / SCALE) + (button.width / SCALE / 2) - (measure / 2), + position.y / SCALE + (button.height / SCALE / 2), + 1.5, + "white", + "middle", + ); + } + } + ctx.restore(); + } +} diff --git a/deno.json b/deno.json index f535d5a..9e2729c 100644 --- a/deno.json +++ b/deno.json @@ -15,6 +15,7 @@ }, "imports": { "$/": "./", - "@std/fs": "jsr:@std/fs@^1.0.23" + "@std/fs": "jsr:@std/fs@^1.0.23", + "marked": "npm:marked@^17.0.3" } } diff --git a/deno.lock b/deno.lock index 0ca6179..4e69697 100644 --- a/deno.lock +++ b/deno.lock @@ -3,7 +3,8 @@ "specifiers": { "jsr:@std/fs@^1.0.23": "1.0.23", "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/path@^1.1.4": "1.1.4" + "jsr:@std/path@^1.1.4": "1.1.4", + "npm:marked@^17.0.3": "17.0.3" }, "jsr": { "@std/fs@1.0.23": { @@ -23,9 +24,16 @@ ] } }, + "npm": { + "marked@17.0.3": { + "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==", + "bin": true + } + }, "workspace": { "dependencies": [ - "jsr:@std/fs@^1.0.23" + "jsr:@std/fs@^1.0.23", + "npm:marked@^17.0.3" ] } }