Compare commits
No commits in common. "5c46a531fa97800bf6763c37fb26f013ce1e8288" and "2bedecb677dec941d24e9e125a1dc2d0db089311" have entirely different histories.
5c46a531fa
...
2bedecb677
|
|
@ -138,11 +138,6 @@ export enum Glyphs {
|
||||||
DIAMOND = '♦',
|
DIAMOND = '♦',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AlignHorizontal {
|
|
||||||
LEFT = 'left',
|
|
||||||
RIGHT = 'right',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isVertical = (char: string) => char === Glyphs.SINGLE_VERTICAL || char === Glyphs.DOUBLE_VERTICAL;
|
export const isVertical = (char: string) => char === Glyphs.SINGLE_VERTICAL || char === Glyphs.DOUBLE_VERTICAL;
|
||||||
export const isHorizontal = (char: string) => char === Glyphs.SINGLE_HORIZONTAL || char === Glyphs.DOUBLE_HORIZONTAL;
|
export const isHorizontal = (char: string) => char === Glyphs.SINGLE_HORIZONTAL || char === Glyphs.DOUBLE_HORIZONTAL;
|
||||||
export const isCorner = (char: string) => [
|
export const isCorner = (char: string) => [
|
||||||
|
|
@ -150,14 +145,7 @@ export const isCorner = (char: string) => [
|
||||||
Glyphs.DOUBLE_TOP_LEFT, Glyphs.DOUBLE_TOP_RIGHT, Glyphs.DOUBLE_BOTTOM_LEFT, Glyphs.DOUBLE_BOTTOM_RIGHT,
|
Glyphs.DOUBLE_TOP_LEFT, Glyphs.DOUBLE_TOP_RIGHT, Glyphs.DOUBLE_BOTTOM_LEFT, Glyphs.DOUBLE_BOTTOM_RIGHT,
|
||||||
].includes(char as Glyphs);
|
].includes(char as Glyphs);
|
||||||
|
|
||||||
interface StringOptions {
|
interface BoxOptions {
|
||||||
fg?: ColorLike;
|
|
||||||
bg?: ColorLike;
|
|
||||||
alignH?: AlignHorizontal;
|
|
||||||
anchorH?: AlignHorizontal;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BoxOptions extends StringOptions {
|
|
||||||
vertical?: string;
|
vertical?: string;
|
||||||
horizontal?: string;
|
horizontal?: string;
|
||||||
topLeft?: string;
|
topLeft?: string;
|
||||||
|
|
@ -165,6 +153,8 @@ interface BoxOptions extends StringOptions {
|
||||||
bottomLeft?: string;
|
bottomLeft?: string;
|
||||||
bottomRight?: string;
|
bottomRight?: string;
|
||||||
fill?: Char;
|
fill?: Char;
|
||||||
|
fg?: ColorLike;
|
||||||
|
bg?: ColorLike;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,14 +313,9 @@ export class TextDisplay {
|
||||||
return [this.chars[y * this.width + x], this.fgs[y * this.width + x], this.bgs[y * this.width + x]];
|
return [this.chars[y * this.width + x], this.fgs[y * this.width + x], this.bgs[y * this.width + x]];
|
||||||
}
|
}
|
||||||
|
|
||||||
setRegion(region: TextRegion, x: number, y: number, anchorH = AlignHorizontal.LEFT) {
|
setRegion(x: number, y: number, region: TextRegion) {
|
||||||
x = x | 0;
|
x = x | 0;
|
||||||
y = y | 0;
|
y = y | 0;
|
||||||
|
|
||||||
if (anchorH === AlignHorizontal.RIGHT) {
|
|
||||||
x -= region.width + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { chars, fgs, bgs } = region[REGION_DATA];
|
const { chars, fgs, bgs } = region[REGION_DATA];
|
||||||
const rw = region.width;
|
const rw = region.width;
|
||||||
const x0 = Math.max(this.clipLeft, x);
|
const x0 = Math.max(this.clipLeft, x);
|
||||||
|
|
@ -390,31 +375,15 @@ export class TextDisplay {
|
||||||
return new TextRegion(data);
|
return new TextRegion(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawString(
|
drawString(text: unknown, x: number, y: number, fg: ColorLike = DEFAULT_FG, bg: ColorLike = DEFAULT_BG) {
|
||||||
text: unknown, x: number, y: number,
|
|
||||||
options: StringOptions,
|
|
||||||
) {
|
|
||||||
x = x | 0; y = y | 0;
|
x = x | 0; y = y | 0;
|
||||||
const {
|
|
||||||
fg = DEFAULT_FG,
|
|
||||||
bg = DEFAULT_BG,
|
|
||||||
alignH = AlignHorizontal.LEFT,
|
|
||||||
anchorH = AlignHorizontal.LEFT,
|
|
||||||
} = options;
|
|
||||||
const lines = String(text).split('\n');
|
const lines = String(text).split('\n');
|
||||||
const maxWidth = lines.reduce((m, line) => Math.max(m, line.length), 0);
|
|
||||||
|
|
||||||
if (anchorH === AlignHorizontal.RIGHT) {
|
|
||||||
x -= maxWidth - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let row = 0; row < lines.length; row++) {
|
for (let row = 0; row < lines.length; row++) {
|
||||||
let line = lines[row];
|
const line = lines[row];
|
||||||
const ry = y + row;
|
const ry = y + row;
|
||||||
if (ry < this.clipTop || ry >= this.clipBottom) continue;
|
if (ry < this.clipTop || ry >= this.clipBottom) continue;
|
||||||
const offset = alignH === AlignHorizontal.RIGHT ? maxWidth - line.length : 0;
|
|
||||||
for (let col = 0; col < line.length; col++) {
|
for (let col = 0; col < line.length; col++) {
|
||||||
this.setCharRaw(x + col + offset, ry, line[col], fg, bg);
|
this.setCharRaw(x + col, ry, line[col], fg, bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -459,13 +428,8 @@ export class TextDisplay {
|
||||||
bg = DEFAULT_BG,
|
bg = DEFAULT_BG,
|
||||||
fill,
|
fill,
|
||||||
title,
|
title,
|
||||||
anchorH = AlignHorizontal.LEFT,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (anchorH === AlignHorizontal.RIGHT) {
|
|
||||||
x -= width + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setCharRaw(x, y, topLeft, fg, bg);
|
this.setCharRaw(x, y, topLeft, fg, bg);
|
||||||
this.setCharRaw(x + width + 1, y, topRight, fg, bg);
|
this.setCharRaw(x + width + 1, y, topRight, fg, bg);
|
||||||
this.setCharRaw(x, y + height + 1, bottomLeft, fg, bg);
|
this.setCharRaw(x, y + height + 1, bottomLeft, fg, bg);
|
||||||
|
|
@ -482,7 +446,7 @@ export class TextDisplay {
|
||||||
this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
|
this.drawVLine(x + width + 1, y + 1, y + height, [vertical, fg, bg]);
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
this.drawString(title, x + 1, y, { fg, bg });
|
this.drawString(title, x + 1, y, fg, bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,26 +455,13 @@ export class TextDisplay {
|
||||||
const {
|
const {
|
||||||
fg = DEFAULT_FG,
|
fg = DEFAULT_FG,
|
||||||
bg = DEFAULT_BG,
|
bg = DEFAULT_BG,
|
||||||
alignH = AlignHorizontal.LEFT,
|
|
||||||
anchorH = AlignHorizontal.LEFT,
|
|
||||||
title,
|
|
||||||
} = options;
|
} = options;
|
||||||
const lines = String(text).split('\n');
|
const lines = String(text).split('\n');
|
||||||
const textWidth = lines.reduce((m, line) => Math.max(m, line.length), 0);
|
const width = lines.reduce((m, line) => Math.max(m, line.length), 0);
|
||||||
const height = lines.length;
|
const height = lines.length;
|
||||||
|
|
||||||
let width = textWidth;
|
|
||||||
if (title) {
|
|
||||||
width = Math.max(width, title.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dx = anchorH === AlignHorizontal.RIGHT ? -1 : 1;
|
|
||||||
if (alignH === AlignHorizontal.LEFT && anchorH === AlignHorizontal.RIGHT) {
|
|
||||||
dx -= width - textWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawBox(x, y, width, height, { ...options, fill: ' ' });
|
this.drawBox(x, y, width, height, { ...options, fill: ' ' });
|
||||||
this.drawString(text, x + dx, y + 1, { fg, bg, alignH, anchorH });
|
this.drawString(text, x + 1, y + 1, fg, bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
fillBox(x: number, y: number, width: number, height: number, char: Char = Glyphs.FULL_BLOCK) {
|
fillBox(x: number, y: number, width: number, height: number, char: Char = Glyphs.FULL_BLOCK) {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,10 @@ import { component } from "../utils/decorators";
|
||||||
import { Stat } from "./stat";
|
import { Stat } from "./stat";
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Defense extends Stat<{ damageType?: string }> { }
|
export class Defense extends Stat<{ damageType: string }> { }
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Damage extends Stat<{ damageType?: string, minDamage?: number, variance?: number }> {
|
export class Damage extends Stat<{ damageType: string, minDamage?: number, variance?: number }> { }
|
||||||
get variance(): number {
|
|
||||||
return this.state.variance ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Crit extends Stat<{ chance: number }> { }
|
export class Crit extends Stat<{ chance: number }> { }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, Entity } from "../core/world";
|
import { Component } from "../core/world";
|
||||||
import type { RPGVariables } from "../types";
|
import type { RPGVariables } from "../types";
|
||||||
import { action, component, ComponentTag, getComponentTags } from "../utils/decorators";
|
import { action, component, ComponentTag, getComponentTags } from "../utils/decorators";
|
||||||
import { Effect } from "./effect";
|
import { Effect } from "./effect";
|
||||||
|
|
@ -60,17 +60,10 @@ export class Equipment extends Component<EquipmentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ItemId in the named slot, or null if empty. */
|
/** ItemId in the named slot, or null if empty. */
|
||||||
getItemId(slotName: string): string | null {
|
getItem(slotName: string): string | null {
|
||||||
return this.#slot(slotName)?.itemId ?? null;
|
return this.#slot(slotName)?.itemId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(slotName: string): Entity | undefined {
|
|
||||||
const id = this.getItemId(slotName);
|
|
||||||
if (!id) return undefined;
|
|
||||||
|
|
||||||
return this.entity.world.getEntity(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the first empty slot compatible with `slotType`.
|
* Find the first empty slot compatible with `slotType`.
|
||||||
* Typed slots that match take priority over generic (untyped) slots.
|
* Typed slots that match take priority over generic (untyped) slots.
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,6 @@ function xpForStep(spec: ThresholdSpec, level: number): number | null {
|
||||||
return Math.floor(spec.base * Math.pow(spec.factor, level - 1));
|
return Math.floor(spec.base * Math.pow(spec.factor, level - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LevelUpEvent {
|
|
||||||
level: number;
|
|
||||||
prev: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Experience extends Component<{
|
export class Experience extends Component<{
|
||||||
xp: number;
|
xp: number;
|
||||||
|
|
@ -44,13 +39,9 @@ export class Experience extends Component<{
|
||||||
/** XP accumulated within the current level. */
|
/** XP accumulated within the current level. */
|
||||||
@variable get xpInLevel(): number { return this.state.xp - this.state.xpAtLevel; }
|
@variable get xpInLevel(): number { return this.state.xp - this.state.xpAtLevel; }
|
||||||
|
|
||||||
get xpForNext(): number | null {
|
|
||||||
return xpForStep(this.state.spec, this.state.level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** XP remaining until the next level, or `null` at max level. */
|
/** XP remaining until the next level, or `null` at max level. */
|
||||||
get xpToNext(): number | null {
|
get xpToNext(): number | null {
|
||||||
const needed = this.xpForNext;
|
const needed = xpForStep(this.state.spec, this.state.level);
|
||||||
return needed === null ? null : needed - this.xpInLevel;
|
return needed === null ? null : needed - this.xpInLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,11 +193,11 @@ export class Inventory extends Component<InventoryState> {
|
||||||
* `slotName` specifies the equipment body slot (otherwise auto-detected by item type).
|
* `slotName` specifies the equipment body slot (otherwise auto-detected by item type).
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
equip(arg: string | { itemId?: string; slotId?: SlotId; slotName?: string } | Entity): boolean {
|
equip(arg: string | { itemId?: string; slotId?: SlotId; slotName?: string }): boolean {
|
||||||
const resolved = this.#resolveItem(arg);
|
const resolved = this.#resolveItem(arg);
|
||||||
if (!resolved) return false;
|
if (!resolved) return false;
|
||||||
const { itemId, slotId } = resolved;
|
const { itemId, slotId } = resolved;
|
||||||
const slotName = typeof arg === 'object' && 'slotName' in arg ? arg.slotName : undefined;
|
const slotName = typeof arg === 'object' ? arg.slotName : undefined;
|
||||||
|
|
||||||
if (this.getAmount(itemId, slotId) === 0) {
|
if (this.getAmount(itemId, slotId) === 0) {
|
||||||
console.warn(`[Inventory] equip: item '${itemId}' not in inventory`);
|
console.warn(`[Inventory] equip: item '${itemId}' not in inventory`);
|
||||||
|
|
@ -262,10 +262,9 @@ export class Inventory extends Component<InventoryState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve an item reference, deriving `itemId` from slot contents if only `slotId` is given. */
|
/** Resolve an item reference, deriving `itemId` from slot contents if only `slotId` is given. */
|
||||||
#resolveItem(arg?: string | { itemId?: string; slotId?: SlotId } | Entity): { itemId: string; slotId?: SlotId } | null {
|
#resolveItem(arg?: string | { itemId?: string; slotId?: SlotId }): { itemId: string; slotId?: SlotId } | null {
|
||||||
if (!arg) return null;
|
if (!arg) return null;
|
||||||
if (typeof arg === 'string') return { itemId: arg };
|
if (typeof arg === 'string') return { itemId: arg };
|
||||||
if (arg instanceof Entity) return { itemId: arg.id };
|
|
||||||
|
|
||||||
if (arg.slotId !== undefined) {
|
if (arg.slotId !== undefined) {
|
||||||
const contents = this.getSlotContents(arg.slotId);
|
const contents = this.getSlotContents(arg.slotId);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { Component } from "../core/world";
|
|
||||||
import { component } from "../utils/decorators";
|
|
||||||
|
|
||||||
@component
|
|
||||||
export class Name extends Component<string> { }
|
|
||||||
|
|
@ -1,49 +1,19 @@
|
||||||
import { type NameOptions, type RNGState, SeededRandom } from "@common/random";
|
import { type RNGState, SeededRandom } from "@common/random";
|
||||||
import { Component, World } from "../core/world";
|
import { Component, World } from "../core/world";
|
||||||
import { component } from "../utils/decorators";
|
import { component } from "../utils/decorators";
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class Random extends Component<{ random: RNGState }> implements Omit<SeededRandom, 'jump' | 'longJump' | 'fork' | 'toJSON' | 'getState' | 'setState'> {
|
export class Random extends Component<{ random: RNGState }> {
|
||||||
private rng?: SeededRandom;
|
private rng?: SeededRandom;
|
||||||
|
|
||||||
constructor(random: SeededRandom | string | number | RNGState = Date.now()) {
|
constructor(random: SeededRandom | string | number | RNGState = Date.now()) {
|
||||||
super({
|
super({
|
||||||
random: (() => {
|
random: (() => {
|
||||||
const rng = random instanceof SeededRandom ? random : new SeededRandom(random);
|
this.rng = random instanceof SeededRandom ? random : new SeededRandom(random);
|
||||||
return rng.getState();
|
return this.rng.getState();
|
||||||
})()
|
})()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
nextFloat(): number {
|
|
||||||
return this.use((rng) => rng.nextFloat());
|
|
||||||
}
|
|
||||||
nextInt(min: number, max: number): number;
|
|
||||||
nextInt(max: number): number;
|
|
||||||
nextInt(minOrMax: number, max?: number): number {
|
|
||||||
return this.use((rng) => rng.nextInt(minOrMax, max as number));
|
|
||||||
}
|
|
||||||
nextBool(): boolean {
|
|
||||||
return this.use((rng) => rng.nextBool());
|
|
||||||
}
|
|
||||||
nextName(options?: NameOptions): string {
|
|
||||||
return this.use((rng) => rng.nextName(options));
|
|
||||||
}
|
|
||||||
choice<T>(iterable: Iterable<T>): T;
|
|
||||||
choice<T>(iterable: Iterable<T>, k: number): T[];
|
|
||||||
choice<T>(iterable: Iterable<T>, k?: number): T | T[] {
|
|
||||||
return this.use((rng) => rng.choice(iterable, k as number));
|
|
||||||
}
|
|
||||||
weightedChoice<T>(items: readonly T[], weights: readonly number[]): T;
|
|
||||||
weightedChoice<T>(items: readonly T[], weights: readonly number[], k: number): T[];
|
|
||||||
weightedChoice<T>(items: readonly T[], weights: readonly number[], k?: number): T | T[] {
|
|
||||||
return this.use((rng) => rng.weightedChoice(items, weights, k as number));
|
|
||||||
}
|
|
||||||
shuffle<T>(arr: T[]): T[] {
|
|
||||||
return this.use((rng) => rng.shuffle(arr));
|
|
||||||
}
|
|
||||||
toShuffled<T>(arr: readonly T[]): T[] {
|
|
||||||
return this.use((rng) => rng.toShuffled(arr));
|
|
||||||
}
|
|
||||||
|
|
||||||
use<T>(fn: (rng: SeededRandom) => T): T {
|
use<T>(fn: (rng: SeededRandom) => T): T {
|
||||||
if (this.rng) {
|
if (this.rng) {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import { ACTION_KEYS, getComponentName, STATE_KEYS, VARIABLE_KEYS } from '../uti
|
||||||
|
|
||||||
interface EntityEvent<T = unknown> {
|
interface EntityEvent<T = unknown> {
|
||||||
target: Entity;
|
target: Entity;
|
||||||
data: T;
|
data?: T;
|
||||||
}
|
}
|
||||||
interface WorldEvent<T = unknown> {
|
interface WorldEvent<T = unknown> {
|
||||||
target: World | Entity;
|
target: World;
|
||||||
data: T;
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntityEventHandler<T = unknown> = (event: EntityEvent<T>) => void;
|
type EntityEventHandler = <T>(event: EntityEvent<T>) => void;
|
||||||
type WorldEventHandler<T = unknown> = (event: WorldEvent<T>) => void;
|
type WorldEventHandler = <T>(event: WorldEvent<T>) => void;
|
||||||
|
|
||||||
export type EvalContext = Entity | World;
|
export type EvalContext = Entity | World;
|
||||||
|
|
||||||
|
|
@ -105,7 +105,6 @@ type ComponentFilter<T> = (component: T) => boolean;
|
||||||
|
|
||||||
export class Entity {
|
export class Entity {
|
||||||
readonly #components = new Map<string | symbol, Component>();
|
readonly #components = new Map<string | symbol, Component>();
|
||||||
#destroyed = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
|
|
@ -113,7 +112,6 @@ export class Entity {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
add<T extends Component<any>>(component: T, k?: string): T {
|
add<T extends Component<any>>(component: T, k?: string): T {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
const key = k ?? Symbol();
|
const key = k ?? Symbol();
|
||||||
|
|
||||||
if (component == null) {
|
if (component == null) {
|
||||||
|
|
@ -129,7 +127,6 @@ export class Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
clone<T extends Component<any>>(component: T, key: string): T {
|
clone<T extends Component<any>>(component: T, key: string): T {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
const clone = Object.create(component.constructor.prototype) as T;
|
const clone = Object.create(component.constructor.prototype) as T;
|
||||||
(clone as unknown as { state: unknown }).state = structuredClone(component.state);
|
(clone as unknown as { state: unknown }).state = structuredClone(component.state);
|
||||||
return this.add(clone, key);
|
return this.add(clone, key);
|
||||||
|
|
@ -139,7 +136,6 @@ export class Entity {
|
||||||
get<T extends Component<any>>(ctor: Class<T>, key?: string): T | undefined;
|
get<T extends Component<any>>(ctor: Class<T>, key?: string): T | undefined;
|
||||||
get<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): T | undefined;
|
get<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): T | undefined;
|
||||||
get<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): T | undefined {
|
get<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): T | undefined {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
if (typeof ctorOrKey === 'string') {
|
if (typeof ctorOrKey === 'string') {
|
||||||
return this.#components.get(ctorOrKey) as T | undefined;
|
return this.#components.get(ctorOrKey) as T | undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +143,7 @@ export class Entity {
|
||||||
const c = this.#components.get(key);
|
const c = this.#components.get(key);
|
||||||
return c instanceof ctorOrKey ? c as T : undefined;
|
return c instanceof ctorOrKey ? c as T : undefined;
|
||||||
}
|
}
|
||||||
if (!key) {
|
if (!key) {
|
||||||
for (const [k, c] of this.#components) {
|
for (const [k, c] of this.#components) {
|
||||||
// prefer registered without key
|
// prefer registered without key
|
||||||
if (typeof k !== 'symbol') continue;
|
if (typeof k !== 'symbol') continue;
|
||||||
|
|
@ -166,7 +162,6 @@ export class Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll<T extends Component<any>>(ctor: Class<T>): T[] {
|
getAll<T extends Component<any>>(ctor: Class<T>): T[] {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
const result: T[] = [];
|
const result: T[] = [];
|
||||||
for (const c of this.#components.values()) {
|
for (const c of this.#components.values()) {
|
||||||
if (c instanceof ctor) result.push(c as T);
|
if (c instanceof ctor) result.push(c as T);
|
||||||
|
|
@ -179,7 +174,6 @@ export class Entity {
|
||||||
has<T extends Component<any>>(ctor: Class<T>, key: string): boolean;
|
has<T extends Component<any>>(ctor: Class<T>, key: string): boolean;
|
||||||
has<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): boolean;
|
has<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): boolean;
|
||||||
has<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): boolean {
|
has<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): boolean {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
if (typeof ctorOrKey === 'string') {
|
if (typeof ctorOrKey === 'string') {
|
||||||
return this.#components.has(ctorOrKey);
|
return this.#components.has(ctorOrKey);
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +195,6 @@ export class Entity {
|
||||||
remove<T extends Component<any>>(ctor: Class<T>, key: string): void;
|
remove<T extends Component<any>>(ctor: Class<T>, key: string): void;
|
||||||
remove<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): void;
|
remove<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): void;
|
||||||
remove<T extends Component<any>>(ctorOrKey: Class<T> | T | string, key?: string | ComponentFilter<T>): void {
|
remove<T extends Component<any>>(ctorOrKey: Class<T> | T | string, key?: string | ComponentFilter<T>): void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
if (typeof ctorOrKey === 'string') {
|
if (typeof ctorOrKey === 'string') {
|
||||||
this.#removeByKey(ctorOrKey);
|
this.#removeByKey(ctorOrKey);
|
||||||
return;
|
return;
|
||||||
|
|
@ -223,7 +216,6 @@ export class Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAll<T extends Component<any>>(ctor: Class<T>, filter?: ComponentFilter<T>): void {
|
removeAll<T extends Component<any>>(ctor: Class<T>, filter?: ComponentFilter<T>): void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
for (const [k, c] of this.#components) {
|
for (const [k, c] of this.#components) {
|
||||||
if (!(c instanceof ctor)) continue;
|
if (!(c instanceof ctor)) continue;
|
||||||
if (typeof filter === 'function' && !filter(c)) continue;
|
if (typeof filter === 'function' && !filter(c)) continue;
|
||||||
|
|
@ -238,27 +230,22 @@ export class Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event: string, data?: unknown): void {
|
emit(event: string, data?: unknown): void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
this.world.emit(this.id, event, data);
|
this.world.emit(this.id, event, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitGlobal(event: string, data?: unknown): void {
|
emitGlobal(event: string, data?: unknown): void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
this.world.emitGlobal(event, data);
|
this.world.emitGlobal(event, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
on<T>(event: string, handler: EntityEventHandler<T>): () => void {
|
on(event: string, handler: EntityEventHandler): () => void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
return this.world.on(this.id, event, handler);
|
return this.world.on(this.id, event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: string, handler: EntityEventHandler): void {
|
off(event: string, handler: EntityEventHandler): void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
this.world.off(this.id, event, handler);
|
this.world.off(this.id, event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
once<T>(event: string, handler: EntityEventHandler<T>): () => void {
|
once(event: string, handler: EntityEventHandler): () => void {
|
||||||
if (this.#destroyed) throw new Error('Entity has been destroyed');
|
|
||||||
return this.world.once(this.id, event, handler);
|
return this.world.once(this.id, event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,8 +253,6 @@ export class Entity {
|
||||||
this.world.destroyEntity(this);
|
this.world.destroyEntity(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get destroyed() { return this.#destroyed; }
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
[Symbol.iterator](): IterableIterator<[string, Component]> {
|
[Symbol.iterator](): IterableIterator<[string, Component]> {
|
||||||
return this.#components.values().map(c => [c.key, c]);
|
return this.#components.values().map(c => [c.key, c]);
|
||||||
|
|
@ -277,7 +262,6 @@ export class Entity {
|
||||||
_destroy(): void {
|
_destroy(): void {
|
||||||
for (const c of this.#components.values()) c.onRemove();
|
for (const c of this.#components.values()) c.onRemove();
|
||||||
this.#components.clear();
|
this.#components.clear();
|
||||||
this.#destroyed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,21 +382,18 @@ export class World {
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(entityId: string, event: string, data?: unknown): void {
|
emit(entityId: string, event: string, data?: unknown): void {
|
||||||
console.debug(`Emitting event ${event} for entity ${entityId}, data:`, data);
|
|
||||||
const entity = this.getEntity(entityId);
|
const entity = this.getEntity(entityId);
|
||||||
if (!entity) return;
|
if (!entity) return;
|
||||||
this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h({ target: entity, data }));
|
this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h({ target: entity, data }));
|
||||||
this.#globalHandlers.get(event)?.forEach(h => h({ target: entity, data }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emitGlobal(event: string, data?: unknown): void {
|
emitGlobal(event: string, data?: unknown): void {
|
||||||
console.debug(`Emitting global event ${event}, data:`, data);
|
|
||||||
this.#globalHandlers.get(event)?.forEach(h => h({ target: this, data }));
|
this.#globalHandlers.get(event)?.forEach(h => h({ target: this, data }));
|
||||||
}
|
}
|
||||||
|
|
||||||
on<T>(event: string, handler: WorldEventHandler<T>): () => void;
|
on(event: string, handler: WorldEventHandler): () => void;
|
||||||
on<T>(entityId: string, event: string, handler: EntityEventHandler<T>): () => void;
|
on(entityId: string, event: string, handler: EntityEventHandler): () => void;
|
||||||
on<T>(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler<T>): () => void {
|
on(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
|
||||||
if (typeof arg2 === 'string') {
|
if (typeof arg2 === 'string') {
|
||||||
return this.#addHandler(this.#handlers, `${arg1}\0${arg2}`, arg3!);
|
return this.#addHandler(this.#handlers, `${arg1}\0${arg2}`, arg3!);
|
||||||
}
|
}
|
||||||
|
|
@ -433,12 +414,12 @@ export class World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
once<T>(event: string, handler: WorldEventHandler<T>): () => void;
|
once(event: string, handler: WorldEventHandler): () => void;
|
||||||
once<T>(entityId: string, event: string, handler: EntityEventHandler<T>): () => void;
|
once(entityId: string, event: string, handler: EntityEventHandler): () => void;
|
||||||
once<T>(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler<T>): () => void {
|
once(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
|
||||||
if (typeof arg2 === 'string') {
|
if (typeof arg2 === 'string') {
|
||||||
const original = arg3!;
|
const original = arg3!;
|
||||||
const wrapped: EntityEventHandler<T> = data => { this.#onceWrappers.delete(original); unsub(); original(data); };
|
const wrapped: EntityEventHandler = data => { this.#onceWrappers.delete(original); unsub(); original(data); };
|
||||||
this.#onceWrappers.set(original, wrapped);
|
this.#onceWrappers.set(original, wrapped);
|
||||||
const unsub = this.on(arg1, arg2, wrapped);
|
const unsub = this.on(arg1, arg2, wrapped);
|
||||||
return () => { this.#onceWrappers.delete(original); unsub(); };
|
return () => { this.#onceWrappers.delete(original); unsub(); };
|
||||||
|
|
@ -450,7 +431,7 @@ export class World {
|
||||||
return () => { this.#onceWrappers.delete(original); unsub(); };
|
return () => { this.#onceWrappers.delete(original); unsub(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
#addHandler<T extends WorldEventHandler<any> | EntityEventHandler<any>>(
|
#addHandler<T extends WorldEventHandler | EntityEventHandler>(
|
||||||
map: Map<string, Set<T>>,
|
map: Map<string, Set<T>>,
|
||||||
key: string,
|
key: string,
|
||||||
handler: T,
|
handler: T,
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,6 @@ import { System, World } from "../core/world";
|
||||||
|
|
||||||
let hitEffectCounter = 0;
|
let hitEffectCounter = 0;
|
||||||
|
|
||||||
export interface HitEvent {
|
|
||||||
attackerId: string;
|
|
||||||
sourceId: string | null;
|
|
||||||
amount: number;
|
|
||||||
damageType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CombatSystem extends System {
|
export class CombatSystem extends System {
|
||||||
override update(world: World) {
|
override update(world: World) {
|
||||||
let random: Random | undefined;
|
let random: Random | undefined;
|
||||||
|
|
@ -26,8 +19,8 @@ export class CombatSystem extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
let damageSum = 0;
|
let damageSum = 0;
|
||||||
const hitEvents: HitEvent[] = [];
|
const hitEvents: { attackerId: string; sourceId: string | null; amount: number; damageType: string }[] = [];
|
||||||
let lastHit: HitEvent | null = null;
|
let lastHit: { attackerId: string; sourceId: string | null } | null = null;
|
||||||
|
|
||||||
for (const attack of target.getAll(Attacked)) {
|
for (const attack of target.getAll(Attacked)) {
|
||||||
const { attackerId, sourceId } = attack.state;
|
const { attackerId, sourceId } = attack.state;
|
||||||
|
|
@ -58,7 +51,7 @@ export class CombatSystem extends System {
|
||||||
if (!random) {
|
if (!random) {
|
||||||
random = getWorldRandom(world);
|
random = getWorldRandom(world);
|
||||||
}
|
}
|
||||||
const variedDamage = random.use(r => r.nextInt(-variance, variance + 1));
|
const variedDamage = random.use(r => r.nextInt(-variance, variance));
|
||||||
|
|
||||||
if (variedDamage > 0) {
|
if (variedDamage > 0) {
|
||||||
damageAmount += variedDamage;
|
damageAmount += variedDamage;
|
||||||
|
|
@ -78,13 +71,9 @@ export class CombatSystem extends System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeDefense = target.get(Defense, (c) => c.state.damageType === damageType);
|
const defense = target.get(Defense, (c) => c.state.damageType === damageType);
|
||||||
if (typeDefense) {
|
if (defense) {
|
||||||
damageAmount -= typeDefense.value;
|
damageAmount -= defense.value;
|
||||||
}
|
|
||||||
const generalDefense = target.get(Defense, (c) => c.state.damageType == null);
|
|
||||||
if (generalDefense) {
|
|
||||||
damageAmount -= generalDefense.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
damageAmount = Math.max(0, minDamage, damageAmount);
|
damageAmount = Math.max(0, minDamage, damageAmount);
|
||||||
|
|
@ -110,8 +99,8 @@ export class CombatSystem extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
damageSum += damageAmount;
|
damageSum += damageAmount;
|
||||||
lastHit = { attackerId, sourceId, amount: damageAmount, damageType };
|
hitEvents.push({ attackerId, sourceId, amount: damageAmount, damageType });
|
||||||
hitEvents.push(lastHit);
|
lastHit = { attackerId, sourceId };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (damageSum === 0) continue;
|
if (damageSum === 0) continue;
|
||||||
|
|
@ -120,11 +109,11 @@ export class CombatSystem extends System {
|
||||||
health.update(-damageSum);
|
health.update(-damageSum);
|
||||||
|
|
||||||
for (const info of hitEvents) {
|
for (const info of hitEvents) {
|
||||||
target.emit('Combat.hit', info);
|
target.emit('hit', info);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasAlive && health.value <= 0 && lastHit) {
|
if (wasAlive && health.value <= 0 && lastHit) {
|
||||||
target.emit('Combat.killed', lastHit);
|
target.emit('kill', lastHit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,11 @@ export class TextDisplaySystem extends System {
|
||||||
const region = data instanceof TextRegion ? data : new TextRegion(data);
|
const region = data instanceof TextRegion ? data : new TextRegion(data);
|
||||||
|
|
||||||
if (absolute || !offset || !viewportClipRect) {
|
if (absolute || !offset || !viewportClipRect) {
|
||||||
this.display.setRegion(region, x, y);
|
this.display.setRegion(x, y, region);
|
||||||
} else {
|
} else {
|
||||||
const clipRect = this.display.getClipRect();
|
const clipRect = this.display.getClipRect();
|
||||||
this.display.setClipRect(viewportClipRect);
|
this.display.setClipRect(viewportClipRect);
|
||||||
this.display.setRegion(region, x - offset.x, y - offset.y);
|
this.display.setRegion(x - offset.x, y - offset.y, region);
|
||||||
this.display.setClipRect(clipRect);
|
this.display.setClipRect(clipRect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,183 +1,17 @@
|
||||||
import { AlignHorizontal, TextDisplay, TextRegion, type ColorLike } from "@common/display/text";
|
import { SeededRandom, ANIMAL_NAME_OPTIONS, FEMALE_NAME_OPTIONS, MALE_NAME_OPTIONS } from "@common/random";
|
||||||
import { gameLoop } from "@common/game";
|
|
||||||
import Input from "@common/input";
|
|
||||||
import { Attacked, Damage } from "@common/rpg/components/combat";
|
|
||||||
import { Equipment } from "@common/rpg/components/equipment";
|
|
||||||
import { Experience, type LevelUpEvent } from "@common/rpg/components/experience";
|
|
||||||
import { Inventory } from "@common/rpg/components/inventory";
|
|
||||||
import { Item, Items } from "@common/rpg/components/item";
|
|
||||||
import { Name } from "@common/rpg/components/name";
|
|
||||||
import { getWorldRandom } from "@common/rpg/components/random";
|
|
||||||
import { Health } from "@common/rpg/components/stat";
|
|
||||||
import { Entity, World } from "@common/rpg/core/world";
|
|
||||||
import { CombatSystem, type HitEvent } from "@common/rpg/systems/combat";
|
|
||||||
import { capitalize } from "@common/utils";
|
|
||||||
|
|
||||||
const createPlayer = (world: World, weapon: Entity): Entity => {
|
export default async function main() {
|
||||||
const player = world.createEntity();
|
const rnd = new SeededRandom(42);
|
||||||
player.add(new Health({ value: 100, max: 100 }));
|
|
||||||
const inv = player.add(new Inventory());
|
|
||||||
player.add(new Equipment('weapon'));
|
|
||||||
player.add(new Experience({ base: 50, factor: 1.5 }));
|
|
||||||
player.add(new Name("Player"));
|
|
||||||
|
|
||||||
inv.add(weapon);
|
for (let i = 0; i < 10; i++) {
|
||||||
inv.equip(weapon);
|
console.log(rnd.nextName(MALE_NAME_OPTIONS));
|
||||||
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createEnemy = (world: World, level: number): Entity => {
|
|
||||||
const enemy = world.createEntity();
|
|
||||||
const inv = enemy.add(new Inventory());
|
|
||||||
enemy.add(new Equipment('weapon'));
|
|
||||||
|
|
||||||
const random = getWorldRandom(world);
|
|
||||||
enemy.add(new Name(random.nextName({ maxLength: 7 }) + ' The Goblin'));
|
|
||||||
|
|
||||||
const hp = random.nextInt(level * 10, level * 20);
|
|
||||||
enemy.add(new Health({ value: hp, max: hp }));
|
|
||||||
|
|
||||||
const attack = random.nextInt(level * 3, level * 6);
|
|
||||||
|
|
||||||
const weaponName = random.choice(['sword', 'axe', 'dagger', 'mace']);
|
|
||||||
const weapon = createWeapon(world, weaponName, attack, level);
|
|
||||||
|
|
||||||
inv.add(weapon);
|
|
||||||
inv.equip(weapon);
|
|
||||||
|
|
||||||
return enemy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createWeapon = (world: World, id: string, damage: number, variance: number): Entity => {
|
|
||||||
const name = id.split(/[-_ ]+/).map(capitalize).join(' ');
|
|
||||||
const weapon = Items.register(world, `${id}_*`, name, { equippable: { slotType: 'weapon' } });
|
|
||||||
|
|
||||||
weapon.add(new Damage({ value: damage, variance }));
|
|
||||||
|
|
||||||
return weapon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawEntity = (display: TextDisplay, entity: Entity, x: number, y: number, anchorH: AlignHorizontal) => {
|
|
||||||
const { value: hp, max: maxHp } = entity.get(Health)!;
|
|
||||||
let text = `HP: ${hp}/${maxHp}`;
|
|
||||||
|
|
||||||
const xp = entity.get(Experience);
|
|
||||||
if (xp) {
|
|
||||||
text += `\nLevel: ${xp.level}`;
|
|
||||||
text += `\nXP: ${xp.xp}/${xp.xpForNext}`;
|
|
||||||
}
|
}
|
||||||
|
console.log('------------------------------------');
|
||||||
const title = entity.get(Name)?.state ?? entity.id;
|
for (let i = 0; i < 10; i++) {
|
||||||
|
console.log(rnd.nextName(FEMALE_NAME_OPTIONS));
|
||||||
display.drawStringInBox(text, x, y, { title, anchorH });
|
}
|
||||||
|
console.log('------------------------------------');
|
||||||
const weapon = entity.get(Equipment)!.getItem('weapon');
|
for (let i = 0; i < 10; i++) {
|
||||||
|
console.log(rnd.nextName(ANIMAL_NAME_OPTIONS));
|
||||||
if (weapon) {
|
|
||||||
const { value: damage, variance } = weapon.get(Damage)!;
|
|
||||||
const minDamage = damage - variance;
|
|
||||||
const maxDamage = damage + variance;
|
|
||||||
display.drawStringInBox(`Damage: ${minDamage}-${maxDamage}`, x, y + (xp ? 5 : 3), { title: weapon.get(Item)!.name, anchorH });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default gameLoop(() => {
|
|
||||||
const world = new World();
|
|
||||||
|
|
||||||
world.addSystem(new CombatSystem());
|
|
||||||
const display = new TextDisplay();
|
|
||||||
|
|
||||||
const fists = createWeapon(world, 'fists', 7, 2);
|
|
||||||
const player = createPlayer(world, fists);
|
|
||||||
const enemy = createEnemy(world, 1);
|
|
||||||
|
|
||||||
const log: TextRegion[] = [];
|
|
||||||
const state = {
|
|
||||||
world,
|
|
||||||
display,
|
|
||||||
player,
|
|
||||||
enemy,
|
|
||||||
needUpdate: true,
|
|
||||||
blocked: false,
|
|
||||||
log,
|
|
||||||
addLog(text: string, color?: ColorLike) {
|
|
||||||
log.push(new TextRegion(text, color));
|
|
||||||
if (log.length > 10) {
|
|
||||||
log.shift();
|
|
||||||
}
|
|
||||||
state.needUpdate = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
world.on<HitEvent>('Combat.hit', ({ target, data }) => {
|
|
||||||
if (target instanceof Entity) {
|
|
||||||
const targetName = target === player ? "Player" : target.get(Name)?.state ?? target.id;
|
|
||||||
state.addLog(`${targetName} hit by ${data.amount}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
world.on('Combat.killed', ({ target }) => {
|
|
||||||
if (target instanceof Entity) {
|
|
||||||
target.destroy();
|
|
||||||
player.get(Experience)!.award(10);
|
|
||||||
state.addLog("Enemy killed!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on<LevelUpEvent>('Experience.levelup', ({ data }) => {
|
|
||||||
const health = player.get(Health)!;
|
|
||||||
|
|
||||||
health.applyModifier(Math.round(health.max! * 0.1), 'max');
|
|
||||||
health.set(health.max!);
|
|
||||||
|
|
||||||
state.addLog(`Player leveled up to level ${data.level}`);
|
|
||||||
})
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}, (_dt, state) => {
|
|
||||||
if (Input.isReleased(Input.KeyCode.SPACE, Input.KeyCode.MOUSE_LEFT) && !state.blocked) {
|
|
||||||
const weapon = state.player.get(Equipment)!.getItem('weapon')!;
|
|
||||||
state.enemy.add(new Attacked({ attackerId: state.player.id, sourceId: weapon.id }));
|
|
||||||
state.blocked = true;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (state.enemy.destroyed) {
|
|
||||||
state.enemy = createEnemy(state.world, state.player.get(Experience)!.level);
|
|
||||||
} else {
|
|
||||||
const enemyWeapon = state.enemy.get(Equipment)!.getItem('weapon')!;
|
|
||||||
state.player.add(new Attacked({ attackerId: state.enemy.id, sourceId: enemyWeapon.id }));
|
|
||||||
}
|
|
||||||
state.blocked = false;
|
|
||||||
state.needUpdate = true;
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
state.needUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.needUpdate) {
|
|
||||||
state.world.update(1);
|
|
||||||
|
|
||||||
state.display.fillBox(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.display.width,
|
|
||||||
state.display.height,
|
|
||||||
' ',
|
|
||||||
);
|
|
||||||
|
|
||||||
drawEntity(state.display, state.player, 0, 0, AlignHorizontal.LEFT);
|
|
||||||
if (!state.enemy.destroyed) {
|
|
||||||
drawEntity(state.display, state.enemy, state.display.width - 1, 0, AlignHorizontal.RIGHT);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < state.log.length; i++) {
|
|
||||||
const entry = state.log[i];
|
|
||||||
state.display.setRegion(entry, 0, state.display.height - state.log.length + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.needUpdate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.display.update();
|
|
||||||
});
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe('Equipment — equip', () => {
|
||||||
const player = makePlayer(w);
|
const player = makePlayer(w);
|
||||||
const result = player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' });
|
const result = player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' });
|
||||||
expect(result).toBeTrue();
|
expect(result).toBeTrue();
|
||||||
expect(player.get(Equipment)!.getItemId('weapon')).toBe('sword');
|
expect(player.get(Equipment)!.getItem('weapon')).toBe('sword');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false for unknown slot', () => {
|
it('returns false for unknown slot', () => {
|
||||||
|
|
@ -73,7 +73,7 @@ describe('Equipment — equip', () => {
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
eq.equip({ slotName: 'weapon', itemId: 'sword1' });
|
eq.equip({ slotName: 'weapon', itemId: 'sword1' });
|
||||||
eq.equip({ slotName: 'weapon', itemId: 'sword2' });
|
eq.equip({ slotName: 'weapon', itemId: 'sword2' });
|
||||||
expect(eq.getItemId('weapon')).toBe('sword2');
|
expect(eq.getItem('weapon')).toBe('sword2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits 'equip' event", () => {
|
it("emits 'equip' event", () => {
|
||||||
|
|
@ -149,7 +149,7 @@ describe('Equipment — unequip', () => {
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
eq.equip({ slotName: 'weapon', itemId: 'sword' });
|
eq.equip({ slotName: 'weapon', itemId: 'sword' });
|
||||||
eq.unequip('weapon');
|
eq.unequip('weapon');
|
||||||
expect(eq.getItemId('weapon')).toBeNull();
|
expect(eq.getItem('weapon')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unequip returns false on empty slot', () => {
|
it('unequip returns false on empty slot', () => {
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ describe('Inventory — equip', () => {
|
||||||
const inv = player.get(Inventory)!;
|
const inv = player.get(Inventory)!;
|
||||||
inv.add({ itemId: 'sword', amount: 1 });
|
inv.add({ itemId: 'sword', amount: 1 });
|
||||||
expect(inv.equip({ itemId: 'sword' })).toBeTrue();
|
expect(inv.equip({ itemId: 'sword' })).toBeTrue();
|
||||||
expect(player.get(Equipment)!.getItemId('weapon')).toBe('sword');
|
expect(player.get(Equipment)!.getItem('weapon')).toBe('sword');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue