Initial commit

This commit is contained in:
2025-02-01 13:13:44 -08:00
commit 4f95714a92
28 changed files with 1796 additions and 0 deletions

BIN
src/art/bgs/sampleroom1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

BIN
src/art/characters/bat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

BIN
src/art/fonts/vga_8x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View 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
View 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
View 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
View File

@ -0,0 +1 @@
export const BG_OUTER = "#000";

9
src/counter.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />