Sunday 17 March 2013

Scripts and conditions

As i've managed to get most of the guts of the game working - battles, internal indices, and so on, i've started to fill out the implementation by adding commands, and hooking up the scriptable actions. Even though i've only spent a few hours here and there it's coming together easily as there isn't much work to hook up a new script point. It's mostly just deciding on a convention and then sticking to it.

And to test the idea simply trying to port over some of the simple scripts and objects, such as absinthe ...

Revisiting the on-drop script for absinthe:

order trigger "get absinthe"
removeItem trigger absinthe
chat trigger "You pour the absinthe out and stare in disbelief as it eats a hole in the ground."
endscript

Took me a while to work out that first two lines simply remove the object from the game: the script is only executed after the default 'drop' action is executed - i.e. to leave it at the current map location. I decided to change the behaviour slightly so that if supplied, a script must implement the default action as well. This makes for a simpler and more obvious script in this case. Functions like 'removeItem' are not working on symbolic names either but the exact object, which should reduce coding errors.

owner.chat("You pour the absinthe out and stare in disbelief as it eats a hole in the ground.");
owner.removeItem(item);

Then I looked at the on-use script:

order trigger "emote wails: Oh, my head!"
addconditionwithduration trigger tired 500
if > trigger inte 10
  inc trigger intelligence -1
end
removeitem trigger absinthe
endscript

Fairly straightforward, except I hadn't filled out the condition stuff - the logic was there just no script hookage.

But I started with the JavaScript and worked backwards from there:

owner.emote("Wails: Oh, my head!");
owner.setCondition("tired", 500);
if (owner.getINT() > 10) {
  owner.addINT(-1);
}
owner.removeItem(item);

Previously, item definitions included fields which defined the scripts to execute. I am scrapping that and scripts are just found based on conventions from the item name. So for example, these scripts are stored in onScript/absinthe.drop and onScript/absinthe.use respectively. This means I don't have to little the objects themselves with references to names of files, and I guess I could also use it to implement an interface-based object model, particularly if i allowed scripts to store arbitrary persistent variables on items.

The existing condition mechanism defines conditions via a two-step process. Conditions are referenced by name, and then details about the conditions such as their firing rate and which scripts to execute are loaded from a file. I decided to scrap that and just let conditions be created on the fly, and use a convention to discover the scripts to execute. There aren't enough details to warrant the hassle required to maintain another directory full of files. So in this case, the "setCondition" is all that is required to set the condition, and the scripts which define the condition behaviour are defined in onCondition/tired.start, onCondition/tired.end, and onCondition/tired.tick. i.e. in a way very analogous to the item scripts, and one which will be repeated for all other script types.

Looking at the actual functionality for these scripts, the start/end is simply a notification to the player.

tired.start:

owner.chat("You suddenly feel very tired.");

tired.end:

owner.chat("Your normal level of conciousness returns.");

And the guts is in the tick script:

number tiredint * rand 150
number tiredint + tiredint 50
#
# You need an int of at least 50 to reduce the effect of tired
# and an int of 200 or more to be immune
#
if > tiredint trigger inte
    order trigger "sleep"
end
endscript

And the converted script, showing that anything with maths and logic is going to be a lot easier to write:

/*
* You need an int of at least 50 to reduce the effect of tired
* and an int of 200 or more to be immune
*/

var test = Math.random() * 150 + 50;

if (owner.getINT() < test) {
   owner.sleep();
}

Incidentally i'm considering making 'sleep' itself a condition, but at this point I haven't.

Current scripts also use conditions as a sort of general-purpose persistent state-holding variable, and I am going to remove that functionality and force the use of variables instead. To help enforce this, conditions will be visible in the client. This will leave conditions to be used for what they are intended: timed and/or periodic scripted behaviour.

I also decided to change the map alias stuff a little bit. Locations are still given symbolic names in the same way, but the visible/action/moveable script names are supplied directly. This lets 'true'/'false' be optimised, and still lets scripts be shared amongst locations. I also made the per-tile-type scripts follow the same conventions so everything is consistent.

Whilst looking at some of the existing scripts like the banks and so on, I noticed they add global commands to the game, but then have to have location checks on them to make sure they only activate at the correct locations. So another fairly simple extension may be to have per-map commands. On further thought I thought it might be more useful to have per-location commands implemented using an 'on-command' script, so then for example an ATM or bank teller for a certain bank could be implemented inside a single script, and then attached to any number of locations throughout the map. Since it's quite easy to add this, such a feature could also be added to tile-types or objects or even mobs instead ... (e.g. if you're standing on/next to an object/mob).

No comments: