From f9a2409dad1201c03d1de9d2d8ed40e29d857598 Mon Sep 17 00:00:00 2001 From: kolaflash Date: Fri, 23 Jan 2026 14:21:21 +0100 Subject: [PATCH] Add basic controls --- bun.lock | 18 +++--- dev/main.css | 7 +-- engine/Entity.ts | 5 +- engine/Keyboard.ts | 73 ++++++++++++++++++++++ engine/Pointer.ts | 84 ++++++++++++++++++++++++++ engine/Scene.ts | 8 +-- entities/barEntity/entity.ts | 4 +- entities/{fooEntity => hero}/entity.ts | 12 +++- entities/portal/entity.ts | 4 +- entities/testEntity/entity.ts | 9 +-- main.ts | 3 + package.json | 2 +- scenes/barScene/data.json | 2 +- scenes/barScene/scene.ts | 4 +- scenes/fooScene/data.json | 4 +- scenes/fooScene/scene.ts | 4 +- scenes/fuzzScene/scene.ts | 4 +- scenes/testScene/data.json | 2 +- scenes/testScene/scene.ts | 23 ++++++- 19 files changed, 225 insertions(+), 47 deletions(-) create mode 100644 engine/Keyboard.ts create mode 100644 engine/Pointer.ts rename entities/{fooEntity => hero}/entity.ts (58%) diff --git a/bun.lock b/bun.lock index fa2a1a9..0cb75b0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,15 +5,15 @@ "": { "name": "nage", "devDependencies": { - "@biomejs/biome": "2.3.11", - "@types/bun": "1.3.5", - "lz-string": "1.5.0", - "solid-js": "1.9.10", - "vite": "7.3.1", - "vite-plugin-solid": "2.11.10", + "@biomejs/biome": "latest", + "@types/bun": "latest", + "lz-string": "latest", + "solid-js": "latest", + "vite": "latest", + "vite-plugin-solid": "latest", }, "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/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=="], @@ -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=="], - "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=="], diff --git a/dev/main.css b/dev/main.css index 6fec878..de47a5c 100644 --- a/dev/main.css +++ b/dev/main.css @@ -40,11 +40,6 @@ --rgb-black: 30, 31, 34; --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-1: rgb(43, 45, 49); --c-bg-2: rgb(49, 51, 56); @@ -81,7 +76,7 @@ *:focus { outline-width: 2px; outline-style: solid; - outline-color: var(--c-primary); + outline-color: var(--c-blurple); } } diff --git a/engine/Entity.ts b/engine/Entity.ts index a14c16f..9da8b5c 100644 --- a/engine/Entity.ts +++ b/engine/Entity.ts @@ -21,14 +21,13 @@ export const entityMap = new Map>(); export const entityAnimateMap = new Map>(); export const entityProcessMap = new Map>(); -export function useEntity( - meta: { url: string }, +export function defineEntity( + name: string, state: T, ): { animate: (fn: Animate) => void; process: (fn: Process) => void; } { - const name = meta.url.match(/entities\/(.+)\/entity\.ts$/)![1]; entityMap.set(name, { name, position: createVector(), diff --git a/engine/Keyboard.ts b/engine/Keyboard.ts new file mode 100644 index 0000000..3ea86fd --- /dev/null +++ b/engine/Keyboard.ts @@ -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); + }; +} diff --git a/engine/Pointer.ts b/engine/Pointer.ts new file mode 100644 index 0000000..68bd143 --- /dev/null +++ b/engine/Pointer.ts @@ -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); + }; +} diff --git a/engine/Scene.ts b/engine/Scene.ts index a1618b1..cbf0a34 100644 --- a/engine/Scene.ts +++ b/engine/Scene.ts @@ -1,5 +1,3 @@ -import { useWithAsyncCache } from "#/lib/cache.ts"; -import { type Graph, getNeighbours } from "#/lib/Graph.ts"; import { type Animate as AnimateEntity, type Entity, @@ -8,7 +6,9 @@ import { entityProcessMap, type Indirect, 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 = { name: string; @@ -121,7 +121,7 @@ function linkScenes(a: string, b: string): void { } } -export function useScene(data: SceneData): { +export function defineScene(data: SceneData): { linkScenes: ( names: T, ) => (name: T[number]) => Promise; diff --git a/entities/barEntity/entity.ts b/entities/barEntity/entity.ts index 0fdede8..da3e686 100644 --- a/entities/barEntity/entity.ts +++ b/entities/barEntity/entity.ts @@ -1,10 +1,10 @@ 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"; 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), stuff: 1, }); diff --git a/entities/fooEntity/entity.ts b/entities/hero/entity.ts similarity index 58% rename from entities/fooEntity/entity.ts rename to entities/hero/entity.ts index 2c11f9b..c666ba9 100644 --- a/entities/fooEntity/entity.ts +++ b/entities/hero/entity.ts @@ -1,11 +1,12 @@ import charIdle from "#/assets/char-idle.png"; 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"; const idle: Sprite = await fromSrc(charIdle, 2, 4); -const { animate, process } = useEntity(import.meta, { +const { animate, process } = defineEntity("hero", { 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; + } +}); diff --git a/entities/portal/entity.ts b/entities/portal/entity.ts index f862c3b..9fa8487 100644 --- a/entities/portal/entity.ts +++ b/entities/portal/entity.ts @@ -1,10 +1,10 @@ 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"; const sprite: Sprite = await fromSrc("./assets/portal.png", 3, 1); -const { animate } = useEntity(import.meta, { +const { animate } = defineEntity("portal", { animation: createAnimation(sprite, 400, 0), }); diff --git a/entities/testEntity/entity.ts b/entities/testEntity/entity.ts index f5d2a37..d110cfe 100644 --- a/entities/testEntity/entity.ts +++ b/entities/testEntity/entity.ts @@ -5,7 +5,7 @@ import { createAnimation, playAnimation, } from "#/engine/Animation.ts"; -import { useEntity } from "#/engine/Entity.ts"; +import { defineEntity } from "#/engine/Entity.ts"; import { viewport } from "#/engine/Viewport.ts"; import { loadImage } from "#/lib/loadImage.ts"; import { createSprite } from "#/lib/Sprite.ts"; @@ -14,19 +14,20 @@ const sprites = { idle: createSprite(await loadImage(charIdle), 2, 4), walk: createSprite(await loadImage(charWalk), 4, 4), }; + const animations = { idle: createAnimation(sprites.idle, 500, 2), walk: createAnimation(sprites.walk, 250, 2), -} as const; +}; -const { animate, process } = useEntity<{ +const { animate, process } = defineEntity<{ animation: Animation; animations: { idle: Animation; walk: Animation; }; stuff: number; -}>(import.meta, { +}>("testEntity", { animations, animation: animations.idle, stuff: 1, diff --git a/main.ts b/main.ts index 038d497..1b82e53 100644 --- a/main.ts +++ b/main.ts @@ -1,3 +1,4 @@ +import { listenPointer } from "#/engine/Pointer.ts"; import { currentScene, setScene } from "#/engine/Scene.ts"; import { initViewport, @@ -30,5 +31,7 @@ function animate(timestamp: number): void { resizeCanvas(currentScene.data.width, currentScene.data.height); }); + listenPointer(); + animate(delta); })(); diff --git a/package.json b/package.json index 9fe6d9c..cdcfbdf 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.11", - "@types/bun": "1.3.5", + "@types/bun": "1.3.6", "lz-string": "1.5.0", "solid-js": "1.9.10", "vite": "7.3.1", diff --git a/scenes/barScene/data.json b/scenes/barScene/data.json index 5fb79f4..4b9a117 100644 --- a/scenes/barScene/data.json +++ b/scenes/barScene/data.json @@ -4,7 +4,7 @@ "height": 160, "entities": [ { - "name": "fooEntity", + "name": "hero", "position": { "x": 0, "y": 0 diff --git a/scenes/barScene/scene.ts b/scenes/barScene/scene.ts index ea96f4a..bccf16b 100644 --- a/scenes/barScene/scene.ts +++ b/scenes/barScene/scene.ts @@ -1,7 +1,7 @@ -import { useScene } from "#/engine/Scene.ts"; +import { defineScene } from "#/engine/Scene.ts"; import data from "./data.json"; -const { process, linkScenes } = useScene(data); +const { process, linkScenes } = defineScene(data); linkScenes(["testScene"] as const); diff --git a/scenes/fooScene/data.json b/scenes/fooScene/data.json index d2c4ea7..ff66eb0 100644 --- a/scenes/fooScene/data.json +++ b/scenes/fooScene/data.json @@ -4,14 +4,14 @@ "height": 48, "entities": [ { - "name": "fooEntity", + "name": "hero", "position": { "x": 0, "y": 0 } }, { - "name": "fooEntity", + "name": "hero", "position": { "x": 24, "y": 16 diff --git a/scenes/fooScene/scene.ts b/scenes/fooScene/scene.ts index d75f519..6c9a409 100644 --- a/scenes/fooScene/scene.ts +++ b/scenes/fooScene/scene.ts @@ -1,7 +1,7 @@ -import { useScene } from "#/engine/Scene.ts"; +import { defineScene } from "#/engine/Scene.ts"; import data from "./data.json"; -const { process, linkScenes } = useScene(data); +const { process, linkScenes } = defineScene(data); linkScenes(["barScene"] as const); diff --git a/scenes/fuzzScene/scene.ts b/scenes/fuzzScene/scene.ts index ea96f4a..bccf16b 100644 --- a/scenes/fuzzScene/scene.ts +++ b/scenes/fuzzScene/scene.ts @@ -1,7 +1,7 @@ -import { useScene } from "#/engine/Scene.ts"; +import { defineScene } from "#/engine/Scene.ts"; import data from "./data.json"; -const { process, linkScenes } = useScene(data); +const { process, linkScenes } = defineScene(data); linkScenes(["testScene"] as const); diff --git a/scenes/testScene/data.json b/scenes/testScene/data.json index fce5cdf..01757ca 100644 --- a/scenes/testScene/data.json +++ b/scenes/testScene/data.json @@ -4,7 +4,7 @@ "height": 48, "entities": [ { - "name": "fooEntity", + "name": "hero", "position": { "x": 0, "y": 0 diff --git a/scenes/testScene/scene.ts b/scenes/testScene/scene.ts index 7ee34d5..3bc8e45 100644 --- a/scenes/testScene/scene.ts +++ b/scenes/testScene/scene.ts @@ -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"; -const { process, preProcess, linkScenes } = useScene(data); +const { process, preProcess, linkScenes } = defineScene(data); linkScenes(["fooScene", "fuzzScene"] as const); @@ -13,4 +15,19 @@ preProcess((entityInstanceMap) => { 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); + } +});