Initial commit
BIN
src/art/bgs/sampleroom1.png
Normal file
After Width: | Height: | Size: 874 B |
BIN
src/art/characters/bat.png
Normal file
After Width: | Height: | Size: 950 B |
BIN
src/art/characters/kobold.png
Normal file
After Width: | Height: | Size: 835 B |
BIN
src/art/characters/raccoon walking 2.aseprite
Normal file
BIN
src/art/characters/raccoon.png
Normal file
After Width: | Height: | Size: 840 B |
BIN
src/art/characters/raccoon_walking.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/art/characters/robot.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
src/art/characters/snake.png
Normal file
After Width: | Height: | Size: 885 B |
BIN
src/art/fonts/vga_8x16.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
71
src/art/mapdata/sampleroom1.ldtk
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"__header__": {
|
||||
"fileType": "LDtk Project JSON",
|
||||
"app": "LDtk",
|
||||
"doc": "https://ldtk.io/json",
|
||||
"schema": "https://ldtk.io/files/JSON_SCHEMA.json",
|
||||
"appAuthor": "Sebastien 'deepnight' Benard",
|
||||
"appVersion": "1.5.3",
|
||||
"url": "https://ldtk.io"
|
||||
},
|
||||
"iid": "018dc430-c210-11ef-bffb-99d47c7356b6",
|
||||
"jsonVersion": "1.5.3",
|
||||
"appBuildId": 473703,
|
||||
"nextUid": 1,
|
||||
"identifierStyle": "Capitalize",
|
||||
"toc": [],
|
||||
"worldLayout": "Free",
|
||||
"worldGridWidth": 256,
|
||||
"worldGridHeight": 256,
|
||||
"defaultLevelWidth": 256,
|
||||
"defaultLevelHeight": 256,
|
||||
"defaultPivotX": 0,
|
||||
"defaultPivotY": 0,
|
||||
"defaultGridSize": 16,
|
||||
"defaultEntityWidth": 16,
|
||||
"defaultEntityHeight": 16,
|
||||
"bgColor": "#40465B",
|
||||
"defaultLevelBgColor": "#696A79",
|
||||
"minifyJson": false,
|
||||
"externalLevels": false,
|
||||
"exportTiled": false,
|
||||
"simplifiedExport": false,
|
||||
"imageExportMode": "None",
|
||||
"exportLevelBg": true,
|
||||
"pngFilePattern": null,
|
||||
"backupOnSave": false,
|
||||
"backupLimit": 10,
|
||||
"backupRelPath": null,
|
||||
"levelNamePattern": "Level_%idx",
|
||||
"tutorialDesc": null,
|
||||
"customCommands": [],
|
||||
"flags": [],
|
||||
"defs": { "layers": [], "entities": [], "tilesets": [], "enums": [], "externalEnums": [], "levelFields": [] },
|
||||
"levels": [
|
||||
{
|
||||
"identifier": "Level_0",
|
||||
"iid": "018deb40-c210-11ef-bffb-151cf6f68a2d",
|
||||
"uid": 0,
|
||||
"worldX": 0,
|
||||
"worldY": 0,
|
||||
"worldDepth": 0,
|
||||
"pxWid": 256,
|
||||
"pxHei": 256,
|
||||
"__bgColor": "#696A79",
|
||||
"bgColor": null,
|
||||
"useAutoIdentifier": true,
|
||||
"bgRelPath": null,
|
||||
"bgPos": null,
|
||||
"bgPivotX": 0.5,
|
||||
"bgPivotY": 0.5,
|
||||
"__smartColor": "#ADADB5",
|
||||
"__bgPos": null,
|
||||
"externalRelPath": null,
|
||||
"fieldInstances": [],
|
||||
"layerInstances": [],
|
||||
"__neighbours": []
|
||||
}
|
||||
],
|
||||
"worlds": [],
|
||||
"dummyWorldIid": "018dc431-c210-11ef-bffb-cb15d2e5f164"
|
||||
}
|
41
src/assets.ts
Normal file
@ -0,0 +1,41 @@
|
||||
class Assets {
|
||||
#images: Record<string, HTMLImageElement>;
|
||||
|
||||
constructor() {
|
||||
this.#images = {};
|
||||
}
|
||||
|
||||
isLoaded(): boolean {
|
||||
// you could use this, if so inclined, to check if a certain
|
||||
// list of assets had been loaded prior to game start
|
||||
//
|
||||
// (to do so, you would call getImage() for each desired asset
|
||||
// and then wait for isLoaded to return true)
|
||||
for (let filename in this.#images) {
|
||||
if (!this.#images[filename].complete) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getImage(filename: string): HTMLImageElement {
|
||||
let element: HTMLImageElement;
|
||||
if (this.#images[filename]) {
|
||||
element = this.#images[filename];
|
||||
} else {
|
||||
element = document.createElement("img");
|
||||
element.src = filename;
|
||||
this.#images[filename] = element;
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
let active: Assets = new Assets();
|
||||
|
||||
export function getAssets(): Assets {
|
||||
return active;
|
||||
}
|
||||
|
44
src/clock.ts
Normal file
@ -0,0 +1,44 @@
|
||||
const MAX_UPDATES_BANKED: number = 20.0;
|
||||
|
||||
// always run physics at 240 hz
|
||||
const UPDATES_PER_MS: number = 1/(1000.0/240.0);
|
||||
|
||||
class Clock {
|
||||
#lastTimestamp: number | undefined;
|
||||
#updatesBanked: number
|
||||
|
||||
constructor() {
|
||||
this.#lastTimestamp = undefined;
|
||||
this.#updatesBanked = 0.0
|
||||
}
|
||||
|
||||
|
||||
recordTimestamp(timestamp: number) {
|
||||
if (this.#lastTimestamp) {
|
||||
let delta = timestamp - this.#lastTimestamp;
|
||||
this.#bankDelta(delta);
|
||||
}
|
||||
this.#lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
popUpdate() {
|
||||
// return true if we should do an update right now
|
||||
// and remove one draw from the bank
|
||||
if (this.#updatesBanked > 1) {
|
||||
this.#updatesBanked -= 1;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#bankDelta(delta: number) {
|
||||
this.#updatesBanked = Math.min(delta * UPDATES_PER_MS, MAX_UPDATES_BANKED);
|
||||
}
|
||||
}
|
||||
|
||||
let active: Clock = new Clock();
|
||||
|
||||
export function getClock(): Clock {
|
||||
return active;
|
||||
}
|
||||
|
||||
|
1
src/colors.ts
Normal file
@ -0,0 +1 @@
|
||||
export const BG_OUTER = "#000";
|
9
src/counter.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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)
|
||||
}
|
146
src/font.ts
Normal file
@ -0,0 +1,146 @@
|
||||
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,
|
||||
}
|
||||
|
||||
class Font {
|
||||
#filename: string;
|
||||
#cx: number;
|
||||
#cy: number;
|
||||
#px: number;
|
||||
#py: number;
|
||||
#tintingCanvas: HTMLCanvasElement;
|
||||
#tintedVersions: Record<string, HTMLImageElement>;
|
||||
|
||||
constructor(filename: string, cx: number, cy: number, px: number, py: number) {
|
||||
this.#filename = filename;
|
||||
this.#cx = cx;
|
||||
this.#cy = cy;
|
||||
this.#px = px;
|
||||
this.#py = py;
|
||||
this.#tintingCanvas = document.createElement("canvas");
|
||||
this.#tintedVersions = {}
|
||||
}
|
||||
|
||||
#getTintedImage(color: string): HTMLImageElement | null {
|
||||
let image = getAssets().getImage(this.#filename);
|
||||
|
||||
if (!image.complete) { return null; }
|
||||
|
||||
let tintedVersion = this.#tintedVersions[color];
|
||||
if (tintedVersion != undefined) {
|
||||
return tintedVersion;
|
||||
}
|
||||
|
||||
let w = image.width;
|
||||
let h = image.height;
|
||||
|
||||
if (!(w == this.#cx * this.#px && h == this.#cy * this.#py)) {
|
||||
throw `unexpected image dimensions for font ${this.#filename}: ${w} x ${h}`
|
||||
}
|
||||
|
||||
this.#tintingCanvas.width = w;
|
||||
this.#tintingCanvas.height = h;
|
||||
let ctx = this.#tintingCanvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
ctx.globalCompositeOperation = "source-in";
|
||||
ctx.fillStyle = color;
|
||||
ctx.rect(0, 0, w, h);
|
||||
ctx.fill();
|
||||
|
||||
let result = new Image();
|
||||
result.src = this.#tintingCanvas.toDataURL();
|
||||
this.#tintedVersions[color] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
drawText({ctx, text, x, y, alignX, alignY, forceWidth, color}: {
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number, y: number, alignX?: AlignX, alignY?: AlignY,
|
||||
forceWidth?: number, color?: string
|
||||
}) {
|
||||
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)
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let wcx = Math.floor(forceWidth / this.#px);
|
||||
|
||||
let sz = this.#glyphwise(text, wcx, () => {});
|
||||
let offsetX = x;
|
||||
let offsetY = 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)
|
||||
|
||||
this.#glyphwise(text, wcx, (cx, cy, char) => {
|
||||
let srcIx = char.charCodeAt(0);
|
||||
this.#drawGlyph({ctx: ctx, image: image, ix: srcIx, x: offsetX + cx * this.#px, y: offsetY + cy * this.#py});
|
||||
})
|
||||
}
|
||||
|
||||
#drawGlyph({ctx, image, ix, x, y}: {ctx: CanvasRenderingContext2D, image: HTMLImageElement, ix: number, x: number, y: number}) {
|
||||
let srcCx = ix % this.#cx;
|
||||
let srcCy = Math.floor(ix / this.#cx);
|
||||
let srcPx = srcCx * this.#px;
|
||||
let srcPy = srcCy * this.#py;
|
||||
ctx.drawImage(
|
||||
image,
|
||||
srcPx, srcPy, this.#px, this.#py,
|
||||
Math.floor(x), Math.floor(y), this.#px, this.#py
|
||||
);
|
||||
}
|
||||
|
||||
measureText({text, forceWidth}: {text: string, forceWidth?: number}): {w: number, h: number} {
|
||||
return this.#glyphwise(text, forceWidth, () => {});
|
||||
}
|
||||
|
||||
#glyphwise(text: string, forceWidth: number | undefined, callback: (x: number, y: number, char: string) => void): {w: number, h: number} {
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
let cw = 0;
|
||||
let ch = 0;
|
||||
let wcx = forceWidth == undefined ? undefined : Math.floor(forceWidth / this.#px);
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
let char = text[i]
|
||||
if (char == '\n') {
|
||||
cx = 0;
|
||||
cy += 1;
|
||||
ch = cy + 1;
|
||||
} else {
|
||||
// console.log("cx, cy, char", [cx, cy, char])
|
||||
callback(cx, cy, char)
|
||||
cx += 1;
|
||||
cw = Math.max(cw, cx);
|
||||
ch = cy + 1;
|
||||
|
||||
if (wcx != undefined && cx > wcx) {
|
||||
cx = 0;
|
||||
cy += 1;
|
||||
ch = cy + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { w: cw * this.#px, h: ch * this.#py };
|
||||
}
|
||||
}
|
||||
|
||||
export let mainFont = new Font(fontSheet, 32, 8, 8, 16);
|
172
src/game.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import {desiredHeight, desiredWidth, getScreen} from "./screen.ts";
|
||||
import {BG_OUTER} from "./colors.ts";
|
||||
import {mainFont} from "./font.ts";
|
||||
import {getInput} from "./input.ts";
|
||||
|
||||
type Point = {x: number, y: number}
|
||||
|
||||
class MenuCamera {
|
||||
// measured in whole screens
|
||||
position: Point;
|
||||
target: Point
|
||||
|
||||
constructor({position, target}: {position: Point, target: Point}) {
|
||||
this.position = position;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
update() {
|
||||
let adjust = (x0: number, x1: number) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
type GameState = "Overview" | "Gameplay" | "Thralls";
|
||||
|
||||
function getScreenLocation(state: GameState): {x: number, y: number} {
|
||||
if (state === "Overview") {
|
||||
return {x: 0.0, y: 0.0}
|
||||
}
|
||||
if (state === "Gameplay") {
|
||||
return {x: 1.0, y: 0.0}
|
||||
}
|
||||
if (state === "Thralls") {
|
||||
return {x: 0.0, y: 1.0}
|
||||
}
|
||||
|
||||
throw `invalid state: ${state}`
|
||||
}
|
||||
|
||||
export class Game {
|
||||
msg: string;
|
||||
camera: MenuCamera;
|
||||
state: GameState;
|
||||
|
||||
constructor() {
|
||||
this.msg = "You have been given a gift.";
|
||||
this.camera = new MenuCamera({
|
||||
position: {x: 0.0, y: 0.0},
|
||||
target: {x: 0.0, y: 0.0}
|
||||
});
|
||||
this.state = "Overview";
|
||||
}
|
||||
|
||||
update() {
|
||||
if (getInput().isPressed("a") || getInput().isPressed("w")) {
|
||||
this.state = "Overview"
|
||||
}
|
||||
if (getInput().isPressed("d")) {
|
||||
this.state = "Gameplay"
|
||||
}
|
||||
if (getInput().isPressed("s")) {
|
||||
this.state = "Thralls"
|
||||
}
|
||||
|
||||
if (getInput().isPressed("leftMouse")) {
|
||||
this.msg = "Left click, asshole!"
|
||||
}
|
||||
if (getInput().isReleased("leftMouse")) {
|
||||
this.msg = "Un-left click, asshole!"
|
||||
}
|
||||
|
||||
this.camera.target = getScreenLocation(this.state);
|
||||
this.camera.update();
|
||||
}
|
||||
|
||||
draw() {
|
||||
let screen = getScreen();
|
||||
let ctx = screen.makeContext();
|
||||
|
||||
// draw screen background
|
||||
ctx.fillStyle = BG_OUTER;
|
||||
ctx.fillRect(0, 0, screen.w, screen.h);
|
||||
|
||||
this.drawOverview();
|
||||
// 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.fillStyle = "#fff";
|
||||
ctx.strokeStyle = "#fff";
|
||||
ctx.fillRect(xy.x - 1, xy.y - 1, 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 overallScreenLocation = getScreenLocation(gameState);
|
||||
|
||||
let bigPaneX = overallScreenLocation.x * w;
|
||||
let bigPaneY = overallScreenLocation.y * h;
|
||||
let bigPaneW = w;
|
||||
let bigPaneH = h;
|
||||
|
||||
let smallPaneW = desiredWidth;
|
||||
let smallPaneH = desiredHeight;
|
||||
let smallPaneX = Math.floor(bigPaneX + (bigPaneW - smallPaneW) / 2)
|
||||
let smallPaneY = Math.floor(bigPaneY + (bigPaneH - smallPaneH) / 2)
|
||||
|
||||
return {
|
||||
big: {
|
||||
position: {x: bigPaneX, y: bigPaneY},
|
||||
size: {x: bigPaneW, y: bigPaneH}
|
||||
},
|
||||
small: {
|
||||
position: {x: smallPaneX, y: smallPaneY},
|
||||
size: {x: smallPaneW, y: smallPaneH}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
drawOverview() {
|
||||
let region = this.getPaneRegionForGameState("Overview")
|
||||
let ctx = this.makeCameraContext()
|
||||
ctx.translate(region.small.position.x, region.small.position.y)
|
||||
|
||||
// ctx.strokeStyle = "#ffffff";
|
||||
// ctx.strokeRect(0.5, 0.5, region.small.size.x - 1, region.small.size.y - 1);
|
||||
|
||||
mainFont.drawText({ctx: ctx, text: this.msg, x: 0, y: 0})
|
||||
}
|
||||
}
|
||||
|
||||
export let game = new Game();
|
108
src/input.ts
Normal file
@ -0,0 +1,108 @@
|
||||
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
Normal file
@ -0,0 +1,87 @@
|
||||
import './style.css'
|
||||
import {pollAndTouch} from "./screen.ts";
|
||||
import {getClock} from "./clock.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')!)
|
||||
*/
|
62
src/screen.ts
Normal file
@ -0,0 +1,62 @@
|
||||
class Screen {
|
||||
#canvas: HTMLCanvasElement
|
||||
w: number
|
||||
h: number
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, w: number, h: number) {
|
||||
this.#canvas = canvas;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
|
||||
makeContext(): CanvasRenderingContext2D {
|
||||
let ctx = this.#canvas.getContext("2d")!;
|
||||
|
||||
// TODO: Other stuff to do here?
|
||||
ctx.strokeStyle = "#000";
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.resetTransform();
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let active: Screen | undefined = undefined
|
||||
export let desiredWidth = 384;
|
||||
export let desiredHeight = 384;
|
||||
|
||||
export function pollAndTouch(canvas: HTMLCanvasElement) {
|
||||
let realWidth = canvas.offsetWidth;
|
||||
let realHeight = canvas.offsetHeight;
|
||||
|
||||
let divisors = [0.25, 0.5, 1, 2, 3, 4, 5, 6];
|
||||
for (let i = 0; i < divisors.length; i++) {
|
||||
// TODO: Is this necessary?
|
||||
divisors[i] /= window.devicePixelRatio;
|
||||
}
|
||||
|
||||
let div = 0;
|
||||
while (
|
||||
(div < divisors.length - 1) &&
|
||||
(realWidth / divisors[div + 1] >= desiredWidth) &&
|
||||
(realHeight / divisors[div + 1] >= desiredHeight)
|
||||
) {
|
||||
div += 1;
|
||||
}
|
||||
realWidth = Math.floor(canvas.offsetWidth / divisors[div]);
|
||||
realHeight = Math.floor(canvas.offsetHeight / divisors[div]);
|
||||
canvas.width = realWidth;
|
||||
canvas.height = realHeight;
|
||||
active = new Screen(canvas, realWidth, realHeight);
|
||||
}
|
||||
|
||||
export function getScreen(): Screen {
|
||||
if (active === undefined) {
|
||||
throw `screen should have been defined: ${active}`
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
|
48
src/sprite.ts
Normal file
@ -0,0 +1,48 @@
|
||||
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);
|
||||
}
|
||||
}
|
14
src/sprites.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Sprite} from "./sprite.ts";
|
||||
import imgBat from "./art/characters/bat.png";
|
||||
import imgKobold from "./art/characters/kobold.png";
|
||||
import imgRaccoon from "./art/characters/raccoon.png";
|
||||
import imgRaccoonWalking from "./art/characters/raccoon_walking.png";
|
||||
import imgRobot from "./art/characters/robot.png";
|
||||
import imgSnake from "./art/characters/snake.png";
|
||||
|
||||
export let sprBat = new Sprite(imgBat, 64, 64, 32, 32, 1, 1, 1);
|
||||
export let sprKobold = new Sprite(imgKobold, 64, 64, 32, 32, 1, 1, 1);
|
||||
export let sprRaccoon = new Sprite(imgRaccoon, 64, 64, 32, 32, 1, 1, 1);
|
||||
export let sprRaccoonWalking = new Sprite(imgRaccoonWalking, 64, 64, 32, 32, 8, 1, 8);
|
||||
export let sprRobot = new Sprite(imgRobot, 64, 64, 32, 32, 1, 1, 1);
|
||||
export let sprSnake = new Sprite(imgSnake, 64, 64, 32, 32, 1, 1, 1);
|
12
src/style.css
Normal file
@ -0,0 +1,12 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#game {
|
||||
image-rendering: pixelated;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|