diff --git a/src/common/rpg/TODO.md b/src/common/rpg/TODO.md index 6f4bede..3ac65cc 100644 --- a/src/common/rpg/TODO.md +++ b/src/common/rpg/TODO.md @@ -10,13 +10,7 @@ the attacker/source that CombatSystem folds into the final damage value. RNG layer on top of damage calculation (crit chance, crit multiplier, random range). ### Rendering -- Sprite rendering system. - - Generic, use `resourceId` to link image. - - `framesCount` - - `currentFrame` - - `animationSpeed` -- Animation support for sprites. - Different display systems - - TextDisplay - - BrickDisplay - - Canvas + - [x] TextDisplay + - [ ] BrickDisplay + - [ ] Canvas diff --git a/src/common/rpg/components/position.ts b/src/common/rpg/components/position.ts index 7ce42d2..f6bf8a5 100644 --- a/src/common/rpg/components/position.ts +++ b/src/common/rpg/components/position.ts @@ -8,4 +8,4 @@ export class Position extends Component<{ x: number, y: number, z: number }> { } } -export const getPosition = (entity: Entity) => entity.get(Position)?.state; \ No newline at end of file +export const getPosition = (entity?: Entity, key?: string) => entity?.get(Position, key)?.state; diff --git a/src/games/text-dungeon/index.ts b/src/games/text-dungeon/index.ts index 110c562..731090f 100644 --- a/src/games/text-dungeon/index.ts +++ b/src/games/text-dungeon/index.ts @@ -3,7 +3,7 @@ import Input from '@common/input'; import { Inventory } from '@common/rpg/components/inventory'; import { Item } from '@common/rpg/components/item'; import { Position } from '@common/rpg/components/position'; -import { Entity, System, World } from '@common/rpg/core/world'; +import { World } from '@common/rpg/core/world'; import { TextDisplaySystem } from '@common/rpg/systems/render/text'; import { nextFrame } from '@common/utils'; import './assets/style.css'; @@ -11,47 +11,37 @@ import { Direction, getOppositeDirection, MAP_HEIGHT, MAP_WIDTH, MAP_X, MAP_Y, R import { getPossibleRoomsCount, getRoom, getRoomsCount, getRoomsForLayer, getMapRoomChar, Room, Door } from './room'; import { createPlayer, Player } from './player'; -let lastMove = Date.now(); -class GameSystem extends System { - private readonly display = new TextDisplay(); - private room: Entity | undefined; - private player: Entity | undefined; +export default async function main() { + const world = new World(); + const display = new TextDisplay(); + world.addSystem(new TextDisplaySystem(display)); - override onAdd(world: World) { - world.addSystem(new TextDisplaySystem(this.display)); + display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN }); - this.display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN }); - - this.room = getRoom(world, 0, 0, 0); - const room = this.room?.get(Room); - if (room) { - room.onEnter(); - this.player = createPlayer(world, (room.x + room.width / 2) | 0, (room.y + room.height / 2) | 0); - } + let roomEntity = getRoom(world, 0, 0, 0); + let room = roomEntity?.get(Room); + if (!room) { + throw new Error('No room found'); } + room.onEnter(); - override update(world: World) { - this.handleInput(world); - - this.drawRoom(); - this.drawMap(world); - this.drawInfo(world); + const playerEntity = createPlayer(world, (room.x + room.width / 2) | 0, (room.y + room.height / 2) | 0); + const player = playerEntity.get(Player); + if (!player) { + throw new Error('No player found'); } - - private drawRoom() { - const room = this.room?.get(Room); + + function drawRoom() { if (!room) return; - this.display.drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] }); - this.display.drawBox(room.x, room.y, room.width, room.height, { fill: ['.', Color.GRAY] }); + display.drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: ' ' }); + display.drawBox(room.x, room.y, room.width, room.height, { fill: ['.', Color.GRAY] }); } - - private drawMap(world: World) { - if (!this.room) return; - - this.display.drawBox(MAP_X, MAP_Y, MAP_WIDTH - 2, MAP_HEIGHT - 2, { fill: [' '], title: 'Map' }); - const worldPos = this.room.get(Position, 'world'); + + function drawMap() { + display.drawBox(MAP_X, MAP_Y, MAP_WIDTH - 2, MAP_HEIGHT - 2, { fill: [' '], title: 'Map' }); + const worldPos = roomEntity.get(Position, 'world'); const centerX = worldPos ? worldPos.state.x - MAP_X - MAP_WIDTH / 2 : 0; const centerY = worldPos ? worldPos.state.y - MAP_Y - MAP_HEIGHT / 2 : 0; const worldZ = worldPos ? worldPos.state.z : 0; @@ -65,31 +55,27 @@ class GameSystem extends System { if (x > MAP_X && x < MAP_X + MAP_WIDTH - 1 && y > MAP_Y && y < MAP_Y + MAP_HEIGHT - 1) { const char = getMapRoomChar(room); - this.display.setChar(x, y, [char, room === this.room ? Color.YELLOW : Color.WHITE]); + display.setChar(x, y, [char, room === roomEntity ? Color.YELLOW : Color.WHITE]); } } } - private drawInfo(world: World) { - if (!this.room || !this.player) return; - - const worldPos = this.room.get(Position, 'world'); + function drawInfo() { + const worldPos = roomEntity.get(Position, 'world'); const coords = worldPos ? `${worldPos.state.x},${worldPos.state.y},${worldPos.state.z}`.padStart(9) : '0,0,0'; const foundRooms = getRoomsCount(world); const totalRooms = getPossibleRoomsCount(world); const rooms = `${foundRooms}/${totalRooms}${foundRooms === totalRooms ? '' : '+'}`.padStart(coords.length - 2); - const inv = this.player.get(Inventory); + const inv = playerEntity.get(Inventory); const foundItems = inv ? Object.values(inv.state.slots).length : 0; const totalItems = Array.from(world.query(Item)).length; const items = `${foundItems}/${totalItems}`.padStart(coords.length - 2); - this.display.drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' }); + display.drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' }); } - private handleInput(world: World) { - const player = this.player?.get(Player); - let room = this.room?.get(Room); - if (!player || !room) return; - + let lastMove = Date.now(); + function handleInput() { + if (!room || !player) return; Input.updateKeys(); const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE); @@ -122,17 +108,17 @@ class GameSystem extends System { } if (moved) { - const activatedDoor = room.getActivatedDoor(newX, newY)?.get(Door); + const activatedDoor = room.getActivatedDoor(newX, newY); if (activatedDoor) { const shouldTravel = ![Direction.UP, Direction.DOWN].includes(activatedDoor.direction) || isSpacePressed; if (shouldTravel) { room.onLeave(); - this.room = getRoom(world, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ); - room = this.room.get(Room); + roomEntity = getRoom(world, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ); + room = roomEntity.get(Room); if (!room) return; room.onEnter(); - const oppositeDoor = room.doors[getOppositeDirection(activatedDoor.direction)]?.get(Door); + const oppositeDoor = room.getDoor(getOppositeDirection(activatedDoor.direction)); if (oppositeDoor) { switch (activatedDoor.direction) { @@ -181,15 +167,15 @@ class GameSystem extends System { player.y = newY; } } -} - -export default async function main() { - const world = new World(); - world.addSystem(new GameSystem()); while (true) { const dt = await nextFrame(); + handleInput(); + + drawRoom(); + drawMap(); + drawInfo(); world.update(dt); } } diff --git a/src/games/text-dungeon/room.ts b/src/games/text-dungeon/room.ts index 494cd3a..cba6ebc 100644 --- a/src/games/text-dungeon/room.ts +++ b/src/games/text-dungeon/room.ts @@ -10,7 +10,7 @@ import { spawnItem } from "./item"; import { Resources } from "@common/rpg/utils/resources"; import { TextRegion } from "@common/display/text"; -type IDoors = [Entity?, Entity?, Entity?, Entity?, Entity?, Entity?]; +type IDoors = [string?, string?, string?, string?, string?, string?]; @component export class Room extends Component<{ doors: IDoors }> { @@ -45,8 +45,11 @@ export class Room extends Component<{ doors: IDoors }> { get doors() { return this.state.doors; } - getActivatedDoor(px: number, py: number): Entity | undefined { - return this.state.doors.find((door) => { + getActivatedDoor(px: number, py: number): Door | undefined { + const id = this.state.doors.find((doorId) => { + if (!doorId) return false; + + const door = this.entity.world.getEntity(doorId); const doorPos = door?.get(Position); if (!doorPos) return false; @@ -56,14 +59,24 @@ export class Room extends Component<{ doors: IDoors }> { const { x, y } = doorPos.state; return px === x - dx && py === y - dy; }); + + if (!id) return undefined; + + return this.entity.world.getEntity(id)?.get(Door); + } + + getDoor(direction: Direction): Door | undefined { + const id = this.doors[direction]; + if (!id) return undefined; + return this.entity.world.getEntity(id)?.get(Door); } onEnter() { - this.doors.forEach(door => door?.remove(Hidden)); + this.doors.forEach(doorId => doorId && this.entity.world.getEntity(doorId)?.remove(Hidden)); } onLeave() { - this.doors.forEach(door => door?.add(new Hidden())); + this.doors.forEach(doorId => doorId && this.entity.world.getEntity(doorId)?.add(new Hidden())); } } @@ -110,7 +123,7 @@ interface IRoomBlank { const roomId = (worldX: number, worldY: number, worldZ: number) => `room_${worldX}_${worldY}_${worldZ}`; -const createDoor = (world: World, x: number, y: number, direction: Direction, worldX: number, worldY: number, worldZ: number): Entity => { +const createDoor = (world: World, x: number, y: number, direction: Direction, worldX: number, worldY: number, worldZ: number): string => { const door = world.createEntity('door_*'); door.add(new Position(x, y)); door.add(new Position(worldX, worldY, worldZ), 'world'); @@ -121,7 +134,7 @@ const createDoor = (world: World, x: number, y: number, direction: Direction, wo if (spr) { door.add(new Position((-spr.width / 2) | 0, (-spr.height / 2) | 0), 'offset'); } - return door; + return door.id; } const generateDoors = (world: World, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => { @@ -195,8 +208,12 @@ const generateItems = (world: World, { x, y, width, height, doors }: IRoomBlank) } const hasObjectAt = (x: number, y: number) => - doors.flat().filter(x => x != null).map(getPosition).some((d) => d?.x === x && d.y === y) || - items.map(getPosition).some((i) => i?.x === x && i.y === y); + doors + .filter(x => x != null) + .map((id) => getPosition(world.getEntity(id))) + .some((d) => d?.x === x && d.y === y) + || items.map((i) => getPosition(i)) + .some((i) => i?.x === x && i.y === y); const oneDoor = doors.filter((d) => Boolean(d)).length === 1; @@ -258,9 +275,10 @@ export const getPossibleRoomsCount = (world: World) => { for (const room of rooms) { const roomComp = room.get(Room); roomComp?.state.doors.forEach((d) => { - const worldPos = d?.get(Position, 'world'); + if (!d) return; + const worldPos = getPosition(world.getEntity(d) ,'world'); if (worldPos) { - const id = roomId(worldPos.state.x, worldPos.state.y, worldPos.state.z); + const id = roomId(worldPos.x, worldPos.y, worldPos.z); roomsSet.add(id); } });