diff --git a/server/frontend/client.js b/server/frontend/client.js index d219812..a618676 100755 --- a/server/frontend/client.js +++ b/server/frontend/client.js @@ -2,7 +2,6 @@ import { crackdown } from "../utils/crackdown.js"; import { parseArgs } from "../utils/parseArgs.js"; import { MessageType } from "../utils/messages.js"; import { sprintf } from "sprintf-js"; -import { Config } from "../config.js"; /** Regex to validate if a :help [topic] command i entered correctly */ const helpRegex = /^:help(?:\s+(.*))?$/; @@ -75,9 +74,10 @@ class MUDClient { connect() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, Config.port); + // TODO Fix. Port should not be hardcoded + const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000"); - console.log(window.location); + console.log(wsUrl); this.updateStatus("Connecting...", "connecting"); diff --git a/server/frontend/manifest.json b/server/frontend/manifest.json index 9e32466..bb83ec2 100755 --- a/server/frontend/manifest.json +++ b/server/frontend/manifest.json @@ -2,19 +2,10 @@ "name": "", "short_name": "", "icons": [ - { - "src": "./img/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "./img/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } + { "src": "./img/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "./img/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } - diff --git a/server/frontend/progen.html b/server/frontend/progen.html new file mode 100755 index 0000000..0c2cdbe --- /dev/null +++ b/server/frontend/progen.html @@ -0,0 +1,61 @@ + + + + + + WebSocket MUD + + + +
+

9×9 Pixel Art Editor

+
+ +
+
Click a pixel to start drawing
+
+ +
+
+

Current Color

+
+ + +

Color Palette

+
+ +
+
+ +
+
+

Tools

