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