UI and about page in game

This commit is contained in:
2026-02-24 16:57:51 -03:00
parent fe885a9e24
commit 86d527ae51
12 changed files with 268 additions and 6 deletions
View File
+97
View File
@@ -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 = `<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.7;
color: #e5e7eb;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
h1, h2, h3, h4 {
font-weight: 600;
line-height: 1.3;
margin-top: 2rem;
margin-bottom: 1rem;
}
h1 {
font-size: 2rem;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.3rem;
}
h2 {
font-size: 1.5rem;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.3rem;
}
h3 {
font-size: 1.25rem;
}
p {
margin: 1rem 0;
}
a {
color: #2563eb;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul, ol {
padding-left: 2rem;
margin: 1rem 0;
}
code {
background: #1f2937;
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.9em;
}
pre {
background: #0f172a;
color: #f9fafb;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
color: inherit;
}
blockquote {
border-left: 4px solid #d1d5db;
padding-left: 1rem;
color: #6b7280;
margin: 1rem 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
th, td {
border: 1px solid #e5e7eb;
padding: 0.5rem;
}
th {
background: #f9fafb;
text-align: left;
}
</style>`;
}
+7 -1
View File
@@ -1,4 +1,4 @@
type Asset = HTMLImageElement | HTMLAudioElement; type Asset = HTMLImageElement | HTMLAudioElement | string;
export class AssetManager { export class AssetManager {
static instance = new AssetManager(); static instance = new AssetManager();
@@ -41,6 +41,12 @@ export class AssetManager {
audio.load(); audio.load();
this.assets[key] = audio; this.assets[key] = audio;
this.loading_promises.push(promise); 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());
} }
} }
+20
View File
@@ -9,11 +9,17 @@ import { Tilemap } from "$/client/components/tilemap.ts";
import { AssetManager } from "$/client/assets.ts"; import { AssetManager } from "$/client/assets.ts";
import { TileEditorSystem } from "$/client/systems/tile_editor_system.ts"; import { TileEditorSystem } from "$/client/systems/tile_editor_system.ts";
import { InventorySystem } from "$/client/systems/inventory_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 { export class ClientWorld extends World {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
paused = false;
debugging = false; debugging = false;
constructor(canvas: HTMLCanvasElement) { constructor(canvas: HTMLCanvasElement) {
@@ -61,6 +67,19 @@ export class ClientWorld extends World {
const player = create_player(); const player = create_player();
this.add_entity(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 InventorySystem());
this.add_system(new PlayerControlsSystem(player)); this.add_system(new PlayerControlsSystem(player));
this.add_system(new MovementSystem()); 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 RenderSystem(this.ctx));
this.add_system(new DebugSystem()); this.add_system(new DebugSystem());
this.add_system(new TileEditorSystem()); this.add_system(new TileEditorSystem());
this.add_system(new UIRenderSystem());
} }
} }
+18
View File
@@ -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;
}
}
+1
View File
@@ -61,6 +61,7 @@ if (!canvas) {
InputManager.initialize(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:player", "/assets/sprites/player.png");
AssetManager.instance.load("bworld:roguelike", "/assets/sprites/roguelike.png"); AssetManager.instance.load("bworld:roguelike", "/assets/sprites/roguelike.png");
AssetManager.instance.load("bworld:tiny_town", "/assets/sprites/tiny_town.png"); AssetManager.instance.load("bworld:tiny_town", "/assets/sprites/tiny_town.png");
+7 -2
View File
@@ -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 { point_inside_rec } from "$/common/utils.ts";
import { SLOT_SIZE } from "$/common/constants.ts"; import { SLOT_SIZE } from "$/common/constants.ts";
import { Container, PlayerInventory } from "$/client/components/inventory.ts"; import { Container, PlayerInventory } from "$/client/components/inventory.ts";
import { InputManager } from "$/client/input_manager.ts"; import { InputManager } from "$/client/input_manager.ts";
import { ClientWorld } from "$/client/client_world.ts";
export class InventorySystem extends System { 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()) { for (const entity of world.get_entities()) {
const player_inventory = entity.get(PlayerInventory); const player_inventory = entity.get(PlayerInventory);
+4
View File
@@ -17,6 +17,10 @@ export class PlayerControlsSystem extends System {
} }
update(world: ClientWorld, _delta: number): void { update(world: ClientWorld, _delta: number): void {
if (world.paused) {
return;
}
const velocity = this.player.get(Velocity)!; const velocity = this.player.get(Velocity)!;
const controls = this.player.get(PlayerControls)!; const controls = this.player.get(PlayerControls)!;
+35
View File
@@ -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();
}
}
}
}
}
+67
View File
@@ -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<HTMLImageElement>("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();
}
}
+2 -1
View File
@@ -15,6 +15,7 @@
}, },
"imports": { "imports": {
"$/": "./", "$/": "./",
"@std/fs": "jsr:@std/fs@^1.0.23" "@std/fs": "jsr:@std/fs@^1.0.23",
"marked": "npm:marked@^17.0.3"
} }
} }
Generated
+10 -2
View File
@@ -3,7 +3,8 @@
"specifiers": { "specifiers": {
"jsr:@std/fs@^1.0.23": "1.0.23", "jsr:@std/fs@^1.0.23": "1.0.23",
"jsr:@std/internal@^1.0.12": "1.0.12", "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": { "jsr": {
"@std/fs@1.0.23": { "@std/fs@1.0.23": {
@@ -23,9 +24,16 @@
] ]
} }
}, },
"npm": {
"marked@17.0.3": {
"integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
"bin": true
}
},
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@std/fs@^1.0.23" "jsr:@std/fs@^1.0.23",
"npm:marked@^17.0.3"
] ]
} }
} }