Skip to content

Commit

Permalink
feat: macros (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
liana-p committed Dec 23, 2023
1 parent 6b83574 commit 8f026ae
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ const config = {
text: 'Known limitations and issues',
link: '/others/scripting-limitations',
},
{
text: 'Macros',
link: '/scripting/macros',
},
],
},
{
Expand Down
125 changes: 125 additions & 0 deletions docs/scripting/macros.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Macros
description: Macros allow you to create your own syntax for narrat scripts. It's useful for creating custom commands, or shortcut commands of your choice.
---

# Macros

## What is a macro?

A macro in narrat is simply a custom command that you can define in a config file. It allows making up your own syntax for writing scripts, or creating shortcuts for commands you use often. Here's an example:

_macros.yaml_

```yaml
macros:
- keyword: t
label: lazy_talk
options:
- name: character
type: string
- name: text
type: string
- keyword: p
label: lazy_player_talk
options:
- name: text
type: string
```
_game.narrat_
```narrat
lazy_talk character text:
talk $character idle $text

lazy_player_talk text:
talk player idle $text

main:
t helper "Hi!"
p "hello"
```
Result: ![Macros screenshot](./macros.png)
## How to use macros
As seen in the example above, to create a macro you need two things:
- A macro definition in the macros.yaml config file
- A narrat label for the macro to run
### macros.yaml
Add a `macros.yaml` file to your game, like with other optional config files, by creating `macros.yaml` in `src/config`, and adding it to your list of config files:

```ts
import achievements from './achievements.yaml';
import animations from './animations.yaml';
import audio from './audio.yaml';
import buttons from './buttons.yaml';
import characters from './characters.yaml';
import choices from './choices.yaml';
import common from './common.yaml';
import items from './items.yaml';
import quests from './quests.yaml';
import screens from './screens.yaml';
import skills from './skills.yaml';
import skillChecks from './skillchecks.yaml';
import tooltips from './tooltips.yaml';
import macros from './macros.yaml'; // [!code focus]
import { ModuleConfigInput } from '@/config/config-input';
const defaultGameConfigs: ModuleConfigInput = {
achievements,
animations,
audio,
buttons,
characters,
choices,
common,
items,
quests,
screens,
skills,
skillChecks,
tooltips,
macros, // [!code focus]
};
export default defaultGameConfigs;
```

Then, in `macros.yaml` you can add a macro. The `macros` field of `macros.yaml` is an array of macros, and the options for a macro are:

```yaml
macros:
- keyword: t
label: lazy_talk
options:
- name: character
type: string
- name: text
type: string
```

- `keyword`: The keyword that will trigger the macro in your narrat script. This is the name of your custom command. For example, in the example above, the keyword is `t` for the `lazy_talk` macro.
- `label`: The narrat label to run when the macro is used.
- `options` [optional]: Those are the arguments of the macro. It's an array which should have one element for each option the label to run can receive:
- `name`: The name of the option.
- `type`: The type of the option (possible values are string, number, boolean, any, rest)
- `optional` [optional]: Whether the option is optional or not. Defaults to false. Only the last option can be optional.

The `options` list informs the narrat compiler of how many arguments your command expects and their type. This helps the compiler show warnings when starting the game if the command is used incorrectly. If `options` isn't passed, the compiler will assume the command has no arguments.

Once the macro has been defined, and the label to run has been defined, you can use the macro in your narrat script:

```narrat
lazy_talk character text:
talk $character idle $text
test_macros:
t myCharacter "Hi!"
```

Here, the `lazy_talk` label is run by the `t` macro, with the arguments `myCharacter` and `"Hi!"`.
Binary file added docs/scripting/macros.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/narrat/src/audio/audio-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { audioConfig } from '@/config';
import { useAudio } from '@/lib';
import { useAudio } from '@/stores/audio-store';
import { getAudio } from '@/utils/audio-loader';

export function dialogAudioConfig() {
Expand Down
11 changes: 11 additions & 0 deletions packages/narrat/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ import {
} from './config/animations-config';
import { isNarratYaml } from './hmr/hmr';
import { processConfigUpdate } from './config/config-helpers';
import {
MacrosConfigSchema,
defaultMacrosConfig,
} from './config/macros-config';
import { createMacro } from './vm/macros';
import { ArgTypes } from './vm/commands/command-plugin';

let config: Config;

Expand All @@ -98,6 +104,7 @@ const splitConfigs = [
['characters', CharactersFilesConfigSchema, defaultCharactersConfig],
['choices', ChoicesInputConfigSchema, defaultChoicesConfig],
['animations', AnimationsConfigSchema, defaultAnimationsConfig],
['macros', MacrosConfigSchema, defaultMacrosConfig],
] as const;

const extendedConfigs = [
Expand Down Expand Up @@ -228,6 +235,10 @@ export async function setupConfig(configInput: ConfigInput) {
newConfig[key as ConfigKey],
);
}
for (const macro of newConfig.macros.macros) {
const macroOptions = (macro.options ?? []) as ArgTypes;
createMacro(macro.keyword, macroOptions, macro.label);
}
return newConfig;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/narrat/src/config/config-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { NarratYaml } from '@/types/app-types';
import { ChoicesInputConfigSchema } from './choices-config';
import { AnimationsConfigSchema } from './animations-config';
import { AchievementsInputConfigSchema } from './achievements-config';
import { MacrosConfigSchema } from './macros-config';

export const BaseConfigInputSchema = Type.Object({
screens: Type.Union([Type.String(), ScreensInputConfigSchema]),
Expand All @@ -48,6 +49,7 @@ export const BaseConfigInputSchema = Type.Object({
animations: Type.Optional(
Type.Union([Type.String(), AnimationsConfigSchema]),
),
macros: Type.Optional(MacrosConfigSchema),
});

export const ConfigInputSchemaWithCommon = Type.Intersect([
Expand Down
3 changes: 3 additions & 0 deletions packages/narrat/src/config/config-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from './skillchecks-config';
import { ChoicesConfig, defaultChoicesConfig } from './choices-config';
import { AnimationsConfig, defaultAnimationsConfig } from './animations-config';
import { MacrosConfig, defaultMacrosConfig } from './macros-config';

export interface Config {
common: CommonConfig;
Expand All @@ -36,6 +37,7 @@ export interface Config {
animations: AnimationsConfig;
tooltips: TooltipsConfig;
choices: ChoicesConfig;
macros: MacrosConfig;
}

export const defaultConfig: Config = {
Expand All @@ -53,6 +55,7 @@ export const defaultConfig: Config = {
characters: defaultCharactersConfig,
choices: defaultChoicesConfig,
animations: defaultAnimationsConfig,
macros: defaultMacrosConfig,
};
// Hack so that the previous config has a static type
export const defaultConfigTyped: Config = defaultConfig;
Expand Down
23 changes: 23 additions & 0 deletions packages/narrat/src/config/macros-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Type, Static } from '@sinclair/typebox';

export const MacroArgumentSchema = Type.Object({
name: Type.String(),
type: Type.String(),
optional: Type.Optional(Type.Boolean()),
});
export type MacroArgument = Static<typeof MacroArgumentSchema>;

export const MacroSchema = Type.Object({
keyword: Type.String(),
label: Type.String(),
options: Type.Optional(Type.Array(MacroArgumentSchema)),
});
export type Macro = Static<typeof MacroSchema>;

export const MacrosConfigSchema = Type.Object({
macros: Type.Array(MacroSchema),
});
export type MacrosConfig = Static<typeof MacrosConfigSchema>;
export const defaultMacrosConfig: MacrosConfig = {
macros: [],
};
2 changes: 2 additions & 0 deletions packages/narrat/src/examples/default/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import screens from './screens.yaml';
import skills from './skills.yaml';
import skillChecks from './skillchecks.yaml';
import tooltips from './tooltips.yaml';
import macros from './macros.yaml';
import { ModuleConfigInput } from '@/config/config-input';

const defaultGameConfigs: ModuleConfigInput = {
Expand All @@ -27,5 +28,6 @@ const defaultGameConfigs: ModuleConfigInput = {
skills,
skillChecks,
tooltips,
macros,
};
export default defaultGameConfigs;
13 changes: 13 additions & 0 deletions packages/narrat/src/examples/default/config/macros.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
macros:
- keyword: t
label: lazy_talk
options:
- name: character
type: string
- name: text
type: string
- keyword: p
label: lazy_player_talk
options:
- name: text
type: string
2 changes: 2 additions & 0 deletions packages/narrat/src/examples/default/scripts/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import choiceTracking from './choice-tracking.narrat';
import tooltips from './tooltips.narrat';
import dialogPanel from './dialog_panel.narrat';
import animations from './animations.narrat';
import macros from './test_macros.narrat';

export default [
game,
Expand All @@ -18,4 +19,5 @@ export default [
tooltips,
dialogPanel,
animations,
macros,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
lazy_talk character text:
talk $character idle $text

lazy_player_talk text:
talk player idle $text

test_macros:
t helper "Hi!"
p "hello"
4 changes: 4 additions & 0 deletions packages/narrat/src/vm/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ import {
objectKeysCommand,
objectValuesCommand,
} from './object-commands';
import { createMacro, createMacroCommand } from '../macros';

export function registerBaseCommands(vm: VM) {
// Choices
Expand Down Expand Up @@ -364,4 +365,7 @@ export function registerBaseCommands(vm: VM) {
// Settings
vm.addCommand(setSettingPlugin);
vm.addCommand(getSettingPlugin);

// Macros
vm.addCommand(createMacroCommand);
}
37 changes: 37 additions & 0 deletions packages/narrat/src/vm/macros.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useVM } from '@/stores/vm-store';
import { ArgTypes, CommandPlugin } from './commands/command-plugin';
import { vm } from './vm';
import { commandRuntimeError } from './commands/command-helpers';

export function createMacro(name: string, argTypes: ArgTypes, label: string) {
const generatedCommand = CommandPlugin.FromOptions<any>({
keyword: name,
argTypes,
runner: async (cmd) => {
const res = await useVM().runLabelFunction(label, ...cmd.args);
return res;
},
});
vm.addCommand(generatedCommand);
}

export const createMacroCommand = CommandPlugin.FromOptions<any>({
keyword: 'create_macro',
argTypes: 'any',
runner: async (cmd) => {
if (cmd.args.length < 2) {
commandRuntimeError(
cmd,
`create_macro command needs at least a name and a label`,
);
}
const macroName = cmd.args[0] as string;
const label = cmd.args[1] as string;
const argNames = cmd.args.slice(2) as string[];
const argTypes = argNames.map((name) => ({
name,
type: 'any',
})) as ArgTypes;
createMacro(macroName, argTypes, label);
},
});

1 comment on commit 8f026ae

@vercel
Copy link

@vercel vercel bot commented on 8f026ae Dec 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

narrat-editor-demo – ./packages/narrat-editor

narrat-editor-demo.vercel.app
narrat-editor-demo-nialna.vercel.app
narrat-editor-demo-git-main-nialna.vercel.app
demo.narrat.dev

Please sign in to comment.