Engine refactors 1

This commit is contained in:
Pyrex 2025-02-01 20:30:03 -08:00
parent 786a1d2e8d
commit 501d0e4dff
19 changed files with 515 additions and 363 deletions

View File

@ -1 +1,5 @@
export const BG_OUTER = "#000"; import {Color} from "./engine/datatypes.ts";
export const BG_OUTER = Color.parseHexCode("#200500");
export const FG_TEXT = Color.parseHexCode("#c0c0c0")
export const FG_BOLD = Color.parseHexCode("#ffffff")

View File

@ -1,9 +0,0 @@
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

147
src/engine/datatypes.ts Normal file
View File

@ -0,0 +1,147 @@
export interface IGame {
update(): void;
draw(): void;
}
export class Color {
readonly r: number;
readonly g: number;
readonly b: number;
readonly a: number;
constructor(r: number, g: number, b: number, a?: number) {
this.r = r;
this.g = g;
this.b = b;
this.a = a ?? 255;
}
static parseHexCode(hexCode: string) {
const regex1 = /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/;
const regex2 = /#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/;
let result = regex1.exec(hexCode) ?? regex2.exec(hexCode);
if (result == null) {
throw `could not parse color: ${hexCode}`
}
let parseGroup = (s: string | undefined): number => {
if (s === undefined) {
return 255;
}
if (s.length == 1) {
return 17 * parseInt(s, 16);
}
return parseInt(s, 16);
}
return new Color(
parseGroup(result[1]),
parseGroup(result[2]),
parseGroup(result[3]),
parseGroup(result[4]),
);
}
toStyle(): string {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a / 255.0})`
}
}
export class Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
offset(other: Point | Size): Point {
if (other instanceof Point) {
return new Point(this.x + other.x, this.y + other.y);
}
return new Point(this.x + other.w, this.y + other.h);
}
negate() {
return new Point(-this.x, -this.y);
}
}
export class Size {
readonly w: number;
readonly h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
}
export class Grid<T> {
data: T[][];
constructor({size, cbDefault}: {size: Size, cbDefault: (xy: Point) => T}) {
this.data = [];
for (let y = 0; y < size.h; y++) {
let row = [];
for (let x = 0; x < size.w; x++) {
row.push(cbDefault(new Point(x, y)))
}
this.data.push(row);
}
}
static createGridFromStringArray(ary: Array<string>): Grid<string> {
let w = 0;
let h = ary.length;
for (let i = 0; i < h - 1; i++) {
let w1 = ary[i].length;
let w2 = ary[i + 1].length;
if (w1 != w2) {
throw `createGridFromStringArray: must be grid-shaped, got ${ary}`
}
w = w1;
}
return new Grid({
size: new Size(w, h),
cbDefault: (xy) => {
return ary[xy.y].charAt(xy.x);
}
})
}
static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> {
let w = 0;
let h = ary.length;
for (let i = 0; i < h - 1; i++) {
let w1 = ary[i].length;
let w2 = ary[i + 1].length;
if (w1 != w2) {
throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}`
}
w = w1;
}
return new Grid({
size: new Size(w, h),
cbDefault: (xy) => {
return ary[xy.y][xy.x];
}
})
}
}
export enum AlignX {
Left = 0,
Center = 1,
Right = 2
}
export enum AlignY {
Top = 0,
Middle = 1,
Right = 2,
}

View File

View File

@ -0,0 +1,88 @@
import {getScreen} from "./screen.ts";
import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts";
import {mainFont} from "./font.ts";
import {Sprite} from "./sprite.ts";
class Drawing {
camera: Point;
constructor() {
this.camera = new Point(0, 0);
}
get size() { return getScreen().size; }
invertRect(position: Point, size: Size) {
position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext();
ctx.globalCompositeOperation = "difference";
ctx.fillStyle = "#fff";
ctx.fillRect(
Math.floor(position.x),
Math.floor(position.y),
Math.floor(size.w),
Math.floor(size.h)
)
}
fillRect(position: Point, size: Size, color: Color) {
position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext();
ctx.fillStyle = color.toStyle();
ctx.fillRect(
Math.floor(position.x),
Math.floor(position.y),
Math.floor(size.w),
Math.floor(size.h)
);
}
drawRect(position: Point, size: Size, color: Color) {
position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext();
ctx.strokeStyle = color.toStyle();
ctx.strokeRect(
Math.floor(position.x) + 0.5,
Math.floor(position.y) + 0.5,
Math.floor(size.w),
Math.floor(size.h)
)
}
drawText(text: string, position: Point, color: Color, options?: {alignX?: AlignX, alignY?: AlignY, forceWidth?: number}) {
position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext();
mainFont.internalDrawText({
ctx,
text,
position,
alignX: options?.alignX,
alignY: options?.alignY,
forceWidth: options?.forceWidth,
color
})
}
measureText(text: string, forceWidth?: number): Size {
return mainFont.measureText({text, forceWidth})
}
drawSprite(sprite: Sprite, position: Point, ix?: number, options?: {xScale?: number, yScale: number, angle: number}) {
position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext();
sprite.internalDraw(ctx, {position, ix, xScale: options?.xScale, yScale: options?.yScale, angle: options?.angle})
}
}
let active: Drawing = new Drawing();
export function getDrawing(): Drawing {
return active;
}

