Skip to content

Commit

Permalink
add scrolling to the endless dungeon crawler
Browse files Browse the repository at this point in the history
we now do not create tunnels anymore for the parts.
instead i create one large dungeon at the beginning.
And if the player moves out of the center,
i will create the third of the dungeon that the player wil see next.
  • Loading branch information
Gryxis committed Aug 25, 2023
1 parent 4fbe989 commit 9a2ebf1
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ <h1>
neighbors are filled with rocks, otherwise it will turn into an empty field.
</label>

<p>
Now try it! Use arrow keys to navigate through the endless cave!
</p>

</div>

<p class="mb-5">
Expand All @@ -54,7 +58,7 @@ <h1>
</button>
</p>

<canvas #endlessCrawlerCanvas [attr.width]="SIZE * PIXEL_SIZE" [attr.height]="SIZE * PIXEL_SIZE">
<canvas #endlessCrawlerCanvas [attr.width]="SIZE * PIXEL_SIZE / 3" [attr.height]="SIZE * PIXEL_SIZE / 3">
</canvas>

<p style="height: 2rem;"></p>
203 changes: 160 additions & 43 deletions src/app/endlessDungeonCrawler/endless-dungeon-crawler.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';

@Component({
selector: 'app-endless-dungeon-crawler',
Expand All @@ -12,9 +12,11 @@ export class EndlessDungeonCrawlerComponent implements AfterViewInit, OnDestroy
public propability: number = 50;
public readonly SIZE = 150;

public position: Position = { x:0, y:0};

private readonly BOTTOM_COLOR = [245,245,220, 255];
private readonly STONE_COLOR = [158,74,74, 255];
public readonly PIXEL_SIZE = 8;
public readonly PIXEL_SIZE = 16;

@ViewChild('endlessCrawlerCanvas')
private endlessCrawlerCanvas!: ElementRef<HTMLCanvasElement>;
Expand Down Expand Up @@ -43,80 +45,195 @@ export class EndlessDungeonCrawlerComponent implements AfterViewInit, OnDestroy


public onStart(): void {
this.generateDungeonPart();
this.fillCanvas();
this.dungeon = [];
const fullDungeon = { x:0, y:0, w: this.SIZE, h: this.SIZE};
this.generateDungeonPart( fullDungeon);
this.constructBitmap().then(
() => this.drawCanvas()
);
}

generateDungeonPart() {

this.dungeon = this.simulateCA(
this.getSprinkle()
);
generateDungeonPart( part: Rectangle) {
this.sprinkleOnDungeon( part);
this.simulateCA( part);
}

private getSprinkle(): number[] {
let sprinkledField = [];
private sprinkleOnDungeon( rectangle: Rectangle): void {
// sprinkle:
for (let i = 0; i < this.SIZE * this.SIZE; i++) {
sprinkledField[i] = Math.random() <= (this.propability * 0.01) ? 1 : 0;
for (let x = rectangle.x; x < rectangle.x + rectangle.w; x++) {
for (let y = rectangle.y; y < rectangle.y + rectangle.h; y++) {
const pos = x + y * this.SIZE;
this.dungeon[pos] = Math.random() <= (this.propability * 0.01) ? 1 : 0;
}
}

return sprinkledField;
}

private simulateCA( rectangle: Rectangle): void {

private simulateCA( oldField: number[]): number[] {

let currentField = [... oldField];
for (let currentStep = 0; currentStep < this.simulationStepCount; currentStep++) {

const nextField = [];
for (let pos = 0; pos < this.SIZE * this.SIZE; pos++) {

// count neighbours, repeating on the edge:
let countNeighbourStones = 0;

for ( let y = -1; y <= 1; y++) {
for ( let x = -1; x <= 1; x++) {
let neighborPos = (pos + x + y * this.SIZE);
neighborPos = (neighborPos + this.SIZE * this.SIZE) % (this.SIZE * this.SIZE);

if (currentField[neighborPos]) {
countNeighbourStones++;
const nextField = [... this.dungeon];
for (let x = rectangle.x; x < rectangle.x + rectangle.w; x++) {
for (let y = rectangle.y; y < rectangle.y + rectangle.h; y++) {
const pos = x + y * this.SIZE;

// count neighbours, repeating on the edge:
let countNeighbourStones = 0;

for ( let y = -1; y <= 1; y++) {
for ( let x = -1; x <= 1; x++) {
let neighborPos = (pos + x + y * this.SIZE);
neighborPos = (neighborPos + this.SIZE * this.SIZE) % (this.SIZE * this.SIZE);

if (this.dungeon[neighborPos]) {
countNeighbourStones++;
}
}
}

nextField[pos] = countNeighbourStones >= this.threshold ? 1 : 0;
}

nextField[pos] = countNeighbourStones >= this.threshold ? 1 : 0;
}

currentField = nextField;
}

return currentField;
this.dungeon = nextField;
}
}

/**
* shifts the content of the dungeon in diraction of dx and dy vector
* @param dx change in x-axis
* @param dy change in y-axis
*/
private shiftDungeon( dx: number, dy: number) {
for ( let pos = 0; pos < this.dungeon.length; pos++) {
if ( pos / this.SIZE + dy >= this.SIZE
|| pos / this.SIZE + dy < 0
|| pos % this.SIZE + dx >= this.SIZE
|| pos % this.SIZE + dx < 0) {
continue;
}
const copyPos = pos + dx + (dy * this.SIZE);
this.dungeon[pos] = this.dungeon[copyPos];
}
}

async fillCanvas() {
async constructBitmap() {
const rgbaPixels: Uint8ClampedArray = new Uint8ClampedArray(this.SIZE * 4 * this.SIZE);

this.dungeon.forEach( (field, i) => {
for ( let i = 0; i < this.dungeon.length; i++) {
const field = this.dungeon[i];
const color = field === 1 ? this.STONE_COLOR : this.BOTTOM_COLOR;
rgbaPixels[i*4 + 0] = color[0]; // R value
rgbaPixels[i*4 + 1] = color[1]; // G value
rgbaPixels[i*4 + 2] = color[2]; // B value
rgbaPixels[i*4 + 3] = color[3]; // A value
});
};

const imageData: ImageData = new ImageData(rgbaPixels, this.SIZE, this.SIZE);

const canvasSize = this.SIZE * this.PIXEL_SIZE;
const imageOptions: ImageBitmapOptions = {
resizeHeight: this.SIZE * this.PIXEL_SIZE,
resizeHeight: canvasSize,
resizeQuality: "pixelated",
resizeWidth: this.SIZE * this.PIXEL_SIZE,
resizeWidth: canvasSize,
};
this.dungeonBitMap?.close();
this.dungeonBitMap = await createImageBitmap(imageData, 0, 0, this.SIZE, this.SIZE, imageOptions);
this.canvasContext.drawImage(this.dungeonBitMap, 0, 0);
}

private drawCanvas() {
const canvasSize = this.SIZE * this.PIXEL_SIZE;
this.canvasContext.drawImage(
this.dungeonBitMap!,
-canvasSize/3 - this.position.x * this.PIXEL_SIZE,
-canvasSize/3 - this.position.y * this.PIXEL_SIZE
);
}


// move logic:
@HostListener('window:keydown.arrowup',['$event'])
@HostListener('window:keydown.arrowdown',['$event'])
@HostListener('window:keydown.arrowright',['$event'])
@HostListener('window:keydown.arrowleft',['$event'])
public async move(event: KeyboardEvent): Promise<void> {
if (!this.dungeonBitMap) {
return;
}

switch (event.key) {
case "ArrowLeft":
this.position.x--;
break;
case "ArrowRight":
this.position.x++;
break;
case "ArrowUp":
this.position.y--;
break;
case "ArrowDown":
this.position.y++;
break;
}

if (
Math.abs(this.position.x ) >= this.SIZE/3
|| Math.abs(this.position.y) >= this.SIZE/3
) {
await this.generateBorderAndResetShift();
}

this.drawCanvas();
}

private async generateBorderAndResetShift(): Promise<void> {

if (Math.abs( this.position.x) >= this.SIZE / 3) {
const newPart: Rectangle = {
x: this.position.x + this.SIZE / 3,
y: 0,
w: this.SIZE / 3,
h: this.SIZE,
};

this.shiftDungeon( this.position.x * 1, 0);
this.generateDungeonPart(newPart);

if (this.position.x >= this.SIZE /3) {
this.position.x -= this.SIZE / 3;
} else {
this.position.x += this.SIZE / 3;
}
}
if (Math.abs( this.position.y) >= this.SIZE / 3) {
const newPart: Rectangle = {
x: 0,
y: this.position.y + this.SIZE / 3,
w: this.SIZE,
h: this.SIZE / 3,
};
this.shiftDungeon( 0, this.position.y * 1);
this.generateDungeonPart(newPart);

if (this.position.y >= this.SIZE / 3) {
this.position.y -= this.SIZE / 3;
} else {
this.position.y += this.SIZE / 3;
}
}

await this.constructBitmap();
}
}

interface Position {
x: number,
y: number,
};

interface Rectangle {
x: number,
y: number,
w:number,
h:number
}

0 comments on commit 9a2ebf1

Please sign in to comment.