Complete refactor text-dungeon to new ECS architecture.
This commit is contained in:
parent
762feba096
commit
2fe3fb1763
|
|
@ -11,7 +11,7 @@ interface ItemState {
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Item extends Component<ItemState> {
|
export class Item extends Component<ItemState> {
|
||||||
constructor(name: string, description = '') {
|
constructor(name = '', description = '') {
|
||||||
super({ name, description });
|
super({ name, description });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ export namespace Items {
|
||||||
equippable?: { slotType: string };
|
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);
|
const entity = world.createEntity(id);
|
||||||
entity.add(new Item(name, options?.description));
|
entity.add(new Item(name, options?.description));
|
||||||
if (options?.maxStack !== undefined) {
|
if (options?.maxStack !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component } from "../core/world";
|
import { Component, Entity } from "../core/world";
|
||||||
import { component } from "../utils/decorators";
|
import { component } from "../utils/decorators";
|
||||||
|
|
||||||
@component({ variables: ['x', 'y', 'z'] })
|
@component({ variables: ['x', 'y', 'z'] })
|
||||||
|
|
@ -7,3 +7,5 @@ export class Position extends Component<{ x: number, y: number, z: number }> {
|
||||||
super({ x, y, z });
|
super({ x, y, z });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPosition = (entity: Entity) => entity.get(Position)?.state;
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Component } from "../core/world";
|
import { Component } from "../core/world";
|
||||||
import { SpriteSystem } from "../systems/render/sprite";
|
|
||||||
import { component } from "../utils/decorators";
|
import { component } from "../utils/decorators";
|
||||||
|
|
||||||
@component
|
@component
|
||||||
|
|
@ -20,9 +19,18 @@ export class Sprite extends Component<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAdd(): void {
|
override async onAdd() {
|
||||||
if (Number.isFinite(this.state.animationDelay) && !this.world.hasSystem(SpriteSystem)) {
|
if (Number.isFinite(this.state.animationDelay)) {
|
||||||
this.world.addSystem(new SpriteSystem());
|
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 { TextRegion, TextDisplay } from "@common/display/text";
|
||||||
import { Position } from "@common/rpg/components/position";
|
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 { System, World } from "@common/rpg/core/world";
|
||||||
import { Resources } from "@common/rpg/utils/resources";
|
import { Resources } from "@common/rpg/utils/resources";
|
||||||
|
|
||||||
|
|
@ -13,7 +13,9 @@ export class TextDisplaySystem extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
override update(world: World) {
|
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 { frames, currentFrame } = sprite.state;
|
||||||
const { x, y } = pos.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 { 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 { 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() {
|
export default async function main() {
|
||||||
const world = new World();
|
const world = new World();
|
||||||
world.addSystem(new QuestSystem());
|
const e = world.createEntity();
|
||||||
|
e.add(new Position(1, 1));
|
||||||
Items.register(world, 'helmet', 'Iron Helmet');
|
e.add(new Inventory());
|
||||||
const boots = Items.register(world, 'boots', 'Leather Boots', { maxStack: 2, equippable: { slotType: 'feet' } });
|
console.log("Hello, world!");
|
||||||
boots.add(new Effect({
|
const display = new TextDisplay();
|
||||||
targetStat: 'agl',
|
new TextDisplaySystem(display);
|
||||||
delta: 10,
|
console.log("Hello, world 2");
|
||||||
}));
|
|
||||||
|
|
||||||
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)));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,6 @@ export const MAP_Y = 0;
|
||||||
export const MAP_WIDTH = GAME_WIDTH - ROOM_AREA_WIDTH - MAP_X;
|
export const MAP_WIDTH = GAME_WIDTH - ROOM_AREA_WIDTH - MAP_X;
|
||||||
export const MAP_HEIGHT = 10;
|
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 {
|
export enum Direction {
|
||||||
NORTH = 0,
|
NORTH = 0,
|
||||||
EAST = 1,
|
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 { Color, TextRegion } from "@common/display/text"
|
||||||
import type { IChar, ISpriteDefinition } from "./types";
|
import { Resources } from "@common/rpg/utils/resources";
|
||||||
|
|
||||||
const l = (line: string, fg = Color.WHITE, bg = Color.BLACK): IChar[] =>
|
export const PLAYER_SPRITE = [
|
||||||
Array.from(line).map((c) => [c, fg, bg]);
|
new TextRegion('@', Color.YELLOW)
|
||||||
|
].map(Resources.add);
|
||||||
|
|
||||||
export const PLAYER_SPRITE: ISpriteDefinition = {
|
export const DOOR_SPRITE = [
|
||||||
frames: [
|
new TextRegion('┘ └'),
|
||||||
[
|
new TextRegion([
|
||||||
l('@', Color.YELLOW),
|
'└',
|
||||||
],
|
' ',
|
||||||
],
|
'┌',
|
||||||
};
|
]),
|
||||||
|
new TextRegion('┐ ┌'),
|
||||||
|
new TextRegion([
|
||||||
|
'┘',
|
||||||
|
' ',
|
||||||
|
'┐',
|
||||||
|
]),
|
||||||
|
new TextRegion('^', Color.CYAN),
|
||||||
|
new TextRegion('_', Color.GREEN),
|
||||||
|
].map(Resources.add);
|
||||||
|
|
||||||
export const DOOR_SPRITE: ISpriteDefinition = {
|
export const ITEM_SPRITE = [
|
||||||
frames: [
|
new TextRegion('ъ', Color.MAGENTA),
|
||||||
[
|
new TextRegion('ъ', Color.YELLOW),
|
||||||
l('┘ └'),
|
new TextRegion('ъ', Color.CYAN),
|
||||||
],
|
new TextRegion('ъ', Color.GREEN),
|
||||||
[
|
].map(Resources.add);
|
||||||
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),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,196 @@
|
||||||
import { TextDisplay } from '@common/display/text';
|
import { Color, TextDisplay } from '@common/display/text';
|
||||||
import Input from '@common/input';
|
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 { nextFrame } from '@common/utils';
|
||||||
import './assets/style.css';
|
import './assets/style.css';
|
||||||
import { Color, Direction, getOppositeDirection } from './const';
|
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 { createItems, getItemsCount } from './item';
|
import { getPossibleRoomsCount, getRoom, getRoomsCount, getRoomsForLayer, getMapRoomChar, Room, Door } from './room';
|
||||||
import { GameMap } from './map';
|
import { createPlayer, Player } from './player';
|
||||||
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);
|
|
||||||
|
|
||||||
let lastMove = Date.now();
|
let lastMove = Date.now();
|
||||||
function handleInput() {
|
|
||||||
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
|
||||||
|
|
||||||
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
class GameSystem extends System {
|
||||||
lastMove = Date.now();
|
private readonly display = new TextDisplay();
|
||||||
let newX = player.x;
|
private room: Entity | undefined;
|
||||||
let newY = player.y;
|
private player: Entity | undefined;
|
||||||
let moved = isSpacePressed;
|
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.UP)) {
|
override onAdd(world: World) {
|
||||||
newY--;
|
world.addSystem(new TextDisplaySystem(this.display));
|
||||||
moved = true;
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.DOWN)) {
|
override update(world: World) {
|
||||||
newY++;
|
this.handleInput(world);
|
||||||
moved = true;
|
|
||||||
|
this.drawRoom();
|
||||||
|
this.drawMap(world);
|
||||||
|
this.drawInfo(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.LEFT)) {
|
private drawRoom() {
|
||||||
newX--;
|
const room = this.room?.get(Room);
|
||||||
moved = true;
|
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] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input.isHeld(Input.KeyCode.RIGHT)) {
|
private drawMap(world: World) {
|
||||||
newX++;
|
if (!this.room) return;
|
||||||
moved = true;
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moved) {
|
private drawInfo(world: World) {
|
||||||
const activatedDoor = currentRoom.getActivatedDoor(newX, newY);
|
if (!this.room || !this.player) return;
|
||||||
if (activatedDoor) {
|
|
||||||
const shouldTravel = ![Direction.UP, Direction.DOWN].includes(activatedDoor.direction) || isSpacePressed;
|
|
||||||
|
|
||||||
if (shouldTravel) {
|
const worldPos = this.room.get(Position, 'world');
|
||||||
currentRoom = getRoom(display, activatedDoor.worldX, activatedDoor.worldY, activatedDoor.worldZ);
|
const coords = worldPos ? `${worldPos.state.x},${worldPos.state.y},${worldPos.state.z}`.padStart(9) : '0,0,0';
|
||||||
currentRoom.invalidate();
|
const foundRooms = getRoomsCount(world);
|
||||||
map.currentRoom = currentRoom;
|
const totalRooms = getPossibleRoomsCount(world);
|
||||||
map.invalidate();
|
const rooms = `${foundRooms}/${totalRooms}${foundRooms === totalRooms ? '' : '+'}`.padStart(coords.length - 2);
|
||||||
player.skipNextBackgroundRestore = true;
|
const inv = this.player.get(Inventory);
|
||||||
const oppositeDoor = currentRoom.doors[getOppositeDirection(activatedDoor.direction)];
|
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' });
|
||||||
|
}
|
||||||
|
|
||||||
if (oppositeDoor) {
|
private handleInput(world: World) {
|
||||||
switch (activatedDoor.direction) {
|
const player = this.player?.get(Player);
|
||||||
case Direction.NORTH:
|
let room = this.room?.get(Room);
|
||||||
newX = oppositeDoor.x + 1;
|
if (!player || !room) return;
|
||||||
newY = oppositeDoor.y - 1;
|
|
||||||
break;
|
Input.updateKeys();
|
||||||
case Direction.SOUTH:
|
|
||||||
newX = oppositeDoor.x + 1;
|
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
||||||
newY = oppositeDoor.y + 1;
|
|
||||||
break;
|
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
||||||
case Direction.EAST:
|
lastMove = Date.now();
|
||||||
newX = oppositeDoor.x + 1;
|
|
||||||
newY = oppositeDoor.y + 1;
|
let newX = player.x;
|
||||||
break;
|
let newY = player.y;
|
||||||
case Direction.WEST:
|
let moved = isSpacePressed;
|
||||||
newX = oppositeDoor.x - 1;
|
|
||||||
newY = oppositeDoor.y + 1;
|
if (Input.isHeld(Input.KeyCode.UP)) {
|
||||||
break;
|
newY--;
|
||||||
case Direction.UP:
|
moved = true;
|
||||||
case Direction.DOWN:
|
}
|
||||||
newX = oppositeDoor.x;
|
|
||||||
newY = oppositeDoor.y;
|
if (Input.isHeld(Input.KeyCode.DOWN)) {
|
||||||
break;
|
newY++;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.LEFT)) {
|
||||||
|
newX--;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.isHeld(Input.KeyCode.RIGHT)) {
|
||||||
|
newX++;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
case Direction.NORTH:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y - 1;
|
||||||
|
break;
|
||||||
|
case Direction.SOUTH:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.EAST:
|
||||||
|
newX = oppositeDoor.x + 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.WEST:
|
||||||
|
newX = oppositeDoor.x - 1;
|
||||||
|
newY = oppositeDoor.y + 1;
|
||||||
|
break;
|
||||||
|
case Direction.UP:
|
||||||
|
case Direction.DOWN:
|
||||||
|
newX = oppositeDoor.x;
|
||||||
|
newY = oppositeDoor.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newX = room.x + room.width / 2;
|
||||||
|
newY = room.y + room.height / 2;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
newX = currentRoom.x + currentRoom.width / 2;
|
} else {
|
||||||
newY = currentRoom.y + currentRoom.height / 2;
|
if (newX < room.x + 1 || newX >= room.x + room.width + 1) {
|
||||||
|
newX = player.x;
|
||||||
|
}
|
||||||
|
if (newY < room.y + 1 || newY >= room.y + room.height + 1) {
|
||||||
|
newY = player.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (newX < currentRoom.x + 1 || newX >= currentRoom.x + currentRoom.width + 1) {
|
|
||||||
newX = player.x;
|
|
||||||
}
|
|
||||||
if (newY < currentRoom.y + 1 || newY >= currentRoom.y + currentRoom.height + 1) {
|
|
||||||
newY = player.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickedItem = currentRoom.pickItem(newX, newY);
|
// const pickedItem = room.pickItem(newX, newY);
|
||||||
if (pickedItem) {
|
// if (pickedItem) {
|
||||||
player.addItem(pickedItem);
|
// player.addItem(pickedItem);
|
||||||
}
|
// }
|
||||||
|
|
||||||
player.x = newX;
|
player.x = newX;
|
||||||
player.y = newY;
|
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() {
|
export default async function main() {
|
||||||
createItems(display);
|
const world = new World();
|
||||||
currentRoom.invalidate();
|
world.addSystem(new GameSystem());
|
||||||
player.invalidate();
|
|
||||||
display.drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN });
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Input.updateKeys();
|
const dt = await nextFrame();
|
||||||
handleInput();
|
|
||||||
handleLogic();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
await nextFrame();
|
world.update(dt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,43 @@
|
||||||
import type { TextDisplay } from "@common/display/text";
|
import { Component, type Entity, type World } from "@common/rpg/core/world";
|
||||||
import { Drawable } from "./drawable";
|
import { Position } from "@common/rpg/components/position";
|
||||||
import { Figure } from "./figure";
|
import { Sprite } from "@common/rpg/components/sprite";
|
||||||
import { ITEM_KEY_SPRITE } from "./images";
|
import { ITEM_SPRITE } from "./images";
|
||||||
|
import { Item, Items } from "@common/rpg/components/item";
|
||||||
|
import { component } from "@common/rpg/utils/decorators";
|
||||||
|
|
||||||
let globalItemId = 0;
|
@component
|
||||||
const idMap = new Map<number, Figure>();
|
class PickupCount extends Component<{ count: number }> {
|
||||||
|
constructor(count = 1) {
|
||||||
export const getItemsCount = () => idMap.size;
|
super({ count });
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Item extends Drawable {
|
export function findItem(world: World, id: number): Entity | undefined {
|
||||||
constructor(
|
for (const [item, , component] of world.query(Item)) {
|
||||||
display: TextDisplay,
|
if (component.state.name === id.toString()) {
|
||||||
public id: number,
|
return item;
|
||||||
public count: number = 1,
|
|
||||||
public x: number = -1,
|
|
||||||
public y: number = -1,
|
|
||||||
) {
|
|
||||||
super(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
override doDraw() {
|
|
||||||
const figure = idMap.get(this.id);
|
|
||||||
if (figure) {
|
|
||||||
figure.x = this.x;
|
|
||||||
figure.y = this.y;
|
|
||||||
figure.doDraw();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { component } from "@common/rpg/utils/decorators";
|
||||||
import { getItemsCount, Item } from "./item";
|
import { Component, Entity, World } from "@common/rpg/core/world";
|
||||||
import { Sprite } from "./sprite";
|
import { Position } from "@common/rpg/components/position";
|
||||||
|
import { Sprite } from "@common/rpg/components/sprite";
|
||||||
|
import { PLAYER_SPRITE } from "./images";
|
||||||
|
|
||||||
export class Player extends Sprite {
|
@component
|
||||||
private inventory: Item[] = [];
|
export class Player extends Component<{}> {
|
||||||
|
constructor() {
|
||||||
addItem(item: Item) {
|
super({});
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasItem(item: Item) {
|
get x() {
|
||||||
for (const i of this.inventory) {
|
const pos = this.entity.get(Position);
|
||||||
if (i.id === item.id) {
|
return pos ? pos.state.x : 0;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get foundItems() {
|
get y() {
|
||||||
return this.inventory.length;
|
const pos = this.entity.get(Position);
|
||||||
|
return pos ? pos.state.y : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
override doDraw() {
|
set x(x: number) {
|
||||||
super.doDraw();
|
const pos = this.entity.get(Position);
|
||||||
|
if (pos) pos.state.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
this.display.drawBox(INVENTORY_X, INVENTORY_Y, getItemsCount(), 1, { fill: [' '], title: 'Inv' });
|
set y(y: number) {
|
||||||
this.inventory.forEach((item) => {
|
const pos = this.entity.get(Position);
|
||||||
if (item.count > 0) {
|
if (pos) pos.state.y = y;
|
||||||
item.doDraw();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { 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 { Direction, DIRECTION_OFFSETS, getOppositeDirection, MAP_ROOM_CHARS, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
|
||||||
import { Drawable } from "./drawable";
|
import { DOOR_SPRITE, ITEM_SPRITE } from "./images";
|
||||||
import { Figure } from "./figure";
|
import { spawnItem } from "./item";
|
||||||
import { DOOR_SPRITE } from "./images";
|
import { Resources } from "@common/rpg/utils/resources";
|
||||||
import { getItemsCount, Item } from "./item";
|
import { TextRegion } from "@common/display/text";
|
||||||
import type { TextDisplay } 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 {
|
@component
|
||||||
constructor(
|
export class Room extends Component<{ doors: IDoors }> {
|
||||||
display: TextDisplay,
|
get x() {
|
||||||
x: number,
|
const pos = this.entity.get(Position);
|
||||||
y: number,
|
return pos ? pos.state.x : 0;
|
||||||
direction: Direction,
|
|
||||||
public worldX: number,
|
|
||||||
public worldY: number,
|
|
||||||
public worldZ: number,
|
|
||||||
) {
|
|
||||||
super(display, x, y, DOOR_SPRITE);
|
|
||||||
this.frame = direction;
|
|
||||||
}
|
}
|
||||||
|
get y() {
|
||||||
isActivated(x: number, y: number): boolean {
|
const pos = this.entity.get(Position);
|
||||||
const cx = Math.floor(this.x + this.width / 2);
|
return pos ? pos.state.y : 0;
|
||||||
const cy = Math.floor(this.y + this.height / 2);
|
|
||||||
|
|
||||||
return (cx === x && cy === y);
|
|
||||||
}
|
}
|
||||||
|
get width() {
|
||||||
get direction(): Direction {
|
const pos = this.entity.get(Position, 'size');
|
||||||
return this.frame as Direction;
|
return pos ? pos.state.x : 0;
|
||||||
}
|
}
|
||||||
}
|
get height() {
|
||||||
|
const pos = this.entity.get(Position, 'size');
|
||||||
export class Room extends Drawable {
|
return pos ? pos.state.y : 0;
|
||||||
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
override doDraw() {
|
const offset = door?.get(Position, 'offset');
|
||||||
this.display.drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] });
|
const { x: dx = 0, y: dy = 0 } = offset?.state ?? {}
|
||||||
this.display.drawBox(this.x, this.y, this.width, this.height, { fill: ['.', Color.GRAY] });
|
|
||||||
|
|
||||||
this.doors.forEach((door) => {
|
const { x, y } = doorPos.state;
|
||||||
if (door) {
|
return px === x - dx && py === y - dy;
|
||||||
door.doDraw();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.items.forEach((item) => item.doDraw());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getActivatedDoor(x: number, y: number): Door | null {
|
onEnter() {
|
||||||
return this.doors.find((door) => door?.isActivated(x, y)) ?? null;
|
this.doors.forEach(door => door?.remove(Hidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
pickItem(x: number, y: number): Item | null {
|
onLeave() {
|
||||||
const itemIndex = this.items.findIndex((item) => item.x === x && item.y === y);
|
this.doors.forEach(door => door?.add(new Hidden()));
|
||||||
if (itemIndex >= 0) {
|
|
||||||
const [item] = this.items.splice(itemIndex, 1);
|
|
||||||
this.invalidate();
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type IRoomBlank = {
|
@component
|
||||||
-readonly [P in keyof Room]?: Room[P]
|
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 generateDoors = (world: World, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
|
||||||
const doors: IDoors = [null, null, null, null, null, null];
|
const doors: IDoors = [];
|
||||||
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
|
||||||
console.error('World coordinates not defined for doors generation');
|
console.error('World coordinates not defined for doors generation');
|
||||||
return doors;
|
return doors;
|
||||||
|
|
@ -101,9 +139,9 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
||||||
const [xoff, yoff] = DIRECTION_OFFSETS[i];
|
const [xoff, yoff] = DIRECTION_OFFSETS[i];
|
||||||
const doorWorldX = worldX + xoff;
|
const doorWorldX = worldX + xoff;
|
||||||
const doorWorldY = worldY + yoff;
|
const doorWorldY = worldY + yoff;
|
||||||
const doorId = generateRoomId(doorWorldX, doorWorldY, worldZ);
|
const doorId = roomId(doorWorldX, doorWorldY, worldZ);
|
||||||
const doorRoom = rooms.get(doorId);
|
const doorRoom = world.getEntity(doorId);
|
||||||
if (doorRoom && !doorRoom.doors[getOppositeDirection(i)]) {
|
if (doorRoom && !doorRoom.get(Room)?.state.doors[getOppositeDirection(i)]) {
|
||||||
continue;
|
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.EAST: dx = x + width + 1; dy = doorY; break;
|
||||||
case Direction.WEST: dx = x; 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 doorX = randInt(x + 1, x + width - 2);
|
||||||
const doorY = randInt(y + 1, y + height - 2);
|
const doorY = randInt(y + 1, y + height - 2);
|
||||||
const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
|
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) {
|
if (worldZ !== 0) {
|
||||||
let upDoorX: number;
|
let upDoorX: number;
|
||||||
|
|
@ -136,7 +174,7 @@ const generateDoors = (display: TextDisplay, { x, y, width, height, worldX, worl
|
||||||
upDoorY = randInt(y + 1, y + height - 2);
|
upDoorY = randInt(y + 1, y + height - 2);
|
||||||
} while (doorX === upDoorX && doorY === upDoorY);
|
} while (doorX === upDoorX && doorY === upDoorY);
|
||||||
const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
|
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 generatedItems = new Set<number>();
|
||||||
|
|
||||||
const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRoomBlank): Item[] => {
|
const generateItems = (world: World, { x, y, width, height, doors }: IRoomBlank): Entity[] => {
|
||||||
const items: Item[] = [];
|
const items: Entity[] = [];
|
||||||
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
|
||||||
console.error('Screen coordinates not defined for items generation');
|
console.error('Screen coordinates not defined for items generation');
|
||||||
return items;
|
return [];
|
||||||
}
|
}
|
||||||
if (typeof doors === 'undefined') {
|
if (typeof doors === 'undefined') {
|
||||||
console.error('Doors not defined for items generation');
|
console.error('Doors not defined for items generation');
|
||||||
return items;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasObjectAt = (x: number, y: number) =>
|
const hasObjectAt = (x: number, y: number) =>
|
||||||
doors.some((d) => d?.x === x && d?.y === y) ||
|
doors.flat().filter(x => x != null).map(getPosition).some((d) => d?.x === x && d.y === y) ||
|
||||||
items.some((i) => i.x === x && i.y === y);
|
items.map(getPosition).some((i) => i?.x === x && i.y === y);
|
||||||
|
|
||||||
const oneDoor = doors.filter((d) => Boolean(d)).length === 1;
|
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)) {
|
if (Math.random() < 0.4 && oneDoor && !generatedItems.has(id)) {
|
||||||
let newItemX: number;
|
let newItemX: number;
|
||||||
|
|
@ -172,7 +210,7 @@ const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRo
|
||||||
newItemX = randInt(x + 1, x + width - 2);
|
newItemX = randInt(x + 1, x + width - 2);
|
||||||
newItemY = randInt(y + 1, y + height - 2);
|
newItemY = randInt(y + 1, y + height - 2);
|
||||||
} while (hasObjectAt(newItemX, newItemY));
|
} while (hasObjectAt(newItemX, newItemY));
|
||||||
const item = new Item(display, id, 1, newItemX, newItemY);
|
const item = spawnItem(world, id, 1, newItemX, newItemY);
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
generatedItems.add(id);
|
generatedItems.add(id);
|
||||||
|
|
@ -181,36 +219,48 @@ const generateItems = (display: TextDisplay, { x, y, width, height, doors }: IRo
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getRoom(display: TextDisplay, worldX: number, worldY: number, worldZ: number) {
|
export function getRoom(world: World, worldX: number, worldY: number, worldZ: number): Entity {
|
||||||
const id = generateRoomId(worldX, worldY, worldZ);
|
const id = roomId(worldX, worldY, worldZ);
|
||||||
const existingRoom = rooms.get(id);
|
const existingRoom = world.getEntity(id);
|
||||||
if (existingRoom) {
|
if (existingRoom) {
|
||||||
return existingRoom;
|
return existingRoom;
|
||||||
}
|
}
|
||||||
|
const room = world.createEntity(id);
|
||||||
|
|
||||||
const width = randInt(5, ROOM_AREA_WIDTH - 5);
|
const width = randInt(5, ROOM_AREA_WIDTH - 5);
|
||||||
const height = randInt(5, ROOM_AREA_HEIGHT - 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 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 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);
|
const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ, doors: [] };
|
||||||
roomBlank.items = generateItems(display, roomBlank);
|
|
||||||
|
|
||||||
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;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRoomsCount = () => rooms.size;
|
export const getAllRooms = (world: World) => Array.from(world.query(Room)).map(x => x[0]);
|
||||||
export const getPossibleRoomsCount = () => {
|
export const getRoomsCount = (world: World) => Array.from(world.query(Room)).length;
|
||||||
const roomsSet = new Set(rooms.keys());
|
export const getPossibleRoomsCount = (world: World) => {
|
||||||
for (const room of rooms.values()) {
|
const rooms = getAllRooms(world);
|
||||||
room.doors.forEach((d) => {
|
const roomsSet = new Set(rooms.map(x => x.id));
|
||||||
if (d) {
|
for (const room of rooms) {
|
||||||
const id = generateRoomId(d.worldX, d.worldY, d.worldZ);
|
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);
|
roomsSet.add(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -219,4 +269,21 @@ export const getPossibleRoomsCount = () => {
|
||||||
return roomsSet.size;
|
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