From 3a9185ca94f1ddd152d0fe037678795136f5133e Mon Sep 17 00:00:00 2001 From: Kim Ravn Hansen Date: Fri, 19 Sep 2025 16:04:11 +0200 Subject: [PATCH] vyy --- WfcConstants.js | 11 - frontend/3d_dungeon_crawler.html | 657 ++++++++++++++++++ frontend/{TrainingCell.js => SourceCell.js} | 36 +- frontend/SourceGrid.js | 42 ++ frontend/TrainingGrid.js | 30 - frontend/WfcCell.js | 20 +- frontend/WfcConstants.js | 21 + frontend/WfcGrid.js | 218 +++--- frontend/ascii_dungeon_crawler.html | 482 +++++++++++++ frontend/cellular_automata_map_generator.html | 407 +++++++++++ frontend/client.js | 4 +- frontend/dungeon_generator.html | 573 +++++++++++++++ frontend/progen.html | 3 +- frontend/progen.js | 349 +++++----- models/game.js | 1 - node_modules/.vite/deps/_metadata.json | 8 +- scenes/authentication/authenticationScene.js | 7 +- scenes/authentication/passwordPrompt.js | 4 +- seeders/itemSeeder.js | 4 - utils/random.js | 3 + 20 files changed, 2520 insertions(+), 360 deletions(-) delete mode 100644 WfcConstants.js create mode 100644 frontend/3d_dungeon_crawler.html rename frontend/{TrainingCell.js => SourceCell.js} (80%) create mode 100755 frontend/SourceGrid.js delete mode 100644 frontend/TrainingGrid.js create mode 100755 frontend/WfcConstants.js create mode 100644 frontend/ascii_dungeon_crawler.html create mode 100644 frontend/cellular_automata_map_generator.html create mode 100644 frontend/dungeon_generator.html diff --git a/WfcConstants.js b/WfcConstants.js deleted file mode 100644 index e9d255f..0000000 --- a/WfcConstants.js +++ /dev/null @@ -1,11 +0,0 @@ -export const Direction = Object.freeze({ - NW: 0, - N: 1, - NE: 2, - E: 3, - C: 4, - W: 5, - SW: 6, - S: 7, - SE: 8, -}); diff --git a/frontend/3d_dungeon_crawler.html b/frontend/3d_dungeon_crawler.html new file mode 100644 index 0000000..7654b79 --- /dev/null +++ b/frontend/3d_dungeon_crawler.html @@ -0,0 +1,657 @@ + + + + + + Simple 3D Dungeon Crawler + + + +
+

First-Person Dungeon Crawler

+

Create a map.txt file to load your own dungeon:

+ +

Controls:

