Skip to content

Commit

Permalink
feat: functions to call javascript from narrat, better logging (#263)
Browse files Browse the repository at this point in the history
* feat: function to call javascript from narrat

* feat: adding call_js_method and run_js commands

* feat: new logging feature, documented js interface functions

* chore: update narrat version in deps
  • Loading branch information
liana-p committed Apr 13, 2024
1 parent 47ad700 commit a6ff74c
Show file tree
Hide file tree
Showing 22 changed files with 244 additions and 23 deletions.
2 changes: 1 addition & 1 deletion demo-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"es6-promise": "^4.2.8",
"narrat": "^3.2.0",
"narrat": "^3.10.2",
"pinia": "^2.1.7",
"steamworks.js": "^0.0.18",
"vue": "^3.4.15"
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ function sidebarScripting(): DefaultTheme.SidebarItem[] {
items: [
{ text: 'Language Syntax', link: 'language-syntax' },
{ text: 'Functions', link: 'functions' },
{ text: 'JavaScript interface', link: 'javascript-interface' },
{
text: 'Known limitations and issues',
link: '/others/scripting-limitations',
Expand Down
32 changes: 22 additions & 10 deletions docs/commands/all-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,27 @@ It is possible to change which character is used by the player (which is used wh
| change_player_character | `change_player_character player_2` | Will use `player_2` as the character for the player's words in choices etc. |
| change_game_character | `change_game_character game_2` | Changes the default character used to represent the game (by default is a character with no name) |

## Interfacing with JavaScript

Narrat can interface with JavaScript more directly thanks for a few functions:

| Command | Example | Description |
| [call_js_method](../scripting/javascript-interface.md) | `call_js_method [target] [method] [...options]` or `call_js_method document createElement canvas` | Calls the JS function [method] on the object [target] with any other arguments passed. `target` can either be an object, or a string. If it's a string, the engine will try to find the object inside `window`. For example `call_js_method document.body appendChild $canvas` will be equivalent to `window.document.body.appendChild($canvas)` (with $canvas being the value of the $canvas variable in narrat here) |
| [run_js](../scripting/javascript-interface.md) | `run_js "1 + 2"` | Runs a piece of JavaScript and returns the result. This is equivalent to using JavaScript `eval`, but is implemented [with the `Function` constructor](https://www.educative.io/answers/eval-vs-function-in-javascript). _Note:_ This is considered inefficient and unsafe. You should never do this if your game contains user-entered scripts. |

## Others

| command | example | description |
| ------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [clear_dialog](clear-dialog.md) | `clear_dialog` | Clears the dialog panel |
| log | `log "what's the value of test? %{test}" // Will print this log in the console` | Prints a log in the browser developer tools. Useful for debugging or checking variable values |
| menu_return | `menu_return` | Exits the game and returns to the main menu |
| [save](save-commands.md#save) | `save [save file name]` | Opens the manual save screen for the player to save the game (optional parameter for the name of the save file, useful to pass the name of the level/chapter for example) |
| `reset_global_save` | `reset_global_save` | Resets the global part of the save |
| [save_prompt](save-commands.md#save_prompt) | `save_prompt [save file name]` | Same as save, but asks the user if they want to save first |
| [wait](wait.md) | `wait 500` | Makes the script pause for x milliseconds |
| load_data | `set data.myData (load_data data/myDataFile.yaml)` | Loads data from the data file path passed and returns it |
| command | example | description |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [clear_dialog](clear-dialog.md) | `clear_dialog` | Clears the dialog panel |
| log | `log "what's the value of test? %{test}" // Will print this log in the console` | Prints a log in the browser developer tools. Useful for debugging or checking variable values |
| menu_return | `menu_return` | Exits the game and returns to the main menu |
| [save](save-commands.md#save) | `save [save file name]` | Opens the manual save screen for the player to save the game (optional parameter for the name of the save file, useful to pass the name of the level/chapter for example) |
| `reset_global_save` | `reset_global_save` | Resets the global part of the save |
| [save_prompt](save-commands.md#save_prompt) | `save_prompt [save file name]` | Same as save, but asks the user if they want to save first |
| [wait](wait.md) | `wait 500` | Makes the script pause for x milliseconds |
| load_data | `set data.myData (load_data data/myDataFile.yaml)` | Loads data from the data file path passed and returns it |
| `json_stringify` | `var jsonString (json_stringify $data.myData)` | Converts an object to a JSON string |
| `json_parse` | `var myObject (json_parse jsonString)` | Converts a JSON string to an object |
| [animate](https://docs.narrat.dev/features/animations.html) | `animate .dialog long-screenshake 150 20` | Animates an element with a preconfigured animations. See linked docs for details |
| [animate_wait](https://docs.narrat.dev/features/animations.html) | `animate_wait .dialog long-screenshake 150 20` | Same as animate, but waits for the animation to finish before continuing |
2 changes: 1 addition & 1 deletion docs/features/hot-module-reloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ window.addEventListener('load', () => {

```json
"dependencies": {
"narrat": "^3.0.0" // [!code ++]
"narrat": "^3.10.2" // [!code ++]
},
```

Expand Down
44 changes: 44 additions & 0 deletions docs/scripting/javascript-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: JavaScript interface in Narrat
description: Narrat can call JavaScript methods, use JavaScript variables, and run arbitrary JavaScript code for cases where you need to interact with the browser or the page.
---

## JS API for Narrat

There are currently two commands that allow using JavaScript more directly in Narrat:

- `call_js_method`: Calls a JavaScript method on the page.
- `run_js`: Builds a function from an arbitrary snippet of JavaScript code and runs it, returning the result.

This is intended to be used in cases where you need to do something narrat scripting can't do, but don't want to bother adding a new command to the language via a [plugin](https://docs.narrat.dev/plugins/plugins.html)

## `call_js_method`

Syntax: `call_js_method [target] [method] [...args [optional]]`

- `target` [string|object]: The target object to call the method on. This can be an object, or a string that will be evaluated to an object. Examples: `$myVariable`, `"document.body"`, `"localStorage"`. Paths are evaluated in the context of the page, looking for the object in `window`.
- `method` [string]: The method to call on the target object.
- `args` [any]: Any number of arguments passed after the method will be passed to the JavaScript function being called too.

Examples:

```narrat
test_js:
var stuff (new Object)
set stuff.hello "world"
call_js_method localStorage setItem "test_js" (json_stringify $stuff)
var stuff2 (json_parse (call_js_method localStorage getItem "test_js"))
log $stuff2
var canvas (call_js_method document createElement "canvas")
call_js_method document.body appendChild $canvas
set canvas.class "test"
call_js_method $canvas requestPointerLock
```

## `run_js`

Syntax: `run_js [code]`

- `code` [string]: The JavaScript code to run.

The code will be built using `Function`, in the format `return ${code}`. This will generate a function returning whatever `code` returns. The command then runs this function and returns the result.
2 changes: 1 addition & 1 deletion packages/create-narrat/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"es6-promise": "^4.2.8",
"narrat": "^3.9.5",
"narrat": "^3.10.2",
"pinia": "^2.1.7",
"steamworks.js": "^0.2.0",
"vue": "^3.4.15"
Expand Down
2 changes: 2 additions & 0 deletions packages/narrat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"files": [
"dist/"
],
"type": "module",
"pnpm": {
"patchedDependencies": {
"@types/[email protected]": "patches-@[email protected]"
Expand Down Expand Up @@ -95,6 +96,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.15.1",
"execa": "^8.0.1",
"jsdom": "^20.0.0",
"kolorist": "^1.5.1",
"pinia": "^2.1.7",
Expand Down
6 changes: 3 additions & 3 deletions packages/narrat/run-example.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env node

// @ts-check
const minimist = require('minimist');
const shell = require('shelljs');
const { red, cyan } = require('kolorist');
import minimist from 'minimist';
import shell from 'shelljs';
import { red, cyan } from 'kolorist';

const args = minimist(process.argv.slice(2));

Expand Down
7 changes: 7 additions & 0 deletions packages/narrat/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ScreenConfig } from './config/screens-config';
import { LogLevel } from './types/logging-types';

export const PRE_SIGNAL = '###_--_~=:;';
export const JUMP_SIGNAL = `###_--_~=:;_JUMP`;
Expand All @@ -21,6 +22,12 @@ export function isReturnSignal(s: string): s is ReturnSignal {
}
export const VERSION = import.meta.env.VITE_BUILD_VERSION;
export const BUILD_DATE = new Date(import.meta.env.VITE_BUILD_DATE);
export const PRODUCTION = import.meta.env.VITE_BUILD_MODE === 'production';
export const GIT_INFO = {
branch: import.meta.env.VITE_GIT_BRANCH,
commit: import.meta.env.VITE_GIT_COMMIT,
};
export const LOG_LEVEL = PRODUCTION ? LogLevel.WARN : LogLevel.DEBUG;

// Default values
export const DEFAULT_DIALOG_WIDTH = 400;
Expand Down
3 changes: 3 additions & 0 deletions packages/narrat/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ interface ImportMetaEnv {
readonly VITE_DEBUG: string;
readonly VITE_BUILD_DATE: string;
readonly VITE_BUILD_VERSION: string;
readonly VITE_BUILD_MODE: string;
readonly VITE_GIT_BRANCH: string;
readonly VITE_GIT_COMMIT: string;
// more env variables...
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dev_test:
run test_js
run test_arrays
run test_regex
run test_text_autoadvance
Expand Down
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 @@ -9,6 +9,7 @@ import dialogPanel from './dialog_panel.narrat';
import animations from './animations.narrat';
import macros from './test_macros.narrat';
import scenes from './test_scenes.narrat';
import js from './test_js.narrat';

export default [
game,
Expand All @@ -22,4 +23,5 @@ export default [
animations,
macros,
scenes,
js,
];
17 changes: 17 additions & 0 deletions packages/narrat/src/examples/default/scripts/test_js.narrat
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
test_js:
var stuff (new Object)
set stuff.hello "world"
call_js_method localStorage setItem "test_js" (json_stringify $stuff)
var stuff2 (json_parse (call_js_method localStorage getItem "test_js"))
log $stuff2
// run test_js_2

test_js_2:
var canvas (call_js_method document createElement "canvas")
call_js_method document.body appendChild $canvas
set canvas.class "test"
call_js_method $canvas requestPointerLock
var test (run_js "1 + 2")
log $test
var hello (run_js "localStorage.getItem('test_js')")
log $hello
12 changes: 9 additions & 3 deletions packages/narrat/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
addMenuButtonsFromPlugins,
registerDefaultMenuButtons,
} from './menu-buttons/menu-buttons';
import { BUILD_DATE, VERSION } from './constants';
import { BUILD_DATE, GIT_INFO, PRODUCTION, VERSION } from './constants';
import { addDirectives } from './utils/vue-directives';
import { useConfig } from './stores/config-store';
import { useStartMenu } from './stores/start-menu-store';
Expand Down Expand Up @@ -62,8 +62,14 @@ export async function startApp(optionsInput: AppOptionsInput) {
registerBaseCommands(vm);
logManager.setupDebugger(options.debug!);
console.log(
`%c Narrat game engine v${VERSION} - Built at ${BUILD_DATE.toLocaleString()}`,
'background: #222; color: #bada55',
`%c 🐀 Narrat v${VERSION} %c ${PRODUCTION ? 'PRODUCTION' : 'DEVELOPMENT'} BUILD %c https://narrat.dev %c
Built at ${BUILD_DATE.toLocaleString()}
Branch: ${GIT_INFO.branch}
Commit: ${GIT_INFO.commit}`,
'font-size: 2rem; background: #222; color: orange',
`font-size: 2rem; background: #222; color: ${PRODUCTION ? '#f00' : '#0f0'}`,
'font-size: 1rem;',
'font-size: 1.3rem; background: #222; color: #bada55',
);
vm.callHook('onNarratSetup');
let container: HTMLElement;
Expand Down
7 changes: 7 additions & 0 deletions packages/narrat/src/types/logging-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum LogLevel {
ERROR = 3,
WARN = 2,
INFO = 1,
DEBUG = 0,
NONE = -1,
}
2 changes: 1 addition & 1 deletion packages/narrat/src/utils/data-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function newFindDataHelper<T>(
}
return [null, path];
}
path = path.substring(1);
path = path.substring(variablePrefix.length);
const opening = /\[/;
const closing = /\]/;
let startIndex = path.search(opening);
Expand Down
32 changes: 32 additions & 0 deletions packages/narrat/src/utils/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { LogLevel } from '@/types/logging-types';

export function generateLoggingFunction(
moduleName: string,
logLevel: LogLevel,
currentLogLevel: LogLevel,
) {
if (currentLogLevel >= logLevel) {
let logKey = 'log';
switch (logLevel) {
case LogLevel.ERROR:
logKey = 'error';
break;
case LogLevel.WARN:
logKey = 'warn';
break;
default:
break;
}
return console.log.bind(console, logKey, `[${moduleName}]`);
} else {
return () => {};
}
}
export function createLogger(moduleName: string, logLevel: LogLevel) {
return {
error: generateLoggingFunction(moduleName, LogLevel.ERROR, logLevel),
warn: generateLoggingFunction(moduleName, LogLevel.WARN, logLevel),
info: generateLoggingFunction(moduleName, LogLevel.INFO, logLevel),
debug: generateLoggingFunction(moduleName, LogLevel.DEBUG, logLevel),
};
}
4 changes: 2 additions & 2 deletions packages/narrat/src/vm/commands/command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export function commandRuntimeError(
console.error(`Command: ${cmd.commandType}`);
console.error('Args: ', cmd.args);
console.error('Options: ', cmd.options);
error(`Runtime error at ${cmd.fileName}:${cmd.line} (${cmd.commandType}) ${cmd.code}. -
error(`Runtime error at ${cmd.fileName}:${cmd.line + 1} (${cmd.commandType}) ${cmd.code}. -
<br />
Error: ${errorText}`);
console.error('============================');
}

export function commandLog(cmd: Parser.Command<any, any>, ...log: any[]) {
console.log(`[${cmd.fileName}:${cmd.line}] log: `, ...log);
console.log(`[${cmd.fileName}:${cmd.line + 1}] log: `, ...log);
}
5 changes: 5 additions & 0 deletions packages/narrat/src/vm/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ import {
} from './object-commands';
import { createMacro, createMacroCommand } from '../macros';
import { changeSceneCommand } from './scene-commands';
import { callMethod, runJS } from './js-commands';

export function registerBaseCommands(vm: VM) {
// Choices
Expand Down Expand Up @@ -386,4 +387,8 @@ export function registerBaseCommands(vm: VM) {

// Scenes
vm.addCommand(changeSceneCommand);

// JS Commands
vm.addCommand(callMethod);
vm.addCommand(runJS);
}
Loading

0 comments on commit a6ff74c

Please sign in to comment.