Skip to content

Commit

Permalink
feat(text): New features for text auto advance and delays before cont…
Browse files Browse the repository at this point in the history
…inue
  • Loading branch information
liana-p committed Apr 10, 2024
1 parent 5da65fd commit 5445799
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 42 deletions.
13 changes: 7 additions & 6 deletions docs/commands/all-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ This page lists all available narrat commands as well as usage examples.

## Dialog

| Command | Example | Description |
| ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [talk](text-commands/talk-function.md) | `talk player idle "Hello everyone"` | Makes a character talk in a specific pose |
| [think](text-commands/talk-function-1.md) | `think player idle "I wonder if they like me"` | Makes a character think in a specific pose (think is the same as talk but without quotes around) |
| text command (Empty command) | `"Hello world"` | Writing text without a command will print that text as if it was said "by the game", without a character |
| |
| Command | Example | Description |
| ---------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [talk](text-commands/talk-function.md) | `talk player idle "Hello everyone"` | Makes a character talk in a specific pose |
| [think](text-commands/think-function.md) | `think player idle "I wonder if they like me"` | Makes a character think in a specific pose (think is the same as talk but without quotes around) |
| [narrate](text-commands/narrate-command.md) | `narrate "Hello world"` | Writing text without a command will print that text as if it was said "by the game", without a character |
| [text command](text-commands/narrate-command.md) (Empty command) | `"Hello world"` | Same thing as the narrate command, but shorter |
| |

## Basic program flow

Expand Down
26 changes: 26 additions & 0 deletions docs/commands/text-commands/narrate-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Narrate command

The `narrate` command is used to let the "game" talk instead of a character. It is similar to the `talk` command, but takes no character or pose option.

It is also the command used when the shorthand `text` command syntax is used (when typing a string on a line directly with no command).

Syntax:

- `narrate "text" [delay] [autoadvance]`
- Or directly: `"text"` (delay/autoadance not possible with the shorthand syntax)

- **text**: The actual line of dialogue to speak
- **delay [optional]**: Delay in miliseconds before the continue button appears or the next line of dialogue is spoken
- **autoadvance [optional, true|false]**: If true, the dialogue will automatically advance to the next line after the delay. If false (or not there), the continue button will appear after the delay

## Example

```narrat
test_text_autoadvance:
narrate "Hi!"
narrate "with delay " 2000
narrate "With delay and auto advance waiting for text to print" 200 true
narrate "With delay and auto advance not waiting for text to print" 0 true
narrate "This is a normal line"
```
6 changes: 4 additions & 2 deletions docs/commands/text-commands/talk-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

The `talk` function is the main function that lets characters speak. It can also be used in choice prompts.

Syntax: `talk [character] [pose] ["Text"]`
Syntax: `talk [character] [pose] ["Text"] [delay] [autoadvance]`

- **character**: This is the id of the character (as defined in `characters.yaml`)
- **pose:** Refers to which sprite to use for the dialogue, as defined in the list of sprites in `characters.yaml` for this character. This allows you to have different expressions per character
- **text**: The actual line of dialogue to speak
- **delay [optional]**: Delay in miliseconds before the continue button appears or the next line of dialogue is spoken
- **autoadvance [optional, true|false]**: If true, the dialogue will automatically advance to the next line after the delay. If false (or not there), the continue button will appear after the delay