+ + +
+ +
+ + + + + + + diff --git a/frontend/TrainingCell.js b/frontend/SourceCell.js similarity index 80% rename from frontend/TrainingCell.js rename to frontend/SourceCell.js index 1ad51d2..2a915e0 100755 --- a/frontend/TrainingCell.js +++ b/frontend/SourceCell.js @@ -3,13 +3,17 @@ import { Direction } from "./WfcConstants.js"; /** * Represents a 3x3 grid of values (sub-cells), that are used as building blocks for procedurally * generated grids. In reality, only the center value will be included in the outputted WfcGrid; - * the 8 surrounding colors are there to establish which TrainingCells can live next to each other. + * the 8 surrounding colors are there to establish which SourceCells can live next to each other. */ -export class TrainingCell { - /** @param {Uint8Array} values The 9 sub cells that make up this TrainingCell */ - constructor() { - this.values = new Uint8Array(9); +export class SourceCell { + /** @param {Uint8Array?} values The 9 sub cells that make up this SourceCell */ + constructor(values) { + if (values === undefined) { + this.values = new Uint8Array(9); + return; + } + this.values = values; } /** @returns {string} The actual value of this Trainin gCell is represented by its center value */ @@ -18,7 +22,18 @@ export class TrainingCell { } /** - * @param {TrainingCell} other + * @param {uint8} value + * Set the default value of this source cell */ + set value(value) { + this.values[Direction.C] = value; + } + + clone() { + return new SourceCell(this.values.slice()); + } + + /** + * @param {SourceCell} other * @param {number} direction * * @returns {boolean} @@ -26,10 +41,9 @@ export class TrainingCell { potentialNeighbours(other, direction) { // sadly, we're not allowed to be friends with ourselves. if (this === other) { - console.log("WTF were checking to be friends with ourselves!", this, other, direction); - return false; + console.log("WTF were checking to be friends with ourselves!", { _this: this, other, direction }); + // throw new Error("WTF were checking to be friends with ourselves!", { _this: this, other, direction }); } - return ( // // if they want to live to my east, @@ -69,7 +83,7 @@ export class TrainingCell { // my north row must match their middle row this.values[Direction.NW] === other.values[Direction.W] && this.values[Direction.N] === other.values[Direction.C] && - this.values[Direction.Direction.NE] === other.values[Direction.E]) || + this.values[Direction.NE] === other.values[Direction.E]) || // // if they want to live to my south, // their two northern rows must match @@ -78,7 +92,7 @@ export class TrainingCell { // my middle row must match their north row this.values[Direction.W] === other.values[Direction.NW] && this.values[Direction.C] === other.values[Direction.N] && - this.values[Direction.E] === other.values[Direction.SE] && + this.values[Direction.E] === other.values[Direction.NE] && // my south row must match their middle row this.values[Direction.SW] === other.values[Direction.W] && this.values[Direction.S] === other.values[Direction.C] && diff --git a/frontend/SourceGrid.js b/frontend/SourceGrid.js new file mode 100755 index 0000000..73edaf6 --- /dev/null +++ b/frontend/SourceGrid.js @@ -0,0 +1,42 @@ +import { SourceCell } from "./SourceCell.js"; + +export class SourceGrid { + /** + * @type {SourceCell[]} cells The cells that make up this source grid. + */ + cells; + + /** + * @type {number} the width and/or height of the source grid + */ + dim; + + /** + * @param {SourceCell[]} cells + */ + constructor(cells) { + if (cells[0] === undefined) { + throw new Error("cells must be a non empty array"); + } + + if (!(cells[0] instanceof SourceCell)) { + throw new Error("cells arg must be an array of SourceCell, but it isn't"); + } + + this.cells = cells; + + this.dim = Math.round(Math.sqrt(cells.length)); + + if (this.dim ** 2 !== cells.length) { + throw new Error("Source grid must be quadratic (height === width), but it isn't"); + } + } + + toString() { + return this.cells.map((cell) => cell.value).join(", "); + } + + clone() { + return new SourceGrid(this.cells.map((sgCell) => sgCell.clone())); + } +} diff --git a/frontend/TrainingGrid.js b/frontend/TrainingGrid.js deleted file mode 100644 index 3a9586c..0000000 --- a/frontend/TrainingGrid.js +++ /dev/null @@ -1,30 +0,0 @@ -import { TrainingCell } from "./TrainingCell.js"; - -export class TrainingGrid { - /** - * @param {TrainingCell[]} cells - */ - constructor(cells) { - if (cells[0] === undefined) { - throw new Error("cells must be a non empty array"); - } - - if (!(cells[0] instanceof TrainingCell)) { - throw new Error("cells arg must be an array of TrainingCell, but it isn't"); - } - - /** @type {TrainingCell[]} cells*/ - this.cells = cells; - - /** @type {number} the width and/or height of the training grid */ - this.dim = Math.round(Math.sqrt(cells.length)); - - if (this.dim ** 2 !== cells.length) { - throw new Error("Training grid must be quadratic (height === width), but it isn't"); - } - } - - clone() { - return new TrainingGrid(this.cells.slice()); - } -} diff --git a/frontend/WfcCell.js b/frontend/WfcCell.js index d7b7833..e29933e 100755 --- a/frontend/WfcCell.js +++ b/frontend/WfcCell.js @@ -1,4 +1,4 @@ -import { TrainingCell } from "./TrainingCell"; +import { SourceCell } from "./SourceCell"; /** * Represents a single cell in a WfcGrid @@ -9,7 +9,7 @@ export class WfcCell { * @param {number} i index in the cell-array of this cell * @param {number} x x-coordinate of cell * @param {number} y y-coordinate of cell - * @param {TrainingCell[]} options - A list of training cells that could potentially live here. + * @param {SourceCell[]} options - A list of source cells that could potentially live here. */ constructor(i, x, y, options) { if (!options.length) { @@ -17,7 +17,7 @@ export class WfcCell { throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options); } - if (!(options[0] instanceof TrainingCell)) { + if (!(options[0] instanceof SourceCell)) { console.log("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options); throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options); } @@ -29,22 +29,14 @@ export class WfcCell { } getEntropy() { - const result = this.options.length; - return result; + return this.options.length; } get lockedIn() { - return this.getEntropy() === 1; - } - - get valid() { - return this.options.length > 0; + return this.options.length === 1; } get value() { - if (this.options[0] === undefined) { - throw new Error("Bad! I do not have any options, and therefore no color"); - } - return this.options[0].value; + return this.options.length === 1 ? this.options[0].value : 0; } } diff --git a/frontend/WfcConstants.js b/frontend/WfcConstants.js new file mode 100755 index 0000000..65409cb --- /dev/null +++ b/frontend/WfcConstants.js @@ -0,0 +1,21 @@ +export const Direction = Object.freeze({ + NW: 0, + N: 1, + NE: 2, + E: 3, + C: 4, + W: 5, + SW: 6, + S: 7, + SE: 8, + + 0: "North west", + 1: "North", + 2: "North east", + 3: "East", + 4: "[center]", + 5: "West", + 6: "South west", + 7: "South", + 8: "South east", +}); diff --git a/frontend/WfcGrid.js b/frontend/WfcGrid.js index b438f7a..e6f1f52 100755 --- a/frontend/WfcGrid.js +++ b/frontend/WfcGrid.js @@ -1,5 +1,7 @@ import { Direction } from "./WfcConstants.js"; import { WfcCell } from "./WfcCell.js"; +import { SourceGrid } from "./SourceGrid.js"; +import { Xorshift32 } from "../utils/random.js"; /** * A WfcGrid represents the output of a Wave Function Collapse operation. @@ -8,18 +10,26 @@ export class WfcGrid { /** * @param {number} w width (in cells) * @param {number} h height (in cells) - * @param {TrainingGrid} trainingGrid the training grid that will be the source from which we populate this grid. + * @param {SourceGrid} sourceGrid the source grid that will be the source from which we populate this grid. * @type {Xorshift32} pre-seeded pseudo random number generator */ - constructor(w, h, trainingGrid, rng) { + constructor(w, h, sourceGrid, rng) { + /** @type {number} */ this.width = w; + /** @type {number} */ this.height = h; - this.trainingGrid = trainingGrid; + /** @type {SourceGrid} */ + this.sourceGrid = sourceGrid; + /** @type {number[]} */ + this.lowEntropyCellIdCache = sourceGrid.cells.keys; + /** @type {number} */ + this.lowestEntropy = sourceGrid.dim ** 2; + /** @type {Xorshift32} */ this.rng = rng; - // - // Populate the cells so each has all available options - // For now, this means *copying* all TrainingCell options into each cell + /** @type {WfcCell[]} */ + this.cells = []; + this.reset(); } @@ -27,139 +37,165 @@ export class WfcGrid { console.log("Resetting Cells"); const [w, h] = [this.width, this.height]; const len = w * h; - this.cells = new Array(len); + this.cells = []; for (let i = 0; i < len; i++) { const x = i % w; const y = Math.floor(i / w); - this.cells[i] = new WfcCell(i, x, y, this.trainingGrid.clone().pixels); + this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells)); } console.log("Done"); } /** * Get the cells that currently have the lowest entropy - * @returns {number[]} */ - cellsIdsWithLowestEntropy() { - console.log("Finding cells with lowest entopy"); - let result = []; + refreshLowEntropyCellIdCache() { + this.lowEntropyCellIdCache = []; // set lowestEntropy to the highest possible entropy, // and let's search for lower entropy in the cells - let lowestEntropy = this.trainingGrid.dim ** 2; + this.lowestEntropy = this.sourceGrid.dim ** 2; this.cells.forEach((cell, idx) => { - console.log("\t checking cell %d (entropy: %d)", idx, cell.getEntropy()); + const entropy = cell.getEntropy(); + + // Cell is locked in, and should not be included + if (entropy <= 1) { + return; + } + // // Have we found cells with low entropy? - if (cell.getEntropy() < lowestEntropy) { + if (entropy < this.lowestEntropy) { // we've found a cell with lower entropy that the ones we've been looking // at so far Clear the search results and start over with this cell's // entropy as our target - result = [idx]; - lowestEntropy = cell.getEntropy(); + this.lowEntropyCellIdCache = [idx]; + this.lowestEntropy = entropy; return; } // // Cell matches current entropy level, add it to search results. - if (cell.getEntropy() === lowestEntropy) { + if (entropy === this.lowestEntropy) { // Cell matches our current level of entropy, so we add it to our search results. // at so far! Clear the results and start over. - result.push(idx); + this.lowEntropyCellIdCache.push(idx); return; } }); - if (result.length <= 0) { - console.log("Found zero lowest-entropy cells.", { lowestEntropy }); + if (this.lowEntropyCellIdCache.length === 0) { + console.log("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy }); } - - return result; } - collapse() { - console.log("Starting collaps()"); - let count = this.cells.length; - while (count > 0) { - count--; - // Get a list of possible target cells - const lowEntropyCellIds = this.cellIdsWithLowestEntropy(); + /** + * Collapse the grid by one iteration by locking in a random option for the given cell. + * + * If no cell given, a random cell will be chosen from the cache of lowest-entropy cells. + * + * @param {number?} The index of the cell that is to be collapsed around + */ + collapse(cellId = undefined) { + if (this.lowEntropyCellIdCache.length === 0) { + this.refreshLowEntropyCellIdCache(); + } - // - // We've hit a dead end - // No appropriate target cells found. - if (lowEntropyCellIds.length === 0) { - console.log("Found no lowest-entropy cells. This should not happen"); - return count; - } - - const rCellId = this.rng.randomElement(lowEntropyCellIds); - const rCell = this.cells[rCellId]; - - /** @type {TrainingCell} a randomly chosen option that was available to rCell */ - const rOption = this.rng.randomElement(rCell.options); - - // Lock in the choice for this cell - rCell.options = [rOption]; - - // _____ ____ _ _ - // | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| | - // | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / | - // | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_| - // |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_) - // Locking in this cell has changed the grid. - // We must look at the cell's cardinal neighbours and update their options. - for (let nArr of this.getNeighboursFor(rCell)) { - /** @type {number} direction of the neighbour */ - const neighbourDirection = nArr[0]; - - /** @type {WfcCell} the neighbouring cell */ - const neighbourCell = nArr[1]; - - // Clear the neighbour's options, and - // repopulate with valid options. - const newOptions = []; - - for (let neighbourOption of neighbourCell.options) { - if (neighbourOption.potentialNeighbours(rOption, neighbourDirection)) { - newOptions.push(neighbourOption); - } - } - - // We've collapsed too deep. - if (newOptions.length === 0) { - console.error("We've removed all options from a neighbour!", { - rCell, - rOption, - neighbourCell, - neighbourDirection, - newOptions, - }); - return false; - } - - neighbourCell.options = newOptions; + if (cellId === undefined) { + cellId = this.rng.randomElement(this.lowEntropyCellIdCache); + if (cellId === undefined) { + throw new Error("Could not find a valid cell to start the collapse"); } } - console.log("Done"); - return 0; + + const targetCell = this.cells[cellId]; + if (!targetCell) { + throw new Error(`Could not find cell with index ${cellId}`); + } + + /** @type {SourceCell} a randomly chosen option that was available to targetCell */ + const targetOption = this.rng.randomElement(targetCell.options); + + // Lock in the choice for this cell + targetCell.options = [targetOption]; + + // _____ ____ _ _ + // | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| | + // | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / | + // | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_| + // |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_) + // Locking in this cell has changed the grid. + // We must look at the cell's cardinal neighbours and update their options. + for (let nArr of this.neighbourCells(targetCell)) { + /** @type {number} direction of the neighbour */ + const neighbourDirection = nArr[0]; + + /** @type {WfcCell} the neighbouring cell */ + const neighbourCell = nArr[1]; + + // Clear the neighbour's options, and + // repopulate with valid options. + const newOptions = []; + + for (let neighbourOption of neighbourCell.options) { + if (neighbourOption.potentialNeighbours(targetOption, neighbourDirection)) { + newOptions.push(neighbourOption); + } + } + + const newEntropyLevel = newOptions.length; + + // We've collapsed too deep. + if (newOptions.length === 0) { + const oldOptions = neighbourCell.options; + neighbourCell.options = newOptions; + console.error("We've removed all options from a neighbour!", { + targetCell, + targetOption, + neighbourCell, + oldOptions, + Direction: Direction[neighbourDirection], + }); + return false; + } + + neighbourCell.options = newOptions; + + if (newEntropyLevel < this.lowestEntropy) { + this.lowestEntropy = newEntropyLevel; + this.lowEntropyCellIdCache = []; + } + if (newEntropyLevel === this.lowestEntropy) { + if (!this.lowEntropyCellIdCache.includes(neighbourCell.i)) { + this.lowEntropyCellIdCache.push(neighbourCell.i); + } + } + } + return true; } /** * Get the neighbours of a cell. + * @param {WfcCell} cell */ - getNeighboursFor(cell) { + neighbourCells(cell) { const result = []; + // + // Northern neighbour + // const yNorth = cell.y - 1; - if (yNorth >= 0) { + if (yNorth > 0) { const xNorth = cell.x; const idx = this.width * yNorth + xNorth; result.push([Direction.N, this.cells[idx]]); } + // + // Southern neighbour + // const ySouth = cell.y + 1; if (ySouth < this.height) { const xSouth = cell.x; @@ -167,6 +203,9 @@ export class WfcGrid { result.push([Direction.S, this.cells[idx]]); } + // + // Eastern neighbour + // const xEast = cell.x + 1; if (xEast < this.width) { const yEast = cell.y; @@ -174,6 +213,9 @@ export class WfcGrid { result.push([Direction.E, this.cells[idx]]); } + // + // Western neighbour + // const xWest = cell.x - 1; if (xWest >= 0) { const yWest = cell.y; diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html new file mode 100644 index 0000000..1c5cf30 --- /dev/null +++ b/frontend/ascii_dungeon_crawler.html @@ -0,0 +1,482 @@ + + + + + + ASCII Dungeon Crawler + + + +
+
+
+
Use WASD or Arrow Keys to move and arrow keys to turn
+
+
+
Load your map (# = walls, space = floor):
+
+ +

+ +
+
+ + + + diff --git a/frontend/cellular_automata_map_generator.html b/frontend/cellular_automata_map_generator.html new file mode 100644 index 0000000..b350530 --- /dev/null +++ b/frontend/cellular_automata_map_generator.html @@ -0,0 +1,407 @@ + + + + + + Cellular Automata Map Generator + + + +
+

Cellular Automata Map Generator

+ +
+
+ + +
+ +
+ + +
+ +
+ + + 45% +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ Wall +
+
+
+ Floor +
+
+
+ Water (Island mode) +
+
+ +
+ How it works: Cellular automata uses simple rules applied iteratively. Each cell becomes a wall or floor based on its neighbors. + This creates organic, cave-like structures perfect for game maps! +
+
+ + + + \ No newline at end of file diff --git a/frontend/client.js b/frontend/client.js index a618676..266fd82 100755 --- a/frontend/client.js +++ b/frontend/client.js @@ -1,7 +1,6 @@ import { crackdown } from "../utils/crackdown.js"; import { parseArgs } from "../utils/parseArgs.js"; import { MessageType } from "../utils/messages.js"; -import { sprintf } from "sprintf-js"; /** Regex to validate if a :help [topic] command i entered correctly */ const helpRegex = /^:help(?:\s+(.*))?$/; @@ -107,6 +106,7 @@ class MUDClient { }; this.websocket.onerror = (error) => { + console.log("Websocket error", error); this.updateStatus("Connection Error", "error"); this.writeToOutput("Connection error occurred. Retrying...", { class: "error" }); }; @@ -203,7 +203,7 @@ class MUDClient { let help = helpRegex.exec(inputText); if (help) { console.log("here"); - help[1] ? this.send(MshType.HELP, help[1].trim()) : this.send(MshType.HELP); + help[1] ? this.send(MessageType.HELP, help[1].trim()) : this.send(MessageType.HELP); this.echo(inputText); return; } diff --git a/frontend/dungeon_generator.html b/frontend/dungeon_generator.html new file mode 100644 index 0000000..7f2f602 --- /dev/null +++ b/frontend/dungeon_generator.html @@ -0,0 +1,573 @@ + + + + + + ASCII Dungeon Generator + + + +
+

⚔️ ASCII DUNGEON GENERATOR ⚔️

+ +
+
+ + + 60 +
+
+ + + 40 +
+
+ + + 10 +
+
+ +
+ + +
+ +
+ +
+

Legend:

+
# - Wall
+
. - Floor
+
+ - Door
+
@ - Player Start
+
$ - Treasure
+
! - Monster
+
^ - Trap
+
+
+ + + + diff --git a/frontend/progen.html b/frontend/progen.html index 8565a32..4c5d918 100755 --- a/frontend/progen.html +++ b/frontend/progen.html @@ -28,11 +28,10 @@

Tools

-
- +
diff --git a/frontend/progen.js b/frontend/progen.js index 6e8c7fb..fcee355 100755 --- a/frontend/progen.js +++ b/frontend/progen.js @@ -1,18 +1,17 @@ import { sprintf } from "sprintf-js"; import { Xorshift32 } from "../utils/random.js"; import { WfcGrid } from "./WfcGrid.js"; -import { TrainingCell } from "./TrainingCell.js"; -import { TrainingGrid } from "./TrainingGrid.js"; +import { SourceCell } from "./SourceCell.js"; +import { SourceGrid } from "./SourceGrid.js"; -class PainApp { - /** @type {string} */ - activeColor = "#000"; - /** @type {string} */ - currentTool = "draw"; +class PainterApp { + /** @type {number} The index of the color we're currently painting with */ + toolPaletteIndex = 0; + + /** @type {string} Mode. Draw or fill */ + mode = "draw"; /** @type {boolean} */ isDrawing = false; - /** @type {boolean} */ - drawingMode = false; /**@param {string[]} pal */ constructor(dim, pal, gridElement, paletteElement, previewElement) { @@ -20,67 +19,65 @@ class PainApp { this.dim = dim; /** @type {string[]} Default color palette */ this.palette = pal; - /** @type {string[]} */ - this.samplePixels = new Array(dim ** 2).fill(pal[pal.length - 1]); /** @type {HTMLElement} */ this.gridElement = gridElement; /** @type {HTMLElement} */ this.previewElement = previewElement; /** @type {HTMLElement} */ this.paletteElement = paletteElement; - /** @type {HTMLInputElement} */ - this.trainingImage = new TrainingGrid( - this.samplePixels.map(() => { - return new TrainingCell(); - }), - ); - - this.createGrid(); - this.createColorPalette(); - this.updatePreview(); - this.setActiveColor(pal[0]); - this.updateTrainingGrid(); + this.reset(); } - createGrid() { + reset() { + // Assume the "background" color is always the last color in the palette. + const fillWith = 0; + this.sourceGrid = new SourceGrid( + Array(this.dim ** 2) + .fill(null) + .map(() => new SourceCell(new Uint8Array(9).fill(fillWith))), + ); + + this.createGridHtmlElements(); + this.createPaletteSwatch(); + this.updatePreview(); + this.setToolPaletteIndex(1); + this.updateSourceGrid(); + } + + createGridHtmlElements() { this.gridElement.innerHTML = ""; for (let i = 0; i < this.dim ** 2; i++) { const pixel = document.createElement("div"); - pixel.className = "pixel"; - pixel.setAttribute("data-index", i); - pixel.style.backgroundColor = this.samplePixels[i]; + pixel.className = `pal-idx-${this.getCell(i)}`; + pixel.setAttribute("id", "cell-idx-" + i); - pixel.addEventListener("mousedown", (e) => this.startDrawing(e, i)); - pixel.addEventListener("mouseenter", (e) => this.continueDrawing(e, i)); - pixel.addEventListener("mouseup", () => this.stopDrawing()); + pixel.addEventListener("mousedown", (e) => this.mouseDown(e, i)); this.gridElement.appendChild(pixel); } // Prevent context menu and handle mouse events this.gridElement.addEventListener("contextmenu", (e) => e.preventDefault()); - document.addEventListener("mouseup", () => this.stopDrawing()); } - createColorPalette() { + createPaletteSwatch() { this.paletteElement.innerHTML = ""; this.palette.forEach((color, paletteIndex) => { const swatch = document.createElement("div"); swatch.classList.add("color-swatch"); swatch.classList.add(`pal-idx-${paletteIndex}`); - swatch.classList.add(`pal-color-${color}`); swatch.style.backgroundColor = color; - swatch.onclick = () => this.setActiveColor(paletteIndex); + swatch.onclick = () => this.setToolPaletteIndex(paletteIndex); this.paletteElement.appendChild(swatch); }); } - setActiveColor(paletteIndex) { + setToolPaletteIndex(paletteIndex) { // - this.activeColor = this.palette[paletteIndex]; + this.toolPaletteIndex = paletteIndex; const colorSwatches = this.paletteElement.querySelectorAll(".color-swatch"); colorSwatches.forEach((swatch) => { @@ -90,7 +87,7 @@ class PainApp { } setTool(tool) { - this.currentTool = tool; + this.mode = tool; document.querySelectorAll(".tools button").forEach((btn) => btn.classList.remove("active")); document.getElementById(tool + "Btn").classList.add("active"); @@ -105,71 +102,56 @@ class PainApp { } } - toggleDrawingMode() { - this.drawingMode = !this.drawingMode; - const btn = document.getElementById("drawModeBtn"); - if (this.drawingMode) { - btn.textContent = "Drawing Mode: ON"; - btn.classList.add("drawing-mode"); - document.getElementById("status").textContent = "Drawing mode ON - Click and drag to paint"; - } else { - btn.textContent = "Drawing Mode: OFF"; - btn.classList.remove("drawing-mode"); - document.getElementById("status").textContent = "Drawing mode OFF - Click individual pixels"; - } - } - - startDrawing(e, index) { + mouseDown(e, index) { e.preventDefault(); - this.isDrawing = true; this.applyTool(index); } - /** @param {MouseEvent} e */ - continueDrawing(e, index) { - if (this.isDrawing && this.drawingMode) { - this.applyTool(index); - return; + getCell(idx, y = undefined) { + if (y === undefined) { + return this.sourceGrid.cells[idx].value; } - this.updateTrainingCell(index); - this.updatePreview(index); - } - - stopDrawing() { - this.isDrawing = false; + // Treat idx as an x-coordinate, and calculate an index + return this.sourceGrid.cells[y * this.dim + idx].value; } applyTool(index) { - switch (this.currentTool) { + switch (this.mode) { case "draw": - this.setPixel(index, this.activeColor); + this.setPixel(index, this.toolPaletteIndex); break; case "fill": - this.floodFill(index, this.samplePixels[index], this.activeColor); + this.floodFill(index, this.toolPaletteIndex); break; } } - setPixel(index, color) { - this.samplePixels[index] = color; - const pixel = document.querySelector(`[data-index="${index}"]`); - pixel.style.backgroundColor = color; + setPixel(cellIdx, palIdx) { + const pixEl = document.getElementById("cell-idx-" + cellIdx); + this.sourceGrid.cells[cellIdx].value = palIdx; + pixEl.className = "pal-idx-" + palIdx; + this.updateSourceCell(cellIdx); this.updatePreview(); } - floodFill(startIndex, targetColor, fillColor) { - if (targetColor === fillColor) return; + floodFill(startIndex, fillColorPalIdx) { + const targetPalIdx = this.getCell(startIndex); + + if (targetPalIdx === fillColorPalIdx) { + return; + } const stack = [startIndex]; const visited = new Set(); while (stack.length > 0) { const index = stack.pop(); - if (visited.has(index) || this.samplePixels[index] !== targetColor) continue; + + if (visited.has(index) || this.getCell(index) !== targetPalIdx) continue; visited.add(index); - this.setPixel(index, fillColor); + this.setPixel(index, fillColorPalIdx); // Add neighbors const row = Math.floor(index / this.dim); @@ -182,113 +164,80 @@ class PainApp { } } - clearCanvas() { - this.samplePixels.fill("#fff"); - this.createGrid(); - this.updatePreview(); - } - randomFill() { for (let i = 0; i < this.dim ** 2; i++) { - const randomColor = this.palette[Math.floor(Math.random() * this.palette.length)]; - this.setPixel(i, randomColor); + this.setPixel(i, Math.floor(Math.random() * this.palette.length)); } } invertColors() { for (let i = 0; i < this.dim ** 2; i++) { - const color = this.samplePixels[i]; - const r = 15 - parseInt(color.substr(1, 1), 16); - const g = 15 - parseInt(color.substr(2, 1), 16); - const b = 15 - parseInt(color.substr(3, 1), 16); - const inverted = - "#" + - r.toString(16) + // red - g.toString(16) + // green - b.toString(16); // blue + const cell = this.getCell(i); + + const inverted = cell % 2 === 0 ? cell + 1 : cell - 1; this.setPixel(i, inverted); - - if (i % 10 === 0) { - console.log("invertion", { - color, - r, - g, - b, - inverted, - }); - } } + this.setToolPaletteIndex( + this.toolPaletteIndex % 2 === 0 ? this.toolPaletteIndex + 1 : this.toolPaletteIndex - 1, + ); } - updatePreview(subImageIdx = undefined) { + updatePreview() { const canvas = document.createElement("canvas"); - if (subImageIdx === undefined) { - canvas.width = this.dim; - canvas.height = this.dim; - const ctx = canvas.getContext("2d"); + canvas.width = this.dim; + canvas.height = this.dim; + const ctx = canvas.getContext("2d"); - for (let i = 0; i < this.dim ** 2; i++) { - const x = i % this.dim; - const y = Math.floor(i / this.dim); - ctx.fillStyle = this.samplePixels[i]; - ctx.fillRect(x, y, 1, 1); - } - } else { - canvas.width = 3; - canvas.height = 3; - const ctx = canvas.getContext("2d"); - - for (let i = 0; i < 3 * 3; i++) { - // - const x = i % 3; - const y = Math.floor(i / 3); - ctx.fillStyle = this.trainingImage.pixels[subImageIdx].subPixels[i]; - ctx.fillRect(x, y, 1, 1); - } + for (let i = 0; i < this.dim ** 2; i++) { + const x = i % this.dim; + const y = Math.floor(i / this.dim); + ctx.fillStyle = this.palette[this.getCell(i)]; + ctx.fillRect(x, y, 1, 1); } - this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`; this.previewElement.style.backgroundSize = "100%"; + return; } - updateTrainingGrid() { - for (let i = 0; i < this.samplePixels.length; i++) { - this.updateTrainingCell(i); + updateSourceGrid() { + for (let i = 0; i < this.dim ** 2; i++) { + this.updateSourceCell(i); } } - updateTrainingCell(i) { + /** @param {number} i */ + updateSourceCell(idx) { const dim = this.dim; - const x = i % dim; - const y = Math.floor(i / dim); + const x = idx % dim; + const y = Math.floor(idx / dim); - const colorAt = (dX, dY) => { + const valueAt = (dX, dY) => { const _x = (x + dim + dX) % dim; // add dim before modulo because JS modulo allows negative results const _y = (y + dim + dY) % dim; - return this.samplePixels[_y * dim + _x]; + return this.getCell(_y * dim + _x); }; - this.trainingImage.pixels[i] = new TrainingCell([ + this.sourceGrid.cells[idx].values = new Uint8Array([ // | neighbour // ---------------------|----------- - colorAt(-1, -1), // | northwest - colorAt(0, -1), // | north - colorAt(1, -1), // | northeast + valueAt(-1, -1), // | northwest + valueAt(0, -1), // | north + valueAt(1, -1), // | northeast - colorAt(-1, 0), // | east - this.samplePixels[i], //| -- self -- - colorAt(1, 0), // | west + valueAt(-1, 0), // | east + this.getCell(idx), // | -- self -- + valueAt(1, 0), // | west - colorAt(-1, 1), // | southwest - colorAt(0, 1), // | south - colorAt(1, 1), // | southeast + valueAt(-1, 1), // | southwest + valueAt(0, 1), // | south + valueAt(1, 1), // | southeast ]); } exportAsImage() { const canvas = document.createElement("canvas"); - canvas.width = this.dim; // 9x upscale + canvas.width = this.dim; canvas.height = this.dim; const ctx = canvas.getContext("2d"); ctx.imageSmoothingEnabled = false; @@ -296,7 +245,7 @@ class PainApp { for (let i = 0; i < this.dim ** 2; i++) { const x = i % this.dim; const y = Math.floor(i / this.dim); - ctx.fillStyle = this.samplePixels[i]; + ctx.fillStyle = this.palette[this.getCell(i)]; ctx.fillRect(x, y, 1, 1); } @@ -307,7 +256,7 @@ class PainApp { } exportAsData() { - const data = JSON.stringify(this.samplePixels); + const data = Array.from({ length: this.dim ** 2 }, (_, i) => this.getCell(i)); const blob = new Blob([data], { type: "application/json" }); const link = document.createElement("a"); link.download = "pixel-art-data.json"; @@ -326,13 +275,15 @@ class PainApp { reader.onload = (event) => { try { const data = JSON.parse(event.target.result); - if (Array.isArray(data) && data.length === this.dim ** 2) { - this.samplePixels = data; - this.createGrid(); - this.updatePreview(); - } else { + if (!Array.isArray(data) && data.length === this.dim ** 2) { alert("Invalid data format!"); } + data.forEach((v, k) => { + this.setPixel(k, v); + }); + this.createGridHtmlElements(); + this.updatePreview(); + this.updateSourceGrid(); } catch (error) { alert("Error reading file!" + error); } @@ -344,50 +295,50 @@ class PainApp { } waveFunction() { - this.updateTrainingGrid(); + this.updateSourceGrid(); const wfcImg = new WfcGrid( // this.previewElement.clientWidth, // this.previewElement.clientHeight, - 30, - 30, - this.trainingImage.clone(), + 10, + 10, + this.sourceGrid.clone(), new Xorshift32(Date.now()), ); // Could not "collapse" the image. // We should reset and try again? - let its = wfcImg.collapse(); + let running = true; + let count = 0; + const maxCount = 1000; - if (its > 0) { - throw new Error(`Function Collapse failed with ${its} iterations left to go`); - } + const collapseFunc = () => { + running = wfcImg.collapse(); - const canvas = document.createElement("canvas"); - canvas.width = wfcImg.width; - canvas.height = wfcImg.height; + const canvas = document.createElement("canvas"); + canvas.width = wfcImg.width; + canvas.height = wfcImg.height; - // debug values - canvas.width = 30; - canvas.height = 30; - // - const ctx = canvas.getContext("2d"); - let i = 0; - for (let y = 0; y < canvas.height; y++) { - for (let x = 0; x < canvas.width; x++) { - console.log("pix"); - const cell = wfcImg.cells[i++]; - if (cell.valid) { - ctx.fillStyle = "magenta"; + const ctx = canvas.getContext("2d"); + let i = 0; + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const cell = wfcImg.cells[i++]; + ctx.fillStyle = cell.value; ctx.fillRect(x, y, 1, 1); } } - } - this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`; - this.previewElement.style.backgroundSize = "100%"; + this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`; + this.previewElement.style.backgroundSize = "100%"; + + if (running && ++count < maxCount) { + setTimeout(collapseFunc, 1); + } + }; + collapseFunc(); } } const base_palette = [ - "#000", + "#FFF", "#007", "#00F", "#070", @@ -403,9 +354,9 @@ const base_palette = [ "#FF0", ]; -const palette = new Array(base_palette.length * 2); +const palette = new Array(); -base_palette.forEach((color, idx) => { +base_palette.forEach((color) => { // // Calc inverted color const invR = 15 - Number.parseInt(color.substr(1, 1), 16); @@ -414,11 +365,11 @@ base_palette.forEach((color, idx) => { const invColor = sprintf("#%x%x%x", invR, invG, invB); // populate the palette - palette[idx] = color; - palette[7 * 4 - 1 - idx] = invColor; + palette.push(color); + palette.push(invColor); }); -window.painter = new PainApp( +window.painter = new PainterApp( 9, palette, document.getElementById("gridContainer"), // @@ -426,5 +377,33 @@ window.painter = new PainApp( document.getElementById("preview"), // ); -// share window.dim with the HTML and CSS +// ____ ____ ____ +// / ___/ ___/ ___| +// | | \___ \___ \ +// | |___ ___) |__) | +// \____|____/____/ +//-------------------- + +// +// share the dimensions of the SourceGrid with CSS/HTML document.getElementsByTagName("body")[0].style.setProperty("--dim", window.painter.dim); + +// +// -------------------------------------- +// Add the palette colors as CSS classes +// -------------------------------------- + +const styleElement = document.createElement("style"); +styleElement.type = "text/css"; + +let cssRules = ""; +palette.forEach((color, index) => { + const className = `pal-idx-${index}`; + cssRules += `.${className} { background-color: ${color} !important; }\n`; +}); + +// Add the CSS to the style element +styleElement.innerHTML = cssRules; + +// Append to head +document.head.appendChild(styleElement); diff --git a/models/game.js b/models/game.js index 085ee9d..6194a17 100755 --- a/models/game.js +++ b/models/game.js @@ -7,7 +7,6 @@ * Serializing this object effectively saves the game. */ -import { Config } from "../config.js"; import { isIdSane, miniUid } from "../utils/id.js"; import { Xorshift32 } from "../utils/random.js"; import { Character } from "./character.js"; diff --git a/node_modules/.vite/deps/_metadata.json b/node_modules/.vite/deps/_metadata.json index 97b41a4..c44c90a 100644 --- a/node_modules/.vite/deps/_metadata.json +++ b/node_modules/.vite/deps/_metadata.json @@ -1,13 +1,13 @@ { - "hash": "b248c40f", + "hash": "a9cc42de", "configHash": "86a557ed", - "lockfileHash": "772b6e1c", - "browserHash": "37f1288b", + "lockfileHash": "56518f4e", + "browserHash": "f6412460", "optimized": { "sprintf-js": { "src": "../../sprintf-js/src/sprintf.js", "file": "sprintf-js.js", - "fileHash": "039885aa", + "fileHash": "41b15421", "needsInterop": true } }, diff --git a/scenes/authentication/authenticationScene.js b/scenes/authentication/authenticationScene.js index 8c26578..ee184ce 100755 --- a/scenes/authentication/authenticationScene.js +++ b/scenes/authentication/authenticationScene.js @@ -30,13 +30,8 @@ export class AuthenticationScene extends Scene { this.session.player = this.player; this.session.sendText(["= Success!", "((but I don't know what to do now...))"]); - return; - if (this.player.admin) { - this.session.setScene("new AdminJustLoggedInScene"); - } else { - this.session.setScene("new JustLoggedInScene"); - } + this.session.setScene("new JustLoggedInScene"); } /** diff --git a/scenes/authentication/passwordPrompt.js b/scenes/authentication/passwordPrompt.js index 094ced3..d2adce3 100755 --- a/scenes/authentication/passwordPrompt.js +++ b/scenes/authentication/passwordPrompt.js @@ -38,7 +38,7 @@ export class PasswordPrompt extends Prompt { // // Block users who enter bad passwords too many times. if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) { - this.blockedUntil = Date.now() + Config.accountLockoutSeconds; + this.blockedUntil = Date.now() + Config.accountLockoutSeconds * 1000; this.calamity("You have been locked out for too many failed password attempts, come back later"); return; } @@ -49,7 +49,7 @@ export class PasswordPrompt extends Prompt { if (this.player.blockedUntil > Date.now()) { // // Try to re-login too soon, and your lockout lasts longer. - this.blockedUntil += Config.accountLockoutSeconds; + this.blockedUntil += Config.accountLockoutSeconds * 1000; this.calamity("You have been locked out for too many failed password attempts, come back later"); return; } diff --git a/seeders/itemSeeder.js b/seeders/itemSeeder.js index db109a9..ecefad0 100755 --- a/seeders/itemSeeder.js +++ b/seeders/itemSeeder.js @@ -1,4 +1,3 @@ -import { ItemBlueprint } from "../models/item.js"; import { gGame } from "../models/globals.js"; // @@ -12,7 +11,6 @@ import { gGame } from "../models/globals.js"; // Seed the Game.ItemBlueprint store export class ItemSeeder { seed() { - // // __ __ // \ \ / /__ __ _ _ __ ___ _ __ ___ // \ \ /\ / / _ \/ _` | '_ \ / _ \| '_ \/ __| @@ -54,7 +52,6 @@ export class ItemSeeder { specialEffect: "TBD", }); - // // _ // / \ _ __ _ __ ___ ___ _ __ ___ // / _ \ | '__| '_ ` _ \ / _ \| '__/ __| @@ -76,7 +73,6 @@ export class ItemSeeder { armorHitPoints: 6, }); - // // _ ___ _ // | |/ (_) |_ ___ // | ' /| | __/ __| diff --git a/utils/random.js b/utils/random.js index 7736dce..5e1f34c 100644 --- a/utils/random.js +++ b/utils/random.js @@ -64,6 +64,9 @@ export class Xorshift32 { * @returns {T} One element from the array. * @return {} */ randomElement(arr) { + if (arr instanceof Set) { + arr = [...arr]; + } const idx = this.lowerThan(arr.length); return arr[idx];