Complete refactor text-dungeon to new ECS architecture.
This commit is contained in:
parent
762feba096
commit
2fe3fb1763
|
|
@ -11,7 +11,7 @@ interface ItemState {
|
|||
|
||||
@component
|
||||
export class Item extends Component<ItemState> {
|
||||
constructor(name: string, description = '') {
|
||||
constructor(name = '', description = '') {
|
||||
super({ name, description });
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ export namespace Items {
|
|||
equippable?: { slotType: string };
|
||||
}
|
||||
|
||||
export function register(world: World, id: string, name: string, options?: RegisterOptions) {
|
||||
export function register(world: World, id: string, name?: string, options?: RegisterOptions) {
|
||||
const entity = world.createEntity(id);
|
||||
entity.add(new Item(name, options?.description));
|
||||
if (options?.maxStack !== undefined) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component } from "../core/world";
|
||||
import { Component, Entity } from "../core/world";
|
||||
import { component } from "../utils/decorators";
|
||||
|
||||
@component({ variables: ['x', 'y', 'z'] })
|
||||
|
|
@ -7,3 +7,5 @@ export class Position extends Component<{ x: number, y: number, z: number }> {
|
|||
super({ x, y, z });
|
||||
}
|
||||
}
|
||||
|
||||
export const getPosition = (entity: Entity) => entity.get(Position)?.state;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Component } from "../core/world";
|
||||
import { SpriteSystem } from "../systems/render/sprite";
|
||||
import { component } from "../utils/decorators";
|
||||
|
||||
@component
|
||||
|
|
@ -20,9 +19,18 @@ export class Sprite extends Component<{
|
|||
});
|
||||
}
|
||||
|
||||
override onAdd(): void {
|
||||
if (Number.isFinite(this.state.animationDelay) && !this.world.hasSystem(SpriteSystem)) {
|
||||
override async onAdd() {
|
||||
if (Number.isFinite(this.state.animationDelay)) {
|
||||
const { SpriteSystem } = await import('../systems/render/sprite'); // dynamic import to break cyclic dependency
|
||||
if (!this.world.hasSystem(SpriteSystem)) {
|
||||
this.world.addSystem(new SpriteSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@component export class Hidden extends Component<{}> {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { TextRegion, TextDisplay } from "@common/display/text";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { Sprite } from "@common/rpg/components/sprite";
|
||||
import { Hidden, Sprite } from "@common/rpg/components/sprite";
|
||||
import { System, World } from "@common/rpg/core/world";
|
||||
import { Resources } from "@common/rpg/utils/resources";
|
||||
|
||||
|
|
@ -13,7 +13,9 @@ export class TextDisplaySystem extends System {
|
|||
}
|
||||
|
||||
override update(world: World) {
|
||||
for (const [, sprite, pos] of world.query(Sprite, Position)) {
|
||||
const sprites = Array.from(world.query(Sprite, Position)).sort((a, b) => a[2].state.z - b[2].state.z);
|
||||
for (const [e, sprite, pos] of sprites) {
|
||||
if (e.has(Hidden)) continue;
|
||||
const { frames, currentFrame } = sprite.state;
|
||||
const { x, y } = pos.state;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,17 @@
|
|||
import { World } from "@common/rpg/core/world";
|
||||
import { TextDisplay } from "@common/display/text";
|
||||
import { Inventory } from "@common/rpg/components/inventory";
|
||||
import { Health, Stat } from "@common/rpg/components/stat";
|
||||
import { Variables } from "@common/rpg/components/variables";
|
||||
import { QuestLog } from "@common/rpg/components/questLog";
|
||||
import { QuestSystem } from "@common/rpg/systems/quest";
|
||||
import { Items } from "@common/rpg/components/item";
|
||||
import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables";
|
||||
import { Serialization } from "@common/rpg/core/serialization";
|
||||
import { Factions } from "@common/rpg/components/faction";
|
||||
import { Effect } from "@common/rpg/components/effect";
|
||||
import { Equipment } from "@common/rpg/components/equipment";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { World } from "@common/rpg/core/world";
|
||||
import { TextDisplaySystem } from "@common/rpg/systems/render/text";
|
||||
|
||||
console.log("Hello, world 1");
|
||||
export default async function main() {
|
||||
const world = new World();
|
||||
world.addSystem(new QuestSystem());
|
||||
|
||||
Items.register(world, 'helmet', 'Iron Helmet');
|
||||
const boots = Items.register(world, 'boots', 'Leather Boots', { maxStack: 2, equippable: { slotType: 'feet' } });
|
||||
boots.add(new Effect({
|
||||
targetStat: 'agl',
|
||||
delta: 10,
|
||||
}));
|
||||
|
||||
const player = world.createEntity('player');
|
||||
const inventory = player.add(new Inventory());
|
||||
player.add(new Equipment('head', { slotName: 'feet', type: 'feet' }));
|
||||
player.add(new Health({ value: 100, max: 100 }));
|
||||
player.add(new Variables());
|
||||
player.add(new QuestLog([{
|
||||
id: 'test',
|
||||
description: 'Test quest',
|
||||
title: 'Test',
|
||||
stages: [],
|
||||
}]));
|
||||
player.add('str', new Stat({ value: 100 }));
|
||||
player.add('agl', new Stat({ value: 100 }));
|
||||
player.add(new Position(42, 69));
|
||||
|
||||
Factions.join(player, 'boobs');
|
||||
Factions.adjustReputation(player, 'guards', 10);
|
||||
Factions.adjustReputation(player, 'bandits', -10);
|
||||
|
||||
console.log(resolveVariables(world));
|
||||
|
||||
inventory.add({ itemId: 'helmet', amount: 1 });
|
||||
inventory.add({ itemId: 'boots', amount: 1 });
|
||||
inventory.equip('boots');
|
||||
|
||||
const vars = player.get(Variables)!;
|
||||
vars.set({ key: 'test', value: 'test' });
|
||||
|
||||
await executeAction('player.QuestLog.test.start', world);
|
||||
|
||||
console.log(resolveActions(world));
|
||||
console.log(resolveVariables(world));
|
||||
console.log(JSON.parse(Serialization.serialize(world)));
|
||||
const e = world.createEntity();
|
||||
e.add(new Position(1, 1));
|
||||
e.add(new Inventory());
|
||||
console.log("Hello, world!");
|
||||
const display = new TextDisplay();
|
||||
new TextDisplaySystem(display);
|
||||
console.log("Hello, world 2");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,24 +16,6 @@ export const MAP_Y = 0;
|
|||
export const MAP_WIDTH = GAME_WIDTH - ROOM_AREA_WIDTH - MAP_X;
|
||||
export const MAP_HEIGHT = 10;
|
||||
|
||||
export enum Color {
|
||||
BLACK = 0b0000,
|
||||
DARK_BLUE = 0b0001,
|
||||
DARK_CYAN = 0b0011,
|
||||
DARK_GREEN = 0b0010,
|
||||
DARK_YELLOW = 0b0110,
|
||||
DARK_RED = 0b0100,
|
||||
DARK_MAGENTA = 0b0101,
|
||||
GRAY = 0b0111,
|
||||
BLUE = 0b1001,
|
||||
CYAN = 0b1011,
|
||||
GREEN = 0b1010,
|
||||
YELLOW = 0b1110,
|
||||
RED = 0b1100,
|
||||
MAGENTA = 0b1101,
|
||||
WHITE = 0b1111,
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
NORTH = 0,
|
||||
EAST = 1,
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import type { TextDisplay } from "@common/display/text";
|
||||
|
||||
export abstract class Drawable {
|
||||
protected dirty: boolean = true;
|
||||
abstract doDraw(): void;
|
||||
constructor(protected display: TextDisplay) { }
|
||||
|
||||
draw() {
|
||||
if (this.dirty) {
|
||||
this.doDraw();
|
||||
this.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import type { TextDisplay } from "@common/display/text";
|
||||
import { Drawable } from "./drawable";
|
||||
import type { IRegion, ISpriteDefinition } from "./types";
|
||||
|
||||
export class Figure extends Drawable {
|
||||
private frames: IRegion[];
|
||||
private animationCounter = 0;
|
||||
private animationPeriod;
|
||||
|
||||
constructor(
|
||||
display: TextDisplay,
|
||||
public x: number,
|
||||
public y: number,
|
||||
definition: ISpriteDefinition,
|
||||
public frame: number = 0,
|
||||
) {
|
||||
super(display);
|
||||
this.x = x | 0;
|
||||
this.y = y | 0;
|
||||
this.frames = definition.frames;
|
||||
this.animationCounter = 0;
|
||||
this.animationPeriod = definition.animationPeriod ?? Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
this.animationCounter++;
|
||||
if (this.animationCounter >= this.animationPeriod) {
|
||||
this.animationCounter = 0;
|
||||
this.frame = (this.frame + 1) % this.numFrames;
|
||||
}
|
||||
|
||||
this.display.setRegion(this.x, this.y, this.width, this.height, this.image);
|
||||
}
|
||||
|
||||
get width() {
|
||||
const frame = this.frames[(this.frame % this.numFrames)];
|
||||
const line = frame[0];
|
||||
return line?.length ?? 0;
|
||||
}
|
||||
|
||||
get height() {
|
||||
const frame = this.frames[(this.frame % this.numFrames)];
|
||||
return frame.length;
|
||||
}
|
||||
|
||||
get numFrames() {
|
||||
return this.frames.length;
|
||||
}
|
||||
|
||||
get image() {
|
||||
return this.frames[(this.frame % this.numFrames)];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,30 @@
|
|||
import { Color } from "./const";
|
||||
import type { IChar, ISpriteDefinition } from "./types";
|
||||
import { Color, TextRegion } from "@common/display/text"
|
||||
import { Resources } from "@common/rpg/utils/resources";
|
||||
|
||||
const l = (line: string, fg = Color.WHITE, bg = Color.BLACK): IChar[] =>
|
||||
Array.from(line).map((c) => [c, fg, bg]);
|
||||
export const PLAYER_SPRITE = [
|
||||
new TextRegion('@', Color.YELLOW)
|
||||
].map(Resources.add);
|
||||
|
||||
export const PLAYER_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('@', Color.YELLOW),
|
||||
],
|
||||
],
|
||||
};
|
||||
export const DOOR_SPRITE = [
|
||||
new TextRegion('┘ └'),
|
||||
new TextRegion([
|
||||
'└',
|
||||
' ',
|
||||
'┌',
|
||||
]),
|
||||
new TextRegion('┐ ┌'),
|
||||
new TextRegion([
|
||||
'┘',
|
||||
' ',
|
||||
'┐',
|
||||
]),
|
||||
new TextRegion('^', Color.CYAN),
|
||||
new TextRegion('_', Color.GREEN),
|
||||
].map(Resources.add);
|
||||
|
||||
export const DOOR_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('┘ └'),
|
||||
],
|
||||
[
|
||||
l('└'),
|
||||
l(' '),
|
||||
l('┌'),
|
||||
],
|
||||
[
|
||||
l('┐ ┌'),
|
||||
],
|
||||
[
|
||||
l('┘'),
|
||||
l(' '),
|
||||
l('┐'),
|
||||
],
|
||||
[
|
||||
l('^', Color.CYAN),
|
||||
],
|
||||
[
|
||||
l('_', Color.GREEN),
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
export const ITEM_KEY_SPRITE: ISpriteDefinition = {
|
||||
frames: [
|
||||
[
|
||||
l('ъ', Color.MAGENTA),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.YELLOW),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.CYAN),
|
||||
],
|
||||
[
|
||||
l('ъ', Color.GREEN),
|
||||
],
|
||||
],
|
||||
}
|
||||
export const ITEM_SPRITE = [
|
||||
new TextRegion('ъ', Color.MAGENTA),
|
||||
new TextRegion('ъ', Color.YELLOW),
|
||||
new TextRegion('ъ', Color.CYAN),
|
||||
new TextRegion('ъ', Color.GREEN),
|
||||
].map(Resources.add);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,102 @@
|
|||
import { TextDisplay } from '@common/display/text';
|
||||
import { Color, TextDisplay } from '@common/display/text';
|
||||
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 { TextDisplaySystem } from '@common/rpg/systems/render/text';
|
||||
import { nextFrame } from '@common/utils';
|
||||
import './assets/style.css';
|
||||
import { Color, Direction, getOppositeDirection } from './const';
|
||||
import { createItems, getItemsCount } from './item';
|
||||
import { GameMap } from './map';
|
||||
import { Player } from './player';
|
||||
import { getPossibleRoomsCount, getRoom, getRoomsCount } from './room';
|
||||
|
||||
const display = new TextDisplay();
|
||||
let currentRoom = getRoom(display, 0, 0, 0);
|
||||
const map = new GameMap(display, currentRoom);
|
||||
currentRoom.draw();
|
||||
const player = new Player(display, currentRoom.x + currentRoom.width / 2, currentRoom.y + currentRoom.height / 2);
|
||||
import { Direction, getOppositeDirection, MAP_HEIGHT, MAP_WIDTH, MAP_X, MAP_Y, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from './const';
|
||||
import { getPossibleRoomsCount, getRoom, getRoomsCount, getRoomsForLayer, getMapRoomChar, Room, Door } from './room';
|
||||
import { createPlayer, Player } from './player';
|
||||
|
||||
let lastMove = Date.now();
|
||||
function handleInput() {
|
||||
|
||||
class GameSystem extends System {
|
||||
private readonly display = new TextDisplay();
|
||||
private room: Entity | undefined;
|
||||
private player: Entity | undefined;
|
||||
|
||||
override onAdd(world: World) {
|
||||
world.addSystem(new TextDisplaySystem(this.display));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
override update(world: World) {
|
||||
this.handleInput(world);
|
||||
|
||||
this.drawRoom();
|
||||
this.drawMap(world);
|
||||
this.drawInfo(world);
|
||||
}
|
||||
|
||||
private drawRoom() {
|
||||
const room = this.room?.get(Room);
|
||||
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] });
|
||||
}
|
||||
|
||||
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');
|
||||
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;
|
||||
|
||||
for (const room of getRoomsForLayer(world, worldZ)) {
|
||||
const roomPos = room.get(Position, 'world');
|
||||
const worldX = roomPos ? roomPos.state.x : 0;
|
||||
const worldY = roomPos ? roomPos.state.y : 0;
|
||||
const x = Math.round(worldX - centerX);
|
||||
const y = Math.round(worldY - centerY);
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawInfo(world: World) {
|
||||
if (!this.room || !this.player) return;
|
||||
|
||||
const worldPos = this.room.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 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' });
|
||||
}
|
||||
|
||||
private handleInput(world: World) {
|
||||
const player = this.player?.get(Player);
|
||||
let room = this.room?.get(Room);
|
||||
if (!player || !room) return;
|
||||
|
||||
Input.updateKeys();
|
||||
|
||||
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
||||
|
||||
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
||||
lastMove = Date.now();
|
||||
|
||||
let newX = player.x;
|
||||
let newY = player.y;
|
||||
let moved = isSpacePressed;
|
||||
|
|
@ -45,17 +122,18 @@ function handleInput() {
|
|||
}
|
||||
|
||||
if (moved) {
|
||||
const activatedDoor = currentRoom.getActivatedDoor(newX, newY);
|
||||
const activatedDoor = room.getActivatedDoor(newX, newY)?.get(Door);
|
||||
console.log({ activatedDoor });
|
||||
if (activatedDoor) {
|
||||
const shouldTravel = ![Direction.UP, Direction.DOWN].includes(activatedDoor.direction) || isSpacePressed;
|
||||
|
||||
if (shouldTravel) {
|
||||
currentRoom = getRoom(display, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ);
|
||||
currentRoom.invalidate();
|
||||
map.currentRoom = currentRoom;
|
||||
map.invalidate();
|
||||
player.skipNextBackgroundRestore = true;
|
||||
const oppositeDoor = currentRoom.doors[getOppositeDirection(activatedDoor.direction)];
|
||||
room.onLeave();
|
||||
this.room = getRoom(world, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ);
|
||||
room = this.room.get(Room);
|
||||
if (!room) return;
|
||||
room.onEnter();
|
||||
const oppositeDoor = room.doors[getOppositeDirection(activatedDoor.direction)]?.get(Door);
|
||||
|
||||
if (oppositeDoor) {
|
||||
switch (activatedDoor.direction) {
|
||||
|
|
@ -82,63 +160,37 @@ function handleInput() {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
newX = currentRoom.x + currentRoom.width / 2;
|
||||
newY = currentRoom.y + currentRoom.height / 2;
|
||||
newX = room.x + room.width / 2;
|
||||
newY = room.y + room.height / 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (newX < currentRoom.x + 1 || newX >= currentRoom.x + currentRoom.width + 1) {
|
||||
if (newX < room.x + 1 || newX >= room.x + room.width + 1) {
|
||||
newX = player.x;
|
||||
}
|
||||
if (newY < currentRoom.y + 1 || newY >= currentRoom.y + currentRoom.height + 1) {
|
||||
if (newY < room.y + 1 || newY >= room.y + room.height + 1) {
|
||||
newY = player.y;
|
||||
}
|
||||
}
|
||||
|
||||
const pickedItem = currentRoom.pickItem(newX, newY);
|
||||
if (pickedItem) {
|
||||
player.addItem(pickedItem);
|
||||
}
|
||||
// const pickedItem = room.pickItem(newX, newY);
|
||||
// if (pickedItem) {
|
||||
// player.addItem(pickedItem);
|
||||
// }
|
||||
|
||||
player.x = newX;
|
||||
player.y = newY;
|
||||
player.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogic() {
|
||||
|
||||
}
|
||||
|
||||
function drawInfo() {
|
||||
const coords = `${currentRoom.worldX},${currentRoom.worldY},${currentRoom.worldZ}`.padStart(9);
|
||||
const foundRooms = getRoomsCount();
|
||||
const totalRooms = getPossibleRoomsCount();
|
||||
const rooms = `${foundRooms}/${totalRooms}${foundRooms === totalRooms ? '' : '+'}`.padStart(coords.length - 2);
|
||||
const items = `${player.foundItems}/${getItemsCount()}`.padStart(coords.length - 2);
|
||||
display.drawTextInBox(0, 0, `Pos: ${coords}\nRooms: ${rooms}\nItems: ${items}`, { fg: Color.YELLOW, title: 'Info' });
|
||||
}
|
||||
|
||||
function draw() {
|
||||
currentRoom.draw();
|
||||
map.draw();
|
||||
player.draw();
|
||||
|
||||
drawInfo();
|
||||
}
|
||||
|
||||
export default async function main() {
|
||||
createItems(display);
|
||||
currentRoom.invalidate();
|
||||
player.invalidate();
|
||||
display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN });
|
||||
const world = new World();
|
||||
world.addSystem(new GameSystem());
|
||||
|
||||
while (true) {
|
||||
Input.updateKeys();
|
||||
handleInput();
|
||||
handleLogic();
|
||||
draw();
|
||||
const dt = await nextFrame();
|
||||
|
||||
await nextFrame();
|
||||
world.update(dt);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,43 @@
|
|||
import type { TextDisplay } from "@common/display/text";
|
||||
import { Drawable } from "./drawable";
|
||||
import { Figure } from "./figure";
|
||||
import { ITEM_KEY_SPRITE } from "./images";
|
||||
import { Component, type Entity, type World } from "@common/rpg/core/world";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { Sprite } from "@common/rpg/components/sprite";
|
||||
import { ITEM_SPRITE } from "./images";
|
||||
import { Item, Items } from "@common/rpg/components/item";
|
||||
import { component } from "@common/rpg/utils/decorators";
|
||||
|
||||
let globalItemId = 0;
|
||||
const idMap = new Map<number, Figure>();
|
||||
|
||||
export const getItemsCount = () => idMap.size;
|
||||
|
||||
export function createItems(display: TextDisplay) {
|
||||
for (let frame = 0; frame < ITEM_KEY_SPRITE.frames.length; frame++) {
|
||||
idMap.set(
|
||||
globalItemId++,
|
||||
new Figure(display, 0, 0, ITEM_KEY_SPRITE, frame),
|
||||
);
|
||||
@component
|
||||
class PickupCount extends Component<{ count: number }> {
|
||||
constructor(count = 1) {
|
||||
super({ count });
|
||||
}
|
||||
}
|
||||
|
||||
export class Item extends Drawable {
|
||||
constructor(
|
||||
display: TextDisplay,
|
||||
public id: number,
|
||||
public count: number = 1,
|
||||
public x: number = -1,
|
||||
public y: number = -1,
|
||||
) {
|
||||
super(display);
|
||||
export function findItem(world: World, id: number): Entity | undefined {
|
||||
for (const [item, , component] of world.query(Item)) {
|
||||
if (component.state.name === id.toString()) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
const figure = idMap.get(this.id);
|
||||
if (figure) {
|
||||
figure.x = this.x;
|
||||
figure.y = this.y;
|
||||
figure.doDraw();
|
||||
export function createItem(world: World, id: number): Entity {
|
||||
const item = Items.register(world, 'item_*', id.toString(), { maxStack: 1 });
|
||||
item.add(new Sprite(ITEM_SPRITE[id]));
|
||||
return item;
|
||||
}
|
||||
|
||||
export function createItems(world: World) {
|
||||
for (let id = 0; id < ITEM_SPRITE.length; id++) {
|
||||
createItem(world, id);
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnItem(world: World, id: number, count: number, x: number, y: number): Entity {
|
||||
const item = findItem(world, id) ?? createItem(world, id);
|
||||
|
||||
item.add(new Position(x, y));
|
||||
item.add(new PickupCount(count));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import type { TextDisplay } from "@common/display/text";
|
||||
import { Color, MAP_HEIGHT, MAP_ROOM_CHARS, MAP_WIDTH, MAP_X, MAP_Y } from "./const";
|
||||
import { Drawable } from "./drawable";
|
||||
import { getRoomsForLayer, Room } from "./room";
|
||||
|
||||
export class GameMap extends Drawable {
|
||||
constructor(display: TextDisplay, public currentRoom: Room) {
|
||||
super(display);
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
this.display.drawBox(MAP_X, MAP_Y, MAP_WIDTH - 2, MAP_HEIGHT - 2, { fill: [' '], title: 'Map' });
|
||||
const centerX = this.currentRoom.worldX - MAP_X - MAP_WIDTH / 2;
|
||||
const centerY = this.currentRoom.worldY - MAP_Y - MAP_HEIGHT / 2;
|
||||
|
||||
for (const room of getRoomsForLayer(this.currentRoom.worldZ)) {
|
||||
const x = Math.round(room.worldX - centerX);
|
||||
const y = Math.round(room.worldY - centerY);
|
||||
|
||||
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.currentRoom ? Color.YELLOW : Color.WHITE]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMapRoomChar(room: Room): string {
|
||||
let doorsMask = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const mask = 1 << i;
|
||||
if (room.doors[i]) {
|
||||
doorsMask |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
return MAP_ROOM_CHARS[doorsMask];
|
||||
}
|
||||
|
|
@ -1,47 +1,43 @@
|
|||
import { INVENTORY_X, INVENTORY_Y } from "./const";
|
||||
import { getItemsCount, Item } from "./item";
|
||||
import { Sprite } from "./sprite";
|
||||
import { component } from "@common/rpg/utils/decorators";
|
||||
import { Component, Entity, World } from "@common/rpg/core/world";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { Sprite } from "@common/rpg/components/sprite";
|
||||
import { PLAYER_SPRITE } from "./images";
|
||||
|
||||
export class Player extends Sprite {
|
||||
private inventory: Item[] = [];
|
||||
|
||||
addItem(item: Item) {
|
||||
for (const i of this.inventory) {
|
||||
if (i.id === item.id) {
|
||||
i.count += item.count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.inventory.push(new Item(
|
||||
this.display,
|
||||
item.id,
|
||||
item.count,
|
||||
INVENTORY_X + 1 + this.inventory.length,
|
||||
INVENTORY_Y + 1,
|
||||
));
|
||||
@component
|
||||
export class Player extends Component<{}> {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
|
||||
hasItem(item: Item) {
|
||||
for (const i of this.inventory) {
|
||||
if (i.id === item.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
get x() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
|
||||
get foundItems() {
|
||||
return this.inventory.length;
|
||||
get y() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
super.doDraw();
|
||||
set x(x: number) {
|
||||
const pos = this.entity.get(Position);
|
||||
if (pos) pos.state.x = x;
|
||||
}
|
||||
|
||||
this.display.drawBox(INVENTORY_X, INVENTORY_Y, getItemsCount(), 1, { fill: [' '], title: 'Inv' });
|
||||
this.inventory.forEach((item) => {
|
||||
if (item.count > 0) {
|
||||
item.doDraw();
|
||||
}
|
||||
});
|
||||
set y(y: number) {
|
||||
const pos = this.entity.get(Position);
|
||||
if (pos) pos.state.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
export const createPlayer = (world: World, x: number, y: number): Entity => {
|
||||
const player = world.createEntity('player');
|
||||
|
||||
player.add(new Position(x, y, 999));
|
||||
player.add(new Position(), 'world');
|
||||
player.add(new Sprite(PLAYER_SPRITE));
|
||||
player.add(new Player());
|
||||
|
||||
return player;
|
||||
}
|
||||
|
|
@ -1,93 +1,131 @@
|
|||
import { Inventory } from "@common/rpg/components/inventory";
|
||||
import { getPosition, Position } from "@common/rpg/components/position";
|
||||
import { Hidden, Sprite } from "@common/rpg/components/sprite";
|
||||
import { Component, type Entity, type World } from "@common/rpg/core/world";
|
||||
import { component } from "@common/rpg/utils/decorators";
|
||||
import { randInt } from "@common/utils";
|
||||
import { Color, Direction, DIRECTION_OFFSETS, getOppositeDirection, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
|
||||
import { Drawable } from "./drawable";
|
||||
import { Figure } from "./figure";
|
||||
import { DOOR_SPRITE } from "./images";
|
||||
import { getItemsCount, Item } from "./item";
|
||||
import type { TextDisplay } from "@common/display/text";
|
||||
import { Direction, DIRECTION_OFFSETS, getOppositeDirection, MAP_ROOM_CHARS, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
|
||||
import { DOOR_SPRITE, ITEM_SPRITE } from "./images";
|
||||
import { spawnItem } from "./item";
|
||||
import { Resources } from "@common/rpg/utils/resources";
|
||||
import { TextRegion } from "@common/display/text";
|
||||
|
||||
type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];
|
||||
type IDoors = [Entity?, Entity?, Entity?, Entity?, Entity?, Entity?];
|
||||
|
||||
export class Door extends Figure {
|
||||
constructor(
|
||||
display: TextDisplay,
|
||||
x: number,
|
||||
y: number,
|
||||
direction: Direction,
|
||||
public worldX: number,
|
||||
public worldY: number,
|
||||
public worldZ: number,
|
||||
) {
|
||||
super(display, x, y, DOOR_SPRITE);
|
||||
this.frame = direction;
|
||||
@component
|
||||
export class Room extends Component<{ doors: IDoors }> {
|
||||
get x() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
|
||||
isActivated(x: number, y: number): boolean {
|
||||
const cx = Math.floor(this.x + this.width / 2);
|
||||
const cy = Math.floor(this.y + this.height / 2);
|
||||
|
||||
return (cx === x && cy === y);
|
||||
get y() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
|
||||
get direction(): Direction {
|
||||
return this.frame as Direction;
|
||||
get width() {
|
||||
const pos = this.entity.get(Position, 'size');
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
get height() {
|
||||
const pos = this.entity.get(Position, 'size');
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
|
||||
export class Room extends Drawable {
|
||||
constructor(
|
||||
display: TextDisplay,
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
public readonly width: number,
|
||||
public readonly height: number,
|
||||
public readonly worldX: number,
|
||||
public readonly worldY: number,
|
||||
public readonly worldZ: number,
|
||||
public readonly doors: IDoors = [null, null, null, null, null, null],
|
||||
public readonly items: Item[] = [],
|
||||
) {
|
||||
super(display);
|
||||
get worldX() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
this.display.drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] });
|
||||
this.display.drawBox(this.x, this.y, this.width, this.height, { fill: ['.', Color.GRAY] });
|
||||
|
||||
this.doors.forEach((door) => {
|
||||
if (door) {
|
||||
door.doDraw();
|
||||
get worldY() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
get worldZ() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.z : 0;
|
||||
}
|
||||
get doors() {
|
||||
return this.state.doors;
|
||||
}
|
||||
getActivatedDoor(px: number, py: number): Entity | undefined {
|
||||
return this.state.doors.find((door) => {
|
||||
const doorPos = door?.get(Position);
|
||||
if (!doorPos) return false;
|
||||
|
||||
const offset = door?.get(Position, 'offset');
|
||||
const { x: dx = 0, y: dy = 0 } = offset?.state ?? {}
|
||||
|
||||
const { x, y } = doorPos.state;
|
||||
return px === x - dx && py === y - dy;
|
||||
});
|
||||
|
||||
this.items.forEach((item) => item.doDraw());
|
||||
}
|
||||
|
||||
getActivatedDoor(x: number, y: number): Door | null {
|
||||
return this.doors.find((door) => door?.isActivated(x, y)) ?? null;
|
||||
onEnter() {
|
||||
this.doors.forEach(door => door?.remove(Hidden));
|
||||
}
|
||||
|
||||
pickItem(x: number, y: number): Item | null {
|
||||
const itemIndex = this.items.findIndex((item) => item.x === x && item.y === y);
|
||||
if (itemIndex >= 0) {
|
||||
const [item] = this.items.splice(itemIndex, 1);
|
||||
this.invalidate();
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
onLeave() {
|
||||
this.doors.forEach(door => door?.add(new Hidden()));
|
||||
}
|
||||
}
|
||||
|
||||
type IRoomBlank = {
|
||||
-readonly [P in keyof Room]?: Room[P]
|
||||
@component
|
||||
export class Door extends Component<{ direction: Direction }> {
|
||||
constructor(direction: Direction) {
|
||||
super({ direction });
|
||||
}
|
||||
get x() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
get y() {
|
||||
const pos = this.entity.get(Position);
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
get direction() {
|
||||
return this.state.direction;
|
||||
}
|
||||
get worldX() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.x : 0;
|
||||
}
|
||||
get worldY() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.y : 0;
|
||||
}
|
||||
get worldZ() {
|
||||
const pos = this.entity.get(Position, 'world');
|
||||
return pos ? pos.state.z : 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface IRoomBlank {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
worldX: number;
|
||||
worldY: number;
|
||||
worldZ: number;
|
||||
doors: IDoors;
|
||||
};
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
const roomId = (worldX: number, worldY: number, worldZ: number) => `room_${worldX}_${worldY}_${worldZ}`;
|
||||
|
||||
const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;
|
||||
const createDoor = (world: World, x: number, y: number, direction: Direction, worldX: number, worldY: number, worldZ: number): Entity => {
|
||||
const door = world.createEntity('door_*');
|
||||
door.add(new Position(x, y));
|
||||
door.add(new Position(worldX, worldY, worldZ), 'world');
|
||||
door.add(new Door(direction));
|
||||
const resourceId = DOOR_SPRITE[direction];
|
||||
door.add(new Sprite(resourceId));
|
||||
const spr = Resources.get(TextRegion, resourceId);
|
||||
if (spr) {
|
||||
door.add(new Position((-spr.width / 2) | 0, (-spr.height / 2) | 0), 'offset');
|
||||
}
|
||||
return door;
|
||||
}
|
||||
|
||||
const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
||||
const doors: IDoors = [null, null, null, null, null, null];
|
||||
const generateDoors = (world: World, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
||||
const doors: IDoors = [];
|
||||
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
||||
console.error('World coordinates not defined for doors generation');
|
||||
return doors;
|
||||
|
|
@ -101,9 +139,9 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
|||
const [xoff, yoff] = DIRECTION_OFFSETS[i];
|
||||
const doorWorldX = worldX + xoff;
|
||||
const doorWorldY = worldY + yoff;
|
||||
const doorId = generateRoomId(doorWorldX, doorWorldY, worldZ);
|
||||
const doorRoom = rooms.get(doorId);
|
||||
if (doorRoom && !doorRoom.doors[getOppositeDirection(i)]) {
|
||||
const doorId = roomId(doorWorldX, doorWorldY, worldZ);
|
||||
const doorRoom = world.getEntity(doorId);
|
||||
if (doorRoom && !doorRoom.get(Room)?.state.doors[getOppositeDirection(i)]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +156,7 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
|||
case Direction.EAST: dx = x + width + 1; dy = doorY; break;
|
||||
case Direction.WEST: dx = x; dy = doorY; break;
|
||||
}
|
||||
doors[i] = new Door(display, dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
||||
doors[i] = createDoor(world, dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +164,7 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
|||
const doorX = randInt(x + 1, x + width - 2);
|
||||
const doorY = randInt(y + 1, y + height - 2);
|
||||
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
||||
doors[Direction.DOWN] = new Door(display, doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
||||
doors[Direction.DOWN] = createDoor(world, doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);
|
||||
|
||||
if (worldZ !== 0) {
|
||||
let upDoorX: number;
|
||||
|
|
@ -136,7 +174,7 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
|||
upDoorY = randInt(y + 1, y + height - 2);
|
||||
} while (doorX === upDoorX && doorY === upDoorY);
|
||||
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
||||
doors[Direction.UP] = new Door(display, upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
||||
doors[Direction.UP] = createDoor(world, upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,24 +183,24 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
|||
|
||||
const generatedItems = new Set<number>();
|
||||
|
||||
const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRoomBlank): Item[] => {
|
||||
const items: Item[] = [];
|
||||
const generateItems = (world: World, { x, y, width, height, doors }: IRoomBlank): Entity[] => {
|
||||
const items: Entity[] = [];
|
||||
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||
console.error('Screen coordinates not defined for items generation');
|
||||
return items;
|
||||
return [];
|
||||
}
|
||||
if (typeof doors === 'undefined') {
|
||||
console.error('Doors not defined for items generation');
|
||||
return items;
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasObjectAt = (x: number, y: number) =>
|
||||
doors.some((d) => d?.x === x && d?.y === y) ||
|
||||
items.some((i) => i.x === x && i.y === y);
|
||||
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);
|
||||
|
||||
const oneDoor = doors.filter((d) => Boolean(d)).length === 1;
|
||||
|
||||
const id = randInt(0, getItemsCount());
|
||||
const id = randInt(0, ITEM_SPRITE.length);
|
||||
|
||||
if (Math.random() < 0.4 && oneDoor && !generatedItems.has(id)) {
|
||||
let newItemX: number;
|
||||
|
|
@ -172,7 +210,7 @@ const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRo
|
|||
newItemX = randInt(x + 1, x + width - 2);
|
||||
newItemY = randInt(y + 1, y + height - 2);
|
||||
} while (hasObjectAt(newItemX, newItemY));
|
||||
const item = new Item(display, id, 1, newItemX, newItemY);
|
||||
const item = spawnItem(world, id, 1, newItemX, newItemY);
|
||||
|
||||
items.push(item);
|
||||
generatedItems.add(id);
|
||||
|
|
@ -181,36 +219,48 @@ const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRo
|
|||
return items;
|
||||
};
|
||||
|
||||
export function getRoom(display: TextDisplay, worldX: number, worldY: number, worldZ: number) {
|
||||
const id = generateRoomId(worldX, worldY, worldZ);
|
||||
const existingRoom = rooms.get(id);
|
||||
export function getRoom(world: World, worldX: number, worldY: number, worldZ: number): Entity {
|
||||
const id = roomId(worldX, worldY, worldZ);
|
||||
const existingRoom = world.getEntity(id);
|
||||
if (existingRoom) {
|
||||
return existingRoom;
|
||||
}
|
||||
const room = world.createEntity(id);
|
||||
|
||||
const width = randInt(5, ROOM_AREA_WIDTH - 5);
|
||||
const height = randInt(5, ROOM_AREA_HEIGHT - 5);
|
||||
|
||||
const x = Math.max(ROOM_AREA_X + randInt(2, ROOM_AREA_WIDTH - width - 2), ROOM_AREA_X + 2);
|
||||
const y = Math.max(ROOM_AREA_Y + randInt(2, ROOM_AREA_HEIGHT - height - 2), ROOM_AREA_Y + 2);
|
||||
|
||||
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };
|
||||
room.add(new Position(x, y));
|
||||
room.add(new Position(width, height), 'size');
|
||||
room.add(new Position(worldX, worldY, worldZ), 'world');
|
||||
|
||||
roomBlank.doors = generateDoors(display, roomBlank);
|
||||
roomBlank.items = generateItems(display, roomBlank);
|
||||
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ, doors: [] };
|
||||
|
||||
const room = new Room(display, x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);
|
||||
const doors = generateDoors(world, roomBlank);
|
||||
room.add(new Room({ doors }));
|
||||
const inv = room.add(new Inventory());
|
||||
|
||||
const items = generateItems(world, roomBlank);
|
||||
|
||||
items.forEach((item) => inv.add(item));
|
||||
|
||||
rooms.set(id, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
export const getRoomsCount = () => rooms.size;
|
||||
export const getPossibleRoomsCount = () => {
|
||||
const roomsSet = new Set(rooms.keys());
|
||||
for (const room of rooms.values()) {
|
||||
room.doors.forEach((d) => {
|
||||
if (d) {
|
||||
const id = generateRoomId(d.worldX, d.worldY, d.worldZ);
|
||||
export const getAllRooms = (world: World) => Array.from(world.query(Room)).map(x => x[0]);
|
||||
export const getRoomsCount = (world: World) => Array.from(world.query(Room)).length;
|
||||
export const getPossibleRoomsCount = (world: World) => {
|
||||
const rooms = getAllRooms(world);
|
||||
const roomsSet = new Set(rooms.map(x => x.id));
|
||||
for (const room of rooms) {
|
||||
const roomComp = room.get(Room);
|
||||
roomComp?.state.doors.forEach((d) => {
|
||||
const worldPos = d?.get(Position, 'world');
|
||||
if (worldPos) {
|
||||
const id = roomId(worldPos.state.x, worldPos.state.y, worldPos.state.z);
|
||||
roomsSet.add(id);
|
||||
}
|
||||
});
|
||||
|
|
@ -219,4 +269,21 @@ export const getPossibleRoomsCount = () => {
|
|||
return roomsSet.size;
|
||||
};
|
||||
|
||||
export const getRoomsForLayer = (z: number): Room[] => Array.from(rooms.values()).filter((r) => r.worldZ === z);
|
||||
export const getRoomsForLayer = (world: World, z: number): Entity[] =>
|
||||
getAllRooms(world).filter(r => r.get(Position, 'world')?.state.z === z);
|
||||
|
||||
export function getMapRoomChar(room: Entity): string {
|
||||
const roomComp = room.get(Room);
|
||||
if (!roomComp) return ' ';
|
||||
|
||||
const doors = roomComp.state.doors;
|
||||
let doorsMask = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const mask = 1 << i;
|
||||
if (doors[i]) {
|
||||
doorsMask |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
return MAP_ROOM_CHARS[doorsMask];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import type { TextDisplay } from "@common/display/text";
|
||||
import { Figure } from "./figure";
|
||||
import { PLAYER_SPRITE } from "./images";
|
||||
import type { IRegion } from "./types";
|
||||
|
||||
export class Sprite extends Figure {
|
||||
private prevX: number;
|
||||
private prevY: number;
|
||||
private prevImage: IRegion;
|
||||
|
||||
public skipNextBackgroundRestore = false;
|
||||
|
||||
constructor(
|
||||
display: TextDisplay,
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
super(display, x, y, PLAYER_SPRITE);
|
||||
|
||||
this.prevX = x | 0;
|
||||
this.prevY = y | 0;
|
||||
this.prevImage = this.display.getRegion(x, y, this.width, this.height);
|
||||
}
|
||||
|
||||
override doDraw() {
|
||||
if (this.skipNextBackgroundRestore) {
|
||||
this.skipNextBackgroundRestore = false;
|
||||
} else {
|
||||
this.display.setRegion(this.prevX, this.prevY, this.width, this.height, this.prevImage);
|
||||
}
|
||||
this.prevImage = this.display.getRegion(this.x, this.y, this.width, this.height);
|
||||
|
||||
super.doDraw();
|
||||
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Color } from "./const";
|
||||
|
||||
export type IColorLike = string | number | Color;
|
||||
export type IChar = [string, IColorLike?, IColorLike?];
|
||||
export type IRegion = IChar[][];
|
||||
|
||||
export interface ISpriteDefinition {
|
||||
frames: IRegion[];
|
||||
animationPeriod?: number;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { randInt } from "@common/utils";
|
||||
|
||||
export const randChar = (min = ' ', max = '~') =>
|
||||
String.fromCharCode(randInt(
|
||||
min.charCodeAt(0),
|
||||
max.charCodeAt(0) + 1,
|
||||
));
|
||||
|
||||
export const generateColors = () => {
|
||||
const colors: string[] = [];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const high = ((i & 0b1000) > 0) ? 'ff' : '7f';
|
||||
|
||||
const b = ((i & 0b0001) > 0) ? high : '00';
|
||||
const g = ((i & 0b0010) > 0) ? high : '00';
|
||||
const r = ((i & 0b0100) > 0) ? high : '00';
|
||||
|
||||
const color = `#${r}${g}${b}`;
|
||||
colors.push(color);
|
||||
}
|
||||
|
||||
return colors;
|
||||
};
|
||||
Loading…
Reference in New Issue