Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [0.8.3]

### _Feature_

- `Persistent.equals`, `Persistent.notEquals`, `Persistent.assign` now support function evaluator as argument
- Added `hidden` and `disabled` config to `Menu` choice
- Added `Menu.hideIf` and `Menu.disableIf` magic methods
- Added `Menu.enableWhen` and `Menu.showWhen`

## [0.8.2]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "narraleaf-react",
"version": "0.8.2",
"version": "0.8.3",
"description": "A React visual novel player framework",
"main": "./dist/main.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/game/nlcore/action/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export type PersistentActionContentType = {
[K in typeof PersistentActionTypes[keyof typeof PersistentActionTypes]]:
K extends "persistent:action" ? any :
K extends "persistent:set" ? [string, unknown | ((value: unknown) => unknown)] :
K extends "persistent:assign" ? [Partial<unknown>] :
K extends "persistent:assign" ? [Partial<unknown> | ((value: unknown) => Partial<unknown>)] :
any;
}
/* Layer */
Expand Down
3 changes: 2 additions & 1 deletion src/game/nlcore/action/actions/persistentAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ export class PersistentAction<T extends Values<typeof PersistentActionTypes> = V

return super.executeAction(gameState, injection);
} else if (action.is<PersistentAction<"persistent:assign">>(PersistentAction, "persistent:assign")) {
const [value] = (action.contentNode as ContentNode<PersistentActionContentType["persistent:assign"]>).getContent() as [Partial<PersistentContent>];
const [arg0] = (action.contentNode as ContentNode<PersistentActionContentType["persistent:assign"]>).getContent() as [Partial<PersistentContent> | ((value: PersistentContent) => Partial<PersistentContent>)];
const namespace = gameState.getStorable().getNamespace(
action.callee.getNamespaceName()
) as Namespace<PersistentContent>;
const prevValue: Partial<PersistentContent> = {};

const value = typeof arg0 === "function" ? arg0(namespace.getContent()) : arg0;
Object.keys(value).forEach((key: string) => {
prevValue[key] = namespace.get(key);
namespace.set(key, value[key]);
Expand Down
4 changes: 2 additions & 2 deletions src/game/nlcore/elements/condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class Condition<Closed extends true | false = false> extends Actionable<n
}

this.conditions.ElseIf.push({
condition: Lambda.isLambda(condition) ? condition : new Lambda(condition),
condition: Lambda.from(condition),
action: this.construct(Array.isArray(action) ? action : [action])
});
return this.chain() as Closed extends false ? Proxied<Condition<true>, Chained<LogicAction.Actions>> : never;
Expand Down Expand Up @@ -206,7 +206,7 @@ export class Condition<Closed extends true | false = false> extends Actionable<n
private createIfCondition(
condition: Lambda | LambdaHandler<boolean>, action: ActionStatements
): Proxied<Condition, Chained<LogicAction.Actions>> {
this.conditions.If.condition = condition instanceof Lambda ? condition : new Lambda(condition);
this.conditions.If.condition = Lambda.from(condition);
this.conditions.If.action = this.construct(action);

const chained = this.chain();
Expand Down
108 changes: 102 additions & 6 deletions src/game/nlcore/elements/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,37 @@ import {Sentence, SentencePrompt} from "@core/elements/character/sentence";
import {Word} from "@core/elements/character/word";
import {MenuAction} from "@core/action/actions/menuAction";
import Actions = LogicAction.Actions;
import { ActionStatements } from "./type";
import { ActionStatements, LambdaHandler } from "./type";
import { Narrator } from "./character";
import { Lambda } from "./condition";
import { StaticScriptWarning } from "../common/Utils";

/* eslint-disable @typescript-eslint/no-empty-object-type */
export type MenuConfig = {};
export type MenuChoice = {
action: ActionStatements;
prompt: SentencePrompt | Sentence;
config?: {
disabled?: Lambda<boolean> | LambdaHandler<boolean>;
hidden?: Lambda<boolean> | LambdaHandler<boolean>;
};
};

export type Choice = {
action: Actions[];
prompt: Sentence;
config: ChoiceConfig;
};

export type MenuData = {
prompt: Sentence | null;
choices: Choice[];
}
};

export type ChoiceConfig = {
disabled?: Lambda<boolean>;
hidden?: Lambda<boolean>;
};

export class Menu extends Actionable<any, Menu> {
/**@internal */
Expand Down Expand Up @@ -83,11 +95,18 @@ export class Menu extends Actionable<any, Menu> {
public choose(arg0: Sentence | MenuChoice | SentencePrompt, arg1?: ActionStatements): Proxied<Menu, Chained<LogicAction.Actions>> {
const chained = this.chain();
if (Sentence.isSentence(arg0) && arg1) {
chained.choices.push({prompt: Sentence.toSentence(arg0), action: this.narrativeToActions(arg1)});
chained.choices.push({prompt: Sentence.toSentence(arg0), action: this.narrativeToActions(arg1), config: {}});
} else if ((Word.isWord(arg0) || Array.isArray(arg0) || typeof arg0 === "string") && arg1) {
chained.choices.push({prompt: Sentence.toSentence(arg0), action: this.narrativeToActions(arg1)});
chained.choices.push({prompt: Sentence.toSentence(arg0), action: this.narrativeToActions(arg1), config: {}});
} else if (typeof arg0 === "object" && "prompt" in arg0 && "action" in arg0) {
chained.choices.push({prompt: Sentence.toSentence(arg0.prompt), action: this.narrativeToActions(arg0.action)});
chained.choices.push({
prompt: Sentence.toSentence(arg0.prompt),
action: this.narrativeToActions(arg0.action),
config: {
disabled: arg0.config?.disabled ? Lambda.from(arg0.config.disabled) : undefined,
hidden: arg0.config?.hidden ? Lambda.from(arg0.config.hidden) : undefined
}
});
} else {
console.warn("No valid choice added to menu, ", {
arg0,
Expand All @@ -97,6 +116,82 @@ export class Menu extends Actionable<any, Menu> {
return chained;
}

/**
* Magic method to hide the last choice if the condition is true
* @example
* ```ts
* menu.choose(
* // ...
* ).hideIf(persis.isTrue("flag"));
* ```
*
* **Note**: This method will override the last choice's config.hidden
*/
public hideIf(condition: Lambda<boolean> | LambdaHandler<boolean>): Proxied<Menu, Chained<LogicAction.Actions>> {
const lastChoice = this.choices[this.choices.length - 1];
if (!lastChoice) {
throw new StaticScriptWarning("Trying to configure the last choice of a menu, but no choice added. This may be caused by calling `menu.hideIf` before `menu.choose`");
}
lastChoice.config.hidden = Lambda.from(condition);
return this.chain();
}

/**
* Magic method to disable the last choice if the condition is true
* @example
* ```ts
* menu.choose(
* // ...
* ).disableIf(persis.isTrue("flag"));
* ```
*/
public disableIf(condition: Lambda<boolean> | LambdaHandler<boolean>): Proxied<Menu, Chained<LogicAction.Actions>> {
const lastChoice = this.choices[this.choices.length - 1];
if (!lastChoice) {
throw new StaticScriptWarning("Trying to configure the last choice of a menu, but no choice added. This may be caused by calling `menu.disableIf` before `menu.choose`");
}
lastChoice.config.disabled = Lambda.from(condition);
return this.chain();
}

/**
* Add a choice, only enable when the condition is true
* @example
* ```ts
* menu.enableWhen(persis.isTrue("flag"), "Go left", [
* character.say("I went left")
* ]);
* ```
*/
public enableWhen(condition: Lambda<boolean> | LambdaHandler<boolean>, prompt: Sentence, action: ActionStatements): Proxied<Menu, Chained<LogicAction.Actions>> {
return this.choose({
prompt,
action,
config: {
disabled: Lambda.from(condition)
}
});
}

/**
* Add a choice, only show when the condition is true
* @example
* ```ts
* menu.showWhen(persis.isTrue("flag"), "Go left", [
* character.say("I went left")
* ]);
* ```
*/
public showWhen(condition: Lambda<boolean> | LambdaHandler<boolean>, prompt: Sentence, action: ActionStatements): Proxied<Menu, Chained<LogicAction.Actions>> {
return this.choose({
prompt,
action,
config: {
hidden: Lambda.from(condition)
}
});
}

/**@internal */
public override fromChained(chained: Proxied<Menu, Chained<LogicAction.Actions>>): LogicAction.Actions[] {
return [
Expand Down Expand Up @@ -150,7 +245,8 @@ export class Menu extends Actionable<any, Menu> {
return this.choices.map(choice => {
return {
action: this.constructNodes(choice.action),
prompt: choice.prompt
prompt: choice.prompt,
config: choice.config ?? {}
};
});
}
Expand Down
56 changes: 31 additions & 25 deletions src/game/nlcore/elements/persistent.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {Actionable} from "@core/action/actionable";
import {StorableType} from "@core/elements/persistent/type";
import {Chained, Proxied} from "@core/action/chain";
import {LogicAction} from "@core/game";
import {PersistentActionContentType, PersistentActionTypes} from "@core/action/actionTypes";
import {PersistentAction} from "@core/action/actions/persistentAction";
import {BooleanValueKeyOf, StringKeyOf, Values} from "@lib/util/data";
import {ContentNode} from "@core/action/tree/actionTree";
import {Lambda} from "@core/elements/condition";
import {Word} from "@core/elements/character/word";
import {DynamicWord, DynamicWordResult} from "@core/elements/character/sentence";
import {LambdaHandler} from "@core/elements/type";
import {Namespace, Storable} from "@core/elements/persistent/storable";
import { Actionable } from "@core/action/actionable";
import { StorableType } from "@core/elements/persistent/type";
import { Chained, Proxied } from "@core/action/chain";
import { LogicAction } from "@core/game";
import { PersistentActionContentType, PersistentActionTypes } from "@core/action/actionTypes";
import { PersistentAction } from "@core/action/actions/persistentAction";
import { BooleanValueKeyOf, StringKeyOf, Values } from "@lib/util/data";
import { ContentNode } from "@core/action/tree/actionTree";
import { Lambda } from "@core/elements/condition";
import { Word } from "@core/elements/character/word";
import { DynamicWord, DynamicWordResult } from "@core/elements/character/sentence";
import { LambdaHandler } from "@core/elements/type";
import { Namespace, Storable } from "@core/elements/persistent/storable";

/**@internal */
export type PersistentContent = {
Expand Down Expand Up @@ -74,7 +74,7 @@ export class Persistent<T extends PersistentContent>
* @param value - The value to assign
* @returns A chainable persistent action
*/
public assign(value: Partial<T>): ChainedPersistent<T> {
public assign(value: Partial<T> | ((value: T) => Partial<T>)): ChainedPersistent<T> {
return this.chain(this.createAction(
PersistentActionTypes.assign,
[value]
Expand All @@ -85,26 +85,32 @@ export class Persistent<T extends PersistentContent>
/**
* Determine whether the values are equal, can be used in {@link Condition}
*/
public equals<K extends StringKeyOf<T>>(key: K, value: T[K]): Lambda<boolean> {
return new Lambda(({storable}) => {
return storable.getNamespace<T>(this.namespace).equals<K>(key, value);
public equals<K extends StringKeyOf<T>>(key: K, value: T[K] | ((value: T[K]) => T[K])): Lambda<boolean> {
return new Lambda(({ storable }) => {
const namespace = storable.getNamespace<T>(this.namespace);
const evaluatedValue = typeof value === "function" ? value(namespace.get<K>(key)) : value;

return namespace.equals<K>(key, evaluatedValue);
});
}

/**
* Determine whether the values aren't equal, can be used in {@link Condition}
*/
public notEquals<K extends StringKeyOf<T>>(key: K, value: T[K]): Lambda<boolean> {
return new Lambda(({storable}) => {
return !storable.getNamespace<T>(this.namespace).equals<K>(key, value);
public notEquals<K extends StringKeyOf<T>>(key: K, value: T[K] | ((value: T[K]) => T[K])): Lambda<boolean> {
return new Lambda(({ storable }) => {
const namespace = storable.getNamespace<T>(this.namespace);
const evaluatedValue = typeof value === "function" ? value(namespace.get<K>(key)) : value;

return !namespace.equals<K>(key, evaluatedValue);
});
}

/**
* Determine whether the value is true, can be used in {@link Condition}
*/
public isTrue<K extends Extract<keyof T, BooleanValueKeyOf<T>>>(key: K): Lambda<boolean> {
return new Lambda(({storable}) => {
return new Lambda(({ storable }) => {
return storable.getNamespace(this.namespace).equals(key, true);
});
}
Expand All @@ -113,7 +119,7 @@ export class Persistent<T extends PersistentContent>
* Determine whether the value is false, can be used in {@link Condition}
*/
public isFalse<K extends Extract<keyof T, BooleanValueKeyOf<T>>>(key: K): Lambda<boolean> {
return new Lambda(({storable}) => {
return new Lambda(({ storable }) => {
return storable.getNamespace(this.namespace).equals(key, false);
});
}
Expand All @@ -122,7 +128,7 @@ export class Persistent<T extends PersistentContent>
* Determine whether the value isn't null or undefined, can be used in {@link Condition}
*/
public isNotNull<K extends StringKeyOf<T>>(key: K): Lambda<boolean> {
return new Lambda(({storable}) => {
return new Lambda(({ storable }) => {
const value = storable.getNamespace(this.namespace).get(key);
return value !== null && value !== undefined;
});
Expand All @@ -140,7 +146,7 @@ export class Persistent<T extends PersistentContent>
* ```
*/
public toWord<K extends StringKeyOf<T>>(key: K): Word<DynamicWord> {
return new Word<DynamicWord>(({storable}) => {
return new Word<DynamicWord>(({ storable }) => {
return [String(storable.getNamespace<T>(this.namespace).get<K>(key))];
});
}
Expand Down Expand Up @@ -182,7 +188,7 @@ export class Persistent<T extends PersistentContent>
* Evaluate the JavaScript function and determine whether the result is true
*/
public evaluate<K extends StringKeyOf<T>>(key: K, fn: (value: T[K]) => boolean): Lambda<boolean> {
return new Lambda(({storable}) => {
return new Lambda(({ storable }) => {
return fn(storable.getNamespace<T>(this.namespace).get<K>(key));
});
}
Expand Down
5 changes: 5 additions & 0 deletions src/game/nlcore/elements/persistent/storable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export class Namespace<T extends NameSpaceContent<keyof T>> {
return this;
}

/**@internal */
getContent(): T {
return this.content as T;
}

/**@internal */
toData(): { [key: string]: WrappedStorableData } {
return this.serialize();
Expand Down
22 changes: 9 additions & 13 deletions src/game/player/elements/menu/PlayerMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import React, { useMemo, useCallback, useRef } from "react";
import clsx from "clsx";
import { IUserMenuProps, MenuElementProps } from "@player/elements/menu/type";
import { Script } from "@core/elements/script";
import { ChoiceEvaluated, IUserMenuProps, MenuElementProps } from "@player/elements/menu/type";
import Inspect from "@player/lib/Inspect";
import Isolated from "@player/lib/isolated";
import { useGame } from "@player/provider/game-state";
import Inspect from "@player/lib/Inspect";
import { Chosen } from "@player/type";
import { Choice } from "@core/elements/menu";
import { Word } from "@core/elements/character/word";
import { Pausing } from "@core/elements/character/pause";
import { Script } from "@core/elements/script";
import { UIMenuContext } from "./UIMenu/context";
import { UIListContext } from "./UIMenu/context";
import GameMenu from "./UIMenu/Menu";
import Item from "./UIMenu/Item";
import clsx from "clsx";
import React, { useCallback, useMemo, useRef } from "react";
import PlayerDialog from "../say/UIDialog";
import { UIListContext, UIMenuContext } from "./UIMenu/context";
import Item from "./UIMenu/Item";
import GameMenu from "./UIMenu/Menu";

/**@internal */
export default function PlayerMenu(
Expand Down Expand Up @@ -44,7 +40,7 @@ export default function PlayerMenu(
}, []);

const MenuConstructor = game.config.menu;
const evaluated: (Choice & { words: Word<Pausing | string>[] })[] =
const evaluated: ChoiceEvaluated[] =
useMemo(
() =>
choices.map(choice => ({
Expand Down
Loading