View File

@ -1,37 +1,27 @@
import {getAssets} from "./assets.ts"; import {getAssets} from "./assets.ts";
import fontSheet from './art/fonts/vga_8x16.png'; import fontSheet from '../../art/fonts/vga_8x16.png';
import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts";
export enum AlignX {
Left = 0,
Center = 1,
Right = 2
}
export enum AlignY {
Top = 0,
Middle = 1,
Right = 2,
}
class Font { class Font {
#filename: string; #filename: string;
#cx: number; #cellsPerSheet: Size;
#cy: number; #pixelsPerCell: Size;
#px: number;
#py: number;
#tintingCanvas: HTMLCanvasElement; #tintingCanvas: HTMLCanvasElement;
#tintedVersions: Record<string, HTMLImageElement>; #tintedVersions: Record<string, HTMLImageElement>;
constructor(filename: string, cx: number, cy: number, px: number, py: number) { constructor(filename: string, cellsPerSheet: Size, pixelsPerCell: Size) {
this.#filename = filename; this.#filename = filename;
this.#cx = cx; this.#cellsPerSheet = cellsPerSheet;
this.#cy = cy; this.#pixelsPerCell = pixelsPerCell;
this.#px = px;
this.#py = py;
this.#tintingCanvas = document.createElement("canvas"); this.#tintingCanvas = document.createElement("canvas");
this.#tintedVersions = {} this.#tintedVersions = {}
} }
get #cx(): number { return this.#cellsPerSheet.w }
get #cy(): number { return this.#cellsPerSheet.h }
get #px(): number { return this.#pixelsPerCell.w }
get #py(): number { return this.#pixelsPerCell.h }
#getTintedImage(color: string): HTMLImageElement | null { #getTintedImage(color: string): HTMLImageElement | null {
let image = getAssets().getImage(this.#filename); let image = getAssets().getImage(this.#filename);
@ -65,25 +55,24 @@ class Font {
return result; return result;
} }
drawText({ctx, text, x, y, alignX, alignY, forceWidth, color}: { internalDrawText({ctx, text, position, alignX, alignY, forceWidth, color}: {
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
text: string, text: string,
x: number, y: number, alignX?: AlignX, alignY?: AlignY, position: Point, alignX?: AlignX, alignY?: AlignY,
forceWidth?: number, color?: string forceWidth?: number, color: Color
}) { }) {
alignX = alignX == undefined ? AlignX.Left : alignX; alignX = alignX == undefined ? AlignX.Left : alignX;
alignY = alignY == undefined ? AlignY.Top : alignY; alignY = alignY == undefined ? AlignY.Top : alignY;
forceWidth = forceWidth == undefined ? 65535 : forceWidth; forceWidth = forceWidth == undefined ? 65535 : forceWidth;
color = color == undefined ? "#ffffff" : color;
let image = this.#getTintedImage(color) let image = this.#getTintedImage(color.toStyle())
if (image == null) { if (image == null) {
return; return;
} }
let sz = this.#glyphwise(text, forceWidth, () => {}); let sz = this.#glyphwise(text, forceWidth, () => {});
let offsetX = x; let offsetX = position.x;
let offsetY = y; let offsetY = position.y;
offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w) offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w)
offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h) offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h)
@ -105,7 +94,7 @@ class Font {
); );
} }
measureText({text, forceWidth}: {text: string, forceWidth?: number}): {w: number, h: number} { measureText({text, forceWidth}: {text: string, forceWidth?: number}): Size {
return this.#glyphwise(text, forceWidth, () => {}); return this.#glyphwise(text, forceWidth, () => {});
} }
@ -141,4 +130,4 @@ class Font {
} }
} }
export let mainFont = new Font(fontSheet, 32, 8, 8, 16); export let mainFont = new Font(fontSheet, new Size(32, 8), new Size(8, 16));

