diff --git a/src/common/display/canvas.ts b/src/common/display/canvas.ts index 3801c5a..cde0442 100644 --- a/src/common/display/canvas.ts +++ b/src/common/display/canvas.ts @@ -1,3 +1,5 @@ +import Input from "@common/input"; + export function loadImageData(dataView: DataView, pointer: number) { const width = dataView.getUint16(pointer + 0, true); const height = dataView.getUint16(pointer + 2, true); @@ -25,6 +27,8 @@ export function createCanvas(width: number, height: number) { document.body.style.alignItems = 'center'; document.body.append(canvas); + Input.setMainCanvas(canvas); + return canvas; } diff --git a/src/common/display/text.ts b/src/common/display/text.ts index 471c23c..0283e6a 100644 --- a/src/common/display/text.ts +++ b/src/common/display/text.ts @@ -1,8 +1,9 @@ import '@common/assets/fonts/vga.font.css'; import { randInt } from "@common/utils"; import { createCanvas } from './canvas'; -import type { Rect } from '@common/geometry'; +import type { Point, Rect } from '@common/geometry'; import { bresenhamCircleGen, bresenhamLineGen, type BresenhamCircleOptions, type BresenhamLineOptions } from '@common/navigation/bresenham'; +import Input from '@common/input'; export type ColorLike = string | number | Color; export type Char = [string, ColorLike?, ColorLike?] | string; @@ -450,6 +451,14 @@ export class TextDisplay { update() { this.redrawDirty(); } + + getMousePosition(): Point { + const pos = Input.getMousePosition(); + return { + x: Math.floor(pos.x / CHAR_W), + y: Math.floor(pos.y / CHAR_H), + }; + } } function expandColors(chars: string[] | Char[][], colors: ColorLike | ColorLike[] | ColorLike[][]): ColorLike[][] { diff --git a/src/common/dom.ts b/src/common/dom.ts index 323d107..de84ee7 100644 --- a/src/common/dom.ts +++ b/src/common/dom.ts @@ -5,15 +5,11 @@ export interface EventWithPoint { export const getRealPoint = (canvas: HTMLCanvasElement, e: EventWithPoint): DOMPoint => { const matrix = new DOMMatrix(); - const scale = Math.min(canvas.clientWidth / canvas.width, canvas.clientHeight / canvas.height); - - const realWidth = canvas.width * scale; - const realHeight = canvas.height * scale; - const offsetLeft = (canvas.clientWidth - realWidth) / 2; - const offsetTop = (canvas.clientHeight - realHeight) / 2; + const box = canvas.getBoundingClientRect(); + const scale = Math.min(box.width / canvas.width, box.height / canvas.height); - matrix.translateSelf(offsetLeft, offsetTop); + matrix.translateSelf(box.left, box.top); matrix.scaleSelf(scale); matrix.invertSelf(); diff --git a/src/common/input.ts b/src/common/input.ts index c1346c0..a6c348e 100644 --- a/src/common/input.ts +++ b/src/common/input.ts @@ -1,3 +1,6 @@ +import { getRealPoint } from "./dom"; +import type { Point } from "./geometry"; + interface IKeyState { state: boolean; prevState?: boolean; @@ -57,6 +60,10 @@ namespace Input { NUM_7 = 'Digit7', NUM_8 = 'Digit8', NUM_9 = 'Digit9', + + MOUSE_LEFT = `Mouse0`, + MOUSE_MIDDLE = `Mouse1`, + MOUSE_RIGHT = `Mouse2`, }; export enum GamepadAxis { @@ -87,11 +94,13 @@ namespace Input { } const DEAD_ZONE = 0.05; - const KEYS: Partial> = {}; + const KEYS: Partial> = {}; + const mousePosition: Point = { x: 0, y: 0 }; + let mainCanvas: HTMLCanvasElement | null = null; let repeatIntervalMs = 10; let repeatDelayMs = 300; - const onStateChange = (keyId: KeyCode, state: boolean) => { + const onStateChange = (keyId: string, state: boolean) => { console.debug(`[Input] ${state ? 'Pressed' : 'Released'} ${keyId}`); const now = Date.now(); @@ -111,18 +120,39 @@ namespace Input { } }; + const onMouseChange = (e: MouseEvent) => { + if (mainCanvas) { + const point = getRealPoint(mainCanvas, e); + mousePosition.x = point.x; + mousePosition.y = point.y; + } else { + mousePosition.x = e.clientX; + mousePosition.y = e.clientY; + } + } + document.body.addEventListener('keydown', (e) => onStateChange(e.code as KeyCode, true)); document.body.addEventListener('keyup', (e) => onStateChange(e.code as KeyCode, false)); + document.body.addEventListener('mousedown', (e) => { + onStateChange(`Mouse${e.button}`, true); + onMouseChange(e); + }); + document.body.addEventListener('mouseup', (e) => { + onStateChange(`Mouse${e.button}`, false); + onMouseChange(e); + }); + document.body.addEventListener('mousemove', (e) => onMouseChange(e)); + document.body.addEventListener('contextmenu', (e) => e.preventDefault()); export const isPressed = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.pressed); export const isReleased = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.released); export const isRepeated = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.repeated); export const isHeld = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.held); - export const hasPressed = () => Object.values(KEYS).some(k => k.pressed); - export const hasReleased = () => Object.values(KEYS).some(k => k.released); - export const hasRepeated = () => Object.values(KEYS).some(k => k.repeated); - export const hasHeld = () => Object.values(KEYS).some(k => k.held); + export const hasPressed = () => Object.values(KEYS).some(k => k?.pressed); + export const hasReleased = () => Object.values(KEYS).some(k => k?.released); + export const hasRepeated = () => Object.values(KEYS).some(k => k?.repeated); + export const hasHeld = () => Object.values(KEYS).some(k => k?.held); export const getGamepad = () => navigator.getGamepads().find(g => g != null); @@ -148,6 +178,8 @@ namespace Input { + getGamepadAxis(GamepadAxis.LY) ); + export const getMousePosition = (): Point => ({ ...mousePosition }); + export const setRepeatInterval = (intervalMs: number) => { repeatIntervalMs = intervalMs; } @@ -156,8 +188,14 @@ namespace Input { repeatDelayMs = delayMs; } + export const setMainCanvas = (canvas: HTMLCanvasElement | null) => { + mainCanvas = canvas; + } + export function updateKeys() { for (const key of Object.values(KEYS)) { + if (!key) continue; + key.released = false; key.pressed = false; diff --git a/src/common/rpg/components/render/viewport.ts b/src/common/rpg/components/render/viewport.ts index bdb5260..5c76440 100644 --- a/src/common/rpg/components/render/viewport.ts +++ b/src/common/rpg/components/render/viewport.ts @@ -1,6 +1,7 @@ import { Component, World } from "@common/rpg/core/world"; import { component } from "@common/rpg/utils/decorators"; import { getPosition, Position } from "../position"; +import type { Point } from "@common/geometry"; export interface ViewportData { screenX: number; @@ -13,7 +14,6 @@ export interface ViewportData { @component export class Viewport extends Component { - get screenX(): number { return Math.round(getPosition(this.entity, 'screen')?.x ?? 0); } @@ -37,6 +37,20 @@ export class Viewport extends Component { get worldY(): number { return Math.round(getPosition(this.entity)?.y ?? 0); } + + screenToWorld = ({ x, y }: Point): Point => { + return { + x: x - this.screenX + this.worldX, + y: y - this.screenY + this.worldY, + }; + } + + worldToScreen = ({ x, y }: Point): Point => { + return { + x: x - this.worldX + this.screenX, + y: y - this.worldY + this.screenY, + }; + } } export const createViewport = (world: World, viewportData: ViewportData) => { @@ -49,16 +63,9 @@ export const createViewport = (world: World, viewportData: ViewportData) => { return viewport; } -export const getViewport = (world: World): ViewportData | null => { +export const getViewport = (world: World): Viewport | null => { for (const [, , viewport] of world.query(Viewport)) { - return { - screenX: viewport.screenX, - screenY: viewport.screenY, - width: viewport.width, - height: viewport.height, - worldX: viewport.worldX, - worldY: viewport.worldY, - } + return viewport; } return null; diff --git a/src/common/rpg/systems/render/text.ts b/src/common/rpg/systems/render/text.ts index d7c0c0c..4b099dc 100644 --- a/src/common/rpg/systems/render/text.ts +++ b/src/common/rpg/systems/render/text.ts @@ -17,16 +17,20 @@ export class TextDisplaySystem extends System { override update(world: World) { const viewport = getViewport(world); - const offset = viewport ? { - x: viewport.worldX - viewport.screenX, - y: viewport.worldY - viewport.screenY, - } : { x: 0, y: 0 }; + const offset = viewport ? viewport.screenToWorld({ x: 0, y: 0 }) : undefined; + const viewportClipRect = viewport ? { + x: viewport.screenX, + y: viewport.screenY, + width: viewport.width, + height: viewport.height, + } : undefined; const sprites = Array.from(world.query(Sprite, Position)) .sort((a, b) => Number(a[2].absolute) - Number(b[2].absolute) || a[2].z - b[2].z); for (const [e, sprite, pos] of sprites) { if (e.has(Hidden)) continue; + const image = sprite.image; const { x, y, absolute } = pos; @@ -37,13 +41,11 @@ export class TextDisplaySystem extends System { const region = data instanceof TextRegion ? data : new TextRegion(data); - if (absolute) { + if (absolute || !offset || !viewportClipRect) { this.display.setRegion(x, y, region); } else { const clipRect = this.display.getClipRect(); - if (viewport) { - this.display.setClipRect({ x: viewport.screenX, y: viewport.screenY, width: viewport.width, height: viewport.height }); - } + this.display.setClipRect(viewportClipRect); this.display.setRegion(x - offset.x, y - offset.y, region); this.display.setClipRect(clipRect); }