Add basic controls

This commit is contained in:
kolaflash
2026-01-23 14:21:21 +01:00
parent 589af23e8c
commit f9a2409dad
19 changed files with 225 additions and 47 deletions
+9 -9
View File
@@ -5,15 +5,15 @@
"": { "": {
"name": "nage", "name": "nage",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.11", "@biomejs/biome": "latest",
"@types/bun": "1.3.5", "@types/bun": "latest",
"lz-string": "1.5.0", "lz-string": "latest",
"solid-js": "1.9.10", "solid-js": "latest",
"vite": "7.3.1", "vite": "latest",
"vite-plugin-solid": "2.11.10", "vite-plugin-solid": "latest",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "5.9.3", "typescript": "latest",
}, },
}, },
}, },
@@ -192,7 +192,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -206,7 +206,7 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001763", "", {}, "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001763", "", {}, "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ=="],
+1 -6
View File
@@ -40,11 +40,6 @@
--rgb-black: 30, 31, 34; --rgb-black: 30, 31, 34;
--c-black: rgb(var(--rgb-black)); --c-black: rgb(var(--rgb-black));
--c-primary: var(--c-blurple);
--c-secondary: var(--c-fuchsia);
--c-alert: var(--c-orange);
--c-error: var(--c-red);
--c-bg-0: rgb(30, 31, 34); --c-bg-0: rgb(30, 31, 34);
--c-bg-1: rgb(43, 45, 49); --c-bg-1: rgb(43, 45, 49);
--c-bg-2: rgb(49, 51, 56); --c-bg-2: rgb(49, 51, 56);
@@ -81,7 +76,7 @@
*:focus { *:focus {
outline-width: 2px; outline-width: 2px;
outline-style: solid; outline-style: solid;
outline-color: var(--c-primary); outline-color: var(--c-blurple);
} }
} }
+2 -3
View File
@@ -21,14 +21,13 @@ export const entityMap = new Map<string, Entity<unknown>>();
export const entityAnimateMap = new Map<string, Animate<Indirect>>(); export const entityAnimateMap = new Map<string, Animate<Indirect>>();
export const entityProcessMap = new Map<string, Process<Indirect>>(); export const entityProcessMap = new Map<string, Process<Indirect>>();
export function useEntity<T extends { animation: Animation }>( export function defineEntity<T extends { animation: Animation }>(
meta: { url: string }, name: string,
state: T, state: T,
): { ): {
animate: (fn: Animate<T>) => void; animate: (fn: Animate<T>) => void;
process: (fn: Process<T>) => void; process: (fn: Process<T>) => void;
} { } {
const name = meta.url.match(/entities\/(.+)\/entity\.ts$/)![1];
entityMap.set(name, { entityMap.set(name, {
name, name,
position: createVector(), position: createVector(),
+73
View File
@@ -0,0 +1,73 @@
export type KeyboardInput = {
arrowUp: boolean;
arrowDown: boolean;
arrowLeft: boolean;
arrowRight: boolean;
enter: boolean;
escape: boolean;
};
export const keyboardInput: KeyboardInput = {
arrowUp: false,
arrowDown: false,
arrowLeft: false,
arrowRight: false,
enter: false,
escape: false,
};
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case "ArrowUp":
keyboardInput.arrowUp = true;
break;
case "ArrowDown":
keyboardInput.arrowDown = true;
break;
case "ArrowLeft":
keyboardInput.arrowLeft = true;
break;
case "ArrowRight":
keyboardInput.arrowRight = true;
break;
case "Enter":
keyboardInput.enter = true;
break;
case "Escape":
keyboardInput.escape = true;
break;
}
}
function onKeyUp(event: KeyboardEvent) {
switch (event.key) {
case "ArrowUp":
keyboardInput.arrowUp = false;
break;
case "ArrowDown":
keyboardInput.arrowDown = false;
break;
case "ArrowLeft":
keyboardInput.arrowLeft = false;
break;
case "ArrowRight":
keyboardInput.arrowRight = false;
break;
case "Enter":
keyboardInput.enter = false;
break;
case "Escape":
keyboardInput.escape = false;
break;
}
}
export function listenKeyboard(): () => void {
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
}
+84
View File
@@ -0,0 +1,84 @@
import type { Vector } from "#/lib/Vector.ts";
import { viewport } from "./Viewport.ts";
export type Pointer = {
isDown: boolean;
position: Vector;
};
export const pointer: Pointer = {
isDown: false,
position: { x: 0, y: 0 },
};
function onMouseDown(event: MouseEvent): void {
adjustPointerPosition(pointer, event.clientX, event.clientY);
pointer.isDown = event.button === 0 ? true : pointer.isDown;
}
function onTouchStart(event: TouchEvent): void {
adjustPointerPosition(
pointer,
event.touches[0].clientX,
event.touches[0].clientY,
);
pointer.isDown = true;
}
function onMouseMove(event: MouseEvent): void {
adjustPointerPosition(pointer, event.clientX, event.clientY);
}
function onTouchMove(event: TouchEvent): void {
adjustPointerPosition(
pointer,
event.touches[0].clientX,
event.touches[0].clientY,
);
}
function onMouseUp(event: MouseEvent): void {
pointer.isDown = event.button === 0 ? false : pointer.isDown;
}
function onTouchEnd(event: TouchEvent): void {
pointer.isDown = event.touches.length === 0 ? false : pointer.isDown;
}
function onContextMenu(event: MouseEvent): void {
event.preventDefault();
}
function adjustPointerPosition(
pointer: Pointer,
clientX: number,
clientY: number,
): void {
const { ctx, translation: position, initialScale } = viewport;
const clientRect = ctx.canvas.getBoundingClientRect();
pointer.position.x = (clientX - clientRect.x + position.x) / initialScale;
pointer.position.y = (clientY - clientRect.y + position.y) / initialScale;
}
export function listenPointer(): () => void {
const canvas = viewport.ctx.canvas;
canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("touchstart", onTouchStart);
canvas.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("touchmove", onTouchMove);
canvas.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("touchend", onTouchEnd);
canvas.addEventListener("contextmenu", onContextMenu);
return () => {
canvas.removeEventListener("mousedown", onMouseDown);
canvas.removeEventListener("touchstart", onTouchStart);
canvas.removeEventListener("mousemove", onMouseMove);
canvas.removeEventListener("touchmove", onTouchMove);
canvas.removeEventListener("mouseup", onMouseUp);
canvas.removeEventListener("touchend", onTouchEnd);
canvas.removeEventListener("contextmenu", onContextMenu);
};
}
+4 -4
View File
@@ -1,5 +1,3 @@
import { useWithAsyncCache } from "#/lib/cache.ts";
import { type Graph, getNeighbours } from "#/lib/Graph.ts";
import { import {
type Animate as AnimateEntity, type Animate as AnimateEntity,
type Entity, type Entity,
@@ -8,7 +6,9 @@ import {
entityProcessMap, entityProcessMap,
type Indirect, type Indirect,
type Process as ProcessEntity, type Process as ProcessEntity,
} from "./Entity.ts"; } from "#/engine/Entity.ts";
import { useWithAsyncCache } from "#/lib/cache.ts";
import { type Graph, getNeighbours } from "#/lib/Graph.ts";
export type SceneData = { export type SceneData = {
name: string; name: string;
@@ -121,7 +121,7 @@ function linkScenes(a: string, b: string): void {
} }
} }
export function useScene(data: SceneData): { export function defineScene(data: SceneData): {
linkScenes: <T extends readonly string[]>( linkScenes: <T extends readonly string[]>(
names: T, names: T,
) => (name: T[number]) => Promise<void>; ) => (name: T[number]) => Promise<void>;
+2 -2
View File
@@ -1,10 +1,10 @@
import { createAnimation, playAnimation } from "#/engine/Animation.ts"; import { createAnimation, playAnimation } from "#/engine/Animation.ts";
import { useEntity } from "#/engine/Entity.ts"; import { defineEntity } from "#/engine/Entity.ts";
import { fromSrc, type Sprite } from "#/lib/Sprite.ts"; import { fromSrc, type Sprite } from "#/lib/Sprite.ts";
const idle: Sprite = await fromSrc("/assets/hero/idle.png", 5, 4); const idle: Sprite = await fromSrc("/assets/hero/idle.png", 5, 4);
const { animate, process } = useEntity(import.meta, { const { animate, process } = defineEntity("barEntity", {
animation: createAnimation(idle, 200, 0), animation: createAnimation(idle, 200, 0),
stuff: 1, stuff: 1,
}); });
@@ -1,11 +1,12 @@
import charIdle from "#/assets/char-idle.png"; import charIdle from "#/assets/char-idle.png";
import { createAnimation, playAnimation } from "#/engine/Animation.ts"; import { createAnimation, playAnimation } from "#/engine/Animation.ts";
import { useEntity } from "#/engine/Entity.ts"; import { defineEntity } from "#/engine/Entity.ts";
import { pointer } from "#/engine/Pointer.ts";
import { fromSrc, type Sprite } from "#/lib/Sprite.ts"; import { fromSrc, type Sprite } from "#/lib/Sprite.ts";
const idle: Sprite = await fromSrc(charIdle, 2, 4); const idle: Sprite = await fromSrc(charIdle, 2, 4);
const { animate, process } = useEntity(import.meta, { const { animate, process } = defineEntity("hero", {
animation: createAnimation(idle, 500, 2), animation: createAnimation(idle, 500, 2),
}); });
@@ -19,4 +20,9 @@ animate((entity, ctx, delta) => {
); );
}); });
process((entity, delta) => {}); process((entity, delta) => {
if (pointer.isDown) {
entity.position.x = pointer.position.x;
entity.position.y = pointer.position.y;
}
});
+2 -2
View File
@@ -1,10 +1,10 @@
import { createAnimation, playAnimation } from "#/engine/Animation.ts"; import { createAnimation, playAnimation } from "#/engine/Animation.ts";
import { useEntity } from "#/engine/Entity.ts"; import { defineEntity } from "#/engine/Entity.ts";
import { fromSrc, type Sprite } from "#/lib/Sprite.ts"; import { fromSrc, type Sprite } from "#/lib/Sprite.ts";
const sprite: Sprite = await fromSrc("./assets/portal.png", 3, 1); const sprite: Sprite = await fromSrc("./assets/portal.png", 3, 1);
const { animate } = useEntity(import.meta, { const { animate } = defineEntity("portal", {
animation: createAnimation(sprite, 400, 0), animation: createAnimation(sprite, 400, 0),
}); });
+5 -4
View File
@@ -5,7 +5,7 @@ import {
createAnimation, createAnimation,
playAnimation, playAnimation,
} from "#/engine/Animation.ts"; } from "#/engine/Animation.ts";
import { useEntity } from "#/engine/Entity.ts"; import { defineEntity } from "#/engine/Entity.ts";
import { viewport } from "#/engine/Viewport.ts"; import { viewport } from "#/engine/Viewport.ts";
import { loadImage } from "#/lib/loadImage.ts"; import { loadImage } from "#/lib/loadImage.ts";
import { createSprite } from "#/lib/Sprite.ts"; import { createSprite } from "#/lib/Sprite.ts";
@@ -14,19 +14,20 @@ const sprites = {
idle: createSprite(await loadImage(charIdle), 2, 4), idle: createSprite(await loadImage(charIdle), 2, 4),
walk: createSprite(await loadImage(charWalk), 4, 4), walk: createSprite(await loadImage(charWalk), 4, 4),
}; };
const animations = { const animations = {
idle: createAnimation(sprites.idle, 500, 2), idle: createAnimation(sprites.idle, 500, 2),
walk: createAnimation(sprites.walk, 250, 2), walk: createAnimation(sprites.walk, 250, 2),
} as const; };
const { animate, process } = useEntity<{ const { animate, process } = defineEntity<{
animation: Animation; animation: Animation;
animations: { animations: {
idle: Animation; idle: Animation;
walk: Animation; walk: Animation;
}; };
stuff: number; stuff: number;
}>(import.meta, { }>("testEntity", {
animations, animations,
animation: animations.idle, animation: animations.idle,
stuff: 1, stuff: 1,
+3
View File
@@ -1,3 +1,4 @@
import { listenPointer } from "#/engine/Pointer.ts";
import { currentScene, setScene } from "#/engine/Scene.ts"; import { currentScene, setScene } from "#/engine/Scene.ts";
import { import {
initViewport, initViewport,
@@ -30,5 +31,7 @@ function animate(timestamp: number): void {
resizeCanvas(currentScene.data.width, currentScene.data.height); resizeCanvas(currentScene.data.width, currentScene.data.height);
}); });
listenPointer();
animate(delta); animate(delta);
})(); })();
+1 -1
View File
@@ -7,7 +7,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.11", "@biomejs/biome": "2.3.11",
"@types/bun": "1.3.5", "@types/bun": "1.3.6",
"lz-string": "1.5.0", "lz-string": "1.5.0",
"solid-js": "1.9.10", "solid-js": "1.9.10",
"vite": "7.3.1", "vite": "7.3.1",
+1 -1
View File
@@ -4,7 +4,7 @@
"height": 160, "height": 160,
"entities": [ "entities": [
{ {
"name": "fooEntity", "name": "hero",
"position": { "position": {
"x": 0, "x": 0,
"y": 0 "y": 0
+2 -2
View File
@@ -1,7 +1,7 @@
import { useScene } from "#/engine/Scene.ts"; import { defineScene } from "#/engine/Scene.ts";
import data from "./data.json"; import data from "./data.json";
const { process, linkScenes } = useScene(data); const { process, linkScenes } = defineScene(data);
linkScenes(["testScene"] as const); linkScenes(["testScene"] as const);
+2 -2
View File
@@ -4,14 +4,14 @@
"height": 48, "height": 48,
"entities": [ "entities": [
{ {
"name": "fooEntity", "name": "hero",
"position": { "position": {
"x": 0, "x": 0,
"y": 0 "y": 0
} }
}, },
{ {
"name": "fooEntity", "name": "hero",
"position": { "position": {
"x": 24, "x": 24,
"y": 16 "y": 16
+2 -2
View File
@@ -1,7 +1,7 @@
import { useScene } from "#/engine/Scene.ts"; import { defineScene } from "#/engine/Scene.ts";
import data from "./data.json"; import data from "./data.json";
const { process, linkScenes } = useScene(data); const { process, linkScenes } = defineScene(data);
linkScenes(["barScene"] as const); linkScenes(["barScene"] as const);
+2 -2
View File
@@ -1,7 +1,7 @@
import { useScene } from "#/engine/Scene.ts"; import { defineScene } from "#/engine/Scene.ts";
import data from "./data.json"; import data from "./data.json";
const { process, linkScenes } = useScene(data); const { process, linkScenes } = defineScene(data);
linkScenes(["testScene"] as const); linkScenes(["testScene"] as const);
+1 -1
View File
@@ -4,7 +4,7 @@
"height": 48, "height": 48,
"entities": [ "entities": [
{ {
"name": "fooEntity", "name": "hero",
"position": { "position": {
"x": 0, "x": 0,
"y": 0 "y": 0
+20 -3
View File
@@ -1,7 +1,9 @@
import { useScene } from "#/engine/Scene.ts"; import { pointer } from "#/engine/Pointer.ts";
import { defineScene } from "#/engine/Scene.ts";
import type { Vector } from "#/lib/Vector.ts";
import data from "./data.json"; import data from "./data.json";
const { process, preProcess, linkScenes } = useScene(data); const { process, preProcess, linkScenes } = defineScene(data);
linkScenes(["fooScene", "fuzzScene"] as const); linkScenes(["fooScene", "fuzzScene"] as const);
@@ -13,4 +15,19 @@ preProcess((entityInstanceMap) => {
instance.position.y = data.height - 16; instance.position.y = data.height - 16;
}); });
process((_ctx, _delta) => {}); function drawPoint(
ctx: CanvasRenderingContext2D,
position: Vector,
radius: number = 4,
): void {
ctx.beginPath();
ctx.arc(position.x, position.y, radius, 0, 2 * Math.PI);
ctx.strokeStyle = "#ff9000";
ctx.stroke();
}
process((ctx, delta) => {
if (!pointer.isDown) {
drawPoint(ctx, pointer.position);
}
});