View File

@ -0,0 +1,34 @@
import './style.css'
import {pollAndTouch} from "./screen.ts";
import {getClock} from "./clock.ts";
import {getInput, setupInput} from "./input.ts";
import {IGame} from "../datatypes.ts";
export function hostGame(game: IGame) {
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
setupInput(gameCanvas);
onFrame(game, undefined); // start on-frame draw loop, set up screen
}
function onFrame(game: IGame, timestamp: number | undefined) {
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
requestAnimationFrame((timestamp: number) => onFrame(game, timestamp));
if (timestamp) {
getClock().recordTimestamp(timestamp);
}
onFrameFixScreen(gameCanvas);
while (getClock().popUpdate()) {
game.update();
getInput().update();
}
game.draw();
}
function onFrameFixScreen(canvas: HTMLCanvasElement) {
pollAndTouch(canvas);
}

View File

@ -0,0 +1,120 @@
import {getScreen} from "./screen.ts";
import {Point} from "../datatypes.ts";
function handleKey(e: KeyboardEvent, down: boolean) {
active.handleKeyDown(e.key, down);
}
function handleMouseOut() {
active.handleMouseMove(-1, -1);
}
function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) {
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
return;
}
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
}
function handleMouseButton(canvas: HTMLCanvasElement, m: MouseEvent, down: boolean) {
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
return;
}
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
let button: MouseButton | null = (
m.button == 0 ? "leftMouse" :
m.button == 1 ? "rightMouse" :
null
)
if (button != null) {
active.handleMouseDown(button, down);
}
}
export function setupInput(canvas: HTMLCanvasElement) {
canvas.addEventListener("keyup", (k) => handleKey(k, false));
document.addEventListener("keyup", (k) => handleKey(k, false));
canvas.addEventListener("keydown", (k) => handleKey(k, true));
document.addEventListener("keydown", (k) => handleKey(k, true));
canvas.addEventListener("mouseout", (_) => handleMouseOut());
canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m));
canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true));
canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false));
}
export type MouseButton = "leftMouse" | "rightMouse";
class Input {
#keyDown: Record<string, boolean>;
#previousKeyDown: Record<string, boolean>;
#mouseDown: Record<string, boolean>;
#previousMouseDown: Record<string, boolean>;
#mousePosition: Point | null;
constructor() {
this.#keyDown = {};
this.#previousKeyDown = {};
this.#mouseDown = {};
this.#previousMouseDown = {};
this.#mousePosition = null;
}
update() {
this.#previousKeyDown = {...this.#keyDown};
this.#previousMouseDown = {...this.#mouseDown};
}
handleMouseDown(name: string, down: boolean) {
this.#mouseDown[name] = down;
}
handleKeyDown(name: string, down: boolean) {
this.#keyDown[name] = down;
}
handleMouseMove(x: number, y: number) {
let screen = getScreen();
if (x < 0.0 || x >= 1.0) { this.#mousePosition = null; }
if (y < 0.0 || y >= 1.0) { this.#mousePosition = null; }
let w = screen.size.w;
let h = screen.size.h;
this.#mousePosition = new Point(
Math.floor(x * w),
Math.floor(y * h),
)
}
isMouseDown(btn: MouseButton) : boolean {
return this.#mouseDown[btn];
}
isMouseClicked(btn: MouseButton) : boolean {
return this.#mouseDown[btn] && !this.#previousMouseDown[btn];
}
isMouseReleased(btn: MouseButton) : boolean {
return !this.#mouseDown[btn] && this.#previousMouseDown[btn];
}
get mousePosition(): Point | null {
return this.#mousePosition
}
isKeyDown(key: string) : boolean {
return this.#keyDown[key];
}
isKeyPressed(key: string) : boolean {
return this.#keyDown[key] && !this.#previousKeyDown[key];
}
isKeyReleased(key: string) : boolean {
return !this.#keyDown[key] && this.#previousKeyDown[key];
}
}
let active = new Input();
export function getInput(): Input {
return active;
}

View File

@ -1,15 +1,17 @@
import {Size} from "../datatypes.ts";
// TODO: Just switch to the same pattern as everywhere else
// (without repeatedly reassigning the variable)
class Screen { class Screen {
#canvas: HTMLCanvasElement #canvas: HTMLCanvasElement
w: number size: Size
h: number
constructor(canvas: HTMLCanvasElement, w: number, h: number) { constructor(canvas: HTMLCanvasElement, size: Size) {
this.#canvas = canvas; this.#canvas = canvas;
this.w = w; this.size = size
this.h = h;
} }
makeContext(): CanvasRenderingContext2D { unsafeMakeContext(): CanvasRenderingContext2D {
let ctx = this.#canvas.getContext("2d")!; let ctx = this.#canvas.getContext("2d")!;
// TODO: Other stuff to do here? // TODO: Other stuff to do here?
@ -51,7 +53,7 @@ export function pollAndTouch(canvas: HTMLCanvasElement) {
realHeight = Math.floor(canvas.offsetHeight / divisors[div]); realHeight = Math.floor(canvas.offsetHeight / divisors[div]);
canvas.width = realWidth; canvas.width = realWidth;
canvas.height = realHeight; canvas.height = realHeight;
active = new Screen(canvas, realWidth, realHeight); active = new Screen(canvas, new Size(realWidth, realHeight));
} }
export function getScreen(): Screen { export function getScreen(): Screen {

View File

@ -0,0 +1,47 @@
import {getAssets} from "./assets.ts";
import {Point, Size} from "../datatypes.ts";
export class Sprite {
readonly imageSet: string;
// spritesheet params
readonly pixelsPerSubimage: Size;
readonly origin: Point;
readonly cellsPerSheet: Size;
// number of frames
readonly nFrames: number;
constructor(imageSet: string, pixelsPerSubimage: Size, origin: Point, cellsPerSheet: Size, nFrames: number) {
this.imageSet = imageSet;
this.pixelsPerSubimage = pixelsPerSubimage;
this.origin = origin;
this.cellsPerSheet = cellsPerSheet;
this.nFrames = nFrames;
let nPossibleFrames = this.cellsPerSheet.w * this.cellsPerSheet.h;
if (this.nFrames > nPossibleFrames) {
throw `can't have ${this.nFrames} with a spritesheet dimension of ${nPossibleFrames}`;
}
}
internalDraw(ctx: CanvasRenderingContext2D, {position, ix, xScale, yScale, angle}: {position: Point, ix?: number, xScale?: number, yScale?: number, angle?: number}) {
ix = ix == undefined ? 0 : ix;
xScale = xScale == undefined ? 1.0 : xScale;
yScale = yScale == undefined ? 1.0 : yScale;
angle = angle == undefined ? 0.0 : angle;
// ctx.translate(Math.floor(x), Math.floor(y));
ctx.translate(position.x, position.y);
ctx.rotate(angle * Math.PI / 180);
ctx.scale(xScale, yScale);
ctx.translate(-this.origin.x, -this.origin.y);
let me = getAssets().getImage(this.imageSet);
let srcCx = ix % this.cellsPerSheet.w;
let srcCy = Math.floor(ix / this.cellsPerSheet.w);
let srcPx = srcCx * this.pixelsPerSubimage.w;
let srcPy = srcCy * this.pixelsPerSubimage.h;
console.log(`src px and py ${srcPx} ${srcPy}`)
ctx.drawImage(me, srcPx, srcPy, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h, 0, 0, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h);
}
}

8
src/engine/public.ts Normal file
View File

@ -0,0 +1,8 @@
import {getInput} from "./internal/input.ts";
import {getDrawing} from "./internal/drawing.ts";
// input reexports
export let I = getInput();
// drawing reexports
export let D = getDrawing();

View File

@ -1,9 +1,8 @@
import {desiredHeight, desiredWidth, getScreen} from "./screen.ts"; import {desiredHeight, desiredWidth, getScreen} from "./engine/internal/screen.ts";
import {BG_OUTER} from "./colors.ts"; import {BG_OUTER, FG_TEXT} from "./colors.ts";
import {getInput} from "./input.ts";
import {checkGrid, ConceptualCell, maps, mapSzX, mapSzY} from "./maps.ts"; import {checkGrid, ConceptualCell, maps, mapSzX, mapSzY} from "./maps.ts";
import {D, I} from "./engine/public.ts";
type Point = {x: number, y: number} import {IGame, Point, Size} from "./engine/datatypes.ts";
class MenuCamera { class MenuCamera {
// measured in whole screens // measured in whole screens
@ -20,33 +19,35 @@ class MenuCamera {
if (Math.abs(x1 - x0) < 0.01) { return x1; } if (Math.abs(x1 - x0) < 0.01) { return x1; }
return (x0 * 8 + x1 * 2) / 10; return (x0 * 8 + x1 * 2) / 10;
} }
this.position.x = adjust(this.position.x, this.target.x); this.position = new Point(
this.position.y = adjust(this.position.y, this.target.y); adjust(this.position.x, this.target.x),
adjust(this.position.y, this.target.y),
);
} }
} }
type GameState = "Gameplay" | "Thralls"; type GameState = "Gameplay" | "Thralls";
function getScreenLocation(state: GameState): {x: number, y: number} { function getScreenLocation(state: GameState): Point {
if (state === "Gameplay") { if (state === "Gameplay") {
return {x: 0.0, y: 0.0} return new Point(0, 0);
} }
if (state === "Thralls") { if (state === "Thralls") {
return {x: 0.0, y: 1.0} return new Point(0, 1);
} }
throw `invalid state: ${state}` throw `invalid state: ${state}`
} }
export class Game { export class Game implements IGame {
camera: MenuCamera; camera: MenuCamera;
state: GameState; state: GameState;
huntMode: HuntMode; huntMode: HuntMode;
constructor() { constructor() {
this.camera = new MenuCamera({ this.camera = new MenuCamera({
position: {x: 0.0, y: 0.0}, position: new Point(0, 0),
target: {x: 0.0, y: 0.0} target: new Point(0, 0),
}); });
this.state = "Gameplay"; this.state = "Gameplay";
@ -54,14 +55,18 @@ export class Game {
} }
update() { update() {
if (getInput().isPressed("w")) { if (I.isKeyPressed("w")) {
this.state = "Gameplay" this.state = "Gameplay"
} }
if (getInput().isPressed("s")) { if (I.isKeyPressed("s")) {
this.state = "Thralls" this.state = "Thralls"
} }
this.camera.target = getScreenLocation(this.state); this.camera.target = getScreenLocation(this.state);
D.camera = new Point(
D.size.w * this.camera.position.x,
D.size.h * this.camera.position.y,
)
this.camera.update(); this.camera.update();
// state-specific updates // state-specific updates
@ -69,65 +74,25 @@ export class Game {
} }
draw() { draw() {
let screen = getScreen();
let ctx = screen.makeContext();
// draw screen background // draw screen background
ctx.fillStyle = BG_OUTER; let oldCamera = D.camera;
ctx.fillRect(0, 0, screen.w, screen.h); D.camera = new Point(0, 0);
D.fillRect(new Point(0, 0), D.size, BG_OUTER);
D.camera = oldCamera;
this.drawGameplay(); this.drawGameplay();
// we draw all states at once and pan between them // we draw all states at once and pan between them
// mainFont.drawText({ctx: ctx, text: "You have been given a gift.", x: 0, y: 0}) // mainFont.drawText({ctx: ctx, text: "You have been given a gift.", x: 0, y: 0})
let xy = this.getCameraMouseXy(); let mouse = I.mousePosition?.offset(D.camera);
if (xy != null) { if (mouse != null) {
ctx = this.makeCameraContext(); D.invertRect(mouse.offset(new Point(-1, -1)), new Size(3, 3))
ctx.globalCompositeOperation = "difference";
if (getInput().isDown("leftMouse")) {
ctx.fillStyle = "#ff0";
} else {
ctx.fillStyle = "#fff";
} }
ctx.fillRect(xy.x - 1, xy.y - 1, 3, 3);
ctx.fillRect(xy.x, xy.y - 1, 1, 3);
ctx.fillRect(xy.x - 1, xy.y, 3, 1);
}
}
getCameraOffset(): Point {
let screen = getScreen();
let {w, h} = screen;
return {
x: Math.floor(w * this.camera.position.x),
y: Math.floor(h * this.camera.position.y)
};
}
getCameraMouseXy(): Point | null {
let xy = getInput().mouseXy();
if (xy == null) {
return null;
}
let {x: dx, y: dy} = this.getCameraOffset();
return {
x: xy.x + dx,
y: xy.y + dy,
};
}
makeCameraContext() {
let screen = getScreen();
let ctx = screen.makeContext();
let {x, y} = this.getCameraOffset();
ctx.translate(-x, -y);
return ctx;
} }
getPaneRegionForGameState(gameState: GameState) { getPaneRegionForGameState(gameState: GameState) {
let screen = getScreen(); let screen = getScreen();
let {w, h} = screen; let {w, h} = screen.size;
let overallScreenLocation = getScreenLocation(gameState); let overallScreenLocation = getScreenLocation(gameState);
let bigPaneX = overallScreenLocation.x * w; let bigPaneX = overallScreenLocation.x * w;
@ -142,12 +107,12 @@ export class Game {
return { return {
big: { big: {
position: {x: bigPaneX, y: bigPaneY}, position: new Point(bigPaneX, bigPaneY),
size: {x: bigPaneW, y: bigPaneH} size: new Size(bigPaneW, bigPaneH),
}, },
small: { small: {
position: {x: smallPaneX, y: smallPaneY}, position: new Point(smallPaneX, smallPaneY),
size: {x: smallPaneW, y: smallPaneH} size: new Size(smallPaneW, smallPaneH),
} }
} }
@ -159,10 +124,9 @@ export class Game {
drawGameplay() { drawGameplay() {
let region = this.getPaneRegionForGameState("Gameplay") let region = this.getPaneRegionForGameState("Gameplay")
let ctx = this.makeCameraContext() // TODO: Draw
ctx.translate(region.small.position.x, region.small.position.y)
D.drawText("hello", region.small.position, FG_TEXT);
} }
} }
@ -243,9 +207,6 @@ class HuntMode {
gsp, gsp, gsp, gsp gsp, gsp, gsp, gsp
])(); ])();
} }
get(at: Point) {
}
} }
function choose<T>(array: Array<T>): T { function choose<T>(array: Array<T>): T {

View File

@ -1,108 +0,0 @@
import {getScreen} from "./screen.ts";
function handleKey(e: KeyboardEvent, down: boolean) {
active.handleDown(e.key, down);
}
function handleMouseOut() {
active.handleMouseMove(-1, -1);
}
function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) {
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
return;
}
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
}
function handleMouseButton(canvas: HTMLCanvasElement, m: MouseEvent, down: boolean) {
if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
return;
}
active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
let button: KnownButton | null = (
m.button == 0 ? "leftMouse" :
m.button == 1 ? "rightMouse" :
null
)
if (button != null) {
active.handleDown(button, down);
}
}
export function setupInput(canvas: HTMLCanvasElement) {
canvas.addEventListener("keyup", (k) => handleKey(k, false));
document.addEventListener("keyup", (k) => handleKey(k, false));
canvas.addEventListener("keydown", (k) => handleKey(k, true));
document.addEventListener("keydown", (k) => handleKey(k, true));
canvas.addEventListener("mouseout", (_) => handleMouseOut());
canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m));
canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true));
canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false));
}
type KnownKey = "w" | "a" | "s" | "d";
type KnownButton = "leftMouse" | "rightMouse";
class Input {
#down: Record<string, boolean>;
#previousDown: Record<string, boolean>;
#mouseXy: {x: number, y: number} | null;
constructor() {
this.#down = {};
this.#previousDown = {};
this.#mouseXy = null;
}
update() {
this.#previousDown = {...this.#down};
}
handleDown(name: string, down: boolean) {
if (down) {
this.#down[name] = down;
}
else {
delete this.#down[name];
}
}
handleMouseMove(x: number, y: number) {
let screen = getScreen();
if (x < 0.0 || x >= 1.0) { this.#mouseXy = null; }
if (y < 0.0 || y >= 1.0) { this.#mouseXy = null; }
let w = screen.w;
let h = screen.h;
this.#mouseXy = {
x: Math.floor(x * w),
y: Math.floor(y * h),
}
}
isDown(key: KnownKey | KnownButton) : boolean {
return this.#down[key];
}
isPressed(key: KnownKey | KnownButton) : boolean {
return this.#down[key] && !this.#previousDown[key];
}
isReleased(key: KnownKey | KnownButton) : boolean {
return !this.#down[key] && this.#previousDown[key];
}
mouseXy(): {x: number, y: number} | null {
if (this.#mouseXy == null) {
return null;
}
return {...this.#mouseXy}
}
}
let active = new Input();
export function getInput(): Input {
return active;
}

View File

@ -1,87 +1,4 @@
import './style.css' import {hostGame} from "./engine/internal/host.ts";
import {pollAndTouch} from "./screen.ts";
import {getClock} from "./clock.ts";
import {game} from "./game.ts"; import {game} from "./game.ts";
import {getInput, setupInput} from "./input.ts";
// import typescriptLogo from './typescript.svg'
// import viteLogo from '/vite.svg'
// import { setupCounter } from './counter.ts'
// import {AlignX, mainFont} from "./font.ts"; hostGame(game);
function setupGame() {
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
setupInput(gameCanvas);
onFrame(undefined); // start on-frame draw loop, set up screen
}
function onFrame(timestamp: number | undefined) {
let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
requestAnimationFrame(onFrame);
if (timestamp) {
getClock().recordTimestamp(timestamp);
}
onFrameFixScreen(gameCanvas);
while (getClock().popUpdate()) {
game.update();
getInput().update();
}
game.draw();
/*
let ctx = getScreen().canvas.getContext("2d")!;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
// ctx.drawImage(getAssets().getImage(font), 0, frame % (getScreen().h + 256) - 128);
// console.log(mainFont.measureText({text: "a!\nb\n"}));
mainFont.drawText({
ctx: ctx,
text: "Hello, world!\nI wish you luck!",
x: gameCanvas.width,
y: 0,
color: "#f00",
alignX: AlignX.Right,
});
mainFont.drawText({
ctx: ctx,
text: "^._.^",
x: gameCanvas.width/2,
y: 32,
color: "#0ff",
alignX: AlignX.Center,
});
*/
}
function onFrameFixScreen(canvas: HTMLCanvasElement) {
pollAndTouch(canvas);
}
setupGame();
/*
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
*/

View File

@ -1,48 +0,0 @@
import {getAssets} from "./assets.ts";
export class Sprite {
readonly imageSet: string;
// spritesheet params
// image size (px, py)
readonly px: number; readonly py: number;
// origin (ox, oy)
readonly ox: number; readonly oy: number;
// dimension in cells (cx, cy)
readonly cx: number; readonly cy: number;
// number of frames
readonly nFrames: number;
constructor(imageSet: string, px: number, py: number, ox: number, oy: number, cx: number, cy: number, nFrames: number) {
this.imageSet = imageSet;
this.px = px; this.py = py;
this.ox = ox; this.oy = oy;
this.cx = cx; this.cy = cy;
this.nFrames = nFrames;
if (this.nFrames < this.cx * this.cy) {
throw `can't have ${this.nFrames} with a spritesheet dimension of ${this.cx * this.cy}`;
}
}
draw(ctx: CanvasRenderingContext2D, {x, y, ix, xScale, yScale, angle}: {x: number, y: number, ix?: number, xScale?: number, yScale?: number, angle?: number}) {
ix = ix == undefined ? 0 : ix;
xScale = xScale == undefined ? 1.0 : xScale;
yScale = yScale == undefined ? 1.0 : yScale;
angle = angle == undefined ? 0.0 : angle;
// ctx.translate(Math.floor(x), Math.floor(y));
ctx.translate(x, y);
ctx.rotate(angle * Math.PI / 180);
ctx.scale(xScale, yScale);
ctx.translate(-this.ox, -this.oy);
let me = getAssets().getImage(this.imageSet);
let srcCx = ix % this.cx;
let srcCy = Math.floor(ix / this.cx);
let srcPx = srcCx * this.px;
let srcPy = srcCy * this.py;
console.log(`src px and py ${srcPx} ${srcPy}`)
ctx.drawImage(me, srcPx, srcPy, this.px, this.py, 0, 0, this.px, this.py);
}
}

View File

@ -1,4 +1,4 @@
import {Sprite} from "./sprite.ts"; import {Sprite} from "./engine/internal/sprite.ts";
import imgBat from "./art/characters/bat.png"; import imgBat from "./art/characters/bat.png";
import imgKobold from "./art/characters/kobold.png"; import imgKobold from "./art/characters/kobold.png";
import imgRaccoon from "./art/characters/raccoon.png"; import imgRaccoon from "./art/characters/raccoon.png";