From 558906f2349403e781c78e0385b70a08d320a21e Mon Sep 17 00:00:00 2001
From: elioat <{ID}+{username}@users.noreply.github.com>
Date: Fri, 27 Dec 2024 10:12:40 -0500
Subject: *
---
.../1_Cat_Idle-Sheet.png | Bin 0 -> 587 bytes
.../FreeCatCharacterAnimations/2_Cat_Run-Sheet.png | Bin 0 -> 1098 bytes
.../3_Cat_Jump-Sheet.png | Bin 0 -> 469 bytes
.../4_Cat_Fall-Sheet.png | Bin 0 -> 442 bytes
.../assets/FreeCatCharacterAnimations/Contact.txt | 4 +
.../assets/FreeCatCharacterAnimations/License.txt | 5 +
html/rogue/index.html | 4 +-
html/rogue/js/config.js | 2 +-
html/rogue/js/hex.js | 67 ++++++
html/rogue/js/hexGrid.js | 67 ------
html/rogue/js/player.js | 67 +++++-
html/text-world/index.html | 41 ++++
html/text-world/js/b.js | 126 +++++++++++
html/text-world/js/ecs.js | 93 ++++++++
html/text-world/js/game.js | 239 +++++++++++++++++++++
html/text-world/js/where.js | 82 +++++++
html/text-world/js/worldgen.js | 130 +++++++++++
17 files changed, 852 insertions(+), 75 deletions(-)
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/Contact.txt
create mode 100644 html/rogue/assets/FreeCatCharacterAnimations/License.txt
create mode 100644 html/rogue/js/hex.js
delete mode 100644 html/rogue/js/hexGrid.js
create mode 100644 html/text-world/index.html
create mode 100644 html/text-world/js/b.js
create mode 100644 html/text-world/js/ecs.js
create mode 100644 html/text-world/js/game.js
create mode 100644 html/text-world/js/where.js
create mode 100644 html/text-world/js/worldgen.js
(limited to 'html')
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png b/html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png
new file mode 100644
index 0000000..466ce64
Binary files /dev/null and b/html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png differ
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png b/html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png
new file mode 100644
index 0000000..65dd33c
Binary files /dev/null and b/html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png differ
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png b/html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png
new file mode 100644
index 0000000..2d8d026
Binary files /dev/null and b/html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png differ
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png b/html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png
new file mode 100644
index 0000000..83d8e1f
Binary files /dev/null and b/html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png differ
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/Contact.txt b/html/rogue/assets/FreeCatCharacterAnimations/Contact.txt
new file mode 100644
index 0000000..b5d14c7
--- /dev/null
+++ b/html/rogue/assets/FreeCatCharacterAnimations/Contact.txt
@@ -0,0 +1,4 @@
+You can contact me at:
+
+email
+ oboropixelart@gmail.com
\ No newline at end of file
diff --git a/html/rogue/assets/FreeCatCharacterAnimations/License.txt b/html/rogue/assets/FreeCatCharacterAnimations/License.txt
new file mode 100644
index 0000000..f6c32f2
--- /dev/null
+++ b/html/rogue/assets/FreeCatCharacterAnimations/License.txt
@@ -0,0 +1,5 @@
+You can use this asset for personal and commercial purpose,
+you can modify this object to your needs.
+Credit is not required but would be appreciated
+
+You can NOT redistribute or resell it.
\ No newline at end of file
diff --git a/html/rogue/index.html b/html/rogue/index.html
index f3338b0..5d1b52b 100644
--- a/html/rogue/index.html
+++ b/html/rogue/index.html
@@ -19,8 +19,10 @@
+
+
-
+
diff --git a/html/rogue/js/config.js b/html/rogue/js/config.js
index cabc068..399a8a5 100644
--- a/html/rogue/js/config.js
+++ b/html/rogue/js/config.js
@@ -26,7 +26,7 @@ const Config = {
player: {
MOVE_SPEED: 0.1,
- SIZE_RATIO: 1/3,
+ SIZE_RATIO: 0.8,
VISION_RANGE: 3 // Number of hexes the player can see
},
diff --git a/html/rogue/js/hex.js b/html/rogue/js/hex.js
new file mode 100644
index 0000000..0d1c2e5
--- /dev/null
+++ b/html/rogue/js/hex.js
@@ -0,0 +1,67 @@
+// Hex grid utilities and calculations
+const HexGrid = {
+ get SIZE() { return Config.hex.SIZE },
+ get WIDTH() { return Config.hex.WIDTH },
+ get HEIGHT() { return Config.hex.HEIGHT },
+ get GRID_SIZE() { return Config.hex.GRID_SIZE },
+ COLOR: Config.colors.GRID,
+ IMPASSABLE_COLOR: Config.colors.BACKGROUND,
+
+ // Convert hex coordinates to pixel coordinates
+ toPixel(hex) {
+ const x = this.SIZE * (3/2 * hex.q);
+ const y = this.SIZE * (Math.sqrt(3)/2 * hex.q + Math.sqrt(3) * hex.r);
+ return {x, y};
+ },
+
+ // Convert pixel coordinates to hex coordinates
+ fromPixel(x, y) {
+ const q = (2/3 * x) / this.SIZE;
+ const r = (-1/3 * x + Math.sqrt(3)/3 * y) / this.SIZE;
+ return this.round(q, r);
+ },
+
+ // Round hex coordinates to nearest hex
+ round(q, r) {
+ let x = q;
+ let z = r;
+ let y = -x-z;
+
+ let rx = Math.round(x);
+ let ry = Math.round(y);
+ let rz = Math.round(z);
+
+ const x_diff = Math.abs(rx - x);
+ const y_diff = Math.abs(ry - y);
+ const z_diff = Math.abs(rz - z);
+
+ if (x_diff > y_diff && x_diff > z_diff) {
+ rx = -ry-rz;
+ } else if (y_diff > z_diff) {
+ ry = -rx-rz;
+ } else {
+ rz = -rx-ry;
+ }
+
+ return {q: rx, r: rz};
+ },
+
+ // Calculate visible hexes
+ getViewportHexes() {
+ const hexes = [];
+ const halfGrid = Math.floor(this.GRID_SIZE / 2);
+
+ for (let r = -halfGrid; r < halfGrid; r++) {
+ for (let q = -halfGrid; q < halfGrid; q++) {
+ hexes.push({q, r});
+ }
+ }
+ return hexes;
+ },
+
+ // Add this method to check if a hex is passable
+ isPassable(hex) {
+ const halfGrid = Math.floor(this.GRID_SIZE / 2);
+ return Math.abs(hex.q) <= halfGrid && Math.abs(hex.r) <= halfGrid;
+ }
+};
\ No newline at end of file
diff --git a/html/rogue/js/hexGrid.js b/html/rogue/js/hexGrid.js
deleted file mode 100644
index 0d1c2e5..0000000
--- a/html/rogue/js/hexGrid.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Hex grid utilities and calculations
-const HexGrid = {
- get SIZE() { return Config.hex.SIZE },
- get WIDTH() { return Config.hex.WIDTH },
- get HEIGHT() { return Config.hex.HEIGHT },
- get GRID_SIZE() { return Config.hex.GRID_SIZE },
- COLOR: Config.colors.GRID,
- IMPASSABLE_COLOR: Config.colors.BACKGROUND,
-
- // Convert hex coordinates to pixel coordinates
- toPixel(hex) {
- const x = this.SIZE * (3/2 * hex.q);
- const y = this.SIZE * (Math.sqrt(3)/2 * hex.q + Math.sqrt(3) * hex.r);
- return {x, y};
- },
-
- // Convert pixel coordinates to hex coordinates
- fromPixel(x, y) {
- const q = (2/3 * x) / this.SIZE;
- const r = (-1/3 * x + Math.sqrt(3)/3 * y) / this.SIZE;
- return this.round(q, r);
- },
-
- // Round hex coordinates to nearest hex
- round(q, r) {
- let x = q;
- let z = r;
- let y = -x-z;
-
- let rx = Math.round(x);
- let ry = Math.round(y);
- let rz = Math.round(z);
-
- const x_diff = Math.abs(rx - x);
- const y_diff = Math.abs(ry - y);
- const z_diff = Math.abs(rz - z);
-
- if (x_diff > y_diff && x_diff > z_diff) {
- rx = -ry-rz;
- } else if (y_diff > z_diff) {
- ry = -rx-rz;
- } else {
- rz = -rx-ry;
- }
-
- return {q: rx, r: rz};
- },
-
- // Calculate visible hexes
- getViewportHexes() {
- const hexes = [];
- const halfGrid = Math.floor(this.GRID_SIZE / 2);
-
- for (let r = -halfGrid; r < halfGrid; r++) {
- for (let q = -halfGrid; q < halfGrid; q++) {
- hexes.push({q, r});
- }
- }
- return hexes;
- },
-
- // Add this method to check if a hex is passable
- isPassable(hex) {
- const halfGrid = Math.floor(this.GRID_SIZE / 2);
- return Math.abs(hex.q) <= halfGrid && Math.abs(hex.r) <= halfGrid;
- }
-};
\ No newline at end of file
diff --git a/html/rogue/js/player.js b/html/rogue/js/player.js
index 22e57e0..09984dc 100644
--- a/html/rogue/js/player.js
+++ b/html/rogue/js/player.js
@@ -6,11 +6,30 @@ const player = {
movementProgress: 0, // Progress of current movement (0 to 1)
moveSpeed: Config.player.MOVE_SPEED, // Movement speed (0 to 1 per frame)
+ // Animation properties
+ sprites: {
+ idle: null,
+ run: null
+ },
+ currentFrame: 0,
+ frameCount: 0,
+ FRAME_DELAY: 8, // Adjust this to control animation speed
+ SPRITE_WIDTH: 32, // Adjust these based on your sprite sheet dimensions
+ SPRITE_HEIGHT: 32,
+ IDLE_FRAMES: 6, // Number of frames in idle animation
+ RUN_FRAMES: 8, // Number of frames in run animation
+ facingLeft: false,
+
// Initialize player
init() {
this.position = { q: 0, r: 0 };
this.target = null;
this.path = [];
+
+ // Load sprites
+ this.sprites.idle = document.getElementById('catIdle');
+ this.sprites.run = document.getElementById('catRun');
+
return this;
},
@@ -132,14 +151,50 @@ const player = {
const screenX = pixelPos.x - camera.x;
const screenY = pixelPos.y - camera.y;
- ctx.fillStyle = Config.colors.PLAYER;
- ctx.beginPath();
- ctx.arc(screenX, screenY, HEX_SIZE * Config.player.SIZE_RATIO, 0, Math.PI * 2);
- ctx.fill();
+ // Update animation frame
+ this.frameCount++;
+ if (this.frameCount >= this.FRAME_DELAY) {
+ this.frameCount = 0;
+ this.currentFrame++;
+
+ // Reset frame counter based on current animation
+ const maxFrames = this.target ? this.RUN_FRAMES : this.IDLE_FRAMES;
+ if (this.currentFrame >= maxFrames) {
+ this.currentFrame = 0;
+ }
+ }
+
+ // Determine facing direction based on movement
+ if (this.target) {
+ this.facingLeft = (this.target.q - this.position.q) < 0;
+ }
+
+ // Draw the sprite
+ const sprite = this.target ? this.sprites.run : this.sprites.idle;
+ const scale = (HEX_SIZE * Config.player.SIZE_RATIO * 2) / this.SPRITE_HEIGHT;
- // Optionally, draw the remaining path
+ ctx.save();
+ ctx.translate(screenX, screenY);
+ if (this.facingLeft) {
+ ctx.scale(-1, 1);
+ }
+
+ ctx.drawImage(
+ sprite,
+ this.currentFrame * this.SPRITE_WIDTH, // Source X
+ 0, // Source Y
+ this.SPRITE_WIDTH, // Source Width
+ this.SPRITE_HEIGHT, // Source Height
+ -this.SPRITE_WIDTH * scale / 2, // Destination X
+ -this.SPRITE_HEIGHT * scale / 2, // Destination Y
+ this.SPRITE_WIDTH * scale, // Destination Width
+ this.SPRITE_HEIGHT * scale // Destination Height
+ );
+ ctx.restore();
+
+ // Draw path (if needed)
if (this.path.length > 0) {
- ctx.strokeStyle = Config.colors.PLAYER + '4D'; // 30% opacity version of player color
+ ctx.strokeStyle = Config.colors.PLAYER + '4D';
ctx.beginPath();
let lastPos = this.target || this.position;
this.path.forEach(point => {
diff --git a/html/text-world/index.html b/html/text-world/index.html
new file mode 100644
index 0000000..42f4984
--- /dev/null
+++ b/html/text-world/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Text Adventure
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/html/text-world/js/b.js b/html/text-world/js/b.js
new file mode 100644
index 0000000..3869f62
--- /dev/null
+++ b/html/text-world/js/b.js
@@ -0,0 +1,126 @@
+/*
+//
+// # The Witch in the Glass
+//
+// My mother says I must not pass
+// Too near that glass;
+// She is afraid that I will see
+// A little witch that looks like me,
+// With a red, red mouth to whisper low
+// The very thing I should not know!
+//
+// Alack for all your mother's care!
+// A bird of the air,
+// A wistful wind, or (I suppose
+// Sent by some hapless boy) a rose,
+// With breath too sweet, will whisper low
+// The very thing you should not know!
+//
+// - Sarah Morgan Bryan Piatt
+//
+*/
+
+// b is for useful stuff
+
+// curry: Converts a function to its curried form, allowing for partial application.
+// pipe: Runs functions from left to right, passing the result of one to the next.
+// compose: Runs functions from right to left, passing the result of one to the next.
+// identity: Returns whatever you give it as is. Useful for composition...sometimes.
+// match: Creates a curried function to match a regular expression in a string.
+// replace: Creates a curried function to replace parts of a string, based on a regex or substring.
+// filter: Curried array filtering by a predicate function.
+// map: Curried array mapping.
+// deepMap: Curried recursive mapping for nested arrays or matrices, of any shape.
+
+'use strict'
+
+const b = {
+ /**
+ * Converts a function into a curried function, allowing for partial application.
+ * A curried function can be partially applied and will return a new function until all arguments are provided.
+ * @param {Function} fn - The function to curry.
+ * @returns {Function} - The curried function.
+ */
+ curry: function (fn) {
+ const curried = (...args) => {
+ if (args.length >= fn.length)
+ return fn(...args)
+ else
+ return (...rest) => curried(...args, ...rest)
+ }
+ return curried
+ },
+
+ /**
+ * Composes functions from left to right.
+ * Takes any number of functions as arguments and returns a function that applies them in sequence to a supplied value.
+ * @param {...Function} fns - The functions to be composed.
+ * @returns {Function} - A function that takes an initial value and applies the composed functions from left to right.
+ */
+ pipe: (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value),
+
+ /**
+ * Composes functions from right to left.
+ * Takes any number of functions as arguments and returns a function that applies them in sequence from right to left.
+ * @param {...Function} fns - The functions to be composed.
+ * @returns {Function} - A function that applies the composed functions from right to left.
+ */
+ compose: (...fns) => (...args) => fns.reduceRight((res, fn) => fn(...[].concat(res)), args),
+
+ /**
+ * A function that returns its input totally and completely unchanged.
+ * @param {*} x - The input value.
+ * @returns {*} - The same input value.
+ */
+ identity: x => x,
+
+ /**
+ * Curried function that matches a regular expression against a string.
+ * @returns {Function} - A curried function that takes a regex and a string and returns the match result.
+ */
+ match: function () {
+ return this.curry((what, s) => s.match(what));
+ },
+
+ /**
+ * Curried function that replaces parts of a string based on a regex or substring.
+ * @returns {Function} - A curried function that takes a regex or substring, a replacement, and a string, and returns the replaced string.
+ */
+ replace: function () {
+ return this.curry((what, replacement, s) => s.replace(what, replacement));
+ },
+
+ /**
+ * Curried function that filters an array based on a predicate function.
+ * @returns {Function} - A curried function that takes a predicate function and an array, and returns the filtered array.
+ */
+ filter: function () {
+ return this.curry((f, xs) => xs.filter(f));
+ },
+
+ /**
+ * Curried function that maps a function over an array.
+ * @returns {Function} - A curried function that takes a mapping function and an array, and returns the mapped array.
+ */
+ map: function () {
+ return this.curry((f, xs) => xs.map(f));
+ },
+
+ /**
+ * Curried function that recursively maps a function over an array or matrix of any shape.
+ * @returns {Function} - A curried function that takes a mapping function and a nested array, and returns the deeply mapped array.
+ */
+ deepMap: function () {
+ return this.curry(function deepMap(f, xs) {
+ return Array.isArray(xs)
+ ? xs.map((x) => deepMap(f, x))
+ : f(xs);
+ });
+ },
+};
+
+
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = b;
+}
diff --git a/html/text-world/js/ecs.js b/html/text-world/js/ecs.js
new file mode 100644
index 0000000..c2c5add
--- /dev/null
+++ b/html/text-world/js/ecs.js
@@ -0,0 +1,93 @@
+'use strict';
+
+// Components remain as simple data structures
+class Component {
+ constructor(data = {}) {
+ Object.assign(this, data);
+ }
+}
+
+class Position extends Component {
+ constructor(x = 0, y = 0) {
+ super({ x, y });
+ }
+}
+
+class Description extends Component {
+ constructor(short = "", long = "", explorable = {}) {
+ super({ short, long, explorable });
+ }
+}
+
+class Inventory extends Component {
+ constructor(items = []) {
+ super({ items });
+ }
+}
+
+class Messages extends Component {
+ constructor(messages = []) {
+ super({ messages });
+ }
+}
+
+class Item extends Component {
+ constructor(name, description, collectable = true) {
+ super({ name, description, collectable });
+ }
+}
+
+// World becomes a pure functional interface
+const World = {
+ create() {
+ return {
+ entities: new Map(),
+ nextEntityId: 1
+ };
+ },
+
+ createEntity(world) {
+ const id = world.nextEntityId;
+ const newEntities = new Map(world.entities);
+ newEntities.set(id, new Map());
+
+ return {
+ ...world,
+ entities: newEntities,
+ nextEntityId: id + 1
+ };
+ },
+
+ addComponent(world, entityId, component) {
+ if (!world.entities.has(entityId)) return world;
+
+ const newEntities = new Map(world.entities);
+ const entityComponents = new Map(world.entities.get(entityId));
+ entityComponents.set(component.constructor.name, component);
+ newEntities.set(entityId, entityComponents);
+
+ return {
+ ...world,
+ entities: newEntities
+ };
+ },
+
+ getComponent(world, entityId, componentType) {
+ if (!world.entities.has(entityId)) return null;
+ return world.entities.get(entityId).get(componentType);
+ },
+
+ removeComponent(world, entityId, componentType) {
+ if (!world.entities.has(entityId)) return world;
+
+ const newEntities = new Map(world.entities);
+ const entityComponents = new Map(world.entities.get(entityId));
+ entityComponents.delete(componentType);
+ newEntities.set(entityId, entityComponents);
+
+ return {
+ ...world,
+ entities: newEntities
+ };
+ }
+};
diff --git a/html/text-world/js/game.js b/html/text-world/js/game.js
new file mode 100644
index 0000000..8f2d4d6
--- /dev/null
+++ b/html/text-world/js/game.js
@@ -0,0 +1,239 @@
+class Game {
+ constructor() {
+ const generated = WorldGenerator.generate();
+ this.state = {
+ world: generated.world,
+ playerId: generated.playerId
+ };
+
+ this.setupUI();
+ this.print("Welcome to the Text Adventure!");
+ this.describeCurrentLocation();
+ }
+
+ setState(newState) {
+ this.state = newState;
+ }
+
+ setupUI() {
+ this.output = document.getElementById('output');
+ this.input = document.getElementById('input');
+
+ this.input.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ const command = this.input.value.toLowerCase();
+ this.input.value = '';
+ this.handleCommand(command);
+ }
+ });
+ }
+
+ print(text) {
+ const line = document.createElement('div');
+ line.textContent = text;
+ this.output.appendChild(line);
+ this.output.scrollTop = this.output.scrollHeight;
+ }
+
+ handleCommand(command) {
+ this.print(`> ${command}`);
+
+ if (command.match(/^(where am i|look around)$/)) {
+ this.describeCurrentLocation();
+ }
+ else if (command.startsWith('move ')) {
+ const newState = this.handleMove(command.split(' ')[1]);
+ if (newState) {
+ this.setState(newState);
+ this.describeCurrentLocation();
+ }
+ }
+ else if (command.startsWith('explore ') || command.startsWith('look at ')) {
+ const target = command.replace(/^(explore|look at) /, '');
+ this.exploreTarget(target);
+ }
+ else if (command === 'phone') {
+ this.checkMessages();
+ }
+ else if (command === 'inventory') {
+ this.checkInventory();
+ }
+ else if (command.startsWith('get ') || command.startsWith('take ')) {
+ const itemName = command.replace(/^(get|take) /, '');
+ const newState = this.collectItem(itemName);
+ if (newState) {
+ this.setState(newState);
+ }
+ }
+ else {
+ this.print("I don't understand that command.");
+ }
+ }
+
+ getCurrentLocation() {
+ const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+
+ if (!playerPos) {
+ console.error('Player position not found');
+ return null;
+ }
+
+ // Find the environment at the player's position
+ const locationEntity = Array.from(this.state.world.entities.entries()).find(([entityId, _]) => {
+ if (entityId === this.state.playerId) return false;
+
+ const pos = World.getComponent(this.state.world, entityId, 'Position');
+ return pos && pos.x === playerPos.x && pos.y === playerPos.y;
+ });
+
+ if (!locationEntity) return null;
+
+ const [entityId, _] = locationEntity;
+ const desc = World.getComponent(this.state.world, entityId, 'Description');
+
+ // Add items to the location description
+ const items = this.getItemsAtLocation(playerPos);
+ return { entityId, desc, items };
+ }
+
+ getItemsAtLocation(pos) {
+ return Array.from(this.state.world.entities.entries())
+ .filter(([entityId, _]) => {
+ const itemPos = World.getComponent(this.state.world, entityId, 'Position');
+ const item = World.getComponent(this.state.world, entityId, 'Item');
+ return itemPos && item &&
+ itemPos.x === pos.x &&
+ itemPos.y === pos.y;
+ })
+ .map(([entityId, _]) => ({
+ entityId,
+ item: World.getComponent(this.state.world, entityId, 'Item')
+ }));
+ }
+
+ describeCurrentLocation() {
+ const location = this.getCurrentLocation();
+ if (location) {
+ this.print(`You are in: ${location.desc.short}`);
+ this.print(location.desc.long);
+
+ if (location.items.length > 0) {
+ this.print("\nYou can see:");
+ location.items.forEach(({ item }) => {
+ this.print(`- ${item.name}`);
+ });
+ }
+ }
+ }
+
+ handleMove(direction) {
+ const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+
+ if (!playerPos) {
+ console.error('Player position not found');
+ return null;
+ }
+
+ const moves = {
+ 'north': { x: 0, y: -1 },
+ 'south': { x: 0, y: 1 },
+ 'east': { x: 1, y: 0 },
+ 'west': { x: -1, y: 0 }
+ };
+
+ if (!moves[direction]) {
+ this.print("Invalid direction. Use north, south, east, or west.");
+ return null;
+ }
+
+ const newX = playerPos.x + moves[direction].x;
+ const newY = playerPos.y + moves[direction].y;
+
+ if (newX < 0 || newX >= 10 || newY < 0 || newY >= 10) {
+ this.print("You can't go that way - you've reached the edge of the world.");
+ return null;
+ }
+
+ const newWorld = World.addComponent(
+ this.state.world,
+ this.state.playerId,
+ new Position(newX, newY)
+ );
+
+ this.print(`You move ${direction}.`);
+
+ return {
+ ...this.state,
+ world: newWorld
+ };
+ }
+
+ exploreTarget(target) {
+ const location = this.getCurrentLocation();
+ if (location && location.desc.explorable[target]) {
+ this.print(location.desc.explorable[target]);
+ } else {
+ this.print("There's nothing special about that.");
+ }
+ }
+
+ checkMessages() {
+ const messages = World.getComponent(this.state.world, this.state.playerId, 'Messages');
+ if (messages.messages.length === 0) {
+ this.print("You have no messages.");
+ } else {
+ this.print("Your messages:");
+ messages.messages.forEach(msg => this.print(`- ${msg}`));
+ }
+ }
+
+ collectItem(itemName) {
+ const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+ const items = this.getItemsAtLocation(playerPos);
+
+ const itemEntry = items.find(({ item }) =>
+ item.name.toLowerCase() === itemName.toLowerCase()
+ );
+
+ if (!itemEntry) {
+ this.print(`There is no ${itemName} here.`);
+ return null;
+ }
+
+ if (!itemEntry.item.collectable) {
+ this.print(`You can't take the ${itemName}.`);
+ return null;
+ }
+
+ // Add to inventory
+ const inventory = World.getComponent(this.state.world, this.state.playerId, 'Inventory');
+ const updatedInventory = new Inventory([...inventory.items, itemEntry.item]);
+
+ // Remove from world and update inventory
+ let newWorld = World.removeComponent(this.state.world, itemEntry.entityId, 'Position');
+ newWorld = World.addComponent(newWorld, this.state.playerId, updatedInventory);
+
+ this.print(`You take the ${itemName}.`);
+ return {
+ ...this.state,
+ world: newWorld
+ };
+ }
+
+ checkInventory() {
+ const inventory = World.getComponent(this.state.world, this.state.playerId, 'Inventory');
+ if (inventory.items.length === 0) {
+ this.print("Your inventory is empty.");
+ } else {
+ this.print("Your inventory contains:");
+ inventory.items.forEach(item => {
+ this.print(`- ${item.name}`);
+ });
+ }
+ }
+}
+
+// Start the game when the page loads
+window.addEventListener('load', () => {
+ window.game = new Game();
+});
diff --git a/html/text-world/js/where.js b/html/text-world/js/where.js
new file mode 100644
index 0000000..60e3797
--- /dev/null
+++ b/html/text-world/js/where.js
@@ -0,0 +1,82 @@
+/**
+ * A mostly functional query filter similar to SQL's WHERE
+ * @param {Array} collection - Array of objects to filter
+ * @param {Object} properties - Object containing properties to match against
+ * @returns {Array} - Filtered array of objects
+ */
+const where = (collection, properties) => {
+
+ if (!collection || !properties) {
+ return [];
+ }
+
+ const propertyPaths = Object.entries(properties).map(([key, value]) => ({
+ path: key,
+ parts: key.split('.'),
+ value
+ }));
+
+ const getNestedValue = (() => {
+ const cache = new WeakMap();
+
+ return (obj, parts) => {
+ if (!obj) return undefined;
+
+ let cached = cache.get(obj);
+ if (!cached) {
+ cached = new Map();
+ cache.set(obj, cached);
+ }
+
+ const pathKey = parts.join('.');
+ if (cached.has(pathKey)) {
+ return cached.get(pathKey);
+ }
+
+ const value = parts.reduce((current, key) =>
+ current && current[key] !== undefined ? current[key] : undefined,
+ obj
+ );
+
+ cached.set(pathKey, value);
+ return value;
+ };
+ })();
+
+ const isEqual = (value1, value2) => {
+
+ if (value2 === undefined) return true;
+ if (value1 === value2) return true;
+ if (value1 === null || value2 === null) return false;
+
+ if (Array.isArray(value1) && Array.isArray(value2)) {
+ return value1.length === value2.length &&
+ value1.every((val, idx) => isEqual(val, value2[idx]));
+ }
+
+ if (typeof value2 === 'object') {
+ return Object.entries(value2).every(([key, val]) =>
+ value1 && isEqual(value1[key], val)
+ );
+ }
+
+ return false;
+ };
+
+ const matchesProperties = item =>
+ propertyPaths.every(({ parts, value }) =>
+ isEqual(getNestedValue(item, parts), value)
+ );
+
+ if (!Array.isArray(collection) || typeof properties !== 'object') {
+ return [];
+ }
+
+ return collection.filter(matchesProperties);
+};
+
+
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = where;
+}
diff --git a/html/text-world/js/worldgen.js b/html/text-world/js/worldgen.js
new file mode 100644
index 0000000..c8f5778
--- /dev/null
+++ b/html/text-world/js/worldgen.js
@@ -0,0 +1,130 @@
+'use strict';
+
+const WorldGenerator = {
+ generate() {
+ let world = World.create();
+
+ // Create player
+ let result = WorldGenerator.createPlayer(world);
+ world = result.world;
+ const playerId = result.entityId;
+
+ // Generate environments
+ world = WorldGenerator.generateEnvironments(world);
+
+ return { world, playerId };
+ },
+
+ createPlayer(world) {
+ const newWorld = World.createEntity(world);
+ const entityId = newWorld.nextEntityId - 1;
+
+ return {
+ world: World.addComponent(
+ World.addComponent(
+ World.addComponent(
+ newWorld,
+ entityId,
+ new Position(0, 0)
+ ),
+ entityId,
+ new Inventory([])
+ ),
+ entityId,
+ new Messages([])
+ ),
+ entityId
+ };
+ },
+
+ generateEnvironments(world) {
+ let newWorld = world;
+
+ for (let x = 0; x < 10; x++) {
+ for (let y = 0; y < 10; y++) {
+ newWorld = WorldGenerator.createEnvironment(newWorld, x, y);
+ }
+ }
+
+ return newWorld;
+ },
+
+ createEnvironment(world, x, y) {
+ const newWorld = World.createEntity(world);
+ const entityId = newWorld.nextEntityId - 1;
+
+ // Add basic components
+ let updatedWorld = World.addComponent(
+ World.addComponent(
+ newWorld,
+ entityId,
+ new Position(x, y)
+ ),
+ entityId,
+ this.generateEnvironmentDescription(x, y)
+ );
+
+ // Add items based on environment type
+ const items = this.generateItems(x, y);
+ items.forEach(item => {
+ const itemEntity = World.createEntity(updatedWorld);
+ updatedWorld = World.addComponent(
+ World.addComponent(
+ updatedWorld,
+ itemEntity,
+ new Position(x, y)
+ ),
+ itemEntity,
+ item
+ );
+ });
+
+ return updatedWorld;
+ },
+
+ generateEnvironmentDescription(x, y) {
+ const environments = [
+ {
+ short: "Forest Clearing",
+ long: "A peaceful clearing surrounded by tall trees. Sunlight filters through the canopy above.",
+ explorable: {
+ "trees": "The trees here are ancient oaks, their bark rough and weathered.",
+ "ground": "The forest floor is covered in soft moss and fallen leaves."
+ }
+ },
+ {
+ short: "Rocky Outcrop",
+ long: "A rocky area with large boulders scattered about. Some seem to hide small caves.",
+ explorable: {
+ "boulders": "The boulders are covered in colorful lichens.",
+ "caves": "Small openings that could shelter wildlife."
+ }
+ }
+ ];
+
+ const index = (x * 7 + y * 13) % environments.length;
+ return new Description(
+ environments[index].short,
+ environments[index].long,
+ environments[index].explorable
+ );
+ },
+
+ generateItems(x, y) {
+ const items = [
+ new Item("mushroom", "A small, colorful mushroom that might be edible."),
+ new Item("stone", "A smooth, flat stone that catches your eye."),
+ new Item("stick", "A sturdy wooden stick."),
+ new Item("flower", "A beautiful wildflower with vibrant petals."),
+ new Item("feather", "A delicate feather from some unknown bird.")
+ ];
+
+ // Use coordinates to deterministically generate items
+ const numItems = ((x * 3 + y * 5) % 3) + 1; // 1-3 items per location
+ const startIndex = (x * 7 + y * 11) % items.length;
+
+ return Array.from({ length: numItems }, (_, i) =>
+ items[(startIndex + i) % items.length]
+ );
+ }
+};
--
cgit 1.4.1-2-gfad0