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 {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private readonly context: CanvasRenderingContext2D;
|
||||
private lastTime: DOMHighResTimeStamp = 0;
|
||||
private isRunning: boolean = false;
|
||||
|
||||
private palette?: Palette;
|
||||
private chunk?: Chunk;
|
||||
|
||||
public constructor(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
|
@ -19,7 +19,7 @@ export class App {
|
|||
|
||||
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() {
|
||||
|
@ -33,14 +33,8 @@ export class App {
|
|||
|
||||
private draw(time: DOMHighResTimeStamp) {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
if (this.palette) {
|
||||
let x = 0;
|
||||
for (let s of this.palette.sprites) {
|
||||
for (let y = 0; y < 4; y++) {
|
||||
s.draw(this.context, y * 128, x, y);
|
||||
}
|
||||
x += 128;
|
||||
}
|
||||
if (this.chunk) {
|
||||
this.chunk.render(this.context);
|
||||
}
|
||||
|
||||
this.lastTime = time;
|
||||
|
@ -56,4 +50,5 @@ export class App {
|
|||
}
|
||||
|
||||
const app = new App(document.getElementById("render") as HTMLCanvasElement);
|
||||
(window as any)._app = app;
|
||||
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