diff options
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png | bin | 0 -> 587 bytes | |||
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png | bin | 0 -> 1098 bytes | |||
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png | bin | 0 -> 469 bytes | |||
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png | bin | 0 -> 442 bytes | |||
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/Contact.txt | 4 | ||||
-rw-r--r-- | html/rogue/assets/FreeCatCharacterAnimations/License.txt | 5 | ||||
-rw-r--r-- | html/rogue/index.html | 4 | ||||
-rw-r--r-- | html/rogue/js/config.js | 2 | ||||
-rw-r--r-- | html/rogue/js/hex.js (renamed from html/rogue/js/hexGrid.js) | 0 | ||||
-rw-r--r-- | html/rogue/js/player.js | 67 | ||||
-rw-r--r-- | html/text-world/index.html | 41 | ||||
-rw-r--r-- | html/text-world/js/b.js | 126 | ||||
-rw-r--r-- | html/text-world/js/ecs.js | 93 | ||||
-rw-r--r-- | html/text-world/js/game.js | 239 | ||||
-rw-r--r-- | html/text-world/js/where.js | 82 | ||||
-rw-r--r-- | html/text-world/js/worldgen.js | 130 |
16 files changed, 785 insertions, 8 deletions
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 --- /dev/null +++ b/html/rogue/assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png Binary files differdiff --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 --- /dev/null +++ b/html/rogue/assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png Binary files differdiff --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 --- /dev/null +++ b/html/rogue/assets/FreeCatCharacterAnimations/3_Cat_Jump-Sheet.png Binary files differdiff --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 --- /dev/null +++ b/html/rogue/assets/FreeCatCharacterAnimations/4_Cat_Fall-Sheet.png Binary files differdiff --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 @@ </head> <body> <canvas id="gameCanvas"></canvas> + <img id="catIdle" src="assets/FreeCatCharacterAnimations/1_Cat_Idle-Sheet.png" style="display: none;"> + <img id="catRun" src="assets/FreeCatCharacterAnimations/2_Cat_Run-Sheet.png" style="display: none;"> <script src="js/config.js"></script> - <script src="js/hexGrid.js"></script> + <script src="js/hex.js"></script> <script src="js/camera.js"></script> <script src="js/fow.js"></script> <script src="js/player.js"></script> 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/hexGrid.js b/html/rogue/js/hex.js index 0d1c2e5..0d1c2e5 100644 --- a/html/rogue/js/hexGrid.js +++ b/html/rogue/js/hex.js 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 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Text Adventure</title> + <style> + #game-container { + max-width: 800px; + margin: 20px auto; + font-family: monospace; + } + #output { + background: #1a1a1a; + color: #fff; + padding: 20px; + min-height: 300px; + max-height: 500px; + overflow-y: auto; + margin-bottom: 10px; + } + #input { + width: 100%; + padding: 10px; + box-sizing: border-box; + } + </style> +</head> +<body> + <div id="game-container"> + <div id="output"></div> + <input type="text" id="input" placeholder="Enter your command..." autocomplete="off"> + </div> + + <script src="js/b.js"></script> + <script src="js/where.js"></script> + <script src="js/ecs.js"></script> + <script src="js/worldgen.js"></script> + <script src="js/game.js"></script> +</body> +</html> \ 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] + ); + } +}; |