Skip to content

Commit

Permalink
feat(state): localize output while streaming (#60), select random des…
Browse files Browse the repository at this point in the history
…t portal from options
  • Loading branch information
ssube committed May 22, 2021
1 parent d5ad920 commit 5ed643c
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 46 deletions.
24 changes: 23 additions & 1 deletion data/base.yml
Expand Up @@ -2,7 +2,28 @@ states: [] # template file
worlds:
- locale:
languages:
en-US: {}
en:
actor.step.look.none: |-
You see nothing.
actor.step.look.room.you: |-
You are a {{actor.meta.name}}: {{actor.meta.desc}} ({{actor.meta.id}}).
actor.step.look.room.seen: |-
You are in {{room.meta.name}}: {{room.meta.desc}} ({{room.meta.id}}).
actor.step.look.room.inventory: |-
You are holding a {{item.meta.name}}: {{item.meta.desc}} ({{item.meta.desc}}).
actor.step.look.room.portal: |-
A {{portal.name}} leads to the {{portal.sourceGroup}} ({{portal.dest}}).
actor.step.look.actor.seen: |-
A {{actor.meta.name}} is in the room: {{actor.meta.desc}} ({{actor.meta.id}}).
actor.step.look.actor.dead: |-
The {{actor.meta.name}} is dead!
actor.step.look.item.seen: |-
You see a {{item.meta.name}}: {{item.meta.desc}} ({{item.meta.id}}).
actor.step.hit.type: |-
{{cmd.target}} is not an actor!
actor.step.hit.item: |-
You cannot hit {{target.meta.name}}, you are not holding anything!
meta:
id: test
name:
Expand Down Expand Up @@ -128,6 +149,7 @@ worlds:
chance: 25
items:
- id: item-sword
chance: 50
portals:
- sourceGroup:
base: west
Expand Down
6 changes: 5 additions & 1 deletion src/model/file/Locale.ts
@@ -1,6 +1,10 @@
import { JSONSchemaType } from 'ajv';

export type LocaleContext = Record<string, number | string>;
import { Command } from '../../service/input';
import { WorldEntity } from '../entity';
import { Portal } from '../entity/Portal';

export type LocaleContext = Record<string, number | string | WorldEntity | Portal | Command>;
export type LocaleLanguage = Record<string, Record<string, string>>;

export interface LocaleBundle {
Expand Down
2 changes: 1 addition & 1 deletion src/module/LocalModule.ts
Expand Up @@ -41,7 +41,7 @@ export class LocalModule extends Module {
protected state: Singleton<StateService>;
protected template: Singleton<TemplateService>;

constructor(render = false) {
constructor(render = true) {
super();

this.counter = new Singleton(() => mustExist(this.container).create(LocalCounter));
Expand Down
2 changes: 1 addition & 1 deletion src/service/locale/NextLocaleService.ts
Expand Up @@ -44,7 +44,7 @@ export class NextLocaleService implements LocaleService {
}
}

public getKey(key: string, scope?: LocaleContext): string {
public translate(key: string, scope?: LocaleContext): string {
return this.getInstance().t(key, scope);
}

Expand Down
2 changes: 1 addition & 1 deletion src/service/locale/index.ts
Expand Up @@ -6,5 +6,5 @@ export interface LocaleService {
addBundle(name: string, bundle: LocaleBundle): void;
deleteBundle(name: string): void;

getKey(key: string, context: LocaleContext): string;
translate(key: string, context?: LocaleContext): string;
}
20 changes: 10 additions & 10 deletions src/service/script/common/ActorStep.ts
Expand Up @@ -111,12 +111,12 @@ export async function ActorStepHit(this: Actor, context: ScriptContext): Promise
const target = indexEntity(results, cmd.index, isActor);

if (isNil(target)) {
await context.focus.show(`${cmd.target} is not an actor`);
await context.focus.show('actor.step.hit.type', { cmd });
return;
}

if (this.items.length === 0) {
await context.focus.show(`You cannot hit ${target.meta.name}, you are not holding anything!`);
await context.focus.show('actor.step.hit.item', { target });
return;
}

Expand Down Expand Up @@ -167,16 +167,16 @@ export async function ActorStepLookTarget(this: Actor, context: ScriptContext):
});
}

await context.focus.show('You see nothing.');
await context.focus.show('actor.step.look.none');
}

export async function ActorStepLookRoom(this: Actor, context: ScriptContext): Promise<void> {
const room = mustExist(context.room);
await context.focus.show(`You are a ${this.meta.name}: ${this.meta.desc} (${this.meta.id})`);
await context.focus.show(`You are in ${room.meta.name} (${room.meta.id}): ${room.meta.desc}`);
await context.focus.show('actor.step.look.room.you', { actor: this });
await context.focus.show('actor.step.look.room.seen', { room });

for (const item of this.items) {
await context.focus.show(`You are holding a ${item.meta.name}: ${item.meta.desc} (${item.meta.id})`);
await context.focus.show('actor.step.look.room.inventory', { item });
}

for (const actor of room.actors) {
Expand All @@ -196,22 +196,22 @@ export async function ActorStepLookRoom(this: Actor, context: ScriptContext): Pr
}

for (const portal of room.portals) {
await context.focus.show(`A ${portal.name} leads to the ${portal.sourceGroup} (${portal.dest})`);
await context.focus.show('actor.step.look.room.portal', { portal });
}
}

export async function ActorStepLookActor(this: Actor, context: ScriptContext): Promise<void> {
const actor = mustExist(context.actor);
await context.focus.show(`A ${actor.meta.name} (${actor.meta.desc}, ${actor.meta.id}) is in the room`);
await context.focus.show('actor.step.look.actor.seen', { actor });
const health = getKey(actor.stats, 'health', 0);
if (health <= 0) {
await context.focus.show(`${actor.meta.name} is dead`);
await context.focus.show('actor.step.look.actor.dead', { actor });
}
}

export async function ActorStepLookItem(this: Actor, context: ScriptContext): Promise<void> {
const item = mustExist(context.item);
await context.focus.show(`You see a ${item.meta.name}: ${item.meta.desc} (${item.meta.id})`);
await context.focus.show('actor.step.look.item.seen', { item });
}

export async function ActorStepMove(this: Actor, context: ScriptContext): Promise<void> {
Expand Down
4 changes: 1 addition & 3 deletions src/service/script/index.ts
Expand Up @@ -11,8 +11,6 @@ import { Immutable, ScriptData } from '../../util/types';
import { Command } from '../input';

export interface ScriptFocus {
flush(): Array<string>;

/**
* Set the currently-focused room.
*/
Expand All @@ -26,7 +24,7 @@ export interface ScriptFocus {
/**
* Display a message from an entity.
*/
show(msg: string, context?: LocaleContext, source?: WorldEntity): Promise<void>;
show(msg: string, context?: LocaleContext): Promise<void>;
}

export interface TransferParams<TEntity extends WorldEntity> {
Expand Down
36 changes: 26 additions & 10 deletions src/service/state/LocalStateService.ts
Expand Up @@ -2,7 +2,7 @@ import { constructorName, isNil, mustExist, NotFoundError } from '@apextoaster/j
import { EventEmitter } from 'events';
import { BaseOptions, Container, Inject, Logger } from 'noicejs';

import { CreateParams, StateService, StepParams, StepResult } from '.';
import { CreateParams, StateService, StepResult } from '.';
import { Actor, ACTOR_TYPE, ActorType, isActor } from '../../model/entity/Actor';
import { Item, ITEM_TYPE } from '../../model/entity/Item';
import { Portal, PortalGroups } from '../../model/entity/Portal';
Expand All @@ -15,13 +15,15 @@ import {
INJECT_COUNTER,
INJECT_INPUT_ACTOR,
INJECT_LOADER,
INJECT_LOCALE,
INJECT_LOGGER,
INJECT_PARSER,
INJECT_RANDOM,
INJECT_SCRIPT,
INJECT_TEMPLATE,
} from '../../module';
import { ActorInputOptions } from '../../module/InputModule';
import { randomItem } from '../../util/array';
import {
KNOWN_VERBS,
META_DEBUG,
Expand All @@ -36,12 +38,13 @@ import {
import { Counter } from '../../util/counter';
import { debugState, graphState } from '../../util/debug';
import { onceWithRemove } from '../../util/event';
import { StateFocusBuffer } from '../../util/state/focus';
import { StateFocusResolver } from '../../util/state/focus';
import { searchState } from '../../util/state/search';
import { StateEntityTransfer } from '../../util/state/transfer';
import { findByTemplateId } from '../../util/template';
import { Input } from '../input';
import { Loader } from '../loader';
import { LocaleService } from '../locale';
import { Parser } from '../parser';
import { RandomGenerator } from '../random';
import { ScriptFocus, ScriptService, ScriptTransfer, SuppliedScope } from '../script';
Expand All @@ -50,6 +53,7 @@ import { TemplateService } from '../template';
export interface LocalStateServiceOptions extends BaseOptions {
[INJECT_COUNTER]: Counter;
[INJECT_LOADER]: Loader;
[INJECT_LOCALE]: LocaleService;
[INJECT_LOGGER]: Logger;
[INJECT_PARSER]: Parser;
[INJECT_RANDOM]: RandomGenerator;
Expand All @@ -60,6 +64,7 @@ export interface LocalStateServiceOptions extends BaseOptions {
@Inject(
INJECT_COUNTER,
INJECT_LOADER,
INJECT_LOCALE,
INJECT_LOGGER,
INJECT_PARSER,
INJECT_RANDOM,
Expand All @@ -74,6 +79,7 @@ export class LocalStateService extends EventEmitter implements StateService {

protected counter: Counter;
protected loader: Loader;
protected locale: LocaleService;
protected logger: Logger;
protected parser: Parser;
protected random: RandomGenerator;
Expand All @@ -92,6 +98,7 @@ export class LocalStateService extends EventEmitter implements StateService {
this.container = options.container;
this.counter = options[INJECT_COUNTER];
this.loader = options[INJECT_LOADER];
this.locale = options[INJECT_LOCALE];
this.logger = options[INJECT_LOGGER].child({
kind: constructorName(this),
});
Expand Down Expand Up @@ -126,14 +133,25 @@ export class LocalStateService extends EventEmitter implements StateService {
this.world = world;

// register focus
this.focus = new StateFocusBuffer(this.state, () => Promise.resolve(), (room) => this.populateRoom(room, params.depth));
this.focus = new StateFocusResolver(this.state, {
onActor: () => Promise.resolve(),
onRoom: (room) => this.populateRoom(room, params.depth),
onShow: async (line, context) => {
const out = this.locale.translate(line, context);
this.logger.debug({ line, out }, 'translated output');
this.onOutput(out);
},
});
this.transfer = new StateEntityTransfer(this.logger, this.state);

// reseed the prng
this.random.reseed(params.seed);

// load the world locale
this.locale.addBundle('world', world.locale);

// pick a starting room and create it
const startRoomRef = world.start.rooms[this.random.nextInt(world.start.rooms.length)];
const startRoomRef = randomItem(world.start.rooms, this.random);
this.logger.debug({
rooms: world.templates.rooms,
startRoomId: startRoomRef,
Expand All @@ -147,7 +165,7 @@ export class LocalStateService extends EventEmitter implements StateService {
state.rooms.push(startRoom);

// pick a starting actor and create it
const startActorRef = world.start.actors[this.random.nextInt(world.start.actors.length)];
const startActorRef = randomItem(world.start.actors, this.random);
const startActorTemplate = findByTemplateId(world.templates.actors, startActorRef.id);
if (isNil(startActorTemplate)) {
throw new NotFoundError('invalid start actor');
Expand Down Expand Up @@ -262,9 +280,6 @@ export class LocalStateService extends EventEmitter implements StateService {
default: {
// step world
const result = await this.step();
const output = mustExist(this.focus).flush();

this.emit('output', output);
this.emit('step', result);
}
}
Expand Down Expand Up @@ -525,10 +540,11 @@ export class LocalStateService extends EventEmitter implements StateService {

const groups = this.groupPortals(portals);
const results: Array<Portal> = [];
const world = mustExist(this.world);

for (const [sourceGroup, group] of groups) {
const world = mustExist(this.world);
const destTemplateId = Array.from(group.dests)[0];
const potentialDests = Array.from(group.dests);
const destTemplateId = randomItem(potentialDests, this.random);
const destTemplate = findByTemplateId(world.templates.rooms, destTemplateId);

if (isNil(destTemplate)) {
Expand Down
5 changes: 5 additions & 0 deletions src/util/array.ts
@@ -0,0 +1,5 @@
import { RandomGenerator } from '../service/random';

export function randomItem<TValue>(items: Array<TValue>, random: RandomGenerator): TValue {
return items[random.nextInt(items.length)];
}
44 changes: 26 additions & 18 deletions src/util/state/focus.ts
Expand Up @@ -5,24 +5,35 @@ import { Actor, ACTOR_TYPE, isActor } from '../../model/entity/Actor';
import { isRoom, Room, ROOM_TYPE } from '../../model/entity/Room';
import { LocaleContext } from '../../model/file/Locale';
import { State } from '../../model/State';
import { ScriptFocus } from '../../service/script';
import { searchState } from './search';

export type RoomFocusChange = (room: Room) => Promise<void>;
export type ActorFocusChange = (actor: Actor) => Promise<void>;
export type FocusChangeRoom = (room: Room) => Promise<void>;
export type FocusChangeActor = (actor: Actor) => Promise<void>;
export type FocusShow = (line: string, context?: LocaleContext) => Promise<void>;

export class StateFocusBuffer {
protected buffer: Array<string>;
interface FocusEvents {
onActor: FocusChangeActor;
onRoom: FocusChangeRoom;
onShow: FocusShow;
}

/**
* Manages the `focus` field within world state, along with filtering output based on the current focus.
*/
export class StateFocusResolver implements ScriptFocus {
protected state: State;

protected onActor: ActorFocusChange;
protected onRoom: RoomFocusChange;
protected onActor: FocusChangeActor;
protected onRoom: FocusChangeRoom;
protected onShow: FocusShow;

constructor(state: State, onActor: ActorFocusChange, onRoom: RoomFocusChange) {
this.buffer = [];
constructor(state: State, events: FocusEvents) {
this.state = state;

this.onActor = onActor;
this.onRoom = onRoom;
this.onActor = events.onActor;
this.onRoom = events.onRoom;
this.onShow = events.onShow;
}

public async setActor(id: string): Promise<void> {
Expand Down Expand Up @@ -59,13 +70,10 @@ export class StateFocusBuffer {
}
}

public async show(msg: string, _context?: LocaleContext, _source?: WorldEntity): Promise<void> {
this.buffer.push(msg); // TODO: translate before sending to render?
}

public flush(): Array<string> {
const result = this.buffer;
this.buffer = [];
return result;
/**
* @todo filter output to the room/actor with focus
*/
public async show(msg: string, context?: LocaleContext, source?: WorldEntity): Promise<void> {
await this.onShow(msg, context);
}
}

0 comments on commit 5ed643c

Please sign in to comment.