Open-source interactive fiction engine, powered by React, Redux and TypeScript. _ https://discoteque.pub/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

157 lines
6.2 KiB

import * as R from 'ramda';
import { ILineOption, EngineState, ILineRaw, ILine, INode, LineFn, IActor, ILocation, Chrono, EngineConfig } from './types';
import makeStore, * as actions from './lib/store';
import { Reducer, CombinedState, Store } from 'redux';
type EngineStore<GS = object, MT = undefined, ST extends string = string> = Store<CombinedState<{
engine: EngineState<GS, MT, ST>,
game: GS,
}>>;
const isLocation = <GS = object, MT = undefined>(node?: INode<GS, MT> | null): node is ILocation<GS, MT> =>
!!node && node.kind === 'location';
export class Engine<GS = object, MT = undefined, ST extends string = string> {
store: EngineStore<GS, MT, ST>;
config: EngineConfig<GS, MT, ST>;
public constructor(state: EngineState<GS, MT, ST>, gameReducer: Reducer, config: EngineConfig<GS, MT, ST>) {
this.config = config;
this.store = makeStore(gameReducer) as EngineStore<GS, MT, ST>;
this.store.dispatch(actions.initEngineState(state))
const firstLine = this.currentLine || this.next(this.gameState);
this.store.dispatch(actions.setState({ backlog: [firstLine] }));
}
private get state(): EngineState<GS, MT, ST> {
return this.store.getState().engine;
}
public get gameState(): GS {
return this.store.getState().game;
}
public get currentLine(): ILine<GS, MT> | null {
const gameState = this.gameState;
const line = R.pathOr<ILineRaw<GS, MT> | LineFn<GS, MT> | null>(null, [this.state.node, 'lines', this.state.line], this.state.nodeMap);
if (line && typeof line === 'function') {
return this.augmentLine(line(this.state, gameState, this.store.dispatch, this.config))
}
return this.augmentLine(line);
}
public get currentNode(): INode<GS, MT> | null {
return R.propOr(null, this.state.node, this.state.nodeMap);
}
public get currentLocation(): ILocation<GS> | null {
return R.propOr(null, this.state.location || '', this.state.nodeMap);
}
public get currentTime(): Chrono | null {
return R.propOr(null, 'chrono', this.state);
}
public get backlog(): ILine<GS, MT>[] {
return R.propOr([], 'backlog', this.state);
}
public rollSkill(skill: string, difficulty: number) {
const roll = Math.floor(Math.random() * (20 - 1 + 1)) + 1; // roll 1d20
const skillLevel = R.pathOr(1, ['skills', skill], this.state);
return (roll + skillLevel) > difficulty;
}
private switchNode(node: string, gameState: GS): ILine<GS, MT>[] | null {
const nextNode = this.getNode(node);
const nextLine = nextNode?.lines[0] || null;
this.store.dispatch(actions.changeNode(node));
if (isLocation(nextNode)) {
this.switchLocation(node);
}
if (typeof nextLine === 'function') {
const result = this.augmentLine(nextLine(this.state, gameState, this.store.dispatch, this.config));
const defaultTime = result?.actor ? 1 : 0;
this.store.dispatch(actions.advanceTime(result?.time || defaultTime));
const line = result
? [result]
: this.next(gameState);
return line;
}
const result = this.augmentLine(nextLine);
const defaultTime = result?.actor ? 1 : 0;
this.store.dispatch(actions.advanceTime(result?.time || defaultTime));
const line = result
? [result]
: this.next(gameState);
return line;
}
private switchLine(line: number, gameState: GS): ILine<GS, MT>[] | null {
this.store.dispatch(actions.advanceLine(line));
const nextLine = this.currentNode?.lines[line];
const chrono = this.currentTime;
if (chrono && chrono.time === 1440 && this.config.onDayCycle) {
const newChrono = this.config.onDayCycle(chrono);
this.store.dispatch(actions.setChrono(newChrono));
}
if (typeof nextLine === 'function') {
const line = this.augmentLine(nextLine(this.state, gameState, this.store.dispatch, this.config));
const defaultTime = line?.actor ? 1 : 0;
this.store.dispatch(actions.advanceTime(line?.time || defaultTime));
return line ? [line] : this.next(gameState);
} else {
const line = this.augmentLine(nextLine);
const defaultTime = line?.actor ? 1 : 0;
this.store.dispatch(actions.advanceTime(line?.time || defaultTime));
return line ? [line] : this.next(gameState);
}
}
private switchLocation(node: string) {
this.store.dispatch(actions.changeLocation(node));
}
public next(gameState: GS, option?: ILineOption<GS>): ILine<GS, MT>[] | null {
if (!option) {
const nextLine = this.currentNode && this.currentNode.lines.length > this.state.line + 1
? this.state.line + 1
: null;
if (this.currentNode && nextLine) {
return this.switchLine(nextLine, gameState);
} else if (this.currentNode && !nextLine && this.currentNode.next) {
return this.switchNode(this.currentNode.next, gameState);
}
} else {
const checkResult = option.skill
? this.rollSkill(option.skill.name, option.skill.difficulty)
: true;
if (checkResult) {
return [
{ text: `${option.text}`, actor: null },
...(this.switchNode(option.value, gameState) || []),
];
}
if (option.skill?.failTo) {
return [
{ text: `${option.text}`, actor: null },
...(this.switchNode(option.skill.failTo, gameState) || []),
];
}
}
return null;
};
private getNode(node: string = ''): INode<GS, MT> | null {
return R.propOr(null, node, this.state.nodeMap);
}
private augmentLine(line: void | ILineRaw<GS, MT> | null): ILine<GS, MT> | null {
if (line) {
const actor = this.getNode(line.actorId) as IActor<GS, MT> | null;
return { ...line, actor };
} if (line === undefined) {
return null;
}
return line;
}
}