diff --git a/CHANGELOG.md b/CHANGELOG.md index c44a487..5cbe7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog -## [0.2.1] +## [0.2.3] + +### _Feature_ + +- Use `usePreference` to manage preference easier + +### Added + +- `image.setTags` +- `image.setPosition` +- Utility component: `Full` +- `usePreference` hook + +### Updated + +- `Player` component now doesn't require a `story` prop + +## [0.2.2] - 2024/12/02 ### _Feature_ diff --git a/package.json b/package.json index 5c28e3e..7c0c72b 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "husky": "^9.1.6", "jest": "^27.0.6", "postcss-loader": "^8.1.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "rimraf": "^6.0.1", "style-loader": "^4.0.0", "tailwindcss": "^3.4.11", @@ -56,8 +56,9 @@ "webpack-dev-server": "^5.1.0" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "files": [ "dist" @@ -78,5 +79,6 @@ "framer-motion": "^11.11.9", "howler": "^2.2.4", "prop-types": "^15.8.1" - } + }, + "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" } diff --git a/src/game/nlcore/action/actions/controlAction.ts b/src/game/nlcore/action/actions/controlAction.ts index 466932f..b21493f 100644 --- a/src/game/nlcore/action/actions/controlAction.ts +++ b/src/game/nlcore/action/actions/controlAction.ts @@ -42,15 +42,17 @@ export class ControlAction { const next = state.game.getLiveGame().executeAction(state, action); if (Awaitable.isAwaitable(next)) { - const {node} = await new Promise((r) => { - next.then((_) => r(next.result as any)); + return new Promise((r) => { + next.then((_) => { + state.logger.debug("Control - Next Action (single)", next); + r(next.result?.node || null); + }); }); - return node; } else { - return next; + return Promise.resolve(next?.contentNode || null); } } @@ -96,14 +98,14 @@ export class ControlAction(v => v); - (async () => { - await Promise.all(content.map(action => this.executeSingleAction(state, action))); - awaitable.resolve({ - type: this.type, - node: this.contentNode.getChild() + Promise.all(content.map(action => this.executeSingleAction(state, action))) + .then(() => { + awaitable.resolve({ + type: this.type, + node: this.contentNode.getChild() + }); + state.stage.next(); }); - state.stage.next(); - })(); return awaitable; } else if (this.type === ControlActionTypes.allAsync) { (async () => { diff --git a/src/game/nlcore/common/Utils.ts b/src/game/nlcore/common/Utils.ts index bb69e42..c7c046f 100644 --- a/src/game/nlcore/common/Utils.ts +++ b/src/game/nlcore/common/Utils.ts @@ -62,6 +62,7 @@ export class RGBColor { } } +/**@internal */ export class Utils { static RGBColor = RGBColor; @@ -118,6 +119,7 @@ export class Utils { } } +/**@internal */ export class UseError> extends Error { static isUseError(error: any): error is UseError { return error instanceof UseError; @@ -132,6 +134,7 @@ export class UseError> extends Error { } } +/**@internal */ export class StaticScriptWarning extends UseError<{ stack?: string; info?: any; @@ -150,6 +153,7 @@ type ImageState = { usedExternalSrc: boolean; }; +/**@internal */ export class StaticChecker { private readonly scene: Scene; @@ -257,6 +261,7 @@ export class StaticChecker { } } +/**@internal */ export class RuntimeScriptError extends Error { static toMessage(msg: string | string[], trace?: Action | Action[]) { const messages: string[] = []; @@ -282,6 +287,7 @@ export class RuntimeScriptError extends Error { } } +/**@internal */ export class RuntimeGameError extends Error { constructor(message: string) { super(message); diff --git a/src/game/nlcore/common/elements.ts b/src/game/nlcore/common/elements.ts index 11eb310..827b2e3 100644 --- a/src/game/nlcore/common/elements.ts +++ b/src/game/nlcore/common/elements.ts @@ -59,6 +59,7 @@ const Image: ImageConstructor = function ( config.tag as TagDefinitions | undefined ); } as unknown as ImageConstructor; +const AbstractImage = ImageClass; export { Character, @@ -78,4 +79,5 @@ export { Text, Pause, Persistent, + AbstractImage, }; \ No newline at end of file diff --git a/src/game/nlcore/elements/character.ts b/src/game/nlcore/elements/character.ts index d956303..bd518f5 100644 --- a/src/game/nlcore/elements/character.ts +++ b/src/game/nlcore/elements/character.ts @@ -8,9 +8,11 @@ import {Sentence, SentencePrompt, SentenceUserConfig, SingleWord} from "@core/el import {CharacterAction} from "@core/action/actions/characterAction"; export type CharacterConfig = {} & Color; +/**@internal */ export type CharacterStateData = { name: string; }; +/**@internal */ export type CharacterState = { name: string; }; diff --git a/src/game/nlcore/elements/character/pause.ts b/src/game/nlcore/elements/character/pause.ts index 326a28e..bb86de2 100644 --- a/src/game/nlcore/elements/character/pause.ts +++ b/src/game/nlcore/elements/character/pause.ts @@ -2,6 +2,7 @@ export type PauseConfig = { duration?: number; }; +/**@internal */ export type PausingShortcut = typeof Pause; export type Pausing = Pause | PausingShortcut; diff --git a/src/game/nlcore/elements/character/sentence.ts b/src/game/nlcore/elements/character/sentence.ts index ae8035e..8c0f351 100644 --- a/src/game/nlcore/elements/character/sentence.ts +++ b/src/game/nlcore/elements/character/sentence.ts @@ -6,6 +6,7 @@ import {Color, Font} from "@core/types"; import type {ScriptCtx} from "@core/elements/script"; import {Pause, Pausing} from "@core/elements/character/pause"; +/**@internal */ export type SentenceConfig = { pause?: boolean | number; voice: Sound | null; @@ -13,22 +14,29 @@ export type SentenceConfig = { voiceId: string | number | null; } & Color & Font; +/**@internal */ export type SentenceDataRaw = { state: SentenceState; }; +/**@internal */ export type SentenceState = { display: boolean; }; export type SentenceUserConfig = Partial & { voice: Sound | string | null | undefined }>; +/**@internal */ export type DynamicWord = (ctx: ScriptCtx) => DynamicWordResult; +/**@internal */ export type DynamicWordResult = string | Word | Pausing | (string | Word | Pausing)[]; +/**@internal */ export type StaticWord = string | Pausing | Word; +/**@internal */ export type SingleWord = StaticWord | DynamicWord; +/**@internal */ export type SentencePrompt = SingleWord[] | SingleWord; export class Sentence { diff --git a/src/game/nlcore/elements/condition.ts b/src/game/nlcore/elements/condition.ts index a0d416a..7d85314 100644 --- a/src/game/nlcore/elements/condition.ts +++ b/src/game/nlcore/elements/condition.ts @@ -48,6 +48,7 @@ export class Lambda { } } +/**@internal */ export type ConditionData = { If: { condition: Lambda | null; diff --git a/src/game/nlcore/elements/control.ts b/src/game/nlcore/elements/control.ts index abd78fb..93f49e8 100644 --- a/src/game/nlcore/elements/control.ts +++ b/src/game/nlcore/elements/control.ts @@ -6,6 +6,7 @@ import {Chained, ChainedActions, Proxied} from "@core/action/chain"; import {ControlAction} from "@core/action/actions/controlAction"; +/**@internal */ type ChainedControl = Proxied>; export class Control extends Actionable { diff --git a/src/game/nlcore/elements/displayable/displayable.ts b/src/game/nlcore/elements/displayable/displayable.ts index f2101a7..5842048 100644 --- a/src/game/nlcore/elements/displayable/displayable.ts +++ b/src/game/nlcore/elements/displayable/displayable.ts @@ -9,12 +9,14 @@ import {Chained, Proxied} from "@core/action/chain"; import {LogicAction} from "@core/action/logicAction"; import {ContentNode} from "@core/action/tree/actionTree"; +/**@internal */ export type DisplayableEventTypes = { "event:displayable.applyTransition": [ITransition]; "event:displayable.applyTransform": [Transform]; "event:displayable.init": []; }; +/**@internal */ export abstract class Displayable< StateData extends Record, Self extends Actionable diff --git a/src/game/nlcore/elements/displayable/image.ts b/src/game/nlcore/elements/displayable/image.ts index ad21ee0..32bcd74 100644 --- a/src/game/nlcore/elements/displayable/image.ts +++ b/src/game/nlcore/elements/displayable/image.ts @@ -29,6 +29,7 @@ import {Control} from "@core/elements/control"; import {ImageAction} from "@core/action/actions/imageAction"; import {Displayable, DisplayableEventTypes} from "@core/elements/displayable/displayable"; +/**@internal */ export type ImageConfig = { display: boolean; /**@internal */ @@ -42,10 +43,12 @@ export type ImageConfig = { autoInit: boolean; } & CommonDisplayable; +/**@internal */ export type ImageDataRaw = { state: Record; }; +/**@internal */ export type ImageEventTypes = { "event:wearable.create": [Image]; } & DisplayableEventTypes; @@ -54,7 +57,9 @@ export type TagDefinitions = groups: T; defaults: SelectElementFromEach; } : never; +/**@internal */ export type TagGroupDefinition = string[][]; +/**@internal */ export type TagSrcResolver = (...tags: SelectElementFromEach) => string; export type RichImageUserConfig = ImageConfig & { /**@internal */ @@ -70,8 +75,8 @@ export type RichImageUserConfig = ImageConf tag: TagDefinitions; } : never); -export type RichImageConfig = RichImageUserConfig & {}; -export type StaticRichConfig = RichImageUserConfig; +type RichImageConfig = RichImageUserConfig & {}; +type StaticRichConfig = RichImageUserConfig; export class Image< @@ -332,20 +337,16 @@ export class Image< * @chainable */ public applyTransform(transform: Transform): Proxied> { - return this.combineActions(new Control(), chain => { - const action = new ImageAction( - chain, - ImageAction.ActionTypes.applyTransform, - new ContentNode().setContent([ - void 0, - transform.copy(), - getCallStack() - ]) - ); - return chain - .chain(action) - .chain(this._flush()); - }); + const chain = this.chain(); + return chain.chain(new ImageAction( + chain, + ImageAction.ActionTypes.applyTransform, + new ContentNode().setContent([ + void 0, + transform.copy(), + getCallStack() + ]) + )); } /** @@ -426,6 +427,34 @@ export class Image< }); } + /** + * Alia of {@link Image.setAppearance} + * @chainable + */ + public setTags( + tags: Tags extends TagGroupDefinition ? FlexibleTuple> : string[], + transition?: IImageTransition + ): Proxied> { + return this.setAppearance(tags, transition); + } + + /** + * Set Image Position + * @chainable + */ + public setPosition( + position: TransformDefinitions.ImageTransformProps["position"], + duration?: number, + easing?: TransformDefinitions.EasingDefinition + ): Proxied> { + return this.applyTransform(new Transform({ + position, + }, { + duration, + ease: easing, + })); + } + /** * Add a wearable to the image * @param children - Wearable image or images diff --git a/src/game/nlcore/elements/displayable/text.ts b/src/game/nlcore/elements/displayable/text.ts index cc20910..5a95c2e 100644 --- a/src/game/nlcore/elements/displayable/text.ts +++ b/src/game/nlcore/elements/displayable/text.ts @@ -26,10 +26,11 @@ export type TextConfig = { text: string; } & CommonDisplayable; +/**@internal */ export type TextDataRaw = { state: Record; }; - +/**@internal */ export type TextEventTypes = { "event:text.show": [Transform]; "event:text.hide": [Transform]; diff --git a/src/game/nlcore/elements/menu.ts b/src/game/nlcore/elements/menu.ts index 1727283..86eeb51 100644 --- a/src/game/nlcore/elements/menu.ts +++ b/src/game/nlcore/elements/menu.ts @@ -16,7 +16,9 @@ export type MenuChoice = { prompt: SentencePrompt | Sentence; }; +/**@internal */ type ChainedAction = Proxied>; +/**@internal */ type ChainedActions = (ChainedAction | ChainedAction[] | Actions | Actions[])[]; export type Choice = { diff --git a/src/game/nlcore/elements/persistent.ts b/src/game/nlcore/elements/persistent.ts index c9bbfcd..c44702d 100644 --- a/src/game/nlcore/elements/persistent.ts +++ b/src/game/nlcore/elements/persistent.ts @@ -4,7 +4,7 @@ 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 {BooleanKeys, StringKeyOf, Values} from "@lib/util/data"; +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"; @@ -12,10 +12,13 @@ import {DynamicWord, DynamicWordResult} from "@core/elements/character/sentence" import {LambdaHandler} from "@core/elements/type"; import {Namespace, Storable} from "@core/elements/persistent/storable"; +/**@internal */ type PersistentContent = { [K in string]: StorableType; }; +/**@internal */ type ChainedPersistent = Proxied, Chained>; +/**@internal */ type DynamicPersistentData = { [K in string]: StorableType; }; @@ -77,7 +80,7 @@ export class Persistent /** * Determine whether the value is true, can be used in {@link Condition} */ - public isTrue>>(key: K): Lambda { + public isTrue>>(key: K): Lambda { return new Lambda(({storable}) => { return storable.getNamespace(this.namespace).equals(key, true); }); @@ -86,7 +89,7 @@ export class Persistent /** * Determine whether the value is false, can be used in {@link Condition} */ - public isFalse>>(key: K): Lambda { + public isFalse>>(key: K): Lambda { return new Lambda(({storable}) => { return storable.getNamespace(this.namespace).equals(key, false); }); diff --git a/src/game/nlcore/elements/persistent/type.ts b/src/game/nlcore/elements/persistent/type.ts index 47850e2..0751b45 100644 --- a/src/game/nlcore/elements/persistent/type.ts +++ b/src/game/nlcore/elements/persistent/type.ts @@ -2,25 +2,34 @@ export type StorableData = { [key in K]: number | boolean | string | StorableData | StorableData[] | undefined | null | Date; }; +/**@internal */ export type BaseStorableType = number | boolean | string | undefined | null | Date; +/**@internal */ export type UnserializableStorableType = Date; +/**@internal */ export type BaseStorableTypeName = "any" | "date"; +/**@internal */ export type StorableType = BaseStorableType | Record | Array; +/**@internal */ export type WrappedStorableData = { type: BaseStorableTypeName; data: T; } +/**@internal */ export type StorableTypeSerializer = (value: T) => WrappedStorableData; +/**@internal */ export type BaseStorableSerializeHandlers = { [K in BaseStorableTypeName]: K extends "any" ? StorableTypeSerializer> : K extends "date" ? StorableTypeSerializer : never; } +/**@internal */ export type BaseStorableDeserializeHandlers = { [K in BaseStorableTypeName]: K extends "any" ? (data: WrappedStorableData>) => Exclude : K extends "date" ? (data: WrappedStorableData) => Date : never; } +/**@internal */ export type NameSpaceContent = { [K in T]?: StorableType }; diff --git a/src/game/nlcore/elements/scene.ts b/src/game/nlcore/elements/scene.ts index 2cbf94a..15b7004 100644 --- a/src/game/nlcore/elements/scene.ts +++ b/src/game/nlcore/elements/scene.ts @@ -23,7 +23,9 @@ import ImageTransformProps = TransformDefinitions.ImageTransformProps; import GameElement = LogicAction.GameElement; import {DynamicPersistent} from "@core/elements/persistent"; +/**@internal */ export type UserImageInput = ImageSrc | RGBColor | ImageColor; +/**@internal */ export type SceneConfig = { invertY: boolean; invertX: boolean; @@ -43,6 +45,7 @@ export interface ISceneConfig { background?: ImageSrc | ImageColor; } +/**@internal */ export type SceneState = { backgroundImageProxy: VirtualImageProxy; }; @@ -51,9 +54,12 @@ export type JumpConfig = { unloadScene: boolean; } +/**@internal */ type ChainableAction = Proxied> | Actions; +/**@internal */ type ChainedScene = Proxied>; +/**@internal */ export type SceneDataRaw = { state: { backgroundMusic?: SoundDataRaw | null; @@ -62,6 +68,7 @@ export type SceneDataRaw = { backgroundImageState?: ImageDataRaw | null; } +/**@internal */ export type SceneEventTypes = { "event:scene.remove": []; "event:scene.load": [], diff --git a/src/game/nlcore/elements/script.ts b/src/game/nlcore/elements/script.ts index c4f8851..29e3b0a 100644 --- a/src/game/nlcore/elements/script.ts +++ b/src/game/nlcore/elements/script.ts @@ -16,6 +16,7 @@ export interface ScriptCtx { } type ScriptRun = (ctx: ScriptCtx) => ScriptCleaner | void; +/**@internal */ export type ScriptCleaner = () => void; export class Script extends Actionable { diff --git a/src/game/nlcore/elements/sound.ts b/src/game/nlcore/elements/sound.ts index a6a5c96..d6f65db 100644 --- a/src/game/nlcore/elements/sound.ts +++ b/src/game/nlcore/elements/sound.ts @@ -17,10 +17,13 @@ export enum SoundType { backgroundMusic = "backgroundMusic", } +/**@internal */ export type SoundDataRaw = { config: SoundConfig; }; +/**@internal */ export type VoiceIdMap = Record; +/**@internal */ export type VoiceSrcGenerator = (id: string | number) => string | Sound; export type SoundConfig = { diff --git a/src/game/nlcore/elements/story.ts b/src/game/nlcore/elements/story.ts index 8c162c0..8bc149a 100644 --- a/src/game/nlcore/elements/story.ts +++ b/src/game/nlcore/elements/story.ts @@ -10,8 +10,8 @@ import {Storable} from "@core/elements/persistent/storable"; /* eslint-disable @typescript-eslint/no-empty-object-type */ export type StoryConfig = {}; +/**@internal */ export type ElementStateRaw = Record; -export type NodeChildIdMap = Map; export class Story extends Constructable< SceneAction<"scene:action">, @@ -22,6 +22,11 @@ export class Story extends Constructable< /**@internal */ static MAX_DEPTH = 10000; + /**@internal */ + public static empty(): Story { + return new Story("empty").entry(new Scene("empty")); + } + /**@internal */ readonly name: string; /**@internal */ diff --git a/src/game/nlcore/elements/transform/position.ts b/src/game/nlcore/elements/transform/position.ts index b80e1d9..5bd75be 100644 --- a/src/game/nlcore/elements/transform/position.ts +++ b/src/game/nlcore/elements/transform/position.ts @@ -18,21 +18,25 @@ export interface IPosition { toCSS(): D2Position; } +/**@internal */ export type Coord2DPosition = { x: number | `${"-" | ""}${number}%`; y: number | `${"-" | ""}${number}%`; } & Partial; +/**@internal */ export type AlignPosition = { xalign: number; yalign: number; } & Partial; +/**@internal */ export type OffsetPosition = { xoffset: number; yoffset: number; } +/**@internal */ export type D2Position = { x: UnknownAble; y: UnknownAble; @@ -40,13 +44,15 @@ export type D2Position = { yoffset: UnknownAble; } +/**@internal */ export type RawPosition = CommonPositionType | (Coord2DPosition & { xalign?: never; yalign?: never }) | (AlignPosition & { x?: never; y?: never }); -export type Unknown = typeof PositionUtils.Unknown; -export type UnknownAble = T | Unknown; +type Unknown = typeof PositionUtils.Unknown; +type UnknownAble = T | Unknown; +/**@internal */ export class PositionUtils { static readonly Unknown: unique symbol = Symbol("Unknown"); diff --git a/src/game/nlcore/elements/transform/transform.ts b/src/game/nlcore/elements/transform/transform.ts index 683ed0a..6dc60d5 100644 --- a/src/game/nlcore/elements/transform/transform.ts +++ b/src/game/nlcore/elements/transform/transform.ts @@ -63,6 +63,18 @@ export class Transform { return CommonPosition.isCommonPositionType(position) || Coord2D.isCoord2DPosition(position) || Align.isAlignPosition(position); } + /** + * Apply transform immediately + */ + public static immediate( + props: SequenceProps, + ): Transform { + return new Transform(props, { + duration: 0, + ease: "linear", + }); + } + /** * Go to the left side of the stage */ diff --git a/src/game/nlcore/elements/transform/type.ts b/src/game/nlcore/elements/transform/type.ts index 6beb67e..b3c5db3 100644 --- a/src/game/nlcore/elements/transform/type.ts +++ b/src/game/nlcore/elements/transform/type.ts @@ -1,15 +1,5 @@ import {color, CommonDisplayable} from "@core/types"; import {DeepPartial} from "@lib/util/data"; -import type { - AnimationPlaybackControls, - AnimationScope, - AnimationSequence, - DOMKeyframesDefinition, - DynamicAnimationOptions, - ElementOrSelector, - MotionValue, - ValueAnimationTransition -} from "framer-motion"; export namespace TransformDefinitions { export type BezierDefinition = [number, number, number, number]; @@ -29,15 +19,6 @@ export namespace TransformDefinitions { | "backInOut" | "anticipate"; - export type GenericKeyframesTarget = [null, ...V[]] | V[]; - export type FramerAnimationScope = AnimationScope; - export type FramerAnimate = { - (from: V, to: V | GenericKeyframesTarget, options?: ValueAnimationTransition | undefined): AnimationPlaybackControls; - (value: MotionValue, keyframes: V_1 | GenericKeyframesTarget, options?: ValueAnimationTransition | undefined): AnimationPlaybackControls; - (value: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: DynamicAnimationOptions | undefined): AnimationPlaybackControls; - (sequence: AnimationSequence, options?: SequenceOptions | undefined): AnimationPlaybackControls; - } - export type CommonTransformProps = { duration: number; ease: EasingDefinition; diff --git a/src/game/nlcore/elements/transition/type.ts b/src/game/nlcore/elements/transition/type.ts index 7d625de..6b57573 100644 --- a/src/game/nlcore/elements/transition/type.ts +++ b/src/game/nlcore/elements/transition/type.ts @@ -3,15 +3,21 @@ import React from "react"; import type {AnimationPlaybackControls, DOMKeyframesDefinition} from "framer-motion"; import {ImageColor, ImageSrc} from "@core/types"; +/**@internal */ export type ElementProp = React.HTMLAttributes> = React.JSX.IntrinsicAttributes & React.ClassAttributes & React.HTMLAttributes & U; +/**@internal */ export type ImgElementProp = ElementProp>; +/**@internal */ export type SpanElementProp = ElementProp>; +/**@internal */ export type DivElementProp = ElementProp>; +/**@internal */ export type CSSElementProp = ElementProp & { style: T }; +/**@internal */ export type CSSProps = React.CSSProperties; export interface ITransition> { @@ -38,6 +44,7 @@ export interface ITextTransition extend copy(): ITextTransition; } +/**@internal */ export type EventTypes = { "start": [null]; "update": T; diff --git a/src/game/nlcore/game.ts b/src/game/nlcore/game.ts index a5562ba..db7bd40 100644 --- a/src/game/nlcore/game.ts +++ b/src/game/nlcore/game.ts @@ -59,6 +59,7 @@ export class Game { cursor: null, cursorHeight: 30, cursorWidth: 30, + showOverflow: false, }, elements: { say: { diff --git a/src/game/nlcore/game/liveGame.ts b/src/game/nlcore/game/liveGame.ts index 7087aa3..3a057f5 100644 --- a/src/game/nlcore/game/liveGame.ts +++ b/src/game/nlcore/game/liveGame.ts @@ -16,6 +16,7 @@ import {LiveGameEventHandler, LiveGameEventToken} from "@core/types"; import {Character} from "@core/elements/character"; import {Sentence} from "@core/elements/character/sentence"; +/**@internal */ type LiveGameEvent = { "event:character.prompt": [{ /** @@ -365,6 +366,11 @@ export class LiveGame { return nextAction; } + /**@internal */ + isPlaying() { + return !!this.currentAction; + } + /**@internal */ abortAwaiting() { if (this.lockedAwaiting) { diff --git a/src/game/nlcore/game/preference.ts b/src/game/nlcore/game/preference.ts index a4a3563..48fa0c4 100644 --- a/src/game/nlcore/game/preference.ts +++ b/src/game/nlcore/game/preference.ts @@ -1,9 +1,11 @@ -import {EventDispatcher} from "@lib/util/data"; +import {BooleanValueKeyOf, EventDispatcher} from "@lib/util/data"; +/**@internal */ type PreferenceEventToken = { cancel: () => void; }; +/**@internal */ type StringKeyof = Extract; export class Preference> { @@ -51,7 +53,7 @@ export class Preference) { for (const key in preferences) { @@ -73,4 +75,8 @@ export class Preference>(key: K) { + this.setPreference(key, !this.getPreference(key) as T[K]); + } } diff --git a/src/game/nlcore/gameTypes.ts b/src/game/nlcore/gameTypes.ts index 76ac67c..f9303eb 100644 --- a/src/game/nlcore/gameTypes.ts +++ b/src/game/nlcore/gameTypes.ts @@ -106,6 +106,10 @@ export type GameConfig = { * Cursor height in pixels */ cursorHeight: number; + /** + * Show overflowed content + */ + showOverflow: boolean; }; elements: { say: { diff --git a/src/game/player/elements/Player.tsx b/src/game/player/elements/Player.tsx index b293688..6eb3bfb 100644 --- a/src/game/player/elements/Player.tsx +++ b/src/game/player/elements/Player.tsx @@ -23,6 +23,7 @@ import Displayables from "@player/elements/displayable/Displayables"; import {ErrorBoundary} from "@player/lib/ErrorBoundary"; import SizeUpdateAnnouncer from "@player/elements/player/SizeUpdateAnnouncer"; import Cursor from "@player/lib/Cursor"; +import {Story} from "@core/elements/story"; function handleAction(state: GameState, action: PlayerAction) { return state.handle(action); @@ -30,7 +31,7 @@ function handleAction(state: GameState, action: PlayerAction) { export default function Player( { - story, + story = Story.empty(), width, height, className, @@ -83,14 +84,14 @@ export default function Player( useEffect(() => { game.getLiveGame().setGameState(state); - if (story) { + if (story && !game.getLiveGame().isPlaying()) { game.getLiveGame().loadStory(story); } return () => { game.getLiveGame().setGameState(undefined); }; - }, [game]); + }, [game, story]); useEffect(() => { return createMicroTask(() => { @@ -191,6 +192,7 @@ export default function Player( {game.config.player.cursor && ( (resolve => { - const eventToken = transition.events.onEvents([ + const eventToken = newTransition.events.onEvents([ { type: TransitionEventTypes.update, - listener: transition.events.on(TransitionEventTypes.update, (progress) => { + listener: newTransition.events.on(TransitionEventTypes.update, (progress) => { setTransitionProps(progress); }), }, { type: TransitionEventTypes.end, - listener: transition.events.on(TransitionEventTypes.end, () => { + listener: newTransition.events.on(TransitionEventTypes.end, () => { setTransition(null); - gameState.logger.debug("transition end", transition); + gameState.logger.debug("transition end", newTransition); }) }, { type: TransitionEventTypes.start, - listener: transition.events.on(TransitionEventTypes.start, () => { - gameState.logger.debug("transition start", transition); + listener: newTransition.events.on(TransitionEventTypes.start, () => { + gameState.logger.debug("transition start", newTransition); }) } ]); - transition.start(() => { + newTransition.start(() => { eventToken.cancel(); resolve(); }); }); } - async function applyTransform(transform: Transform) { - assignStyle(transform.propToCSS(gameState, displayableState.state, displayable.transformOverwrites)); + async function applyTransform(newTransform: Transform) { + assignStyle(newTransform.propToCSS(gameState, displayableState.state, displayable.transformOverwrites)); - setTransform(transform); - await transform.animate({ + setTransform(newTransform); + await newTransform.animate({ scope, target: displayableState, overwrites: displayable.transformOverwrites @@ -180,7 +182,7 @@ export default function Displayable( displayableState.state = deepMerge(displayableState.state, after); setTransformProps({ - style: transform.propToCSS(gameState, displayableState.state, displayable.transformOverwrites) as any, + style: newTransform.propToCSS(gameState, displayableState.state, displayable.transformOverwrites) as any, }); setTransform(null); diff --git a/src/game/player/elements/displayable/Displayables.tsx b/src/game/player/elements/displayable/Displayables.tsx index 6d931ed..05d9abc 100644 --- a/src/game/player/elements/displayable/Displayables.tsx +++ b/src/game/player/elements/displayable/Displayables.tsx @@ -7,6 +7,7 @@ import {Image as GameImage} from "@core/elements/displayable/image"; import {default as StageImage} from "@player/elements/image/Image"; +/**@internal */ export default function Displayables( {state, displayable}: Readonly<{ state: GameState; diff --git a/src/game/player/elements/displayable/Text.tsx b/src/game/player/elements/displayable/Text.tsx index 27e390d..557c072 100644 --- a/src/game/player/elements/displayable/Text.tsx +++ b/src/game/player/elements/displayable/Text.tsx @@ -11,6 +11,7 @@ import {TransformDefinitions} from "@core/elements/transform/type"; import Inspect from "@player/lib/Inspect"; import {useRatio} from "@player/provider/ratio"; +/**@internal */ export default function Text({state, text}: Readonly<{ state: GameState; text: GameText; diff --git a/src/game/player/elements/displayable/type.ts b/src/game/player/elements/displayable/type.ts index 47ac784..8a597d4 100644 --- a/src/game/player/elements/displayable/type.ts +++ b/src/game/player/elements/displayable/type.ts @@ -3,6 +3,7 @@ import React from "react"; import {Transform} from "@core/elements/transform/transform"; import {GameState} from "@player/gameState"; +/**@internal */ export type DisplayableChildProps = { transition: ITransition | null; transform: Transform | null; @@ -10,7 +11,9 @@ export type DisplayableChildProps = { transformRef: React.MutableRefObject; state: GameState; }; +/**@internal */ export type DisplayableChildHandler = (props: Readonly) => React.ReactElement; +/**@internal */ export type StatefulDisplayable = { state: Record; }; diff --git a/src/game/player/elements/image/AspectScaleImage.tsx b/src/game/player/elements/image/AspectScaleImage.tsx index 224f8c2..82b5ca2 100644 --- a/src/game/player/elements/image/AspectScaleImage.tsx +++ b/src/game/player/elements/image/AspectScaleImage.tsx @@ -5,6 +5,7 @@ import {usePreloaded} from "@player/provider/preloaded"; import {Image} from "@core/elements/displayable/image"; import {useGame} from "@core/common/player"; +/**@internal */ export default function AspectScaleImage( { props, diff --git a/src/game/player/elements/image/Image.tsx b/src/game/player/elements/image/Image.tsx index 72fb11f..e890987 100644 --- a/src/game/player/elements/image/Image.tsx +++ b/src/game/player/elements/image/Image.tsx @@ -10,13 +10,15 @@ import AspectScaleImage from "@player/elements/image/AspectScaleImage"; import {useRatio} from "@player/provider/ratio"; import clsx from "clsx"; -export default function Image({ - image, - state, - }: Readonly<{ - image: GameImage; - state: GameState; -}>) { +/**@internal */ +export default function Image( + { + image, + state, + }: Readonly<{ + image: GameImage; + state: GameState; + }>) { /** * Slow load warning */ @@ -42,6 +44,7 @@ export default function Image({ ); }; +/**@internal */ function DisplayableImage( { transition, diff --git a/src/game/player/elements/menu/Menu.tsx b/src/game/player/elements/menu/Menu.tsx index 917a342..9819adf 100644 --- a/src/game/player/elements/menu/Menu.tsx +++ b/src/game/player/elements/menu/Menu.tsx @@ -12,6 +12,7 @@ import {Word} from "@core/elements/character/word"; import {Pausing} from "@core/elements/character/pause"; import {Script} from "@core/elements/script"; +/**@internal */ export default function Menu( { prompt, diff --git a/src/game/player/elements/player/KeyEventAnnouncer.tsx b/src/game/player/elements/player/KeyEventAnnouncer.tsx index 8fff6df..42d3b12 100644 --- a/src/game/player/elements/player/KeyEventAnnouncer.tsx +++ b/src/game/player/elements/player/KeyEventAnnouncer.tsx @@ -4,6 +4,7 @@ import {GameState} from "@player/gameState"; import {throttle} from "@lib/util/data"; import {Game} from "@core/common/game"; +/**@internal */ export function KeyEventAnnouncer({state}: Readonly<{ state: GameState; }>) { diff --git a/src/game/player/elements/player/SizeUpdateAnnouncer.tsx b/src/game/player/elements/player/SizeUpdateAnnouncer.tsx index 022f670..c0ccd2c 100644 --- a/src/game/player/elements/player/SizeUpdateAnnouncer.tsx +++ b/src/game/player/elements/player/SizeUpdateAnnouncer.tsx @@ -1,6 +1,7 @@ import React, {useEffect} from "react"; import {useRatio} from "@player/provider/ratio"; +/**@internal */ export default function SizeUpdateAnnouncer( {containerRef}: Readonly<{ containerRef: React.RefObject }> ) { diff --git a/src/game/player/elements/preload/Preload.tsx b/src/game/player/elements/preload/Preload.tsx index c0bbcc0..67df0d1 100644 --- a/src/game/player/elements/preload/Preload.tsx +++ b/src/game/player/elements/preload/Preload.tsx @@ -6,6 +6,7 @@ import {Preloaded} from "@player/lib/Preloaded"; import {TaskPool} from "@lib/util/data"; import {useGame} from "@player/provider/game-state"; +/**@internal */ export function Preload( { state, diff --git a/src/game/player/elements/say/Say.tsx b/src/game/player/elements/say/Say.tsx index bee7844..0dc044e 100644 --- a/src/game/player/elements/say/Say.tsx +++ b/src/game/player/elements/say/Say.tsx @@ -8,6 +8,7 @@ import {useRatio} from "@player/provider/ratio"; import Inspect from "@player/lib/Inspect"; import {Game} from "@core/game"; +/**@internal */ export default function Say( { action, diff --git a/src/game/player/elements/say/Sentence.tsx b/src/game/player/elements/say/Sentence.tsx index e3f251e..f1c3f72 100644 --- a/src/game/player/elements/say/Sentence.tsx +++ b/src/game/player/elements/say/Sentence.tsx @@ -8,6 +8,7 @@ import {Pause, Pausing} from "@core/elements/character/pause"; import Inspect from "@player/lib/Inspect"; import {Script} from "@core/elements/script"; +/**@internal */ type SplitWord = { text: string; config: Partial; @@ -15,6 +16,7 @@ type SplitWord = { tag2?: any; } | "\n" | Pausing; +/**@internal */ export default function Sentence( { sentence, diff --git a/src/game/player/elements/say/TypingEffect.tsx b/src/game/player/elements/say/TypingEffect.tsx deleted file mode 100644 index cd94e12..0000000 --- a/src/game/player/elements/say/TypingEffect.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, {useEffect, useState} from "react"; - -interface TypingEffectProps { - text: string; - color: string; - speed: number; - onComplete?: () => void; - className?: string; -} - -const TypingEffect: React.FC = ( - {text, speed, onComplete, className, color} -) => { - const [displayedText, setDisplayedText] = useState(""); - const [currentIndex, setCurrentIndex] = useState(0); - - useEffect(() => { - if (currentIndex < text.length) { - if (text[currentIndex] === " ") { - setDisplayedText((prev) => prev.concat(text[currentIndex])); - setCurrentIndex((prev) => prev + 1); - } else { - const timeoutId = setTimeout(() => { - setDisplayedText((prev) => prev.concat(text[currentIndex])); - setCurrentIndex((prev) => prev + 1); - }, speed); - return () => clearTimeout(timeoutId); - } - } else if (onComplete) { - onComplete(); - } - }, [currentIndex]); - - return ; -}; - -export function Lines({text}: { text: string }) { - return (<>{text.split("\n").map((line, index) => ( - - {line} -
-
- ))}); -} - -export default TypingEffect; \ No newline at end of file diff --git a/src/game/player/elements/scene/Background.tsx b/src/game/player/elements/scene/Background.tsx index 506ffeb..f0fcf2f 100644 --- a/src/game/player/elements/scene/Background.tsx +++ b/src/game/player/elements/scene/Background.tsx @@ -4,11 +4,13 @@ import type {ReactNode} from "react"; import React, {useEffect, useRef} from "react"; import {useGame} from "@player/provider/game-state"; -export default function Background({ - children - }: Readonly<{ - children: ReactNode; -}>) { +/**@internal */ +export default function Background( + { + children + }: Readonly<{ + children: ReactNode; + }>) { const {ratio} = useRatio(); const {game} = useGame(); const contentContainerRef = useRef(null); diff --git a/src/game/player/elements/scene/BackgroundTransition.tsx b/src/game/player/elements/scene/BackgroundTransition.tsx index a24f3e0..1c0dd31 100644 --- a/src/game/player/elements/scene/BackgroundTransition.tsx +++ b/src/game/player/elements/scene/BackgroundTransition.tsx @@ -11,6 +11,7 @@ import Displayable from "@player/elements/displayable/Displayable"; import {useRatio} from "@player/provider/ratio"; import {usePreloaded} from "@player/provider/preloaded"; +/**@internal */ export default function BackgroundTransition({scene, props, state}: { scene: GameScene, props: Record, @@ -34,6 +35,7 @@ export default function BackgroundTransition({scene, props, state}: { ); } +/**@internal */ function DisplayableBackground( { transformRef, diff --git a/src/game/player/elements/scene/Scene.tsx b/src/game/player/elements/scene/Scene.tsx index bf3cbbd..0b2b088 100644 --- a/src/game/player/elements/scene/Scene.tsx +++ b/src/game/player/elements/scene/Scene.tsx @@ -6,6 +6,7 @@ import {GameState} from "@player/gameState"; import {Sound} from "@core/elements/sound"; import {Utils} from "@core/common/Utils"; +/**@internal */ export default function Scene( { scene, diff --git a/src/game/player/elements/type.ts b/src/game/player/elements/type.ts index 6d72c8c..667b37a 100644 --- a/src/game/player/elements/type.ts +++ b/src/game/player/elements/type.ts @@ -29,7 +29,7 @@ export type PlayerEventContext = { } export interface PlayerProps { - story: Story; + story?: Story; width?: string | number; height?: string | number; className?: clsx.ClassValue; diff --git a/src/game/player/gameState.ts b/src/game/player/gameState.ts index d725d38..f598ab5 100644 --- a/src/game/player/gameState.ts +++ b/src/game/player/gameState.ts @@ -40,6 +40,7 @@ export type PlayerStateData = { }; }[] }; +/**@internal */ export type PlayerAction = CalledActionResult; interface StageUtils { diff --git a/src/game/player/gameState.type.ts b/src/game/player/gameState.type.ts index 5517710..481383e 100644 --- a/src/game/player/gameState.type.ts +++ b/src/game/player/gameState.type.ts @@ -4,16 +4,19 @@ import {Sentence} from "@core/elements/character/sentence"; import {Word} from "@core/elements/character/word"; import {Pausing} from "@core/elements/character/pause"; +/**@internal */ export type Clickable = { action: T; onClick: U extends undefined ? () => void : (arg0: U) => void; }; +/**@internal */ export type TextElement = { character: Character | null; sentence: Sentence; id: string; words: Word[]; }; +/**@internal */ export type MenuElement = { prompt: Sentence; choices: Choice[]; diff --git a/src/game/player/lib/PlayerFrames.tsx b/src/game/player/lib/PlayerFrames.tsx index d955996..188b3da 100644 --- a/src/game/player/lib/PlayerFrames.tsx +++ b/src/game/player/lib/PlayerFrames.tsx @@ -2,6 +2,7 @@ import React from "react"; import {useRatio} from "@player/provider/ratio"; import Isolated from "@player/lib/isolated"; import clsx from "clsx"; +import {useGame} from "@player/provider/game-state"; type ForwardSize = { width?: React.CSSProperties["width"]; @@ -12,6 +13,10 @@ type ForwardChildren = { children?: React.ReactNode; }; +type ForwardClassName = { + className?: string; +}; + type FrameComponentProps = ForwardSize & ForwardChildren; function BaseFrame( @@ -62,6 +67,31 @@ const BottomLeft = (props: FrameComponentProps) => ; const BottomRight = (props: FrameComponentProps) => ; +function Full({children, className}: ForwardChildren & ForwardClassName) { + const {ratio} = useRatio(); + const {game} = useGame(); + + return ( + +
+
+ {children} +
+
+
+ ); +} + const Top = { Left: TopLeft, Center: TopCenter, @@ -82,4 +112,5 @@ export { Top, Center, Bottom, + Full, }; diff --git a/src/game/player/lib/UtilComponents.tsx b/src/game/player/lib/UtilComponents.tsx index 49d3721..91f25d1 100644 --- a/src/game/player/lib/UtilComponents.tsx +++ b/src/game/player/lib/UtilComponents.tsx @@ -17,7 +17,7 @@ const ForwardClassNamePropType = { className: PropTypes.string, } as const; const ForwardChildrenPropType = { - children: PropTypes.node, + children: PropTypes.element, } as const; function forwardBox( diff --git a/src/game/player/lib/isolated.tsx b/src/game/player/lib/isolated.tsx index c8b6fe8..71269fc 100644 --- a/src/game/player/lib/isolated.tsx +++ b/src/game/player/lib/isolated.tsx @@ -35,7 +35,6 @@ export default function Isolated( style={{ ...styles, position: "relative", - overflow: "hidden", ...(props?.style || {}), ...(style || {}), }} diff --git a/src/game/player/lib/preferences.tsx b/src/game/player/lib/preferences.tsx new file mode 100644 index 0000000..68f8cae --- /dev/null +++ b/src/game/player/lib/preferences.tsx @@ -0,0 +1,24 @@ +import {StringKeyOf} from "@lib/util/data"; +import {GamePreference} from "@core/game"; +import React, {useEffect} from "react"; +import {useGame} from "@player/provider/game-state"; + +export function usePreference>( + key: K +): [GamePreference[K], (value: GamePreference[K]) => void] { + const {game} = useGame(); + const [currentValue, setCurrentValue] = React.useState(game.preference.getPreference(key)); + + const setPreference = (value: GamePreference[K]) => { + game.preference.setPreference(key, value); + setCurrentValue(value); + }; + + useEffect(() => { + return game.preference.onPreferenceChange(key, setCurrentValue).cancel; + }, []); + + return [currentValue, setPreference]; +} + + diff --git a/src/game/player/libElements.ts b/src/game/player/libElements.ts index 1af1be4..5dd4c87 100644 --- a/src/game/player/libElements.ts +++ b/src/game/player/libElements.ts @@ -1,7 +1,8 @@ import Isolated from "@player/lib/isolated"; import Say from "@player/elements/say/Say"; -import {Top, Center, Bottom} from "@player/lib/PlayerFrames"; +import {Top, Center, Bottom, Full} from "@player/lib/PlayerFrames"; import {VBox, HBox} from "@player/lib/UtilComponents"; +import {usePreference} from "@player/lib/preferences"; export { Isolated, @@ -11,4 +12,6 @@ export { Bottom, VBox, HBox, + Full, + usePreference, }; \ No newline at end of file diff --git a/src/game/player/provider/game-state.tsx b/src/game/player/provider/game-state.tsx index a407663..779cf98 100644 --- a/src/game/player/provider/game-state.tsx +++ b/src/game/player/provider/game-state.tsx @@ -10,6 +10,9 @@ type GameContextType = { const GameContext = React.createContext(null); +/** + * @internal + */ export function GameProvider({children, game}: { children?: ReactNode, game?: Game }) { "use client"; const DefaultValue = new Game({}); diff --git a/src/game/player/provider/preloaded.tsx b/src/game/player/provider/preloaded.tsx index f26bbaa..6d5d32e 100644 --- a/src/game/player/provider/preloaded.tsx +++ b/src/game/player/provider/preloaded.tsx @@ -11,6 +11,7 @@ type PreloadedContextType = { const Context = createContext(null); +/**@internal */ export function PreloadedProvider({children}: { children: React.ReactNode }) { @@ -26,6 +27,7 @@ export function PreloadedProvider({children}: { ); } +/**@internal */ export function usePreloaded(): PreloadedContextType { if (!Context) throw new Error("usePreloaded must be used within a PreloadedProvider"); return useContext(Context) as PreloadedContextType; diff --git a/src/game/player/provider/ratio.tsx b/src/game/player/provider/ratio.tsx index 06758bc..ba6cd0b 100644 --- a/src/game/player/provider/ratio.tsx +++ b/src/game/player/provider/ratio.tsx @@ -3,6 +3,7 @@ import React, {createContext, useContext, useEffect, useReducer, useState} from "react"; import {EventDispatcher} from "@lib/util/data"; +/**@internal */ export type AspectRatioState = { width: number; height: number; @@ -121,6 +122,7 @@ class AspectRatio { const RatioContext = createContext(null); +/**@internal */ export function RatioProvider({children}: { children: React.ReactNode }) { @@ -133,6 +135,7 @@ export function RatioProvider({children}: { ); } +/**@internal */ export function useRatio(): { ratio: AspectRatio } { const context = useContext(RatioContext); if (!RatioContext || !context) throw new Error("useRatio must be used within a RatioProvider"); diff --git a/src/util/data.ts b/src/util/data.ts index 752537f..ddad6d6 100644 --- a/src/util/data.ts +++ b/src/util/data.ts @@ -694,9 +694,9 @@ export type StringKeyOf = Extract; export type ValuesWithin = { [K in keyof T]: T[K] extends U ? K : never; }[keyof T]; -export type BooleanKeys = { +export type BooleanValueKeyOf = Extract<{ [K in keyof T]: T[K] extends boolean ? K : never; -}[keyof T]; +}[keyof T], string>; export function createMicroTask(t: () => (() => void) | void): () => void { let executed = false;