Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
build/
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
shout out to kenney
|
||||||
|
|
||||||
|
# Tiny town
|
||||||
|
|
||||||
|
https://kenney.nl/assets/tiny-town
|
||||||
|
|
||||||
|
license: [Creative Commons CC0](https://creativecommons.org/publicdomain/zero/1.0/)
|
||||||
|
|
||||||
|
# Roguelike
|
||||||
|
|
||||||
|
https://kenney.nl/assets/roguelike-rpg-pack
|
||||||
|
|
||||||
|
license: [Creative Commons CC0](https://creativecommons.org/publicdomain/zero/1.0/)
|
||||||
|
|
||||||
|
# UI
|
||||||
|
|
||||||
|
https://kenney.nl/assets/ui-pack-pixel-adventure
|
||||||
|
|
||||||
|
license: [Creative Commons CC0](https://creativecommons.org/publicdomain/zero/1.0/)
|
||||||
|
|
||||||
|
# m6x11.ttf
|
||||||
|
|
||||||
|
Font by Daniel Linssen (https://managore.itch.io)
|
||||||
|
|
||||||
|
https://managore.itch.io/m6x11 (https://web.archive.org/web/20260217032856/https://managore.itch.io/m6x11)
|
||||||
|
|
||||||
|
license: `free to use with attribution`
|
||||||
|
|
||||||
|
# Tiny wonder farm
|
||||||
|
|
||||||
|
https://butterymilk.itch.io/tiny-wonder-farm-asset-pack
|
||||||
|
(https://web.archive.org/web/20260204234306/https://butterymilk.itch.io/tiny-wonder-farm-asset-pack)
|
||||||
|
|
||||||
|
file: assets/sprites/player.png
|
||||||
|
|
||||||
|
license:
|
||||||
|
|
||||||
|
```
|
||||||
|
There's two versions, free and paid (premium):
|
||||||
|
|
||||||
|
Free: It includes a limited number of items available for free.
|
||||||
|
|
||||||
|
You are free to use sprites included in the pack in any of your non-commercial projects as well as edit the sprites and
|
||||||
|
add something new!
|
||||||
|
|
||||||
|
Premium: It includes everything shown from the showcase, all tilemaps, sprites, characters and more!
|
||||||
|
|
||||||
|
You are free to use sprites included in the pack in any of your commercial or non-commercial projects as well as edit
|
||||||
|
the sprites and add something new!
|
||||||
|
|
||||||
|
You can't resell the sprites, even if changes were made. You cannot use it in any projects related to NFT."
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
import { copy } from "@std/fs";
|
||||||
|
|
||||||
|
const BUILD_FOLDER = "build";
|
||||||
|
|
||||||
|
function clear_folder() {
|
||||||
|
Deno.removeSync(BUILD_FOLDER, { recursive: true });
|
||||||
|
Deno.mkdir(BUILD_FOLDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_assets() {
|
||||||
|
copy("assets", `${BUILD_FOLDER}/assets`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build_client() {
|
||||||
|
const _result = await Deno.bundle({
|
||||||
|
entrypoints: ["./client/index.html"],
|
||||||
|
outputDir: `${BUILD_FOLDER}/client`,
|
||||||
|
platform: "browser",
|
||||||
|
minify: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_build = 0;
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const watcher = Deno.watchFs(["assets", "client", "common"], { recursive: true });
|
||||||
|
for await (const event of watcher) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - last_build < 500) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (["create", "modify", "rename", "remove"].includes(event.kind)) {
|
||||||
|
last_build = now;
|
||||||
|
console.log("Rebuilding...");
|
||||||
|
clear_folder();
|
||||||
|
build_assets();
|
||||||
|
await build_client();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
type Asset = HTMLImageElement | HTMLAudioElement;
|
||||||
|
|
||||||
|
export class AssetManager {
|
||||||
|
static instance = new AssetManager();
|
||||||
|
|
||||||
|
assets: Record<string, Asset> = {};
|
||||||
|
loading_promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
load(key: string, src: string) {
|
||||||
|
if (this.assets[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.endsWith(".png")) {
|
||||||
|
const img = new Image();
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
|
||||||
|
});
|
||||||
|
img.src = src;
|
||||||
|
this.assets[key] = img;
|
||||||
|
this.loading_promises.push(promise);
|
||||||
|
} else if (src.endsWith(".ogg")) {
|
||||||
|
const audio = new Audio();
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
audio.addEventListener(
|
||||||
|
"canplaythrough",
|
||||||
|
() => resolve(),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
audio.addEventListener(
|
||||||
|
"error",
|
||||||
|
() => reject(new Error(`Failed to load audio: ${src}`)),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.src = src;
|
||||||
|
audio.load();
|
||||||
|
this.assets[key] = audio;
|
||||||
|
this.loading_promises.push(promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_all() {
|
||||||
|
await Promise.all(this.loading_promises);
|
||||||
|
this.loading_promises = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(key: string): T {
|
||||||
|
const asset = this.assets[key];
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`Asset not found: ${key}`);
|
||||||
|
}
|
||||||
|
return asset as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Entity, World } from "$/common/ecs/mod.ts";
|
||||||
|
import { MovementSystem } from "$/common/systems/movement_system.ts";
|
||||||
|
import { RenderSystem } from "$/client/systems/render_system.ts";
|
||||||
|
import { PlayerControlsSystem } from "$/client/systems/player_controls.ts";
|
||||||
|
import { DebugUI } from "$/client/debug_ui.ts";
|
||||||
|
import { DebugSystem } from "$/client/systems/debug_system.ts";
|
||||||
|
import { create_player } from "$/client/player.ts";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export class ClientWorld extends World {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
debugging = false;
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Could not get CanvasRenderingContext2D");
|
||||||
|
}
|
||||||
|
this.ctx = ctx;
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
canvas.width = self.innerWidth;
|
||||||
|
canvas.height = self.innerHeight;
|
||||||
|
|
||||||
|
canvas.style.width = canvas.width + "px";
|
||||||
|
canvas.style.height = canvas.height + "px";
|
||||||
|
|
||||||
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener("resize", resize);
|
||||||
|
resize();
|
||||||
|
|
||||||
|
canvas.addEventListener("contextmenu", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
DebugUI.initialize(this.ctx);
|
||||||
|
|
||||||
|
const tilemap = new Entity("backgroundtiles");
|
||||||
|
tilemap.add(
|
||||||
|
new Tilemap(
|
||||||
|
AssetManager.instance.get("bworld:roguelike"),
|
||||||
|
16,
|
||||||
|
[],
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.add_entity(tilemap);
|
||||||
|
|
||||||
|
const player = create_player();
|
||||||
|
this.add_entity(player);
|
||||||
|
|
||||||
|
this.add_system(new InventorySystem());
|
||||||
|
this.add_system(new PlayerControlsSystem(player));
|
||||||
|
this.add_system(new MovementSystem());
|
||||||
|
|
||||||
|
// render systems
|
||||||
|
this.add_system(new RenderSystem(this.ctx));
|
||||||
|
this.add_system(new DebugSystem());
|
||||||
|
this.add_system(new TileEditorSystem());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from "$/common/ecs/mod.ts";
|
||||||
|
|
||||||
|
export class Camera extends Component {
|
||||||
|
active = true;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zoom: number;
|
||||||
|
|
||||||
|
constructor(x = 0, y = 0, zoom = 1) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.zoom = zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Component } from "$/common/ecs/mod.ts";
|
||||||
|
import { SLOT_SIZE } from "$/common/constants.ts";
|
||||||
|
|
||||||
|
export class ItemStack {
|
||||||
|
type_id: string;
|
||||||
|
amount: number;
|
||||||
|
max_amount: number;
|
||||||
|
|
||||||
|
constructor(type_id: string | string, amount: number = 1, max_amount: number = 64) {
|
||||||
|
this.type_id = type_id;
|
||||||
|
this.amount = amount;
|
||||||
|
this.max_amount = max_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(): ItemStack {
|
||||||
|
return new ItemStack(this.type_id, this.amount, this.max_amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContainerSlot {
|
||||||
|
#item_stack: ItemStack | undefined;
|
||||||
|
|
||||||
|
has_item() {
|
||||||
|
return this.#item_stack !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_item(item_stack: ItemStack | undefined) {
|
||||||
|
this.#item_stack = item_stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_item() {
|
||||||
|
return this.#item_stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type_id() {
|
||||||
|
return this.#item_stack?.type_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get amount() {
|
||||||
|
return this.#item_stack?.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get max_amount() {
|
||||||
|
return this.#item_stack?.max_amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Container {
|
||||||
|
#slots: ContainerSlot[] = [];
|
||||||
|
readonly size: number;
|
||||||
|
|
||||||
|
constructor(size: number) {
|
||||||
|
this.size = size;
|
||||||
|
for (let i = 0; i < size; i += 1) {
|
||||||
|
this.#slots.push(new ContainerSlot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_item(item_stack: ItemStack) {
|
||||||
|
// TODO: merge stacks
|
||||||
|
for (const slot of this.#slots) {
|
||||||
|
if (!slot.has_item()) {
|
||||||
|
slot.set_item(item_stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: drop item on ground
|
||||||
|
console.error("Failed to place item in container");
|
||||||
|
}
|
||||||
|
|
||||||
|
get_item(slot: number): ItemStack | undefined {
|
||||||
|
return this.#slots[slot].get_item();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_slot(slot: number): ContainerSlot {
|
||||||
|
return this.#slots[slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerLayout {
|
||||||
|
slots: { type: string; x: number; y: number }[];
|
||||||
|
offset_x: number;
|
||||||
|
offset_y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Inventory extends Component {
|
||||||
|
container: Container;
|
||||||
|
|
||||||
|
is_open: boolean = false;
|
||||||
|
layout: ContainerLayout;
|
||||||
|
|
||||||
|
hovering_slot: number = -1;
|
||||||
|
|
||||||
|
constructor(container: Container, layout: ContainerLayout) {
|
||||||
|
super();
|
||||||
|
this.container = container;
|
||||||
|
this.layout = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayerInventory extends Inventory {
|
||||||
|
owner_id: string;
|
||||||
|
holding_item: ItemStack | undefined;
|
||||||
|
|
||||||
|
constructor(owner_id: string) {
|
||||||
|
const container = new Container(9 * 4);
|
||||||
|
const layout: ContainerLayout = {
|
||||||
|
slots: [],
|
||||||
|
offset_x: 10,
|
||||||
|
offset_y: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let row = 0; row < 4; row += 1) {
|
||||||
|
for (let column = 0; column < 9; column += 1) {
|
||||||
|
layout.slots.push({ type: "*", x: column * SLOT_SIZE, y: row * SLOT_SIZE });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super(container, layout);
|
||||||
|
this.owner_id = owner_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Component } from "$/common/ecs/mod.ts";
|
||||||
|
import { KeyCode } from "$/client/input_manager.ts";
|
||||||
|
|
||||||
|
export class PlayerControls extends Component {
|
||||||
|
move_speed: number = 100;
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
move_up: KeyCode = "KeyW";
|
||||||
|
move_down: KeyCode = "KeyS";
|
||||||
|
move_left: KeyCode = "KeyA";
|
||||||
|
move_right: KeyCode = "KeyD";
|
||||||
|
|
||||||
|
open_inventory: KeyCode = "KeyE";
|
||||||
|
|
||||||
|
open_debug: KeyCode = "F3";
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Component } from "$/common/ecs/mod.ts";
|
||||||
|
import { AssetManager } from "$/client/assets.ts";
|
||||||
|
|
||||||
|
export class Sprite extends Component {
|
||||||
|
image: HTMLImageElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
source_x: number;
|
||||||
|
source_y: number;
|
||||||
|
source_width: number;
|
||||||
|
source_height: number;
|
||||||
|
flip_x = false;
|
||||||
|
flip_y = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
image: HTMLImageElement | string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
source_x = 0,
|
||||||
|
source_y = 0,
|
||||||
|
source_width = width,
|
||||||
|
source_height = height,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
if (typeof image === "string") {
|
||||||
|
this.image = AssetManager.instance.get<HTMLImageElement>(image);
|
||||||
|
} else {
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.source_x = source_x;
|
||||||
|
this.source_y = source_y;
|
||||||
|
this.source_width = source_width;
|
||||||
|
this.source_height = source_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnimatedSpritePiece {
|
||||||
|
source_x: number[];
|
||||||
|
source_y: number[];
|
||||||
|
source_width: number;
|
||||||
|
source_height: number;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnimatedSprite extends Component {
|
||||||
|
image: HTMLImageElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
flip_x = false;
|
||||||
|
flip_y = false;
|
||||||
|
|
||||||
|
current_state: string;
|
||||||
|
states: Record<string, AnimatedSpritePiece>;
|
||||||
|
timer = 0;
|
||||||
|
animation_frame = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
image: HTMLImageElement | string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
states: Record<string, AnimatedSpritePiece>,
|
||||||
|
initial_state: string,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
if (typeof image === "string") {
|
||||||
|
this.image = AssetManager.instance.get<HTMLImageElement>(image);
|
||||||
|
} else {
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
this.states = states;
|
||||||
|
this.current_state = initial_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_state(state: string) {
|
||||||
|
if (this.current_state !== state) {
|
||||||
|
this.current_state = state;
|
||||||
|
this.timer = 0;
|
||||||
|
this.animation_frame = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component } from "$/common/ecs/mod.ts";
|
||||||
|
|
||||||
|
interface Tile {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tilemap extends Component {
|
||||||
|
image: HTMLImageElement;
|
||||||
|
tile_size: number;
|
||||||
|
rows: number;
|
||||||
|
columns: number;
|
||||||
|
tiles: Tile[];
|
||||||
|
scale: number;
|
||||||
|
margin: number;
|
||||||
|
|
||||||
|
selected_tile = 0;
|
||||||
|
editing = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
image: HTMLImageElement,
|
||||||
|
tile_size: number,
|
||||||
|
tiles: Tile[],
|
||||||
|
scale: number = 1,
|
||||||
|
margin: number = 0,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.image = image;
|
||||||
|
this.tile_size = tile_size;
|
||||||
|
this.columns = Math.floor((image.width + margin) / (tile_size + margin));
|
||||||
|
this.rows = Math.floor((image.height + margin) / (tile_size + margin));
|
||||||
|
this.tiles = tiles;
|
||||||
|
this.scale = scale;
|
||||||
|
this.margin = margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
import { point_inside_rec } from "$/common/utils.ts";
|
||||||
|
import { InputManager } from "$/client/input_manager.ts";
|
||||||
|
import { draw_text, measure_text } from "$/client/text_rendering.ts";
|
||||||
|
|
||||||
|
const HEADER_HEIGHT = 24;
|
||||||
|
const RESIZE_SIZE = 12;
|
||||||
|
|
||||||
|
const PADDING = 4;
|
||||||
|
|
||||||
|
interface WindowState {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
dragging: boolean;
|
||||||
|
resizing: boolean;
|
||||||
|
scroll_y: number;
|
||||||
|
content_height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextInputState {
|
||||||
|
value: string;
|
||||||
|
caret: number;
|
||||||
|
focused: boolean;
|
||||||
|
last_blink: number;
|
||||||
|
show_caret: boolean;
|
||||||
|
key_repeat_timer: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DebugUI {
|
||||||
|
static ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
static windows = new Map<string, WindowState>();
|
||||||
|
static current_window: WindowState | undefined = undefined;
|
||||||
|
|
||||||
|
static text_inputs = new Map<string, TextInputState>();
|
||||||
|
static active_text_input: string | undefined = undefined;
|
||||||
|
|
||||||
|
static open_sections = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// cursor as in like for drawing
|
||||||
|
static cursor_x = 0;
|
||||||
|
static cursor_y = 0;
|
||||||
|
static content_width = 0;
|
||||||
|
|
||||||
|
static initialize(ctx: CanvasRenderingContext2D) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
static begin(title: string, x: number, y: number, width = 300, height = 300) {
|
||||||
|
let win = this.windows.get(title);
|
||||||
|
if (!win) {
|
||||||
|
win = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
dragging: false,
|
||||||
|
resizing: false,
|
||||||
|
scroll_y: 0,
|
||||||
|
content_height: 0,
|
||||||
|
};
|
||||||
|
this.windows.set(title, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current_window = win;
|
||||||
|
|
||||||
|
// header
|
||||||
|
const mouse = InputManager.get_mouse_position();
|
||||||
|
const delta = InputManager.get_mouse_delta();
|
||||||
|
|
||||||
|
const hovering_header = point_inside_rec(mouse.x, mouse.y, win.x, win.y, win.width, HEADER_HEIGHT);
|
||||||
|
|
||||||
|
if (hovering_header && InputManager.is_mouse_pressed(0)) {
|
||||||
|
InputManager.consume_mouse(0);
|
||||||
|
win.dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InputManager.is_mouse_down(0)) {
|
||||||
|
win.dragging = false;
|
||||||
|
win.resizing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (win.dragging) {
|
||||||
|
win.x += delta.x;
|
||||||
|
win.y += delta.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resize square
|
||||||
|
const hovering_resize = point_inside_rec(
|
||||||
|
mouse.x,
|
||||||
|
mouse.y,
|
||||||
|
win.x + win.width - RESIZE_SIZE,
|
||||||
|
win.y + win.height - RESIZE_SIZE,
|
||||||
|
RESIZE_SIZE,
|
||||||
|
RESIZE_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hovering_resize && InputManager.is_mouse_pressed(0)) {
|
||||||
|
InputManager.consume_mouse(0);
|
||||||
|
win.resizing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (win.resizing) {
|
||||||
|
win.width += delta.x;
|
||||||
|
win.height += delta.y;
|
||||||
|
|
||||||
|
win.width = Math.max(150, win.width);
|
||||||
|
win.height = Math.max(120, win.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrolling
|
||||||
|
const wheel = -InputManager.get_wheel_delta();
|
||||||
|
|
||||||
|
const hovering_content = point_inside_rec(mouse.x, mouse.y, win.x, win.y, win.width, win.height);
|
||||||
|
|
||||||
|
if (hovering_content) {
|
||||||
|
win.scroll_y -= wheel * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const max_scroll = Math.max(0, win.content_height - (win.height - HEADER_HEIGHT - 10));
|
||||||
|
win.scroll_y = Math.max(0, Math.min(win.scroll_y, max_scroll));
|
||||||
|
|
||||||
|
// render
|
||||||
|
this.ctx.save();
|
||||||
|
|
||||||
|
// transparent background
|
||||||
|
this.ctx.fillStyle = "rgba(25,25,25,0.95)";
|
||||||
|
this.ctx.fillRect(win.x, win.y, win.width, win.height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "#aaa";
|
||||||
|
this.ctx.strokeRect(win.x, win.y, win.width, win.height);
|
||||||
|
|
||||||
|
// header and title
|
||||||
|
this.ctx.fillStyle = "#333";
|
||||||
|
this.ctx.fillRect(win.x, win.y, win.width, HEADER_HEIGHT);
|
||||||
|
|
||||||
|
draw_text(this.ctx, title, win.x + 8, win.y + 4, 2);
|
||||||
|
|
||||||
|
// resize grip
|
||||||
|
this.ctx.fillStyle = hovering_resize ? "#888" : "#666";
|
||||||
|
this.ctx.fillRect(
|
||||||
|
win.x + win.width - RESIZE_SIZE,
|
||||||
|
win.y + win.height - RESIZE_SIZE,
|
||||||
|
RESIZE_SIZE,
|
||||||
|
RESIZE_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// clip it !
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(
|
||||||
|
win.x,
|
||||||
|
win.y + HEADER_HEIGHT,
|
||||||
|
win.width,
|
||||||
|
win.height - HEADER_HEIGHT,
|
||||||
|
);
|
||||||
|
this.ctx.clip();
|
||||||
|
|
||||||
|
// move cursor_x to match the windows x and add some padding
|
||||||
|
// same idea for cursor_y but with scrolling
|
||||||
|
this.cursor_x = win.x + PADDING * 2;
|
||||||
|
this.cursor_y = win.y + HEADER_HEIGHT + PADDING * 2 - win.scroll_y;
|
||||||
|
// 20 padding (to count for scrollbar)
|
||||||
|
this.content_width = win.width - 20;
|
||||||
|
|
||||||
|
win.content_height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static end() {
|
||||||
|
if (!this.current_window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see how far we've come to save content height
|
||||||
|
this.current_window.content_height = this.cursor_y - this.current_window.y + this.current_window.scroll_y;
|
||||||
|
|
||||||
|
this.#draw_scrollbar();
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
|
||||||
|
this.current_window = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static advance(height: number) {
|
||||||
|
if (!this.current_window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cursor_y += height;
|
||||||
|
// maybe dont reset on every advance? eh whatever
|
||||||
|
this.cursor_x = this.current_window.x + PADDING * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static separator() {
|
||||||
|
this.ctx.strokeStyle = "#555";
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(this.cursor_x, this.cursor_y);
|
||||||
|
this.ctx.lineTo(this.cursor_x + this.content_width, this.cursor_y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
this.cursor_y += PADDING * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static text(content: string) {
|
||||||
|
draw_text(this.ctx, content, this.cursor_x, this.cursor_y, 2);
|
||||||
|
this.advance(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
static button(label: string): boolean {
|
||||||
|
const height = 24;
|
||||||
|
const x = this.cursor_x;
|
||||||
|
const y = this.cursor_y;
|
||||||
|
const width = this.content_width;
|
||||||
|
|
||||||
|
const mouse = InputManager.get_mouse_position();
|
||||||
|
const hovered = point_inside_rec(mouse.x, mouse.y, x, y, width, height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = hovered ? "#666" : "#444";
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "white";
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "white";
|
||||||
|
draw_text(this.ctx, label, x + 6, y + 4, 2);
|
||||||
|
|
||||||
|
this.advance(height + PADDING);
|
||||||
|
|
||||||
|
if (hovered && InputManager.is_mouse_pressed(0)) {
|
||||||
|
InputManager.consume_mouse(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static checkbox(label: string, value: boolean): boolean {
|
||||||
|
// shoutout to booleans
|
||||||
|
const size = 16;
|
||||||
|
const x = this.cursor_x;
|
||||||
|
const y = this.cursor_y;
|
||||||
|
|
||||||
|
const mouse = InputManager.get_mouse_position();
|
||||||
|
const hovered = point_inside_rec(mouse.x, mouse.y, x, y, size, size);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#222";
|
||||||
|
this.ctx.fillRect(x, y, size, size);
|
||||||
|
this.ctx.strokeStyle = "white";
|
||||||
|
this.ctx.strokeRect(x, y, size, size);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.ctx.fillStyle = "white";
|
||||||
|
this.ctx.fillRect(x + 4, y + 4, size - 8, size - 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "white";
|
||||||
|
draw_text(this.ctx, label, x + size + 6, y, 2);
|
||||||
|
|
||||||
|
this.advance(size + PADDING);
|
||||||
|
|
||||||
|
if (hovered && InputManager.is_mouse_pressed(0)) {
|
||||||
|
InputManager.consume_mouse(0);
|
||||||
|
return !value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static slider_float(
|
||||||
|
label: string,
|
||||||
|
value: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): number {
|
||||||
|
const height = 20;
|
||||||
|
const x = this.cursor_x;
|
||||||
|
const y = this.cursor_y;
|
||||||
|
const width = this.content_width;
|
||||||
|
|
||||||
|
const mouse = InputManager.get_mouse_position();
|
||||||
|
const hovered = point_inside_rec(mouse.x, mouse.y, x, y, width, height);
|
||||||
|
|
||||||
|
if (hovered && InputManager.is_mouse_down(0)) {
|
||||||
|
const t = (mouse.x - x) / width;
|
||||||
|
value = min + (max - min) * Math.min(Math.max(t, 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#333";
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
|
||||||
|
const percent = (value - min) / (max - min);
|
||||||
|
this.ctx.fillStyle = "#0a84ff";
|
||||||
|
this.ctx.fillRect(x, y, width * percent, height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "white";
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "white";
|
||||||
|
draw_text(this.ctx, `${label}: ${value.toFixed(2)}`, x + 5, y + 2, 2);
|
||||||
|
|
||||||
|
this.advance(height + PADDING);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static progress_bar(value: number) {
|
||||||
|
const height = 16;
|
||||||
|
const x = this.cursor_x;
|
||||||
|
const y = this.cursor_y;
|
||||||
|
const width = this.content_width;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#333";
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#366992";
|
||||||
|
this.ctx.fillRect(x, y, width * value, height);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "white";
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
this.advance(height + PADDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
static collapsing_header(label: string): boolean {
|
||||||
|
// i only did the ## for ids on this one cause its the only one that mattered
|
||||||
|
const open = this.open_sections.get(label) ?? false;
|
||||||
|
|
||||||
|
const clicked = this.button((open ? "- " : "+ ") + label.replace(/##.*/, ""));
|
||||||
|
if (clicked) {
|
||||||
|
this.open_sections.set(label, !open);
|
||||||
|
return !open;
|
||||||
|
}
|
||||||
|
|
||||||
|
return open;
|
||||||
|
}
|
||||||
|
|
||||||
|
static combo(
|
||||||
|
label: string,
|
||||||
|
index: number,
|
||||||
|
options: string[],
|
||||||
|
): number {
|
||||||
|
if (this.button(`${label}: ${options[index]}`)) {
|
||||||
|
index = (index + 1) % options.length;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
static text_input(label: string, value: string): string {
|
||||||
|
// yeah just use the text widget
|
||||||
|
this.text(label);
|
||||||
|
|
||||||
|
const id = label;
|
||||||
|
|
||||||
|
let state = this.text_inputs.get(id);
|
||||||
|
if (!state) {
|
||||||
|
// initial state
|
||||||
|
state = {
|
||||||
|
value,
|
||||||
|
caret: value.length,
|
||||||
|
focused: false,
|
||||||
|
last_blink: performance.now(),
|
||||||
|
show_caret: true,
|
||||||
|
key_repeat_timer: 0,
|
||||||
|
};
|
||||||
|
this.text_inputs.set(id, state);
|
||||||
|
}
|
||||||
|
// update on every call
|
||||||
|
state.value = value;
|
||||||
|
|
||||||
|
const height = 24;
|
||||||
|
const x = this.cursor_x;
|
||||||
|
const y = this.cursor_y;
|
||||||
|
const width = this.content_width;
|
||||||
|
|
||||||
|
const mouse = InputManager.get_mouse_position();
|
||||||
|
const hovered = point_inside_rec(mouse.x, mouse.y, x, y, width, height);
|
||||||
|
|
||||||
|
// set focus and move caret to end
|
||||||
|
if (hovered && InputManager.is_mouse_pressed(0)) {
|
||||||
|
InputManager.consume_mouse(0);
|
||||||
|
this.active_text_input = id;
|
||||||
|
state.focused = true;
|
||||||
|
state.caret = value.length;
|
||||||
|
} else if (InputManager.is_mouse_pressed(0) && !hovered) {
|
||||||
|
if (this.active_text_input === id) {
|
||||||
|
state.focused = false;
|
||||||
|
this.active_text_input = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_active = this.active_text_input === id;
|
||||||
|
|
||||||
|
if (is_active) {
|
||||||
|
this.#handle_text_editing(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// overkill?
|
||||||
|
if (is_active) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - state.last_blink > 500) {
|
||||||
|
state.show_caret = !state.show_caret;
|
||||||
|
state.last_blink = now;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.show_caret = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw box
|
||||||
|
this.ctx.fillStyle = is_active ? "#555" : "#333";
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
|
||||||
|
// highlight
|
||||||
|
this.ctx.strokeStyle = is_active ? "#0a84ff" : "#aaa";
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
const text_x = x + 6;
|
||||||
|
|
||||||
|
draw_text(this.ctx, state.value, x + 6, y + 4, 2);
|
||||||
|
|
||||||
|
// caret !
|
||||||
|
if (is_active && state.show_caret) {
|
||||||
|
const before_text = state.value.substring(0, state.caret);
|
||||||
|
const caret_x = text_x + measure_text(this.ctx, before_text, 2);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "white";
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(caret_x, y + 4);
|
||||||
|
this.ctx.lineTo(caret_x, y + height - 4);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor_y += height + PADDING;
|
||||||
|
|
||||||
|
return state.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float_input(label: string, value: number) {
|
||||||
|
// TODO: do something better when input needs to be empty
|
||||||
|
const float_value = parseFloat(this.text_input(label, String(value)));
|
||||||
|
if (Number.isNaN(float_value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return float_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_inside_windows(x: number, y: number) {
|
||||||
|
if (!this.current_window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return point_inside_rec(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
this.current_window.x,
|
||||||
|
this.current_window.y,
|
||||||
|
this.current_window.width,
|
||||||
|
this.current_window.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #draw_scrollbar() {
|
||||||
|
const win = this.current_window!;
|
||||||
|
const content_visible_height = win.height - HEADER_HEIGHT;
|
||||||
|
|
||||||
|
if (win.content_height <= content_visible_height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollbarWidth = 6;
|
||||||
|
const x = win.x + win.width - scrollbarWidth - 2;
|
||||||
|
const y = win.y + HEADER_HEIGHT;
|
||||||
|
const h = content_visible_height - 24;
|
||||||
|
|
||||||
|
const ratio = content_visible_height / win.content_height;
|
||||||
|
const thumbHeight = h * ratio;
|
||||||
|
|
||||||
|
const maxScroll = win.content_height - content_visible_height;
|
||||||
|
const thumbY = y + (win.scroll_y / maxScroll) * (h - thumbHeight);
|
||||||
|
|
||||||
|
// track
|
||||||
|
this.ctx.fillStyle = "#222";
|
||||||
|
this.ctx.fillRect(x, y, scrollbarWidth, h);
|
||||||
|
|
||||||
|
// thumb
|
||||||
|
this.ctx.fillStyle = "#888";
|
||||||
|
this.ctx.fillRect(x, thumbY, scrollbarWidth, thumbHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #handle_text_editing(state: TextInputState) {
|
||||||
|
const now = performance.now();
|
||||||
|
const repeat_delay = 400;
|
||||||
|
const repeat_rate = 40;
|
||||||
|
|
||||||
|
function allow_repeat(): boolean {
|
||||||
|
if (InputManager.is_key_pressed("Backspace")) {
|
||||||
|
state.key_repeat_timer = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_down("Backspace")) {
|
||||||
|
if (now - state.key_repeat_timer > repeat_delay) {
|
||||||
|
state.key_repeat_timer = now - (repeat_delay - repeat_rate);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allow_repeat()) {
|
||||||
|
if (state.caret > 0) {
|
||||||
|
state.value = state.value.slice(0, state.caret - 1) + state.value.slice(state.caret);
|
||||||
|
state.caret -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_pressed("Delete")) {
|
||||||
|
state.value = state.value.slice(0, state.caret) + state.value.slice(state.caret + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_pressed("ArrowLeft")) {
|
||||||
|
state.caret = Math.max(0, state.caret - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_pressed("ArrowRight")) {
|
||||||
|
state.caret = Math.min(state.value.length, state.caret + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_pressed("Home")) {
|
||||||
|
state.caret = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputManager.is_key_pressed("End")) {
|
||||||
|
state.caret = state.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typed = InputManager.get_typed_characters();
|
||||||
|
|
||||||
|
for (const char of typed) {
|
||||||
|
state.value = state.value.slice(0, state.caret) + char + state.value.slice(state.caret);
|
||||||
|
state.caret += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>bworld</title>
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
Font by Daniel Linssen (https://managore.itch.io)
|
||||||
|
https://managore.itch.io/m6x11
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: "m6x11";
|
||||||
|
src: url("/assets/fonts/m6x11.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-smooth: never;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="game"></canvas>
|
||||||
|
<script src="main.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
export type KeyCode =
|
||||||
|
// letters
|
||||||
|
| "KeyA"
|
||||||
|
| "KeyB"
|
||||||
|
| "KeyC"
|
||||||
|
| "KeyD"
|
||||||
|
| "KeyE"
|
||||||
|
| "KeyF"
|
||||||
|
| "KeyG"
|
||||||
|
| "KeyH"
|
||||||
|
| "KeyI"
|
||||||
|
| "KeyJ"
|
||||||
|
| "KeyK"
|
||||||
|
| "KeyL"
|
||||||
|
| "KeyM"
|
||||||
|
| "KeyN"
|
||||||
|
| "KeyO"
|
||||||
|
| "KeyP"
|
||||||
|
| "KeyQ"
|
||||||
|
| "KeyR"
|
||||||
|
| "KeyS"
|
||||||
|
| "KeyT"
|
||||||
|
| "KeyU"
|
||||||
|
| "KeyV"
|
||||||
|
| "KeyW"
|
||||||
|
| "KeyX"
|
||||||
|
| "KeyY"
|
||||||
|
| "KeyZ"
|
||||||
|
// top numbers
|
||||||
|
| "Digit0"
|
||||||
|
| "Digit1"
|
||||||
|
| "Digit2"
|
||||||
|
| "Digit3"
|
||||||
|
| "Digit4"
|
||||||
|
| "Digit5"
|
||||||
|
| "Digit6"
|
||||||
|
| "Digit7"
|
||||||
|
| "Digit8"
|
||||||
|
| "Digit9"
|
||||||
|
// arrows
|
||||||
|
| "ArrowUp"
|
||||||
|
| "ArrowDown"
|
||||||
|
| "ArrowLeft"
|
||||||
|
| "ArrowRight"
|
||||||
|
// controls
|
||||||
|
| "Space"
|
||||||
|
| "Escape"
|
||||||
|
| "Enter"
|
||||||
|
| "Tab"
|
||||||
|
| "ShiftLeft"
|
||||||
|
| "ShiftRight"
|
||||||
|
| "ControlLeft"
|
||||||
|
| "ControlRight"
|
||||||
|
| "AltLeft"
|
||||||
|
| "AltRight"
|
||||||
|
| "Home"
|
||||||
|
| "End"
|
||||||
|
| "Backspace"
|
||||||
|
| "Delete"
|
||||||
|
// f
|
||||||
|
| "F1"
|
||||||
|
| "F2"
|
||||||
|
| "F3"
|
||||||
|
| "F4"
|
||||||
|
| "F5"
|
||||||
|
| "F6"
|
||||||
|
| "F7"
|
||||||
|
| "F8"
|
||||||
|
| "F9"
|
||||||
|
| "F10"
|
||||||
|
| "F11"
|
||||||
|
| "F12";
|
||||||
|
|
||||||
|
export class InputManager {
|
||||||
|
static keys_down = new Set<string>();
|
||||||
|
static keys_pressed = new Set<string>();
|
||||||
|
static keys_released = new Set<string>();
|
||||||
|
|
||||||
|
static mouse_buttons_down = new Set<number>();
|
||||||
|
static mouse_buttons_pressed = new Set<number>();
|
||||||
|
static mouse_buttons_released = new Set<number>();
|
||||||
|
static mouse_buttons_consumed = new Set<number>();
|
||||||
|
|
||||||
|
static mouse_x = 0;
|
||||||
|
static mouse_y = 0;
|
||||||
|
static mouse_delta_x = 0;
|
||||||
|
static mouse_delta_y = 0;
|
||||||
|
static wheel_delta = 0;
|
||||||
|
|
||||||
|
static typed_characters = new Set<string>();
|
||||||
|
|
||||||
|
static initialize(canvas: HTMLCanvasElement) {
|
||||||
|
self.addEventListener("keydown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.keys_down.has(e.code)) {
|
||||||
|
this.keys_pressed.add(e.code);
|
||||||
|
}
|
||||||
|
this.keys_down.add(e.code);
|
||||||
|
if (e.key.length === 1) {
|
||||||
|
this.typed_characters.add(e.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("keyup", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.keys_down.delete(e.code);
|
||||||
|
this.keys_released.add(e.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("mousedown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.mouse_buttons_down.has(e.button)) {
|
||||||
|
this.mouse_buttons_pressed.add(e.button);
|
||||||
|
}
|
||||||
|
this.mouse_buttons_down.add(e.button);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("mouseup", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mouse_buttons_down.delete(e.button);
|
||||||
|
this.mouse_buttons_released.add(e.button);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("mousemove", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const new_x = e.clientX - rect.left;
|
||||||
|
const new_y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
this.mouse_delta_x += new_x - this.mouse_x;
|
||||||
|
this.mouse_delta_y += new_y - this.mouse_y;
|
||||||
|
|
||||||
|
this.mouse_x = new_x;
|
||||||
|
this.mouse_y = new_y;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("wheel", (e) => {
|
||||||
|
this.wheel_delta += e.deltaY;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_key_down(code: KeyCode): boolean {
|
||||||
|
return this.keys_down.has(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_key_pressed(code: KeyCode): boolean {
|
||||||
|
return this.keys_pressed.has(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_key_released(code: KeyCode): boolean {
|
||||||
|
return this.keys_released.has(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_mouse_down(button: number): boolean {
|
||||||
|
return this.mouse_buttons_down.has(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_mouse_pressed(button: number): boolean {
|
||||||
|
return this.mouse_buttons_pressed.has(button) &&
|
||||||
|
!this.mouse_buttons_consumed.has(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
static is_mouse_released(button: number): boolean {
|
||||||
|
return this.mouse_buttons_released.has(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get_mouse_position() {
|
||||||
|
return { x: this.mouse_x, y: this.mouse_y };
|
||||||
|
}
|
||||||
|
|
||||||
|
static get_mouse_delta() {
|
||||||
|
return { x: this.mouse_delta_x, y: this.mouse_delta_y };
|
||||||
|
}
|
||||||
|
|
||||||
|
static get_wheel_delta(): number {
|
||||||
|
return this.wheel_delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
static consume_mouse(button: number) {
|
||||||
|
this.mouse_buttons_consumed.add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get_typed_characters(): string[] {
|
||||||
|
const chars = [...this.typed_characters];
|
||||||
|
this.typed_characters.clear();
|
||||||
|
return chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
static update() {
|
||||||
|
this.keys_pressed.clear();
|
||||||
|
this.keys_released.clear();
|
||||||
|
this.mouse_buttons_pressed.clear();
|
||||||
|
this.mouse_buttons_released.clear();
|
||||||
|
this.mouse_delta_x = 0;
|
||||||
|
this.mouse_delta_y = 0;
|
||||||
|
this.wheel_delta = 0;
|
||||||
|
this.typed_characters.clear();
|
||||||
|
this.mouse_buttons_consumed.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { AssetManager } from "./assets.ts";
|
||||||
|
import { ClientWorld } from "./client_world.ts";
|
||||||
|
import { InputManager } from "./input_manager.ts";
|
||||||
|
|
||||||
|
export class ClientLoop {
|
||||||
|
running = false;
|
||||||
|
last_time = 0;
|
||||||
|
world: ClientWorld;
|
||||||
|
|
||||||
|
frame_count = 0;
|
||||||
|
last_fps_time = 0;
|
||||||
|
|
||||||
|
constructor(world: ClientWorld) {
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.running = true;
|
||||||
|
const now = performance.now();
|
||||||
|
this.last_time = now;
|
||||||
|
this.frame_count = 0;
|
||||||
|
this.last_fps_time = this.last_time;
|
||||||
|
requestAnimationFrame((time) => this.loop(time));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop(time: number) {
|
||||||
|
if (!this.running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = (time - this.last_time) / 1000;
|
||||||
|
|
||||||
|
const ctx = this.world.ctx;
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
this.world.update(delta);
|
||||||
|
|
||||||
|
this.frame_count += 1;
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - this.last_fps_time >= 1000) {
|
||||||
|
const fps = (this.frame_count * 1000) / (now - this.last_fps_time);
|
||||||
|
console.log(`FPS: ${fps.toFixed(2)}`);
|
||||||
|
this.frame_count = 0;
|
||||||
|
this.last_fps_time = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputManager.update();
|
||||||
|
|
||||||
|
this.last_time = time;
|
||||||
|
requestAnimationFrame((time) => this.loop(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.getElementById("game") as HTMLCanvasElement;
|
||||||
|
if (!canvas) {
|
||||||
|
throw Error("Canvas was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
InputManager.initialize(canvas);
|
||||||
|
|
||||||
|
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");
|
||||||
|
AssetManager.instance.load("bworld:ui", "/assets/sprites/ui.png");
|
||||||
|
|
||||||
|
await AssetManager.instance.load_all();
|
||||||
|
|
||||||
|
const client_world = new ClientWorld(canvas);
|
||||||
|
|
||||||
|
const loop = new ClientLoop(client_world);
|
||||||
|
loop.start();
|
||||||
|
|
||||||
|
console.log("Game started");
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Position } from "$/common/components/position.ts";
|
||||||
|
import { Velocity } from "$/common/components/velocity.ts";
|
||||||
|
import { Entity } from "$/common/ecs/mod.ts";
|
||||||
|
import { Camera } from "$/client/components/camera.ts";
|
||||||
|
import { ItemStack, PlayerInventory } from "$/client/components/inventory.ts";
|
||||||
|
import { PlayerControls } from "$/client/components/player_controls.ts";
|
||||||
|
import { AnimatedSprite } from "$/client/components/sprite.ts";
|
||||||
|
|
||||||
|
export function create_player() {
|
||||||
|
const player = new Entity("player");
|
||||||
|
player.add(new Position(10, 10));
|
||||||
|
player.add(new Velocity(0, 0));
|
||||||
|
player.add(
|
||||||
|
new AnimatedSprite("bworld:player", 72, 72, {
|
||||||
|
"idle": {
|
||||||
|
source_x: [0, 24],
|
||||||
|
source_y: [0, 0],
|
||||||
|
source_width: 24,
|
||||||
|
source_height: 24,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
"running": {
|
||||||
|
source_x: [0, 24, 48, 72, 96, 120, 144, 168],
|
||||||
|
source_y: [24, 24, 24, 24, 24, 24, 24, 24],
|
||||||
|
source_width: 24,
|
||||||
|
source_height: 24,
|
||||||
|
duration: 20,
|
||||||
|
},
|
||||||
|
}, "idle"),
|
||||||
|
);
|
||||||
|
player.add(new PlayerControls());
|
||||||
|
// TODO: actual player ids
|
||||||
|
player.add(new PlayerInventory(player.id));
|
||||||
|
player.get(PlayerInventory)!.container.add_item(new ItemStack("bworld:pickaxe", 1, 1));
|
||||||
|
player.get(PlayerInventory)!.container.add_item(new ItemStack("bworld:bomb", 5));
|
||||||
|
player.get(PlayerInventory)!.container.add_item(new ItemStack("bworld:bomb", 64));
|
||||||
|
player.add(new Camera(-100, -100));
|
||||||
|
return player;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const TEXT_SCALE = 8;
|
||||||
|
|
||||||
|
export function measure_text(ctx: CanvasRenderingContext2D, text: string, scale = 1): number {
|
||||||
|
ctx.font = `${scale * TEXT_SCALE}px m6x11`;
|
||||||
|
return ctx.measureText(text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draw_text(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
text: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
scale = 1,
|
||||||
|
color = "white",
|
||||||
|
baseline: CanvasTextBaseline = "top",
|
||||||
|
align: CanvasTextAlign = "left",
|
||||||
|
) {
|
||||||
|
ctx.textBaseline = baseline;
|
||||||
|
ctx.textAlign = align;
|
||||||
|
ctx.font = `${scale * TEXT_SCALE}px m6x11`;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillText(text, Math.floor(x), Math.floor(y));
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from "$/common/ecs/component.ts";
|
||||||
|
|
||||||
|
export class Position extends Component {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
|
||||||
|
constructor(x: number, y: number) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from "$/common/ecs/component.ts";
|
||||||
|
|
||||||
|
export class Velocity extends Component {
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
|
||||||
|
constructor(vx: number, vy: number) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const SLOT_SIZE = 18 * 3;
|
||||||
|
|
||||||
|
export interface SpriteRegion {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ITEM_SPRITES: Record<string, SpriteRegion> = {
|
||||||
|
"bworld:pickaxe": { x: 7, y: 9 },
|
||||||
|
"bworld:bomb": { x: 9, y: 8 },
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export abstract class Component {
|
||||||
|
__component = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Component } from "./component.ts";
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
type ComponentConstructor<T extends Component> = new (...args: any[]) => T;
|
||||||
|
|
||||||
|
export class Entity {
|
||||||
|
id: string;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
components = new Map<ComponentConstructor<any>, Component>();
|
||||||
|
active = true;
|
||||||
|
|
||||||
|
constructor(id: string = crypto.randomUUID()) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
add<T extends Component>(component: T): T {
|
||||||
|
this.components.set(component.constructor as ComponentConstructor<T>, component);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T extends Component>(type: ComponentConstructor<T>): T | undefined {
|
||||||
|
return this.components.get(type) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_all(): Iterable<Component> {
|
||||||
|
return this.components.values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./component.ts";
|
||||||
|
export * from "./entity.ts";
|
||||||
|
export * from "./system.ts";
|
||||||
|
export * from "./world.ts";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { World } from "./world.ts";
|
||||||
|
|
||||||
|
export abstract class System {
|
||||||
|
abstract update(world: World, delta: number): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Entity } from "./entity.ts";
|
||||||
|
import type { System } from "./system.ts";
|
||||||
|
|
||||||
|
export class World {
|
||||||
|
entities = new Set<Entity>();
|
||||||
|
systems = new Set<System>();
|
||||||
|
tags = new Map<string, Entity[]>();
|
||||||
|
|
||||||
|
add_entity(entity: Entity) {
|
||||||
|
this.entities.add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_system(system: System) {
|
||||||
|
this.systems.add(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_tag(tag: string, entities: Entity[]) {
|
||||||
|
this.tags.set(tag, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_tag(tag: string) {
|
||||||
|
return this.tags.get(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
for (const system of this.systems) {
|
||||||
|
system.update(this, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_entities() {
|
||||||
|
return this.entities;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { System, World } from "$/common/ecs/mod.ts";
|
||||||
|
import { Position } from "$/common/components/position.ts";
|
||||||
|
import { Velocity } from "$/common/components/velocity.ts";
|
||||||
|
|
||||||
|
export class MovementSystem extends System {
|
||||||
|
update(world: World, delta: number): void {
|
||||||
|
for (const entity of world.get_entities()) {
|
||||||
|
const position = entity.get(Position);
|
||||||
|
const velocity = entity.get(Velocity);
|
||||||
|
|
||||||
|
if (position && velocity) {
|
||||||
|
position.x += velocity.vx * delta;
|
||||||
|
position.y += velocity.vy * delta;
|
||||||
|
position.x = Math.round(position.x);
|
||||||
|
position.y = Math.round(position.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ITEM_SPRITES, SpriteRegion } from "./constants.ts";
|
||||||
|
|
||||||
|
export function point_inside_rec(
|
||||||
|
point_x: number,
|
||||||
|
point_y: number,
|
||||||
|
rec_x: number,
|
||||||
|
rec_y: number,
|
||||||
|
rec_w: number,
|
||||||
|
rec_h: number,
|
||||||
|
) {
|
||||||
|
return point_x > rec_x &&
|
||||||
|
point_x < rec_x + rec_w &&
|
||||||
|
point_y > rec_y &&
|
||||||
|
point_y < rec_y + rec_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_sprite_region(id: string): SpriteRegion {
|
||||||
|
return ITEM_SPRITES[id] ?? { x: 0, y: 0 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"build": "deno run -A build.ts",
|
||||||
|
"serve:client": "deno run --allow-net --allow-read jsr:@std/http/file-server build"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.asynciterable", "dom.iterable", "deno.ns", "deno.unstable"]
|
||||||
|
},
|
||||||
|
"unstable": ["bundle"],
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": true,
|
||||||
|
"indentWidth": 4,
|
||||||
|
"lineWidth": 120,
|
||||||
|
"newLineKind": "lf"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"$/": "./",
|
||||||
|
"@std/fs": "jsr:@std/fs@^1.0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"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/fs@1.0.23": {
|
||||||
|
"integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal",
|
||||||
|
"jsr:@std/path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/internal@1.0.12": {
|
||||||
|
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||||
|
},
|
||||||
|
"@std/path@1.1.4": {
|
||||||
|
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/fs@^1.0.23"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user