+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+
+ + + + diff --git a/server/frontend/progen.js b/server/frontend/progen.js new file mode 100755 index 0000000..5256ba2 --- /dev/null +++ b/server/frontend/progen.js @@ -0,0 +1,393 @@ +class PaintProg { + /** @type {string} */ + currentColor = "#000"; + /** @type {string} */ + currentTool = "draw"; + /** @type {boolean} */ + isDrawing = false; + /** @type {boolean} */ + drawingMode = false; + + /**@param {string[]} pal */ + constructor(dim, pal, gridElement, paletteElement, currentColorElement, colorPickerElement, previewElement) { + /** @type {number} */ + 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 {HTMLElement} */ + this.currentColorElement = currentColorElement; + /** @type {HTMLInputElement} */ + this.colorPickerElement = colorPickerElement; + + this.subImages = Array.from({ length: this.samplePixels.length }, () => [...this.samplePixels]); + + this.createGrid(); + this.createColorPalette(); + this.updatePreview(); + this.setCurrentColor(pal[0]); + this.updateAllSubimages(); + } + + createGrid() { + 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.addEventListener("mousedown", (e) => this.startDrawing(e, i)); + pixel.addEventListener("mouseenter", (e) => this.continueDrawing(e, i)); + pixel.addEventListener("mouseup", () => this.stopDrawing()); + + this.gridElement.appendChild(pixel); + } + + // Prevent context menu and handle mouse events + this.gridElement.addEventListener("contextmenu", (e) => e.preventDefault()); + document.addEventListener("mouseup", () => this.stopDrawing()); + } + + createColorPalette() { + this.paletteElement.innerHTML = ""; + + this.palette.forEach((color) => { + const swatch = document.createElement("div"); + swatch.className = "color-swatch"; + swatch.style.backgroundColor = color; + swatch.onclick = () => this.setCurrentColor(color); + this.paletteElement.appendChild(swatch); + }); + } + + setCurrentColor(color) { + this.currentColor = color; + this.currentColorElement.style.backgroundColor = color; + this.colorPickerElement.value = color; + + // Update active swatch + // NOTE: this was "document.querySelectorAll " + this.paletteElement.querySelectorAll(".color-swatch").forEach((swatch) => { + swatch.classList.toggle("active", swatch.style.backgroundColor === this.colorToRgb(color)); + }); + } + + colorToRgb(hex) { + const r = parseInt(hex.substr(1, 2), 16); + const g = parseInt(hex.substr(3, 2), 16); + const b = parseInt(hex.substr(5, 2), 16); + return `rgb(${r}, ${g}, ${b})`; + } + + openColorPicker() { + this.colorPickerElement.click(); + } + + setTool(tool) { + this.currentTool = tool; + document.querySelectorAll(".tools button").forEach((btn) => btn.classList.remove("active")); + document.getElementById(tool + "Btn").classList.add("active"); + + const status = document.getElementById("status"); + switch (tool) { + case "draw": + status.textContent = "Drawing mode - Click to paint pixels"; + break; + case "fill": + status.textContent = "Fill mode - Click to fill area"; + break; + } + } + + 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) { + e.preventDefault(); + this.isDrawing = true; + this.applyTool(index); + } + + /** @param {MouseEvent} e */ + continueDrawing(e, index) { + if (this.isDrawing && this.drawingMode) { + this.applyTool(index); + return; + } + + this.updateSingleSubImage(index); + this.updatePreview(index); + } + + stopDrawing() { + this.isDrawing = false; + } + + applyTool(index) { + switch (this.currentTool) { + case "draw": + this.setPixel(index, this.currentColor); + break; + case "fill": + this.floodFill(index, this.samplePixels[index], this.currentColor); + break; + } + } + + setPixel(index, color) { + this.samplePixels[index] = color; + const pixel = document.querySelector(`[data-index="${index}"]`); + pixel.style.backgroundColor = color; + this.updatePreview(); + } + + floodFill(startIndex, targetColor, fillColor) { + if (targetColor === fillColor) 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; + + visited.add(index); + this.setPixel(index, fillColor); + + // Add neighbors + const row = Math.floor(index / this.dim); + const col = index % this.dim; + + if (row > 0) stack.push(index - this.dim); // up + if (row < 8) stack.push(index + this.dim); // down + if (col > 0) stack.push(index - 1); // left + if (col < 8) stack.push(index + 1); // right + } + } + + 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); + } + } + + 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 + + this.setPixel(i, inverted); + + if (i % 10 === 0) { + console.log("invertion", { + color, + r, + g, + b, + inverted, + }); + } + } + } + + updatePreview(subImageIdx = undefined) { + const canvas = document.createElement("canvas"); + if (subImageIdx === undefined) { + 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.subImages[subImageIdx][i]; + ctx.fillRect(x, y, 1, 1); + } + } + + this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`; + this.previewElement.style.backgroundSize = "100%"; + } + + updateAllSubimages() { + for (let i = 0; i < this.samplePixels.length; i++) { + this.updateSingleSubImage(i); + } + } + + updateSingleSubImage(i) { + const dim = this.dim; + const len = dim ** 2; + const x = i % dim; + const y = Math.floor(i / dim); + + const colorAt = (dX, dY) => { + const _x = (x + dim + dX) % dim; // add dim before modulo because JS modulo allows negative results + const _y = (y + dim + dY) % dim; + if (y == 0 && dY < 0) { + console.log(_x, _y); + } + return this.samplePixels[_y * dim + _x]; + }; + + this.subImages[i] = [ + // | neighbour + // ---------------------|----------- + colorAt(-1, -1), // | northwest + colorAt(0, -1), // | north + colorAt(1, -1), // | northeast + + colorAt(-1, 0), // | east + this.samplePixels[i], //| -- self -- + colorAt(1, 0), // | west + + colorAt(-1, 1), // | southwest + colorAt(0, 1), // | south + colorAt(1, 1), // | southeast + ]; + } + + exportAsImage() { + const canvas = document.createElement("canvas"); + canvas.width = 90; // 9x upscale + canvas.height = 90; + const ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + + for (let i = 0; i < this.dim ** 2; i++) { + const x = (i % this.dim) * 10; + const y = Math.floor(i / this.dim) * 10; + ctx.fillStyle = this.samplePixels[i]; + ctx.fillRect(x, y, 10, 10); + } + + const link = document.createElement("a"); + link.download = `pixel-art-${this.dim}x${this.dim}.png`; + link.href = canvas.toDataURL(); + link.click(); + } + + exportAsData() { + const data = JSON.stringify(this.samplePixels); + const blob = new Blob([data], { type: "application/json" }); + const link = document.createElement("a"); + link.download = "pixel-art-data.json"; + link.href = URL.createObjectURL(blob); + link.click(); + } + + importData() { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + 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 { + alert("Invalid data format!"); + } + } catch (error) { + alert("Error reading file!"); + } + }; + reader.readAsText(file); + } + }; + input.click(); + } + + // Initialize the editor +} +const palette = [ + "#000", + "#008", + "#00f", + "#080", + "#088", + "#0f0", + "#0ff", + "#800", + "#808", + "#80f", + "#880", + "#888", + "#88f", + "#8f8", + "#8ff", + "#ccc", + "#f00", + "#f0f", + "#f80", + "#f88", + "#f8f", + "#ff0", + "#ff8", + "#fff", +]; + +window.painter = new PaintProg( + 9, + palette, + document.getElementById("gridContainer"), // + document.getElementById("colorPalette"), // + document.getElementById("currentColor"), // + document.getElementById("colorPicker"), // + document.getElementById("preview"), // +); + +// share window.dim with the HTML and CSS +document.getElementsByTagName("body")[0].style.setProperty("--dim", window.painter.dim); diff --git a/server/frontend/progen.scss b/server/frontend/progen.scss new file mode 100644 index 0000000..acc4618 --- /dev/null +++ b/server/frontend/progen.scss @@ -0,0 +1,184 @@ +// number number of cells per side in the source picture +$dim: 9; +$pixSize: 80px; +$gridSize: calc($dim * $pixSize); + +body { + margin: 0; + padding: 20px; + font-family: Arial, sans-serif; + background-color: #2c3e50; + color: white; + display: flex; + gap: 30px; + min-height: 100vh; +} + +.editor-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(var(--dim), 1fr); + grid-template-rows: repeat(var(--dim), 1fr); + gap: 1px; + width: $gridSize; + height: $gridSize; + background-color: #34495e; + padding: 10px; + border-radius: 8px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); +} + +.pixel { + background-color: #ffffff; + cursor: pointer; + border: 1px solid #7f8c8d; + transition: transform 0.1s ease; + position: relative; + aspect-ratio: 1; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + + .subpixel { + width: 100%; + height: 100%; + background-color: transparent; + } +} + +.pixel:hover { + transform: scale(1.1); + z-index: 1; + border-color: #3498db; +} + +.controls-panel { + background: #34495e; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + width: 300px; +} + +.color-picker-section { + margin-bottom: 20px; +} + +.current-color { + width: 60px; + height: 60px; + border: 3px solid white; + border-radius: 8px; + margin: 10px 0; + cursor: pointer; + background-color: #000000; +} + +.color-palette { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + margin: 10px 0; +} + +.color-swatch { + width: 25px; + height: 25px; + cursor: pointer; + border: 2px solid transparent; + border-radius: 4px; + transition: transform 0.1s ease; +} + +.color-swatch:hover { + transform: scale(1.2); + border-color: white; +} + +.color-swatch.active { + border-color: #3498db; + transform: scale(1.1); +} + +.tools { + margin: 20px 0; +} + +.tool-group { + margin: 15px 0; +} + +.tool-group h3 { + margin: 0 0 10px 0; + color: #ecf0f1; + font-size: 14px; +} + +button { + background: #3498db; + color: white; + border: none; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + margin: 2px; + font-size: 12px; + transition: background 0.2s ease; +} + +button:hover { + background: #2980b9; +} + +button.active { + background: #e74c3c; +} + +input[type="color"] { + width: 40px; + height: 30px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.export-section { + margin-top: 5px; + padding-top: 5px; + border-top: 1px solid #7f8c8d; +} + +.preview { + width: 100%; + aspect-ratio: 1 / 1; + /* height: 90px; */ + background: white; + border: 2px solid #bdc3c7; + border-radius: 4px; + margin: 10px 0; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; +} + +.grid-lines { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + opacity: 0.3; +} + +.drawing-mode { + font-weight: bold; + /* color: #e74c3c; */ + background: #383; +} diff --git a/server/frontend/subpixel.js b/server/frontend/subpixel.js new file mode 100755 index 0000000..4fe067c --- /dev/null +++ b/server/frontend/subpixel.js @@ -0,0 +1,137 @@ +const NW = 0; +const N = 1; +const NE = 2; +const E = 3; +const C = 4; +const W = 5; +const SW = 6; +const S = 7; +const SE = 8; + +export class TrainingImage { + /** @param {number} w Width (in pixels) of the training image */ + /** @param {number} h Height (in pixels) of the training image */ + /** @param {Array} pixels + * + */ + constructor(w, h, pixels) { + this.pixels = pixels; + + this.w = w; + + this.h = h; + } + + establishFriendships() { + // + // This can be optimized a helluvalot much + this.pixels.forEach((pix1) => { + this.pixels.forEach((pix2) => { + pix1.addFriend(pix2); + }); + }); + } +} + +/** + * Represents a 3x3 grid of trianing-pixels, that are used as building blocks for procedurally generated + * images. In reality, only the center color will be included in the output image. The 8 surrounding + * colors are there to establish restrictions/options for the WFC algorithm. + */ +export class TrainingPixel { + /** @type {string[9]} The 9 sub pixels that make up this TrainingPixel */ + subPixels; + + /** @type {TrainingPixel[]} The other TrainingPixels that we can share eastern borders with */ + friendsEast = new Set(); + + /** @type {TrainingPixel[]} The other TrainingPixels that we can share western borders with */ + friendsWest = new Set(); + + /** @type {TrainingPixel[]} The other TrainingPixels that we can share northern borders with */ + friendsNorth = new Set(); + + /** @type {TrainingPixel[]} The other TrainingPixels that we can share southern borders with */ + friendsSouth = new Set(); + + /** @type {TrainingPixel[]} Superset of all the friends this TrainingPixel has */ + friendsTotal = new Set(); + + constructor(subPixels) { + this.subPixels = subPixels; + } + + /** + * @param {TrainingPixel} other + * + * @returns {N,S,E,W,false} + */ + addFriend(other) { + // sadly, we're not allowed to be friends with ourselves. + if (this === other) { + return false; + } + + // Check if other can be placed to the east of me + if ( + // My center col must match their west col + this.subPixels[N] === other.subPixels[NW] && + this.subPixels[C] === other.subPixels[W] && + this.subPixels[S] === other.subPixels[SW] && + // My east col must match their center col + this.subPixels[NE] === other.subPixels[N] && + this.subPixels[E] === other.subPixels[C] && + this.subPixels[SE] === other.subPixels[S] + ) { + this.friendsEast.add(other); + other.friendsWest.add(this); + } + + // check if other can be placed west of me + if ( + // My center col must match their east col + this.subPixels[N] === other.subPixels[NE] && + this.subPixels[C] === other.subPixels[E] && + this.subPixels[S] === other.subPixels[SE] && + // My west col must match their center col + this.subPixels[NW] === other.subPixels[N] && + this.subPixels[W] === other.subPixels[C] && + this.subPixels[SW] === other.subPixels[S] + ) { + this.friendsWest.add(other); + other.friendsEast.add(this); + } + + // check if other can be placed to my north + if ( + // my middle row must match their south row + this.subPixels[W] === other.subPixels[SW] && + this.subPixels[C] === other.subPixels[S] && + this.subPixels[E] === other.subPixels[SE] && + // my north row must match their middle row + this.subPixels[NW] === other.subPixels[W] && + this.subPixels[NC] === other.subPixels[C] && + this.subPixels[NE] === other.subPixels[E] + ) { + this.friendsNorth.add(other); + other.friendsSouth.add(this); + } + + // check if other can be placed to my south + if ( + // my middle row must match their north row + this.subPixels[W] === other.subPixels[NW] && + this.subPixels[C] === other.subPixels[N] && + this.subPixels[E] === other.subPixels[SE] && + // my south row must match their middle row + this.subPixels[SW] === other.subPixels[W] && + this.subPixels[SC] === other.subPixels[c] && + this.subPixels[SE] === other.subPixels[E] + ) { + this.friendsSouth.add(other); + other.friendsNorth.add(this); + } + + return false; + } +} diff --git a/server/ideas.md b/server/ideas.md index 905686c..0fa289b 100755 --- a/server/ideas.md +++ b/server/ideas.md @@ -83,12 +83,19 @@ Each group chat has a name. - `Donjons` - GameMode = _Crawling_: Lady Gurthie Firefoot is Crawling the Donjons of Speematoforr. - Played like `Knights of Pen and Paper` + - WebGL: Actual rendered 3d, but black and white. + - Texture pack is just ascii text (TEXTures). + - Possibly Procedurally generated + - Most likely like this: https://www.youtube.com/watch?v=D1jLK4TG6O4&list=LL + - Animations like `Stonekeep` - only showing animation when the player moves. + - Monsters are just outlines of monster shapes, shaded with variably-sized letters + that spell out the monster's name. - Procedurally (pre-)generated dungeons - - Very simple square dungeon layout (like KoPaP). - Every time you enter a non-explored space, you roll a die, and see what happens. - - Combat is like `Dark Queen of Krynn` + - Combat is like `Dark Queen of Krynn` (i.e. third person semi-iso) - 1 Location == 1 donjon room/area - BSP (binary space partition) https://www.youtube.com/watch?v=TlLIOgWYVpI&t=374s + - `Overland` - GameMode = _Traveling_: Swift Dangledonk the Slow is Traveling the Marshes of Moohfaahsaah - Travel is like `Rogue` diff --git a/server/package.json b/server/package.json index 796b3a4..d609081 100755 --- a/server/package.json +++ b/server/package.json @@ -10,7 +10,7 @@ "frontend:build": "vite build frontend", "frontend:dev": "vite frontend", "dev": "concurrently 'npm run server:dev' 'npm run frontend:dev'", - "dev:color": "concurrently -n 'API,UI' -c 'blue,green' 'npm run server:dev' 'npm run frontend:dev'" + "dev:color": "concurrently -n 'Server,Client' -c 'blue,green' 'npm run server:dev' 'npm run frontend:dev'" }, "keywords": [ "mud",