klembot / chapbook

A simple, readable story format for Twine 2.
https://klembot.github.io/chapbook
MIT License
80 stars 25 forks source link

Ability to add JavaScript functions for reuse ("libraries") #10

Open japanoise opened 4 years ago

japanoise commented 4 years ago

Apologies if there's some way to do this that I haven't seen, but it isn't described in the guide.

Is your feature request related to a problem? Please describe.

I cannot find a way to define a function to use later. For example, if I write a JavaScript section in a passage that defines a function:

:: Start
…
[JavaScript]
function advanceTime() {
  timeOfDay += 2;
  if (timeOfDay >= 6) {
    timeOfDay %= 6;
    dayOfWeek = (dayOfWeek+1)%7;
  }
}

[cont]
…

It seems to crash if I call it later, saying the function is undefined:

You go to sleep.

[JavaScript]
advanceTime(); /* Crashes here */

[cont]
…

If I add it to the global state with engine.state.set, it's not a function (typeof returns undefined), and trying to call it results in an error.

I would like the ability to do this as it'd make dynamic tasks involving JavaScript a lot easier if I could define functions.

Describe the solution you'd like

Either allow the above to work somehow, or allow me to use a JavaScript passage (something like including a .js file in my source directory when using Twee), or add some other way to add library functionality.

Describe alternatives you've considered

greyelf commented 4 years ago

@japanoise Your issue is one of Scope.

Each JavaScript modifier is executed within its own Private Scope, thus the advanceTime() function you defined in your first JavaScript modifier is is not visible to any other unless you deliberately raise its scope to 'global'. There are a number of ways you can do this, the two most common are:

  1. Define your 'global' functions (& variables) on the web-browser's special 'global like' window object.

    [JavaScript]
    window.advanceTime = function () {
    timeOfDay += 2;
    if (timeOfDay >= 6) {
    timeOfDay %= 6;
    dayOfWeek = (dayOfWeek + 1) % 7;
    }
    };

    pros: you can reference the function directly like so advanceTime() in other JavaScript modifier. cons: Chapbook defines its variables on the window object, and developers like the web-browser's and those of some third-party JavaScript libraries also use that special object, so there is a chance you may override existing 'global' functions & variables.

  2. Define your own 'global' Namespace object and then define your 'global' functions & variables on that object instead. (the following example uses the same setup Namespace as the Twine Cookbook, you can either use it as well or make up your own unique name.)

    
    [JavaScript]
    /* Define your Namespace. do this only once per project. */
    window.setup = {};

setup.advanceTime = function () { timeOfDay += 2; if (timeOfDay >= 6) { timeOfDay %= 6; dayOfWeek = (dayOfWeek + 1) % 7; } };


**pros**: You don't have to worry about your function & variable names conflicting with others.
**cons**: you have to include the Namespace with you reference them. `setup.advanceTime()`

**NOTE**: [if using Twine 2.x] Ideally you would define your 'global' functions & variables, as well as include the contents of any third-party JavaScript libraries) within the **Story JavaScript** area of yur project.
japanoise commented 4 years ago

That seems to have worked. Note that I had to explicitly ask for window.namespace rather than just namespace when calling my function, but it fixed the issue.

klembot commented 4 years ago

Documentation task for me to add this to the guide. Also, curious what your use case was. As much as possible, I’d like to make it so that authors don’t need to use JavaScript. Was it just this time tracking?

japanoise commented 4 years ago

… curious what your use case was. As much as possible, I’d like to make it so that authors don’t need to use JavaScript. Was it just this time tracking?

Yeah, I'm making a bit more of an open-ended game, and I want to include a day-night and weekday cycle. Presumably I could do this in the vars section, but being a programmer, my first instinct is to jump to programming.

klembot commented 4 years ago

Here's a more idiomatic way to do it that I should document :)

[JavaScript]
engine.extend('1.0.0', () => {
  engine.state.setLookup('timeOfDay', () => engine.state.get('hours') % 6);
  engine.state.setLookup('dayOfWeek', () => (Math.floor(engine.state.get('hours') / 6)) % 7);
});

With this, you should be able to change hours to anything you want in story code and timeOfDay and dayOfWeek should update to match. The limitation is that you cannot set these lookups directly, though.

I didn't test this exact code, though, so I may have a bug in here, but hopefully the idea is clear from this example.