A shorthand for the narrator speaking (without any character sprite or name) is to just write a line of dialogue without the talk command (`"My line of dialogue"`
A shorthand for the narrator speaking (without any character sprite or name) is to just write a line of dialogue without the talk command (`"My line of dialogue"`).

## Example

Expand Down
2 changes: 2 additions & 0 deletions packages/narrat/src/dialog-box.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { useNavigation } from './inputs/useNavigation';
import { InputListener, useInputs } from '@/stores/inputs-store';
import { Interval, Timeout } from '@/utils/time-helpers';
import { playLetterAudio, playDialogLineAudio } from '@/audio/audio-helpers';
import { useVM } from '@/stores/vm-store';
export interface TextAnimation {
text: string;
Expand Down Expand Up @@ -420,6 +421,7 @@ function endTextAnimation({
unmounted,
pressedSpace,
}: { unmounted?: boolean; pressedSpace?: boolean } = {}) {
useVM().endTextAnimation();
createTextFieldListener();
setTimeout(() => {
if (navigation.value) {
Expand Down
14 changes: 13 additions & 1 deletion packages/narrat/src/examples/default/scripts/default.narrat
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
dev_test:
run test_multiline
run test_text_autoadvance
menu_return

test_text_autoadvance:
narrate "Hi!"
narrate "with delay " 2000
narrate "With delay and auto advance waiting for text to print" 200 true
narrate "With delay and auto advance not waiting for text to print" 0 true
narrate "This is a normal line"

talk player idle "Hello!"
talk player idle "This will auto advance" 1 true
talk player idle "This will not auto advance but has a delay" 2000 false
talk player idle "this is a normal line"

test_multiline:
talk player idle "This is a very long string that \
I want to split into multiple lines for readability"
Expand Down
3 changes: 3 additions & 0 deletions packages/narrat/src/stores/dialog-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export const useDialogStore = defineStore('dialog', {
clearDialog() {
this.dialog.splice(0, this.dialog.length);
},
makeLastDialogInteractive() {
this.dialog[this.dialog.length - 1].interactive = true;
},
reset() {
this.dialog = [];
this.playMode = 'normal';
Expand Down
13 changes: 13 additions & 0 deletions packages/narrat/src/stores/vm-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface DataState {

export interface VMState {
commandsWaitingForPlayerAnswer: Parser.Command<any, any>[];
promisesWaitingForTextAnimation: (() => void)[];
stack: MachineFrame[];
data: DataState;
globalData: DataState;
Expand All @@ -86,6 +87,7 @@ export const useVM = defineStore('vm', {
script: {},
labelStack: ['main'],
commandsWaitingForPlayerAnswer: [],
promisesWaitingForTextAnimation: [],
hasJumped: false,
}) as VMState,
actions: {
Expand Down Expand Up @@ -140,6 +142,17 @@ export const useVM = defineStore('vm', {
waitForPlayerAnswer(cmd: Parser.Command<any, any>) {
this.commandsWaitingForPlayerAnswer.push(cmd);
},
waitForEndTextAnimation() {
return new Promise<void>((resolve) => {
this.promisesWaitingForTextAnimation.push(resolve);
});
},
endTextAnimation() {
for (const resolve of this.promisesWaitingForTextAnimation) {
resolve();
}
this.promisesWaitingForTextAnimation = [];
},
popAnswerQueue() {
const cmd = this.commandsWaitingForPlayerAnswer.pop();
return cmd;
Expand Down
8 changes: 7 additions & 1 deletion packages/narrat/src/vm/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ import {
setButtonCommand,
setScreenCommand,
} from './screen-commands';
import { talkCommand, textCommandPlugin, thinkCommand } from './text';
import {
narrateCommand,
talkCommand,
textCommandPlugin,
thinkCommand,
} from './text';
import { VM } from '../vm';
import {
addItemPlugin,
Expand Down Expand Up @@ -241,6 +246,7 @@ export function registerBaseCommands(vm: VM) {
vm.addCommand(textCommandPlugin);
vm.addCommand(talkCommand);
vm.addCommand(thinkCommand);
vm.addCommand(narrateCommand);

// // functions and labels
vm.addCommand(jumpCommand);
Expand Down
124 changes: 92 additions & 32 deletions packages/narrat/src/vm/commands/text.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,137 @@
import {
ArgTypes,
CommandParserFunction,
CommandPlugin,
generateParser,
} from './command-plugin';
import { textCommand } from '../vm-helpers';
import { useConfig } from '@/stores/config-store';
import { useVM } from '@/stores/vm-store';
import { timeout } from '@/utils/promises';
import { playerAnswered } from '../vm';
import { Parser } from '@/types/parser';
import { useDialogStore } from '@/stores/dialog-store';
export interface BaseTextCommandArgs {
text: string;
delay?: number;
autoAdvance?: boolean;
}
export class BaseTextCommand<
Options extends BaseTextCommandArgs,
> extends CommandPlugin<Options> {
static async ManageAutoAdvance(cmd: Parser.Command<TalkArgs>) {
console.log('not interactive');
await useVM().waitForEndTextAnimation();
console.log('text animation ended');
await timeout(cmd.options.delay || 0);
console.log('delay ended');
if (cmd.options.autoAdvance) {
console.log('player answered');
playerAnswered(0);
} else {
console.log('make dialog interactive');
useDialogStore().makeLastDialogInteractive();
}
}

static ShouldBeInteractive(cmd: Parser.Command<TalkArgs>) {
return !cmd.options.delay && !cmd.options.autoAdvance;
}
}
export interface TalkArgs {
speaker: string;
pose: string;
text: string;
delay?: number;
autoAdvance?: boolean;
}
export const talkCommand = CommandPlugin.FromOptions<TalkArgs>({
export const textCommandArgs: ArgTypes = [
{
name: 'speaker',
type: 'string',
},
{
name: 'pose',
type: 'string',
},
{
name: 'text',
type: 'string',
},
{
name: 'delay',
type: 'number',
optional: true,
},
{
name: 'autoAdvance',
type: 'boolean',
optional: true,
},
];
export const talkCommand = BaseTextCommand.FromOptions<TalkArgs>({
keyword: 'talk',
argTypes: [
{
name: 'speaker',
type: 'string',
},
{
name: 'pose',
type: 'string',
},
{
name: 'text',
type: 'string',
},
],
argTypes: textCommandArgs,
runner: async (cmd, choices) => {
const interactive = BaseTextCommand.ShouldBeInteractive(cmd);
await textCommand({
speaker: cmd.options.speaker,
pose: cmd.options.pose,
cssClass: 'talk-command',
text: `"${cmd.options.text}"`,
choices,
interactive: true,
interactive,
});
if (!interactive) {
BaseTextCommand.ManageAutoAdvance(cmd);
}
},
returnAfterPlayerAnswer: true,
});

export const thinkCommand = CommandPlugin.FromOptions<TalkArgs>({
export const thinkCommand = BaseTextCommand.FromOptions<TalkArgs>({
keyword: 'think',
argTypes: [
{
name: 'speaker',
type: 'string',
},
{
name: 'pose',
type: 'string',
},
{
name: 'text',
type: 'string',
},
],
argTypes: textCommandArgs,
runner: async (cmd, choices) => {
const interactive = BaseTextCommand.ShouldBeInteractive(cmd);
await textCommand({
speaker: cmd.options.speaker,
pose: cmd.options.pose,
cssClass: 'think-command',
text: `${cmd.options.text}`,
choices,
interactive: true,
interactive,
});
if (!interactive) {
BaseTextCommand.ManageAutoAdvance(cmd);
}
},
returnAfterPlayerAnswer: true,
});

export const narrateCommand = BaseTextCommand.FromOptions<TalkArgs>({
keyword: 'narrate',
argTypes: textCommandArgs.slice(2, 5),
runner: async (cmd, choices) => {
const interactive = BaseTextCommand.ShouldBeInteractive(cmd);
await textCommand({
speaker: useConfig().gameCharacter,
cssClass: 'text-command',
text: `${cmd.options.text}`,
choices,
interactive,
});
if (!interactive) {
BaseTextCommand.ManageAutoAdvance(cmd);
}
},
returnAfterPlayerAnswer: true,
});

export const textParser = (): CommandParserFunction<{}, { text: string }> => {
const parser = generateParser<{}, { text: string }>('text', []);
return (ctx, parsed) => {
console.log(parsed);
const result = parser(ctx, parsed);
parsed.command.staticOptions = {
text: parsed.code.substring(1, parsed.code.length - 1),
Expand Down

0 comments on commit 5445799

Please sign in to comment.