diff --git a/README.md b/README.md index 821b99f..c29b44c 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,13 @@ running from 4 July 2023 till 22 August 2023. coordinates over to some new `VIEWPORT` coordinates. - In fact, it was this straightforward. `camera.ts` now incorporates the new constants. -### Week 5 - 1 August 2023 - Items, inventory and ranged targeting +### [Week 5](https://old.reddit.com/r/roguelikedev/comments/15fd445/roguelikedev_does_the_complete_roguelike_tutorial/) - 1 August 2023 - Items, inventory and ranged targeting + +- There's a sensible de-coupling of the input from the visuals, which allows for nice things like animations in the + future. +- The tutorial implements a mouse-pointer investigation mechanic--I don't like that, so I've opted for something more + traditional. To investigate items in a tile, a player must go to the tile and press `i`, and they will get a log of + the items in that tile to the message log. This is implemented in `investigatePosition()` in `actions.ts`. ### Week 6 - 8 August 2023 - Save/load and leveling up @@ -165,7 +171,7 @@ never to return. Steeling your nerves, you set forth into the dungeon. - [x] Player has field-of-view - [x] Spawn monsters - [x] Players can fight monsters -- [ ] Add items +- [ ] Add items and inventory - [ ] Add Orb of Yezriel, let the player win when getting it - [ ] Game over when the player dies diff --git a/src/actions.ts b/src/actions.ts index 3e2cfdc..a925b46 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -60,6 +60,11 @@ export function tryMoveEntity(game: Game, entity: Entity, delta: Vector2, absolu } } +/** + * Inflict damage on an entity safely while checking for multiple sources of IncomingDamage. + * @param entity Target entity + * @param amount Amount of damage to inflict + */ export function inflictDamage(entity: Entity, amount: number) { if (!entity.hasComponent(Components.IncomingDamage)) { entity.addComponent(Components.IncomingDamage, { amount }); @@ -67,4 +72,48 @@ export function inflictDamage(entity: Entity, amount: number) { const incomingDamage = entity.getMutableComponent(Components.IncomingDamage)!; incomingDamage.amount += amount; } -} \ No newline at end of file +} + +/** + * Investigate the items in the current position and log them to the message log. + * @param game Game object + * @param position Target tile position + */ +export function investigatePosition(game: Game, position: Vector2) { + // get the list of entities at this position that are an Item + const items = game.map + .getTileContent(position) + .filter((item) => item.hasComponent(Components.Item)); + + if (items.length === 0) { + // if there wasn't an item, return to log + game.log.addMessage('There\'s nothing here.'); + } else { + // go through and log each item to the player + for (const item of items) { + const itemName = item.getComponent(Components.Name)?.name || 'mysterious thing'; + game.log.addMessage(`There's a ${ itemName } here.`); + } + } +} + +/** + * Attempts to pick up an item at the given Position by the Entity. + * @param game Game object + * @param entity Target entity + * @param position Target tile position + */ +export function attemptToPickUp(game: Game, entity: Entity, position: Vector2) { + // get the list of entities at this position that are an Item + const items = game.map + .getTileContent(position) + .filter((item) => item.hasComponent(Components.Item)); + + // get the first + const targetItem = items[0]; + + // if there wasn't an item, return to log + if (targetItem === undefined) game.log.addMessage('No item to pick up here.'); + // otherwise Attempt To Pickup the Item + else entity.addComponent(Components.AttemptToPickupItem, { item: targetItem }); +} diff --git a/src/components.ts b/src/components.ts index f27a364..b9d5d70 100644 --- a/src/components.ts +++ b/src/components.ts @@ -49,13 +49,16 @@ export class Name extends Component { /** * Renderable component * @param glyph `Terminal.Glyph` for rendering on the canvas + * @param zIndex rendering order, highest drawn last */ export class Renderable extends Component { // glyph can never be undefined glyph!: Terminal.Glyph; + zIndex = 0; static schema: ComponentSchema = { glyph: { type: Types.Ref }, + zIndex: { type: Types.Number }, }; } diff --git a/src/level-gen.ts b/src/level-gen.ts index d8552a0..edc8d90 100644 --- a/src/level-gen.ts +++ b/src/level-gen.ts @@ -1,5 +1,5 @@ import { Entity, World } from 'ecsy'; -import { CharCode, Color, Glyph, Rand } from "malwoden"; +import { CharCode, Color, Glyph, Rand, Vector2 } from "malwoden"; import * as Components from './components.ts'; import * as Constants from './constants.ts'; @@ -29,10 +29,14 @@ export function generateLevel(config: GenerateLevelConfig): LevelData { .createEntity() .addComponent(Components.Player) .addComponent(Components.Position, startRoom.center()) - .addComponent(Components.Renderable, { glyph: Glyph.fromCharCode(CharCode.at, Color.Yellow) }) + .addComponent(Components.Renderable, { + glyph: Glyph.fromCharCode(CharCode.at, Color.Yellow), + zIndex: 10, + }) .addComponent(Components.Viewshed, { range: Constants.PLAYER_FOV_RANGE }) .addComponent(Components.BlocksTile) .addComponent(Components.Name, { name: 'Player' }) + .addComponent(Components.Inventory) .addComponent(Components.Attributes, { hp: 30, maxHp: 30, @@ -40,7 +44,7 @@ export function generateLevel(config: GenerateLevelConfig): LevelData { defense: 2, }); - // create monsters + // create monsters and items // i = 1 to skip the first room for (let i = 1; i < map.rooms.length; i++) { const room = map.rooms[i]; @@ -63,17 +67,42 @@ export function generateLevel(config: GenerateLevelConfig): LevelData { const creatureType = rng.nextInt(0, 100); if (creatureType < 50) { entity - .addComponent(Components.Renderable, { glyph: Glyph.fromCharCode(CharCode.gLower, Color.Red) }) + .addComponent(Components.Renderable, { + glyph: Glyph.fromCharCode(CharCode.gLower, Color.Red), + zIndex: 9, + }) .addComponent(Components.Name, { name: 'Goblin' }); } else { entity - .addComponent(Components.Renderable, { glyph: Glyph.fromCharCode(CharCode.oLower, Color.Red) }) + .addComponent(Components.Renderable, { + glyph: Glyph.fromCharCode(CharCode.oLower, Color.Red), + zIndex: 9, + }) .addComponent(Components.Name, { name: 'Orc' }); } + + // create items + if (rng.next() < 0.2) { + // get a random spot in the room + const randomX = rng.nextInt(room.v1.x, room.v2.x + 1); + const randomY = rng.nextInt(room.v1.y, room.v2.y + 1); + spawnBandage(world, { x: randomX, y: randomY }); + } } return { map, player, }; +} + +function spawnBandage(world: World, position: Vector2) { + world + .createEntity() + .addComponent(Components.Item) + .addComponent(Components.Position, position) + .addComponent(Components.Name, { name: 'Bandage' }) + .addComponent(Components.Renderable, { + glyph: Glyph.fromCharCode(CharCode.bLower, Color.Orange), + }); } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 406ee50..532608d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -97,6 +97,7 @@ export class Game { .registerSystem(Systems.MeleeCombat, this) .registerSystem(Systems.DamageSystem, this) .registerSystem(Systems.DeathSystem, this) + .registerSystem(Systems.InventorySystem, this) .registerSystem(Systems.MapIndexing, this) .registerSystem(Systems.RenderSystem, this); } @@ -122,6 +123,18 @@ export class Game { break; } + // investigate + case Input.KeyCode.I: { + Actions.investigatePosition(this, this.player.getComponent(Components.Position)!); + break; + } + + // pick up an item + case Input.KeyCode.P: { + Actions.attemptToPickUp(this, this.player, this.player.getComponent(Components.Position)!); + break; + } + // arrow keys case Input.KeyCode.LeftArrow: { Actions.tryMoveEntity(this, this.player, { x: -1, y: 0 }); diff --git a/src/systems/index.ts b/src/systems/index.ts index 68007a3..04b159c 100644 --- a/src/systems/index.ts +++ b/src/systems/index.ts @@ -4,4 +4,5 @@ export { EnemyAISystem } from './enemy-ai'; export { MapIndexing } from './map-indexing'; export { MeleeCombat } from './melee-combat.ts'; export { DamageSystem } from './damage-system.ts'; -export { DeathSystem } from './death-system.ts'; \ No newline at end of file +export { DeathSystem } from './death-system.ts'; +export { InventorySystem } from './inventory-system.ts'; \ No newline at end of file diff --git a/src/systems/inventory-system.ts b/src/systems/inventory-system.ts new file mode 100644 index 0000000..144e990 --- /dev/null +++ b/src/systems/inventory-system.ts @@ -0,0 +1,45 @@ +import { System, SystemQueries, World } from 'ecsy'; + +import { Game } from '../main.ts'; +import * as Components from '../components.ts'; + +export class InventorySystem extends System { + game: Game; + + constructor(world: World, game: Game) { + super(world, game); + this.game = game; + } + + static queries: SystemQueries = { + wantsToPickup: { + components: [Components.AttemptToPickupItem], + }, + }; + + execute() { + const { results } = this.queries.wantsToPickup; + + for (const entity of results) { + const wantsToPickup = entity.getComponent(Components.AttemptToPickupItem)!; + const inventory = entity.getComponent(Components.Inventory); + const entityName = entity.getComponent(Components.Name)?.name || 'Someone'; + const itemName = wantsToPickup.item.getComponent(Components.Name)?.name || 'something'; + + // we must have an inventory to pick something up + if (inventory) { + // add the item to inventory + inventory.items.push(wantsToPickup.item); + + // remove it from the map + wantsToPickup.item.removeComponent(Components.Position); + + // log the interaction + this.game.log.addMessage(`${ entityName } picked up ${ itemName }.`); + } + + // interaction is complete, remove the Attempt + entity.removeComponent(Components.AttemptToPickupItem); + } + } +} \ No newline at end of file diff --git a/src/systems/render.ts b/src/systems/render.ts index bb54e74..1b56a58 100644 --- a/src/systems/render.ts +++ b/src/systems/render.ts @@ -143,8 +143,15 @@ export class RenderSystem extends System { } } - // draw each entity - for (const entity of results) { + // sort entities in the tile by zIndex + const zIndexResults = results.sort((entityA, entityB) => { + const entityARender = entityA.getComponent(Components.Renderable)!; + const entityBRender = entityB.getComponent(Components.Renderable)!; + return entityARender.zIndex - entityBRender.zIndex; + }); + + // draw each entity from the sorted list of results by zIndex + for (const entity of zIndexResults) { const position = entity.getComponent(Components.Position)!; const renderable = entity.getComponent(Components.Renderable)!;