diff --git a/cypress/e2e/actions/dialog.spec.js b/cypress/e2e/actions/dialog.spec.js index 371d31e..2f5c8ba 100644 --- a/cypress/e2e/actions/dialog.spec.js +++ b/cypress/e2e/actions/dialog.spec.js @@ -811,7 +811,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'Hello {/shake}shaking{/shake} world!' + 'Hello {shake}shaking{/shake} world!' ] }); @@ -829,7 +829,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'This is {/wave}wavy text{/wave}!' + 'This is {wave}wavy text{/wave}!' ] }); @@ -844,7 +844,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'A {/glitch}corrupted{/glitch} message' + 'A {glitch}corrupted{/glitch} message' ] }); @@ -859,7 +859,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/angry}I AM FURIOUS!{/angry}' + '{angry}I AM FURIOUS!{/angry}' ] }); @@ -874,7 +874,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'This is {/bold}important{/bold} and {/italic}emphasized{/italic}!' + 'This is {bold}important{/bold} and {italic}emphasized{/italic}!' ] }); @@ -891,7 +891,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'The {/redacted}CLASSIFIED{/redacted} information' + 'The {redacted}CLASSIFIED{/redacted} information' ] }); @@ -906,7 +906,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/shake}Scary{/shake} and {/happy}joyful{/happy}!' + '{shake}Scary{/shake} and {happy}joyful{/happy}!' ] }); @@ -922,7 +922,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimation', false); this.monogatari.script ({ 'Start': [ - 'Hello {/shake}world{/shake}!' + 'Hello {shake}world{/shake}!' ] }); @@ -937,7 +937,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimation', false); this.monogatari.script ({ 'Start': [ - '{/angry}FURIOUS{/angry} and {/wave}wavy{/wave} and {/glitch}glitchy{/glitch}!' + '{angry}FURIOUS{/angry} and {wave}wavy{/wave} and {glitch}glitchy{/glitch}!' ] }); @@ -953,7 +953,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'Hello{pause:50} {/shake}shaking{/shake}{speed:50}...done!' + 'Hello{pause:50} {shake}shaking{/shake}{speed:50}...done!' ] }); @@ -971,7 +971,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'centered {/mysterious}A mysterious message{/mysterious}' + 'centered {mysterious}A mysterious message{/mysterious}' ] }); @@ -989,7 +989,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'nvl {/scared}Trembling with fear{/scared}' + 'nvl {scared}Trembling with fear{/scared}' ] }); @@ -1004,7 +1004,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/shake-hard}HARD{/shake-hard} {/shake-slow}slow{/shake-slow} {/shake-little}little{/shake-little}' + '{shake-hard}HARD{/shake-hard} {shake-slow}slow{/shake-slow} {shake-little}little{/shake-little}' ] }); @@ -1020,7 +1020,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/wave-slow}slow{/wave-slow} {/wave-fast}fast{/wave-fast}' + '{wave-slow}slow{/wave-slow} {wave-fast}fast{/wave-fast}' ] }); @@ -1035,7 +1035,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/glitch-hard}HARD{/glitch-hard} {/glitch-slow}slow{/glitch-slow}' + '{glitch-hard}HARD{/glitch-hard} {glitch-slow}slow{/glitch-slow}' ] }); @@ -1050,7 +1050,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/fade}fading{/fade} {/scale}scaling{/scale} {/blur}blurry{/blur}' + '{fade}fading{/fade} {scale}scaling{/scale} {blur}blurry{/blur}' ] }); @@ -1066,7 +1066,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/rainbow}colorful{/rainbow} {/glow}glowing{/glow}' + '{rainbow}colorful{/rainbow} {glow}glowing{/glow}' ] }); @@ -1081,7 +1081,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 5); this.monogatari.script ({ 'Start': [ - '{/happy}joy{/happy} {/sad}tears{/sad} {/excited}wow{/excited} {/whisper}shh{/whisper}' + '{happy}joy{/happy} {sad}tears{/sad} {excited}wow{/excited} {whisper}shh{/whisper}' ] }); @@ -1098,7 +1098,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - '{/wave}Hello{/wave}' + '{wave}Hello{/wave}' ] }); @@ -1116,7 +1116,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimationSpeed', 10); this.monogatari.script ({ 'Start': [ - 'y:happy {/excited}I am so happy!{/excited}' + 'y:happy {excited}I am so happy!{/excited}' ] }); @@ -1131,7 +1131,7 @@ context ('Dialog', function () { this.monogatari.setting ('TypeAnimation', false); this.monogatari.script ({ 'Start': [ - '{/shake}a{/shake}{/wave}b{/wave}{/glitch}c{/glitch}{/bold}d{/bold}{/angry}e{/angry}{/redacted}f{/redacted}{pause:100}{speed:50}g' + '{shake}a{/shake}{wave}b{/wave}{glitch}c{/glitch}{bold}d{/bold}{angry}e{/angry}{redacted}f{/redacted}{pause:100}{speed:50}g' ] }); diff --git a/src/actions/Dialog.ts b/src/actions/Dialog.ts index f6d186d..382ba85 100644 --- a/src/actions/Dialog.ts +++ b/src/actions/Dialog.ts @@ -89,10 +89,15 @@ export class Dialog extends Action { static override async bind(selector: string): Promise { // Add listener for the text speed setting (TypeWriter reads from preference directly) + const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max); $_(`${selector} [data-action="set-text-speed"]`).on('change mouseover', function (this: HTMLInputElement) { + const textbox = Dialog.engine.element().find('[data-component="text-box"] [data-component="type-writer"]').get(0) as TypeWriter | undefined; const maxTextSpeed = Dialog.engine.setting('maxTextSpeed') as number; - const value = maxTextSpeed - parseInt(this.value); + const minPlaySpeed = Dialog.engine.setting('minTextSpeed') as number; + const value = clamp(parseInt(this.value), minPlaySpeed, maxTextSpeed); + Dialog.engine.preference('TextSpeed', value); + textbox?.setState({ config: { typeSpeed: value } }); }); // Detect scroll on the text element to remove the unread class used when @@ -115,6 +120,7 @@ export class Dialog extends Action { } this.engine.setting('maxTextSpeed', parseInt(($_(`${selector} [data-action="set-text-speed"]`).attribute('max') || '0'))); + this.engine.setting('minTextSpeed', parseInt(($_(`${selector} [data-action="set-text-speed"]`).attribute('min') || '0'))); } static override async reset({ keepNVL = false, saveNVL = false }: { keepNVL?: boolean; saveNVL?: boolean } = {}): Promise { diff --git a/src/actions/Message.ts b/src/actions/Message.ts index a86ef09..aea4123 100644 --- a/src/actions/Message.ts +++ b/src/actions/Message.ts @@ -15,7 +15,7 @@ export class Message extends Action { // The close action removes the active class from the element it // points to. this.engine.on('click', '[data-component="message-modal"] [data-action="close"]', () => { - Message.blocking = true; + Message.blocking = false; this.engine.element().find('[data-component="message-modal"]').remove(); this.engine.proceed({ userInitiated: true, skip: false, autoPlay: false }); }); diff --git a/src/components/settings-screen/index.ts b/src/components/settings-screen/index.ts index 2e9cdad..d7cf239 100644 --- a/src/components/settings-screen/index.ts +++ b/src/components/settings-screen/index.ts @@ -163,9 +163,7 @@ class SettingsScreen extends ScreenComponent { this.element().find('[data-platform="electron"]').remove(); } - this.element().find('[data-action="set-text-speed"]').value( - (this.engine.setting('maxTextSpeed') as number) - (this.engine.preference('TextSpeed') as number) - ); + this.element().find('[data-action="set-text-speed"]').value((this.engine.preference('TextSpeed') as number)); }); // Disable audio settings in iOS since they are not supported @@ -176,8 +174,9 @@ class SettingsScreen extends ScreenComponent { } const engine = this.engine; + const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max); this.content('auto-play-speed-controller').on('change mouseover', function(this: HTMLInputElement) { - const value = (engine.setting('MaxAutoPlaySpeed') as number) - parseInt(this.value); + const value = clamp(parseInt(this.value), 0, engine.setting('MaxAutoPlaySpeed') as number); engine.preference('AutoPlaySpeed', value); }); diff --git a/src/components/type-writer/index.ts b/src/components/type-writer/index.ts index 09eea4c..811387f 100644 --- a/src/components/type-writer/index.ts +++ b/src/components/type-writer/index.ts @@ -38,7 +38,7 @@ export interface TypedConfig extends TypingCallbacks { export interface ParsedAction { action: string; n?: string; - options?: Record; + options?: Record | string[]; text?: string; id?: string; } @@ -71,7 +71,7 @@ export interface TypeWriterState extends Properties { // Pre-compiled regex patterns for better performance // ============================================================================ -export const QUOTED_VALUE_PATTERN = /=["'](.*?)\1/g; +export const QUOTED_VALUE_PATTERN = /=(["'])(.*?)\1/g; export const CSS_VALUE_PATTERN = /(:[ ]?)(.*?);/g; export const QUOTE_CONTENT_PATTERN = /(["'])(.*?)\1/g; @@ -416,8 +416,8 @@ class TypeWriter extends Component { // Matches {action:N} or {action N} result = result.replace(new RegExp(`\\{${actionName}[:\\s]\\d+\\}`, 'g'), ''); } else if (action.type === 'enclosed') { - // Matches {/action} or {/action options} - result = result.replace(new RegExp(`\\{\\/${actionName}(?:\\s[^}]*)?\\}`, 'g'), ''); + // Matches {action} or {action options} or {/action} or {/action options} + result = result.replace(new RegExp(`\\{/?${actionName}(?:\\s[^}]*)?\\}`, 'g'), ''); } else if (action.type === 'instance') { // Matches {action/} result = result.replace(new RegExp(`\\{${actionName}\\/\\}`, 'g'), ''); @@ -505,7 +505,7 @@ class TypeWriter extends Component { patterns.push(`\\{(?:${this._numberActionsCache})[:\\s]\\d+\\}`); } if (this._enclosedActionsCache) { - patterns.push(`\\{\\/(?:${this._enclosedActionsCache}).*?\\}`); + patterns.push(`\\{/?(?:${this._enclosedActionsCache}).*?\\}`); } if (this._instanceActionsCache) { patterns.push(`\\{(?:${this._instanceActionsCache})\\/\\}`); @@ -746,7 +746,8 @@ class TypeWriter extends Component { container.innerHTML = (this.constructor as typeof TypeWriter).stripActionMarkers(str); } else { // Rush through animation at maximum speed - this.speed = 0; + const minSpeed = this.engine.setting('minTextSpeed') as number; + this.speed = minSpeed > 0 ? 0 : minSpeed; this.ignorePause = true; if (this.loops) { @@ -921,20 +922,27 @@ class TypeWriter extends Component { // Try enclosed action if (!match && this._enclosedActionsCache) { type = 'enclosed'; - match = section.match(new RegExp(`^\\{\\/(?${this._enclosedActionsCache})(?.*)\\}$`)); + match = section.match(new RegExp(`^\\{/?(?${this._enclosedActionsCache})(?.*)\\}$`)); if (match?.groups) { - if (this.enclosedID.length) { - // Extract action name from enclosedID (format: "actionName-parseIndex") - // Action names can contain hyphens (e.g., "shake-hard"), so we find the last hyphen - const lastId = this.enclosedID[this.enclosedID.length - 1]; - const lastDashIndex = lastId.lastIndexOf('-'); - const idAction = lastDashIndex > 0 ? lastId.substring(0, lastDashIndex) : lastId; - if (idAction === match.groups.action) { - this.enclosedID.pop(); - return undefined; + const isClosing = section.startsWith('{/'); + if (isClosing) { + if (this.enclosedID.length) { + // Extract action name from enclosedID (format: "actionName-parseIndex") + // Action names can contain hyphens (e.g., "shake-hard"), so we find the last hyphen + const lastId = this.enclosedID[this.enclosedID.length - 1]; + const lastDashIndex = lastId.lastIndexOf('-'); + const idAction = lastDashIndex > 0 ? lastId.substring(0, lastDashIndex) : lastId; + if (idAction === match.groups.action) { + this.enclosedID.pop(); + return undefined; + } else { + this.engine.debug.error('Mismatched closing action:', match.groups.action); + return undefined; + } } else { - this.enclosedID.push(`${match.groups.action}-${this.parseIndex}`); + this.engine.debug.error('Closing action without opening:', match.groups.action); + return undefined; } } else { this.enclosedID.push(`${match.groups.action}-${this.parseIndex}`); @@ -953,7 +961,7 @@ class TypeWriter extends Component { return undefined; } - let options: Record | undefined; + let options: Record | string[] | undefined; if (type === 'enclosed' && match.groups.options) { options = this._parseOptions(match.groups.options); @@ -982,8 +990,8 @@ class TypeWriter extends Component { /** * Parse options from an enclosed action. */ - private _parseOptions(optionsStr: string): Record { - const options: Record = {}; + private _parseOptions(optionsStr: string): Record | string[] { + const options: Record | string[] = {}; let opts: string | string[] = optionsStr.trim(); if (QUOTED_VALUE_PATTERN.test(opts)) { @@ -1002,6 +1010,10 @@ class TypeWriter extends Component { .replace(/\s/g, ':') .replace(/\[~\]/g, ' ') .split(/:/g); + } else { + // If no special patterns, split by whitespace and return as an array of strings. + opts = opts.split(/\s+/g); + return opts; } if (Array.isArray(opts)) { @@ -1121,6 +1133,15 @@ class TypeWriter extends Component { this._lastFrameTime = timestamp; this._accumulatedTime += deltaTime; + // Some developers may want the text to appear instantly, + // while disabling "InstantText". + if (this._targetWaitTime < 0) { + while (this.elements && this.nodeCounter < this.elements.length) { + this._revealNextCharacter(true); + } + return; + } + // Check if we've waited long enough if (this._accumulatedTime >= this._targetWaitTime) { this._revealNextCharacter(); @@ -1133,7 +1154,7 @@ class TypeWriter extends Component { /** * Reveal the next character and schedule the next one. */ - private _revealNextCharacter(): void { + private _revealNextCharacter(instant: boolean = false): void { if (this.nextPause) { this.nextPause = null; @@ -1161,6 +1182,24 @@ class TypeWriter extends Component { } this.nodeCounter += 1; + // If the developer wants instant text while keeping "InstantText" off, + // this will handle executing all remaining actions semi-instantly. + if (instant && this.elements && this.nodeCounter < this.elements.length) { + if (this.actions[this.nodeCounter]) { + this.executeAction(this.actions[this.nodeCounter]!); + + this.actionsPlayed++; + return; + } + + if (this.actionsPlayed) { + this.nodeCounter -= this.actionsPlayed; + this.actionsPlayed = 0; + } + + return; + } + if (this.elements && this.nodeCounter < this.elements.length) { // Continue typing - start new timing cycle this.typewrite(); diff --git a/src/index.ts b/src/index.ts index 7671585..4241b7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ import '@fortawesome/fontawesome-free/js/all'; export * from '@aegis-framework/artemis'; +export { tsParticles } from '@tsparticles/engine'; + export * from '@tsparticles/slim'; export * as RandomJS from 'random-js'; diff --git a/src/monogatari.ts b/src/monogatari.ts index 05b9335..94caddd 100644 --- a/src/monogatari.ts +++ b/src/monogatari.ts @@ -2992,6 +2992,12 @@ class Monogatari { * @returns {void} */ static stopTyping (component: TypeWriterComponent): void { + // Main differences between instant text & speed text: + // Instant Text: + // -- Appear instantly & removes all non-node formatting. + // Speed Text: + // -- Appear gradually at the fastest speed while keeping all non-node formatting. + // -- Setting min-speed to -1 or lower results in the benefits of speed text and instant text. const instant = this.setting('InstantText') as boolean; // TypeWriter.finish() handles setting finished_typing and triggering events @@ -3439,7 +3445,7 @@ class Monogatari { // auto-play feature this.registerListener ('auto-play', { callback: () => { - this.autoPlay (this.global ('_auto_play_timer') === null); + this.autoPlay (this.global ('_auto_play_timer') === undefined); } }); @@ -3517,6 +3523,15 @@ class Monogatari { screenElement?.setState ({ open: true }); + + // If auto play or skip were enabled, disable them. + if (this.global ('_auto_play_timer')) { + this.autoPlay (false); + } + + if (this.global ('skip')) { + this.skip (false); + } } static hideScreens (): void { @@ -3551,9 +3566,9 @@ class Monogatari { } $_('.forceAspectRatio').style ({ - width: widthCss, - height: heightCss, - marginTop: marginTopCss + 'width': widthCss, + 'height': heightCss, + 'margin-top': marginTopCss }); } @@ -3685,11 +3700,11 @@ class Monogatari { const promises = []; for (const component of this.components ()) { - promises.push (component.bind ()); + promises.push (component.bind (selector)); } for (const action of this.actions ()) { - promises.push (action.bind ()); + promises.push (action.bind (selector)); } return Promise.all (promises).then (() => { @@ -3898,11 +3913,11 @@ class Monogatari { const init: Promise[] = []; for (const component of this.components ()) { - init.push (component.init ()); + init.push (component.init (selector)); } for (const action of this.actions ()) { - init.push (action.init ()); + init.push (action.init (selector)); } if (this.setting ('AutoSave') != 0 && typeof this.setting ('AutoSave') === 'number') {