Engine refactors 1
This commit is contained in:
		| @@ -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") | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										147
									
								
								src/engine/datatypes.ts
									
									
									
									
									
										Normal 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, | ||||
| } | ||||
							
								
								
									
										0
									
								
								src/engine/internal/abstract.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/engine/internal/abstract.ts
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										88
									
								
								src/engine/internal/drawing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/engine/internal/drawing.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -1,37 +1,27 @@ | ||||
| import {getAssets} from "./assets.ts"; | ||||
| import fontSheet from './art/fonts/vga_8x16.png'; | ||||
| 
 | ||||
| export enum AlignX { | ||||
|   Left = 0, | ||||
|   Center = 1, | ||||
|   Right = 2 | ||||
| } | ||||
| 
 | ||||
| export enum AlignY { | ||||
|   Top = 0, | ||||
|   Middle = 1, | ||||
|   Right = 2, | ||||
| } | ||||
| import fontSheet from '../../art/fonts/vga_8x16.png'; | ||||
| import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts"; | ||||
| 
 | ||||
| class Font { | ||||
|   #filename: string; | ||||
|   #cx: number; | ||||
|   #cy: number; | ||||
|   #px: number; | ||||
|   #py: number; | ||||
|   #cellsPerSheet: Size; | ||||
|   #pixelsPerCell: Size; | ||||
|   #tintingCanvas: HTMLCanvasElement; | ||||
|   #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.#cx = cx; | ||||
|     this.#cy = cy; | ||||
|     this.#px = px; | ||||
|     this.#py = py; | ||||
|     this.#cellsPerSheet = cellsPerSheet; | ||||
|     this.#pixelsPerCell = pixelsPerCell; | ||||
|     this.#tintingCanvas = document.createElement("canvas"); | ||||
|     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 { | ||||
|     let image = getAssets().getImage(this.#filename); | ||||
| 
 | ||||
| @@ -65,25 +55,24 @@ class Font { | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   drawText({ctx, text, x, y, alignX, alignY, forceWidth, color}: { | ||||
|   internalDrawText({ctx, text, position, alignX, alignY, forceWidth, color}: { | ||||
|     ctx: CanvasRenderingContext2D, | ||||
|     text: string, | ||||
|     x: number, y: number, alignX?: AlignX, alignY?: AlignY, | ||||
|     forceWidth?: number, color?: string | ||||
|     position: Point, alignX?: AlignX, alignY?: AlignY, | ||||
|     forceWidth?: number, color: Color | ||||
|   }) { | ||||
|     alignX = alignX == undefined ? AlignX.Left : alignX; | ||||
|     alignY = alignY == undefined ? AlignY.Top : alignY; | ||||
|     forceWidth = forceWidth == undefined ? 65535 : forceWidth; | ||||
|     color = color == undefined ? "#ffffff" : color; | ||||
| 
 | ||||
|     let image = this.#getTintedImage(color) | ||||
|     let image = this.#getTintedImage(color.toStyle()) | ||||
|     if (image == null) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let sz = this.#glyphwise(text, forceWidth, () => {}); | ||||
|     let offsetX = x; | ||||
|     let offsetY = y; | ||||
|     let offsetX = position.x; | ||||
|     let offsetY = position.y; | ||||
|     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) | ||||
| 
 | ||||
| @@ -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, () => {}); | ||||
|   } | ||||
| 
 | ||||
| @@ -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)); | ||||
							
								
								
									
										34
									
								
								src/engine/internal/host.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/engine/internal/host.ts
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
							
								
								
									
										120
									
								
								src/engine/internal/input.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/engine/internal/input.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -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 { | ||||
|   #canvas: HTMLCanvasElement | ||||
|   w: number | ||||
|   h: number | ||||
|   size: Size | ||||
| 
 | ||||
|   constructor(canvas: HTMLCanvasElement, w: number, h: number) { | ||||
|   constructor(canvas: HTMLCanvasElement, size: Size) { | ||||
|     this.#canvas = canvas; | ||||
|     this.w = w; | ||||
|     this.h = h; | ||||
|     this.size = size | ||||
|   } | ||||
| 
 | ||||
|   makeContext(): CanvasRenderingContext2D { | ||||
|   unsafeMakeContext(): CanvasRenderingContext2D { | ||||
|     let ctx = this.#canvas.getContext("2d")!; | ||||
| 
 | ||||
|     // TODO: Other stuff to do here?
 | ||||
| @@ -51,7 +53,7 @@ export function pollAndTouch(canvas: HTMLCanvasElement) { | ||||
|   realHeight = Math.floor(canvas.offsetHeight / divisors[div]); | ||||
|   canvas.width = realWidth; | ||||
|   canvas.height = realHeight; | ||||
|   active = new Screen(canvas, realWidth, realHeight); | ||||
|   active = new Screen(canvas, new Size(realWidth, realHeight)); | ||||
| } | ||||
| 
 | ||||
| export function getScreen(): Screen { | ||||
							
								
								
									
										47
									
								
								src/engine/internal/sprite.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/engine/internal/sprite.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										8
									
								
								src/engine/public.ts
									
									
									
									
									
										Normal 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(); | ||||
							
								
								
									
										107
									
								
								src/game.ts
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								src/game.ts
									
									
									
									
									
								
							| @@ -1,9 +1,8 @@ | ||||
| import {desiredHeight, desiredWidth, getScreen} from "./screen.ts"; | ||||
| import {BG_OUTER} from "./colors.ts"; | ||||
| import {getInput} from "./input.ts"; | ||||
| import {desiredHeight, desiredWidth, getScreen} from "./engine/internal/screen.ts"; | ||||
| import {BG_OUTER, FG_TEXT} from "./colors.ts"; | ||||
| import {checkGrid, ConceptualCell, maps, mapSzX, mapSzY} from "./maps.ts"; | ||||
|  | ||||
| type Point = {x: number, y: number} | ||||
| import {D, I} from "./engine/public.ts"; | ||||
| import {IGame, Point, Size} from "./engine/datatypes.ts"; | ||||
|  | ||||
| class MenuCamera { | ||||
|   // measured in whole screens | ||||
| @@ -20,33 +19,35 @@ class MenuCamera { | ||||
|       if (Math.abs(x1 - x0) < 0.01) { return x1; } | ||||
|       return (x0 * 8 + x1 * 2) / 10; | ||||
|     } | ||||
|     this.position.x = adjust(this.position.x, this.target.x); | ||||
|     this.position.y = adjust(this.position.y, this.target.y); | ||||
|     this.position = new Point( | ||||
|       adjust(this.position.x, this.target.x), | ||||
|       adjust(this.position.y, this.target.y), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| type GameState = "Gameplay" | "Thralls"; | ||||
|  | ||||
| function getScreenLocation(state: GameState): {x: number, y: number} { | ||||
| function getScreenLocation(state: GameState): Point { | ||||
|   if (state === "Gameplay") { | ||||
|     return {x: 0.0, y: 0.0} | ||||
|     return new Point(0, 0); | ||||
|   } | ||||
|   if (state === "Thralls") { | ||||
|     return {x: 0.0, y: 1.0} | ||||
|     return new Point(0, 1); | ||||
|   } | ||||
|  | ||||
|   throw `invalid state: ${state}` | ||||
| } | ||||
|  | ||||
| export class Game { | ||||
| export class Game implements IGame { | ||||
|   camera: MenuCamera; | ||||
|   state: GameState; | ||||
|   huntMode: HuntMode; | ||||
|  | ||||
|   constructor() { | ||||
|     this.camera = new MenuCamera({ | ||||
|       position: {x: 0.0, y: 0.0}, | ||||
|       target: {x: 0.0, y: 0.0} | ||||
|       position: new Point(0, 0), | ||||
|       target: new Point(0, 0), | ||||
|     }); | ||||
|     this.state = "Gameplay"; | ||||
|  | ||||
| @@ -54,14 +55,18 @@ export class Game { | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     if (getInput().isPressed("w")) { | ||||
|     if (I.isKeyPressed("w")) { | ||||
|       this.state = "Gameplay" | ||||
|     } | ||||
|     if (getInput().isPressed("s")) { | ||||
|     if (I.isKeyPressed("s")) { | ||||
|       this.state = "Thralls" | ||||
|     } | ||||
|  | ||||
|     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(); | ||||
|  | ||||
|     // state-specific updates | ||||
| @@ -69,65 +74,25 @@ export class Game { | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     let screen = getScreen(); | ||||
|     let ctx = screen.makeContext(); | ||||
|  | ||||
|     // draw screen background | ||||
|     ctx.fillStyle = BG_OUTER; | ||||
|     ctx.fillRect(0, 0, screen.w, screen.h); | ||||
|     let oldCamera = D.camera; | ||||
|     D.camera = new Point(0, 0); | ||||
|     D.fillRect(new Point(0, 0), D.size, BG_OUTER); | ||||
|     D.camera = oldCamera; | ||||
|  | ||||
|     this.drawGameplay(); | ||||
|  | ||||
|     // 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}) | ||||
|     let xy = this.getCameraMouseXy(); | ||||
|     if (xy != null) { | ||||
|       ctx = this.makeCameraContext(); | ||||
|       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); | ||||
|     let mouse = I.mousePosition?.offset(D.camera); | ||||
|     if (mouse != null) { | ||||
|       D.invertRect(mouse.offset(new Point(-1, -1)), new Size(3, 3)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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) { | ||||
|     let screen = getScreen(); | ||||
|     let {w, h} = screen; | ||||
|     let {w, h} = screen.size; | ||||
|     let overallScreenLocation = getScreenLocation(gameState); | ||||
|  | ||||
|     let bigPaneX = overallScreenLocation.x * w; | ||||
| @@ -142,12 +107,12 @@ export class Game { | ||||
|  | ||||
|     return { | ||||
|       big: { | ||||
|         position: {x: bigPaneX, y: bigPaneY}, | ||||
|         size: {x: bigPaneW, y: bigPaneH} | ||||
|         position: new Point(bigPaneX, bigPaneY), | ||||
|         size: new Size(bigPaneW, bigPaneH), | ||||
|       }, | ||||
|       small: { | ||||
|         position: {x: smallPaneX, y: smallPaneY}, | ||||
|         size: {x: smallPaneW, y: smallPaneH} | ||||
|         position: new Point(smallPaneX, smallPaneY), | ||||
|         size: new Size(smallPaneW, smallPaneH), | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -159,10 +124,9 @@ export class Game { | ||||
|  | ||||
|   drawGameplay() { | ||||
|     let region = this.getPaneRegionForGameState("Gameplay") | ||||
|     let ctx = this.makeCameraContext() | ||||
|     ctx.translate(region.small.position.x, region.small.position.y) | ||||
|  | ||||
|     // TODO: Draw | ||||
|  | ||||
|     D.drawText("hello", region.small.position, FG_TEXT); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -243,9 +207,6 @@ class HuntMode { | ||||
|       gsp, gsp, gsp, gsp | ||||
|     ])(); | ||||
|   } | ||||
|  | ||||
|   get(at: Point) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| function choose<T>(array: Array<T>): T  { | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/input.ts
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								src/input.ts
									
									
									
									
									
								
							| @@ -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; | ||||
| } | ||||
							
								
								
									
										87
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								src/main.ts
									
									
									
									
									
								
							| @@ -1,87 +1,4 @@ | ||||
| import './style.css' | ||||
| import {pollAndTouch} from "./screen.ts"; | ||||
| import {getClock} from "./clock.ts"; | ||||
| import {hostGame} from "./engine/internal/host.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"; | ||||
|  | ||||
|  | ||||
| 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')!) | ||||
|  */ | ||||
| hostGame(game); | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {Sprite} from "./sprite.ts"; | ||||
| import {Sprite} from "./engine/internal/sprite.ts"; | ||||
| import imgBat from "./art/characters/bat.png"; | ||||
| import imgKobold from "./art/characters/kobold.png"; | ||||
| import imgRaccoon from "./art/characters/raccoon.png"; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user