Initial commit

This commit is contained in:
2026-02-24 15:36:10 -03:00
commit fe885a9e24
42 changed files with 2383 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import { System } from "$/common/ecs/mod.ts";
import { ClientWorld } from "$/client/client_world.ts";
import { DebugUI } from "$/client/debug_ui.ts";
export class DebugSystem extends System {
constructor() {
super();
}
update(world: ClientWorld, _delta: number): void {
if (!world.debugging) {
return;
}
DebugUI.ctx.save();
DebugUI.ctx.resetTransform();
DebugUI.begin("Entities", 10, 10, 300);
for (const entity of world.get_entities()) {
if (DebugUI.collapsing_header("Entity - " + entity.id)) {
for (const component of entity.get_all()) {
if (DebugUI.collapsing_header(`${component.constructor.name}##${entity.id}`)) {
this.render_component(component);
}
}
}
}
DebugUI.end();
DebugUI.ctx.restore();
}
// deno-lint-ignore no-explicit-any
render_component(component: any) {
for (const key in component) {
if (key === "__component") {
continue;
}
if (typeof component[key] === "number") {
component[key] = DebugUI.float_input(
key,
component[key],
);
} else if (typeof component[key] === "string") {
component[key] = DebugUI.text_input(
key,
component[key],
);
} else if (typeof component[key] === "boolean") {
component[key] = DebugUI.checkbox(
key,
component[key],
);
} else {
DebugUI.text(`${key}: ${JSON.stringify(component[key])}`);
}
DebugUI.separator();
}
}
}
+140
View File
@@ -0,0 +1,140 @@
import { System, World } 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";
export class InventorySystem extends System {
update(world: World, _delta: number): void {
for (const entity of world.get_entities()) {
const player_inventory = entity.get(PlayerInventory);
if (player_inventory) {
player_inventory.hovering_slot = -1;
const container = player_inventory.container;
if (player_inventory.is_open) {
for (const [index, slot] of player_inventory.layout.slots.entries()) {
const mouse = InputManager.get_mouse_position();
const slot_x = slot.x + player_inventory.layout.offset_x;
const slot_y = slot.y + player_inventory.layout.offset_y;
const hovering = point_inside_rec(mouse.x, mouse.y, slot_x, slot_y, SLOT_SIZE, SLOT_SIZE);
if (hovering) {
player_inventory.hovering_slot = index;
if (InputManager.is_mouse_pressed(0)) {
InputManager.consume_mouse(0);
this.handle_left_click(player_inventory, container, index);
return;
}
if (InputManager.is_mouse_pressed(2)) {
InputManager.consume_mouse(2);
this.handle_right_click(player_inventory, container, index);
return;
}
}
}
} else {
if (player_inventory.holding_item) {
container.add_item(player_inventory.holding_item);
player_inventory.holding_item = undefined;
}
}
if (player_inventory.holding_item?.amount === 0) {
player_inventory.holding_item = undefined;
}
}
}
}
switch_item_with_holding(player_inventory: PlayerInventory, index: number) {
const container_slot = player_inventory.container.get_slot(index);
const original_holding_item = player_inventory.holding_item;
player_inventory.holding_item = container_slot.get_item();
container_slot.set_item(original_holding_item);
}
handle_left_click(player_inventory: PlayerInventory, container: Container, index: number) {
const holding = player_inventory.holding_item;
const slot = container.get_slot(index);
const slot_item = slot.get_item();
// if you aren't holding anything
// "swap" with nothing on your hand (pick it up)
if (!holding) {
this.switch_item_with_holding(player_inventory, index);
return;
}
// if you are holding something and slot type equals holding type
// try to add to stack
if (slot_item && slot.type_id === holding.type_id) {
const space_left = slot.max_amount! - slot_item.amount!;
const amount_to_add = Math.min(space_left, holding.amount);
slot_item.amount += amount_to_add;
holding.amount -= amount_to_add;
return;
}
// if something on hand but not the same
// swap
this.switch_item_with_holding(player_inventory, index);
}
handle_right_click(player_inventory: PlayerInventory, container: Container, index: number) {
const holding = player_inventory.holding_item;
const slot = container.get_slot(index);
const slot_item = slot.get_item();
// if holding something
if (holding) {
// and slot type equals holding type
// add 1 to matching stack
if (slot_item && slot.type_id === holding.type_id) {
if (slot_item.amount < slot_item.max_amount!) {
slot_item.amount += 1;
holding.amount -= 1;
}
return;
}
// (actually the same as the last one but creates a new one techinically)
// place 1 into empty slot
if (!slot_item) {
const new_item = holding.clone();
new_item.amount = 1;
slot.set_item(new_item);
holding.amount -= 1;
return;
}
// if something on hand but not the same
// swap
this.switch_item_with_holding(player_inventory, index);
return;
}
// if player is holding nothing and clicks nothing, nothing happens
if (!slot_item) {
return;
}
// pick up half of the stack
const original_amount = slot_item.amount;
const half = Math.floor(original_amount / 2);
slot_item.amount = half;
const picked_up = slot_item.clone();
picked_up.amount = original_amount - half;
player_inventory.holding_item = picked_up;
if (slot_item.amount === 0) {
slot.set_item(undefined);
}
}
}
+84
View File
@@ -0,0 +1,84 @@
import { Entity, System } from "$/common/ecs/mod.ts";
import { Velocity } from "$/common/components/velocity.ts";
import { InputManager } from "../input_manager.ts";
import { AnimatedSprite } from "../components/sprite.ts";
import { ClientWorld } from "../client_world.ts";
import { PlayerControls } from "../components/player_controls.ts";
import { PlayerInventory } from "../components/inventory.ts";
import { Camera } from "../components/camera.ts";
import { Position } from "../../common/components/position.ts";
export class PlayerControlsSystem extends System {
player: Entity;
constructor(player: Entity) {
super();
this.player = player;
}
update(world: ClientWorld, _delta: number): void {
const velocity = this.player.get(Velocity)!;
const controls = this.player.get(PlayerControls)!;
if (InputManager.is_key_down(controls.move_left)) {
velocity.vx = -controls.move_speed;
} else if (InputManager.is_key_down(controls.move_right)) {
velocity.vx = controls.move_speed;
} else {
velocity.vx = 0;
}
if (InputManager.is_key_down(controls.move_up)) {
velocity.vy = -controls.move_speed;
} else if (InputManager.is_key_down(controls.move_down)) {
velocity.vy = controls.move_speed;
} else {
velocity.vy = 0;
}
const animated_sprite = this.player.get(AnimatedSprite)!;
if (
InputManager.is_key_down(controls.move_up) || InputManager.is_key_down(controls.move_down) ||
InputManager.is_key_down(controls.move_left) || InputManager.is_key_down(controls.move_right)
) {
animated_sprite.set_state("running");
} else {
animated_sprite.set_state("idle");
}
if (InputManager.is_key_down(controls.move_left)) {
animated_sprite.flip_x = false;
} else if (InputManager.is_key_down(controls.move_right)) {
animated_sprite.flip_x = true;
}
if (InputManager.is_key_pressed(controls.open_inventory)) {
const inventory = this.player.get(PlayerInventory)!;
inventory.is_open = !inventory.is_open;
}
if (InputManager.is_key_pressed(controls.open_debug)) {
world.debugging = !world.debugging;
}
const position = this.player.get(Position)!;
const camera = this.player.get(Camera)!;
camera.x = Math.round(position.x);
camera.y = Math.round(position.y);
// --- APPLY BOUNDS ---
/*if (camera.bounds) {
camera.x = Math.max(
camera.bounds.min_x,
Math.min(camera.x, camera.bounds.max_x),
);
camera.y = Math.max(
camera.bounds.min_y,
Math.min(camera.y, camera.bounds.max_y),
);
}*/
}
}
+75
View File
@@ -0,0 +1,75 @@
import { System } from "$/common/ecs/mod.ts";
import { World } from "$/common/ecs/world.ts";
import { Position } from "$/common/components/position.ts";
import { AnimatedSprite, Sprite } from "$/client/components/sprite.ts";
import { Tilemap } from "$/client/components/tilemap.ts";
import { PlayerInventory } from "$/client/components/inventory.ts";
import { Camera } from "$/client/components/camera.ts";
import { render_animated_sprite, render_sprite } from "./rendering/sprites.ts";
import { render_tilemap } from "./rendering/tilemap.ts";
import { render_player_inventory } from "./rendering/player.ts";
export class RenderSystem extends System {
ctx: CanvasRenderingContext2D;
constructor(ctx: CanvasRenderingContext2D) {
super();
this.ctx = ctx;
}
update(world: World, _delta: number): void {
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
const camera_entity = world.get_entities().values().find((e) => e.get(Camera)?.active);
const camera = camera_entity?.get(Camera);
if (!camera) {
return;
}
for (const entity of world.get_entities()) {
this.ctx.save();
this.ctx.translate(
Math.floor(this.ctx.canvas.width / 2),
Math.floor(this.ctx.canvas.height / 2),
);
this.ctx.scale(camera.zoom, camera.zoom);
this.ctx.translate(
-Math.floor(camera.x),
-Math.floor(camera.y),
);
const position = entity.get(Position);
const sprite = entity.get(Sprite);
if (sprite && position) {
render_sprite(this.ctx, sprite, position);
}
const animated_sprite = entity.get(AnimatedSprite);
if (animated_sprite && position) {
render_animated_sprite(this.ctx, animated_sprite, position);
}
const tilemap = entity.get(Tilemap);
if (tilemap) {
render_tilemap(this.ctx, tilemap);
}
// Ui
this.ctx.restore();
const player_inventory = entity.get(PlayerInventory);
if (player_inventory && player_inventory.is_open) {
render_player_inventory(this.ctx, player_inventory);
}
}
}
}
+62
View File
@@ -0,0 +1,62 @@
import { SLOT_SIZE } from "$/common/constants.ts";
import { AssetManager } from "$/client/assets.ts";
import { PlayerInventory } from "$/client/components/inventory.ts";
import { InputManager } from "$/client/input_manager.ts";
import { draw_item, draw_nine_slice } from "./render_utils.ts";
export function render_player_inventory(ctx: CanvasRenderingContext2D, player_inventory: PlayerInventory) {
const PADDING = 10;
const ui = AssetManager.instance.get<HTMLImageElement>("bworld:ui");
const layout = player_inventory.layout.slots;
draw_nine_slice(
ctx,
ui,
160,
0,
16,
16,
4,
4,
4,
4,
0,
0,
PADDING * 2 + SLOT_SIZE * 9,
PADDING * 2 + SLOT_SIZE * 4,
);
for (const [index, slot] of layout.entries()) {
const x = slot.x + PADDING;
const y = slot.y + PADDING;
draw_nine_slice(
ctx,
ui,
player_inventory.hovering_slot === index ? 19 * 16 : 160 + 32,
player_inventory.hovering_slot === index ? 16 : 0,
16,
16,
4,
4,
4,
4,
x,
y,
SLOT_SIZE,
SLOT_SIZE,
);
}
for (const [index, slot] of layout.entries()) {
const x = slot.x + PADDING;
const y = slot.y + PADDING;
const item = player_inventory.container.get_item(index);
if (item) {
draw_item(ctx, item, x, y);
}
}
if (player_inventory.holding_item) {
const mouse = InputManager.get_mouse_position();
draw_item(ctx, player_inventory.holding_item, mouse.x, mouse.y);
}
}
+142
View File
@@ -0,0 +1,142 @@
import { SLOT_SIZE } from "$/common/constants.ts";
import { get_sprite_region } from "$/common/utils.ts";
import { AssetManager } from "$/client/assets.ts";
import { ItemStack } from "$/client/components/inventory.ts";
import { draw_text } from "$/client/text_rendering.ts";
export function draw_nine_slice(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
source_x: number,
source_y: number,
source_width: number,
source_height: number,
left: number,
right: number,
top: number,
bottom: number,
x: number,
y: number,
width: number,
height: number,
) {
const center_width = source_width - left - right;
const center_height = source_height - top - bottom;
const dest_center_width = width - left - right;
const dest_center_height = height - top - bottom;
// top left
ctx.drawImage(image, source_x, source_y, left, top, x, y, left, top);
// top right
ctx.drawImage(image, source_x + source_width - right, source_y, right, top, x + width - right, y, right, top);
// bottom left
ctx.drawImage(
image,
source_x,
source_y + source_height - bottom,
left,
bottom,
x,
y + height - bottom,
left,
bottom,
);
// bottom right
ctx.drawImage(
image,
source_x + source_width - right,
source_y + source_height - bottom,
right,
bottom,
x + width - right,
y + height - bottom,
right,
bottom,
);
// top
ctx.drawImage(image, source_x + left, source_y, center_width, top, x + left, y, dest_center_width, top);
// bottom
ctx.drawImage(
image,
source_x + left,
source_y + source_height - bottom,
center_width,
bottom,
x + left,
y + height - bottom,
dest_center_width,
bottom,
);
// left
ctx.drawImage(image, source_x, source_y + top, left, center_height, x, y + top, left, dest_center_height);
// right
ctx.drawImage(
image,
source_x + source_width - right,
source_y + top,
right,
center_height,
x + width - right,
y + top,
right,
dest_center_height,
);
// center !
ctx.drawImage(
image,
source_x + left,
source_y + top,
center_width,
center_height,
x + left,
y + top,
dest_center_width,
dest_center_height,
);
}
export function draw_item(ctx: CanvasRenderingContext2D, item: ItemStack, x: number, y: number) {
const sprite_region = get_sprite_region(item.type_id);
ctx.drawImage(
AssetManager.instance.get("bworld:tiny_town"),
sprite_region.x * 16,
sprite_region.y * 16,
16,
16,
x + 3,
y + 3,
SLOT_SIZE - 6,
SLOT_SIZE - 6,
);
if (item.max_amount !== 1) {
draw_text(
ctx,
String(item.amount),
x + SLOT_SIZE + 2 - 4,
y + SLOT_SIZE + 2 - 4,
2,
"#3f3f3f",
"bottom",
"right",
);
draw_text(
ctx,
String(item.amount),
x + SLOT_SIZE - 4,
y + SLOT_SIZE - 4,
2,
"white",
"bottom",
"right",
);
}
}
+76
View File
@@ -0,0 +1,76 @@
import { Position } from "$/common/components/position.ts";
import { AnimatedSprite, Sprite } from "$/client/components/sprite.ts";
export function render_sprite(ctx: CanvasRenderingContext2D, sprite: Sprite, position: Position) {
ctx.save();
ctx.translate(
Math.floor(sprite.flip_x ? position.x + sprite.width : position.x),
Math.floor(sprite.flip_y ? position.y + sprite.height : position.y),
);
ctx.scale(
sprite.flip_x ? -1 : 1,
sprite.flip_y ? -1 : 1,
);
ctx.drawImage(
sprite.image,
sprite.source_x,
sprite.source_y,
sprite.source_width,
sprite.source_height,
0,
0,
sprite.width,
sprite.height,
);
ctx.restore();
}
export function render_animated_sprite(
ctx: CanvasRenderingContext2D,
animated_sprite: AnimatedSprite,
position: Position,
) {
ctx.save();
const current_animation = animated_sprite.states[animated_sprite.current_state];
if (!current_animation) {
console.error(`Missing animation for state ${animated_sprite.current_state}`);
return;
}
ctx.translate(
Math.floor(animated_sprite.flip_x ? position.x + animated_sprite.width : position.x),
Math.floor(animated_sprite.flip_y ? position.y + animated_sprite.height : position.y),
);
ctx.scale(
animated_sprite.flip_x ? -1 : 1,
animated_sprite.flip_y ? -1 : 1,
);
ctx.drawImage(
animated_sprite.image,
current_animation.source_x[animated_sprite.animation_frame],
current_animation.source_y[animated_sprite.animation_frame],
current_animation.source_width,
current_animation.source_height,
0,
0,
animated_sprite.width,
animated_sprite.height,
);
animated_sprite.timer += 1;
if (animated_sprite.timer >= current_animation.duration) {
animated_sprite.timer = 0;
animated_sprite.animation_frame += 1;
if (animated_sprite.animation_frame >= current_animation.source_x.length) {
animated_sprite.animation_frame = 0;
}
}
ctx.restore();
}
+30
View File
@@ -0,0 +1,30 @@
import { Tilemap } from "$/client/components/tilemap.ts";
export function render_tilemap(ctx: CanvasRenderingContext2D, tilemap: Tilemap) {
ctx.save();
ctx.translate(
-Math.floor(ctx.canvas.width / 2),
-Math.floor(ctx.canvas.height / 2),
);
for (const tile of tilemap.tiles) {
const tx = tile.index % tilemap.columns;
const ty = Math.floor(tile.index / tilemap.columns);
const sx = tx * tilemap.tile_size + (tx * tilemap.margin);
const sy = ty * tilemap.tile_size + (ty * tilemap.margin);
ctx.drawImage(
tilemap.image,
sx,
sy,
tilemap.tile_size,
tilemap.tile_size,
tile.x * tilemap.tile_size * tilemap.scale,
tile.y * tilemap.tile_size * tilemap.scale,
tilemap.tile_size * tilemap.scale,
tilemap.tile_size * tilemap.scale,
);
}
ctx.restore();
}
+99
View File
@@ -0,0 +1,99 @@
import { System } from "$/common/ecs/mod.ts";
import { point_inside_rec } from "$/common/utils.ts";
import { ClientWorld } from "$/client/client_world.ts";
import { Camera } from "$/client/components/camera.ts";
import { Tilemap } from "$/client/components/tilemap.ts";
import { DebugUI } from "$/client/debug_ui.ts";
import { InputManager } from "$/client/input_manager.ts";
export class TileEditorSystem extends System {
constructor() {
super();
}
update(world: ClientWorld, _delta: number): void {
if (!world.debugging) {
return;
}
DebugUI.ctx.save();
DebugUI.ctx.setTransform(1, 0, 0, 1, 0, 0);
const ctx = world.ctx;
const camera_entity = world.get_entities().values().find((e) => e.get(Camera)?.active);
const camera = camera_entity?.get(Camera);
if (!camera) {
return;
}
for (const entity of world.get_entities()) {
const tilemap = entity.get(Tilemap);
if (!tilemap) {
continue;
}
const scaled_size = tilemap.tile_size * tilemap.scale;
const mouse = InputManager.get_mouse_position();
DebugUI.begin("Tile Editor", 500, 10, 300);
DebugUI.progress_bar(0.5);
if (DebugUI.button("Save")) {
navigator.clipboard.writeText(JSON.stringify(tilemap));
}
for (let i = 0; i < tilemap.rows; i += 1) {
for (let j = 0; j < tilemap.columns; j += 1) {
const x = DebugUI.cursor_x + j * scaled_size;
const y = DebugUI.cursor_y;
const hovered = point_inside_rec(mouse.x, mouse.y, x, y, scaled_size, scaled_size);
if (DebugUI.is_inside_windows(mouse.x, mouse.y) && hovered && InputManager.is_mouse_pressed(0)) {
InputManager.consume_mouse(0);
tilemap.selected_tile = i * tilemap.columns + j;
}
ctx.drawImage(
tilemap.image,
j * tilemap.tile_size + (j * tilemap.margin),
i * tilemap.tile_size + (i * tilemap.margin),
tilemap.tile_size,
tilemap.tile_size,
x,
y,
scaled_size,
scaled_size,
);
}
DebugUI.advance(scaled_size);
}
DebugUI.end();
if (tilemap.editing) {
const world_x = mouse.x + camera.x;
const world_y = mouse.y + camera.y;
const tx = Math.floor(world_x / (tilemap.tile_size * tilemap.scale));
const ty = Math.floor(world_y / (tilemap.tile_size * tilemap.scale));
if (InputManager.is_mouse_pressed(0)) {
const maybe_tile = tilemap.tiles.find((tile) => tile.x === tx && tile.y === ty);
if (maybe_tile?.index !== tilemap.selected_tile) {
tilemap.tiles.push({ x: tx, y: ty, index: tilemap.selected_tile });
}
} else if (InputManager.is_mouse_pressed(1)) {
InputManager.consume_mouse(1);
const index = tilemap.tiles.findLastIndex((tile) => tile.x === tx && tile.y === ty);
if (index !== -1) {
tilemap.tiles.splice(index, 1);
}
}
}
}
DebugUI.ctx.restore();
}
}