add grid and generator
This commit is contained in:
parent
446a052832
commit
fb6f58817c
3 changed files with 171 additions and 12 deletions
19
src/app.ts
19
src/app.ts
|
@ -1,12 +1,12 @@
|
||||||
import {fetchPalette, Palette} from "./sprite";
|
import {fetchPalette} from "./sprite";
|
||||||
|
import {Chunk} from "./chunk";
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
private canvas: HTMLCanvasElement;
|
private canvas: HTMLCanvasElement;
|
||||||
private readonly context: CanvasRenderingContext2D;
|
private readonly context: CanvasRenderingContext2D;
|
||||||
private lastTime: DOMHighResTimeStamp = 0;
|
private lastTime: DOMHighResTimeStamp = 0;
|
||||||
private isRunning: boolean = false;
|
private isRunning: boolean = false;
|
||||||
|
private chunk?: Chunk;
|
||||||
private palette?: Palette;
|
|
||||||
|
|
||||||
public constructor(canvas: HTMLCanvasElement) {
|
public constructor(canvas: HTMLCanvasElement) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
|
@ -19,7 +19,7 @@ export class App {
|
||||||
|
|
||||||
this.updateSize(window.innerWidth, window.innerHeight);
|
this.updateSize(window.innerWidth, window.innerHeight);
|
||||||
window.onresize = () => this.updateSize(window.innerWidth, window.innerHeight);
|
window.onresize = () => this.updateSize(window.innerWidth, window.innerHeight);
|
||||||
fetchPalette("").then(p => this.palette = p);
|
fetchPalette("").then(palette => this.chunk = new Chunk(palette));
|
||||||
}
|
}
|
||||||
|
|
||||||
public run() {
|
public run() {
|
||||||
|
@ -33,14 +33,8 @@ export class App {
|
||||||
|
|
||||||
private draw(time: DOMHighResTimeStamp) {
|
private draw(time: DOMHighResTimeStamp) {
|
||||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
if (this.palette) {
|
if (this.chunk) {
|
||||||
let x = 0;
|
this.chunk.render(this.context);
|
||||||
for (let s of this.palette.sprites) {
|
|
||||||
for (let y = 0; y < 4; y++) {
|
|
||||||
s.draw(this.context, y * 128, x, y);
|
|
||||||
}
|
|
||||||
x += 128;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastTime = time;
|
this.lastTime = time;
|
||||||
|
@ -56,4 +50,5 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new App(document.getElementById("render") as HTMLCanvasElement);
|
const app = new App(document.getElementById("render") as HTMLCanvasElement);
|
||||||
|
(window as any)._app = app;
|
||||||
app.run();
|
app.run();
|
||||||
|
|
161
src/chunk.ts
Normal file
161
src/chunk.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { IRender } from "./render";
|
||||||
|
import { Direction, Palette, Sprite } from "./sprite";
|
||||||
|
|
||||||
|
/** Either a tile (if collapsed), or a list of possible tiles (if not) */
|
||||||
|
type MaybeTile = Tile | Tile[];
|
||||||
|
|
||||||
|
/** Number of tiles per row and column of a chunk */
|
||||||
|
const CHUNK_DIM = 16 >>> 0;
|
||||||
|
|
||||||
|
export class Chunk implements IRender {
|
||||||
|
private palette: Palette;
|
||||||
|
private grid: MaybeTile[] = [];
|
||||||
|
private complete: boolean = false;
|
||||||
|
|
||||||
|
public constructor(palette: Palette) {
|
||||||
|
this.palette = palette;
|
||||||
|
|
||||||
|
// pregenerate an array with all possible tiles
|
||||||
|
let allTileVariants = [];
|
||||||
|
for (let s of palette.sprites) {
|
||||||
|
for (let dir = 0; dir < 4; dir++) {
|
||||||
|
allTileVariants.push(new Tile(s, dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the grid with perfect entropy
|
||||||
|
for (let i = 0; i < CHUNK_DIM ** 2; i++) {
|
||||||
|
this.grid.push(allTileVariants);
|
||||||
|
}
|
||||||
|
|
||||||
|
// collapse a single tile to generate an initial disturbance in entropy
|
||||||
|
this.collapseAndUpdate(0, 0);
|
||||||
|
|
||||||
|
window.onmousedown = () => this.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(context: CanvasRenderingContext2D) {
|
||||||
|
const res = this.palette.resolution;
|
||||||
|
this.grid.forEach((tile, index) => {
|
||||||
|
const y = Math.floor(index / CHUNK_DIM);
|
||||||
|
const x = index % CHUNK_DIM;
|
||||||
|
if (!Array.isArray(tile)) {
|
||||||
|
tile.draw(context, x * res, y * res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick() {
|
||||||
|
if (this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowest: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
let lowestCoords: number[][] = [];
|
||||||
|
this.grid.forEach((tile, index) => {
|
||||||
|
if (Array.isArray(tile)) {
|
||||||
|
if (tile.length <= lowest) {
|
||||||
|
const x = index % CHUNK_DIM;
|
||||||
|
const y = Math.floor(index / CHUNK_DIM);
|
||||||
|
if (tile.length < lowest) {
|
||||||
|
lowest = tile.length;
|
||||||
|
lowestCoords = [[x, y]];
|
||||||
|
} else {
|
||||||
|
lowestCoords.push([x, y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (lowestCoords.length > 0) {
|
||||||
|
let coords = pickRandom(lowestCoords);
|
||||||
|
this.collapseAndUpdate(coords[0], coords[1]);
|
||||||
|
} else {
|
||||||
|
this.complete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse a tile and update entropy on surrounding tiles */
|
||||||
|
private collapseAndUpdate(x: number, y: number) {
|
||||||
|
const index = y * CHUNK_DIM + x;
|
||||||
|
const candidates = this.grid[index];
|
||||||
|
if (!Array.isArray(candidates)) {
|
||||||
|
return; // tile is already collapsed (or out of bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.grid[index] = pickRandom(candidates);
|
||||||
|
|
||||||
|
if (y > 0) {
|
||||||
|
this.updateEntropy(x, y - 1); // north
|
||||||
|
}
|
||||||
|
if (x < CHUNK_DIM - 1) {
|
||||||
|
this.updateEntropy(x + 1, y); // east
|
||||||
|
}
|
||||||
|
if (y < CHUNK_DIM - 1) {
|
||||||
|
this.updateEntropy(x, y + 1); // south
|
||||||
|
}
|
||||||
|
if (x > 0) {
|
||||||
|
this.updateEntropy(x - 1, y); // west
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove all candidates that are no longer possible */
|
||||||
|
private updateEntropy(x: number, y: number) {
|
||||||
|
const i = y * CHUNK_DIM + x;
|
||||||
|
const candidates = this.grid[i];
|
||||||
|
if (!Array.isArray(candidates)) {
|
||||||
|
return; // tile is already collapsed (or out of bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterTile = (tile: MaybeTile) => Array.isArray(tile) ? null : tile;
|
||||||
|
const neighs: (Tile | null)[] = [null, null, null, null];
|
||||||
|
if (y > 0) {
|
||||||
|
neighs[Direction.NORTH] = filterTile(this.grid[i - CHUNK_DIM]);
|
||||||
|
}
|
||||||
|
if (x < CHUNK_DIM - 1) {
|
||||||
|
neighs[Direction.EAST] = filterTile(this.grid[i + 1]);
|
||||||
|
}
|
||||||
|
if (y < CHUNK_DIM - 1) {
|
||||||
|
neighs[Direction.SOUTH] = filterTile(this.grid[i + CHUNK_DIM]);
|
||||||
|
}
|
||||||
|
if (x > 0) {
|
||||||
|
neighs[Direction.WEST] = filterTile(this.grid[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to be careful to create an entirely new array here
|
||||||
|
// because the constructor inserts a reference to the same array
|
||||||
|
// of MaybeTiles for every cell, so manipulating the array in-place
|
||||||
|
// would break everything
|
||||||
|
this.grid[i] = candidates.filter(c => !neighs.some((n, dir) => n && c.conflicts(dir, n)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tile {
|
||||||
|
private readonly sprite: Sprite;
|
||||||
|
private readonly orientation: Direction;
|
||||||
|
|
||||||
|
public constructor(sprite: Sprite, orientation: Direction) {
|
||||||
|
this.sprite = sprite;
|
||||||
|
this.orientation = orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public matches(ownEdge: Direction, neighbor: Tile): boolean {
|
||||||
|
const ownEdgeAbs = (ownEdge + 4 - this.orientation) % 4;
|
||||||
|
const neighEdge = (ownEdge + 2) % 4;
|
||||||
|
const neighEdgeAbs = (neighEdge + 4 - neighbor.orientation) % 4;
|
||||||
|
return this.sprite.matches(ownEdgeAbs, neighbor.sprite, neighEdgeAbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public conflicts(ownEdge: Direction, neighbor: Tile): boolean {
|
||||||
|
return !this.matches(ownEdge, neighbor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(context: CanvasRenderingContext2D, x: number, y: number) {
|
||||||
|
this.sprite.draw(context, x, y, this.orientation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickRandom<T>(a: T[]): T {
|
||||||
|
return a[Math.round(Math.random() * (a.length - 1))];
|
||||||
|
}
|
3
src/render.ts
Normal file
3
src/render.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IRender {
|
||||||
|
render(context: CanvasRenderingContext2D): void;
|
||||||
|
}
|
Loading…
Reference in a new issue