A TADS3 module for implementing crafting systems

Here’s a TADS3 module for (hopefully easily) implementing crafting systems: craftingSystem github repo.

It’s built on the ruleEngine and stateMachine modules I’ve discussed previously.

I’ll just dive into a simple example: making toast. First, as always, we need to declare a RuleEngine instance (if we don’t, or if we comment it out, then none of the rule-based stuff will be enabled, which can be useful for testing):

              // Declare a RuleEngine instance.
              RuleEngine;

Then we declare a CraftingSystem instance. It can be anonymous, but you probably want to add a label so you can refer to it elsewhere in your code:

              // Declare the crafting system.
              cookingSystem: CraftingSystem;

Recipes are declared by adding them to the CraftingSystem instance using the normal TADS3 +[object declaration] syntax. Here’s just the barebones nuts and bolts of our “make toast” recipe (we’ll fill it out later):

              +Recipe 'toast' @Toast ->toaster;
              ++Ingredient @Bread;
              ++RecipeAction @toaster ->TurnOnAction;

Line by line, that’s:

  • A Recipe declaration. It creates a Recipe with the ID “toast” (the single-quoted string). The recipe will produce an instance of Toast (the class name after the @). The created object will end up in the toaster object (the object after the ->). Note that the Toast class and toaster object need to be declared elsewhere.
  • An Ingredient declaration. This says that the recipe wants a Bread object (the class after the @). Ingredient declarations can include locations, but this one doesn’t. So by default the recipe will expect the ingredients to go in the same place the recipe results will go, in this case the toaster.
  • A RecipeAction declaration. This is an action trigger (under the hood a Trigger like used in the ruleEngine module). It fires when the toaster (object after the @) receives the TurnOnAction (action name after the ->). That is, the trigger will fire on the >TURN TOASTER ON command.

All together, this recipe will be matched by the command sequence:

              >PUT BREAD IN TOASTER
              >TURN TOASTER ON

…assuming the player has bread to put into the toaster, the toaster is in the room, and so on.

When the sequence is completed, the bread will be whisked away (moved into nil) and a new Toast instance will be placed in the toaster.

This all works, but note that there are no informational messages or other feedback supplied in the recipe as written above. So let’s go through it again to make it more friendly:

              +Recipe 'toast' @Toast ->toaster
                      "The toaster produces a slice of toast. ";
              ++RecipeNoAction @toaster ->TurnOnAction
                      "The toaster won't start without bread. ";
              ++IngredientList
                      "{You/He} put{s} the bread in the toaster. ";
              +++Ingredient @Bread;
              ++RecipeAction @toaster ->TurnOnAction
                      "{You/he} start{s} the toaster. ";

The first thing to notice is all the double-quoted strings. These will be displayed when the corresponding part of the recipe is completed. For example, the Recipe declaration:

              +Recipe 'toast' @Toast ->toaster
                      "The toaster produces a slice of toast. ";

…means that "The toaster produces a slice of toast. " will be output when the recipe is completed.

The double-quoted strings are an optional part of the template for most of the recipe parts, but you can alternately add a recipeAction() method to the declaration:

              +Recipe 'toast' @Toast ->toaster
                      recipeAction() {
                              "The toaster produces a slice of toast. ";
                      }
              ;

In this case it would work exactly the same as the more concise version, but this allows arbitrary code to be executed during the appropriate point in the recipe.

Additionally, the expanded version of the recipe includes a RecipeNoAction declaration:

              ++RecipeNoAction @toaster ->TurnOnAction
                      "The toaster won't start without bread. ";

This works like a RecipeAction, but doesn’t change the recipe state. In this case it’s just for an informational message if the toaster is turned on before bread is added.

The next thing to notice in the expanded version of the recipe is an explicit IngredientList declaration. In the simpler version of the recipe the Ingredient was added directly to the Recipe.

All Ingredient declarations are added to the “nearest” IngredientList above them. If no IngredientList exists in the recipe, one is created automagically.

For recipes with only one “combine a bunch of ingredients” step you don’t have to declare an explicit IngredientList, but if you have multiple steps combining ingredients you will.

Giving an explicit IngredientList also allows you to attach an informational message and/or a full recipeAction() method, which you cannot do with a Ingredient declaration.

Here’s a complete example that will compile with the craftingSystem module:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

#include "craftingSystem.h"

versionInfo: GameID;

gameMain: GameMainDef initialPlayerChar = me;

class Slice: Thing, CraftingIngredient
        desc = "It's <<aName>>. "
        isEquivalent = true
;

class Bread: Slice '(slice) bread' 'slice of bread';
class Toast: Slice '(slice) toast' 'slice of toast';

startRoom: Room 'Void' "This is a featureless void.";
+toaster: Container, CraftingGear '(silver) (metal) toaster slot' 'toaster'
        "A silver toaster with a single slot on the top. "
        dobjFor(TurnOn) { verify() {} }
        iobjFor(PutIn) {
                verify() {
                        if(contents.length != 0)
                                illogicalNow('The toaster can only hold one
                                        thing at a time. ');
                }
        }
        canFitObjThruOpening(obj) { return(obj.ofKind(Slice)); }
;

+me: Person;
++Bread;
++Bread;

RuleEngine;

CraftingSystem;

+Recipe 'toast' @Toast ->toaster "The toaster produces a slice of toast. ";
++RecipeNoAction @toaster ->TurnOnAction
        "The toaster won't start without bread. ";
++IngredientList "{You/He} put{s} the bread in the toaster. ";
+++Ingredient @Bread;
++RecipeAction @toaster ->TurnOnAction "{You/he} start{s} the toaster. ";

Thrilling transcript:

Void
This is a featureless void.

You see a toaster here.

>turn toaster on
The toaster won't start without bread.

>put bread in toaster
You put the bread in the toaster.

>put bread in toaster
The toaster can only hold one thing at a time.

>turn toaster on
You start the toaster.  The toaster produces a slice of toast.

>turn toaster on
The toaster won't start without bread.

>l
Void
This is a featureless void.

You see a toaster (which contains a slice of toast) here.

>take toast
Taken.

>turn toaster on
The toaster won't start without bread.

>put bread in toaster
You put the bread in the toaster.

>turn toaster on
You start the toaster.  The toaster produces a slice of toast.
6 Likes

My dude, oh my gawd, if I were not a dedicated Adv3Lite user. The absolute gems you are posting here…!!

6 Likes

How deeply dependant on adv3 are these libraries? Because it’d be really awesome to have them on adv3lite too, and (in a couple months when I have the time/energy) I might take a crack at ports if you think it might be feasible

3 Likes

I’ll look @ it and see if is feasible to port it to a3Lite.

(BTW, studying the prior contribs from Jbg led to the discovery of interesting things and details in a3Lite and adv3 libraries: definitively TADS & its libraries’s documentation is an IF in itself…)

Best regards from Italy,
dott. Piergiorgio.

2 Likes

I don’t know. Internally most of it’s pretty simple, but I don’t know enough about adv3lite’s turn lifecycle to know whether the hooks used to insert everything into the turn order are the same.

A quick breakdown on the moving parts:

  • The beforeAfter module.
    It creates a singleton and then modifies the Action constructor such that the singleton’s beforeAction() and afterAction() methods will get called every turn.
    The singleton maintains a list of subscribers and notifies them before and after every action.
    The whole module is dead simple, only around a hundred lines of code, but I don’t know if the core mechanism (modifying the Action constructor and using addBeforeAfterObj()) works in adv3lite.
  • The ruleEngine module.
    It defines Rule, Rulebook, and RuleUser. A Rule is an object with a boolean state (true/nil) decided by the return value of a method (matchRule()). A Rulebook is an object with a collection of Rule instances and a boolean state decided by the state of all the Rules. A RuleUser is a object with a collection of Rulebooks.
    Each game defines a RuleEngine instance. It automatically subscribes to notifications via the beforeAfter mechanism described above. In addition it creates a Daemon that runs every turn.
    The RuleEngine keeps track of a list of active Rulebooks. Each turn it evaluates the active Rulebooks (during beforeAction() for action triggers and during afterAction() for non-trigger Rules). Rulebooks whose state is true for the turn have a method (callback() called when the Daemon is polled).
    Most of this logic is independent of adv3, but I don’t know about the specifics of the timing of turn phases versus adv3lite.
  • The stateMachine module.
    It defines StateMachine, State, and Transition. A StateMachine is a collection of State instances, exactly one of which is the current state of the StateMachine at any time. A State is a kind of RuleUser, and a Transition is a Rulebook whose callback() changes the active state of the StateMachine it’s a part of.
    The mechanics of making a state active or inactive are mostly just adding or removing the affected State instance’s Rulebooks from the game’s RuleEngine instance.
    I think the only potential “gotcha” here is that when a Transition attempts to interrupt/replace an turn’s Action it uses exit, which (in adv3) throws a special Exception handled by the main event loop. I don’t know enough about adv3lite to know if the same mechanism works there.
  • The craftingSystem module.
    It defines CraftingSystem, Recipe, RecipeStep (usually used via subclasses, like IngredientList and RecipeAction), and Ingredient.
    These define a very rudimentary grammar which the module “compiles” automagically into StateMachine instances at preinit.
    I don’t think that there are any new hidden “gotchas” in this one, just all the ones above that are included because of the dependencies.

That is: under the hood there’s one basic hook (modifying the Action constructor); that’s used by a couple levels of widgets (beforeAfterController and RuleEngine) to notify lists of subscribers; and then a bunch of simple-but-with-lots-of-steps juggling of who the active subscribers are (via the Transition logic in StateMachine). The craftingSystem module doesn’t change the complexity of any of the stuff just described, it’s just a sort of micro-grammar for specifying linear(-ish) state machines (a Recipe is just a specification for a StateMachine that only has “forward” and “backward” transitions).

3 Likes

Since there’s only really one place where all of this interfaces with the library itself it seems like even if adv3lite didn’t have exactly 1:1 equivalents for that one thing, the porting wouldn’t be too hard, since I can’t imagine it simply wouldn’t be capable of doing something like this. And in fact as it happens, although I don’t know the specific mechanics of the action class in adv3lite, this sounds like precisely how it works, extrapolating from what I do know about how the library is designed, so honestly these should be very easy to port, just adjusting for minor differences. Theoretically someone could just copy paste the beforeAfter library and make a few modifications to the internals for adv3lite while keeping the API the same, and the rest could run seamlessly on top of that. Especially since I do know exit does the same thing in adv3lite.

1 Like

Another thing to consider is if adv3lite provides better interfaces for handling some of the specific bits (like Doer.doInstead() for action replacement). But I’m not familiar enough with adv3lite internals to have any intuition about likely any of that is, or how much work/rewriting it would entail.

2 Likes

There are also a couple additional bits that I’m pretty sure I’m going to end up adding to craftingSystem.

One is a clean way of handling single-step “recipes”. To extend the toaster example, consider buttering toast (or bread). >PUT BUTTER ON TOAST. That’s one step, no intermediate states. A StateMachine really isn’t set up to handle that kind of thing, and it’s much more straightforward to handle it via “normal” TADS3 action handlers (e.g. putting everything in Toast.iobjFor(PutOn) and checking that the gDobj is an instance of Butter, or something like that). But it would be nice to be able to handle everything using the same “recipe” semantics.

And one of the reasons I want to be able to handle this via Recipe mechanics (apart from aesthetic considerations) is that I also want to implement a “recipe learning” mechanism, so that after you “manually” do the recipe for toast, then you can do something like >MAKE TOAST to repeat the process without having to do all the individual steps.

1 Like

Probably an insane idea, but I’m wondering if there’s maybe a layer of macros and class modifications that could be an interface layer between Adv3 modules above and Adv3Lite below… Not sure, though, because Adv3 handles a lot of stuff by setting a large number of properties, and then the class handles the behavior from those, it seems. When I was combing through the Adv3 library reference, it seemed like Adv3 has a lot more moving parts, but less flexibility.

That, or (a more insane idea)…

…create an interface layer for modules that could translate to both Adv3 and Adv3Lite, roflmao. The stack would be:

  1. TADS 3, Adv3, module interface for Adv3, module 1, module 2, etc…
  2. TADS 3 Adv3Lite module interface for Adv3Lite, module 1, module 2, etc…

Mostly because after a few short explorations into Adv3, it feels like cross-compatible modules aren’t really possible, unless there’s an explicit interface there.

EDIT: Really funny that I say this, as I’m making quite an advanced branch off of Adv3Lite. :clown_face:

3 Likes

After a successful porting a ~170k adv3 work into a3lite, I can assert that extras.t and some of the optional extensions are solid glue code, the brunt of the porting becoming mainly replacing the vocabWords and names with vocabs.

The lone major failure was, well, that abuse of Candle class, that is, an abuse so abusive that disgusted fueled.t… but the streamlining of that part of the story should cover that failure (the streamlining is still in process: currently, too many conditions fired simultaneously, leading to a massive wall of text…)

I’m unsure if few lines added to extras.t accomodates the needs of jbg’s libraries, or there will be the need of a more extensive, say, extensions/diegesis.t, but provisionally I estimate that both cases together render 30-50% feasible the support of jbg’s extensions thru adv3lite’s core library, a possibility I think is worth exploring, albeit needs Eric’s approval.

Best regards from Italy,
dott. Piergiorgio.

1 Like

I just pushed a fairly major change to ruleEngine and the (fairly minor) updates necessary to make stateMachine and craftingSystem to work with them.

One is a nomenclature change: the RuleUser class is now RuleSystem. Most code probably doesn’t use this class directly (as opposed to using subclasses, like State from StateMachine), so this probably doesn’t require much code to be re-written (in the unlikely event anyone’s actively writing code using these modules at this point).

The bigger change is that the modules no longer expect for there to be a single, global RuleEngine instance to handle everything. Instead, all of the “stuff” managed by a RuleEngine is expected to be declared on it (via the standard +[object declaration] syntax.

As a side effect of this, every StateMachine instance is now its own RuleEngine. This means that most code written to use the old StateMachine stuff shouldn’t need any modification apart from deleting/commenting out the old RuleEngine declaration. Code that was written using the “pure” ruleEngine interface—that is, using Rulebooks and so on directly—will require minor re-organization to look something like:

// Replacing an old declaration like:
//
//      RuleEngine;
//      Rulebook;
//      +Rule [whatever];
//
// ...with something like:
RuleEngine;
+RuleSystem;
++Rulebook;
+++Rule [whatever];

This re-shuffling is part of preparation for (optionally) having rule stuff automagically confined to specific scopes, more or less purely as a performance/optimization thing (to minimize the number of rulebooks that need to be checked per action/turn).

I think StateMachine will stop being a subclass of RuleEngine (which it is now basically just because that was the easiest way to make the new multi-instance RuleEngine logic work with the existing syntax). The declaration syntax will almost certainly remain the same (so requiring no changes to code using the module), but it will probably create an implicit “all state machine” RuleEngine instance (and craftingSystem will do the same thing because it’s mostly just stateMachine logic under the hood).

But on top of that you’ll be able to optionally specify a specific RuleEngine instance to manage a given StateMachine/Recipe, which will allow automatic enabling/disabling of swaths of rulebooks via normal changes in scope. Basically by allowing crafting system actions to be local to e.g. crafting gear or a crafting room, and so use Thing.beforeAction()/Room.roomBeforeAction(), Thing.afterAction()/Room.roomAfterAction(), and Room.roomDaemon() to handle notifications (instead of the beforeAfter module mechanics), which gets you scope-specificity automagically.

This is all mostly of a concern for large “bottom heavy” crafting systems—rule sets where a there a lot of checks to be made in the “default” state.

3 Likes

As adv3Lite users probably already know, adv3Lite contains its own Rules extension that defines a Rule class and a RuleBook class, so any adv3Lite port of jbg’s work would presumably want to avoid any clashes with or duplications of that.

4 Likes

I’m happy to approve in principle. Of course it may be worth discussing what it is the best way to proceed in practice (to leverage and interface with what’s already in adv3Lite and its built-in extensions).

4 Likes

Indeed my tentative idea is, considering that the majority of these extensions depend on jbg’s rules extensions, the porting of those into a3Lite should be more precisely porting those to extensions/rules.h

Best regards from Italy,
dott. Piergiorgio.

2 Likes

I wasn’t aware that adv3lite had a rule/rulebook implementation…so there’s obviously been no attempt to write things in a way that would make porting straightforward. So it might make sense to implement similar functionality from scratch instead of trying to port the stuff I’ve written.

I had looked for an existing implementation in adv3, but didn’t find anything. The closest thing I found was a contrib module in ifarchive, but it wasn’t really the sort of thing I was looking for. Which is a sort of general rule inference engine.

In terms of the adv3 modules I’m working on now, I’d expect a fair amount of churn in terms of the internal stuff, but the programming interface/API-ish stuff hopefully shouldn’t change that much. In particular I’m anticipating doing a lot of performance and optimization work on the ruleEngine internals (I think I eventually want to build a parse tree/graph of the rules and use it for rulebook evaluation, instead of just using lists of active and inactive rulebooks and rule short-circuiting).

Which I guess I’m offering as a sort of disclaimer to anyone looking to port the code—it’s probably still a moving target. But on the other hand it might make sense to do a work-alike thing instead of a straight port (if you just want the same sort of functionality in adv3lite). Not trying to talk anyone into or out of anything, just trying to do full disclosure and so on. For the record I’m 100% cool with anyone doing whatever they want with any and all code I made public, with or without attribution.

I just wanna make it clear that I’m mostly doing this because I need the functionality for a WIP and I reserve the right to make changes to the stuff in the repos as needed for my own purposes, and that has the potential to pull the rug out from under anything else that’s using the code.

3 Likes

Minor update: location-based rule stuff is done-ish.

In ruleEngine there’s still the base RuleEngine class, which is global: stuff added to a RuleEngine instance is always evaluated, regardless of scope. This is useful for things that always want to update (general world/game state stuff).

Additionally there are new RuleEngineThing and RuleEngineRoom classes to use as mixins for Thing and Room, whose rule stuff is only evaluated when the player (or some other actor taking an action via normal command/action execution) is in scope. This is useful for things like the vending machine and crafting examples, where the actions the rules apply to will only ever apply in specific locations/contexts.

Similarly for stateMachine, the base StateMachine class is global and there are new StateMachineThing and StateMachineRoom classes for scope-limited state machines.

As a bit of implementation chrome, StateMachine has a new statefulObject property, automatically propagated to all of the stateMachine bits (State, Transition, and all Rule/Trigger instances placed in a state machine). This is intended to make it easier to write modular state machine code. To illustrate, the vending machine object in the vending machine example now looks like:

class VendingMachine: Fixture '(pebble) (vending) machine' 'vending machine'
        "The vending machine is implausibly labelled <q>Hot, Fresh Pebbles</q>.
        Below that there's a coin slot and a button. "

        stateMachine = nil

        sign = VendingMachineSign
        slot = VendingMachineSlot
        button = VendingMachineButton

        initializeThing() {
                inherited();
                _initializeVendingMachine();
        }

        _initializeVendingMachine() {
                (sign = new VendingMachineSign).moveInto(self);
                (slot = new VendingMachineSlot).moveInto(self);
                (button = new VendingMachineButton).moveInto(self);
        }
;

…so its components are accessible via the slot and button properties, for example. And then the state machine to control it sets statefulObject to be the instance, and can then write a trigger for >PUSH BUTTON via something like:

+++Trigger
        dstObject = statefulObject.button
        action = PushAction
;

Similarily, transitionAction() and so on can do things like:

        transitionAction() {
                "The coin clatters down into <<statefulObject.theNamePossNoun>>
                        innards and <<statefulObject.button.theName>>
                        lights up. ";
                gDobj.moveInto(nil);
        }

…and so on.

There are also new debugging verbs, >DEBUGRULEENGINES and >DEBUGSTATEMACHINES to display (very verbose) debugging information for ruleEngine and stateMachine, respectively. The former keeps track of the number of rules, rulebooks, and rule systems checked and matched for the past 10 turns. The latter displays the current state of each state machine and which of its rulebooks are currently enabled and disabled and the number of rules in each.

The debugging actions are available when compiled with the -D SYSLOG flag.

2 Likes

A few more updates.

First, the handling of scope-specific rules has changed a little. Instead of using different base classes for different rule scopes (global vs room-based versus Thing-based), there’s a single RuleEngine class again, and you can now specify a ruleScheduler to handle notifications for each rule engine. This can either by via lexical inheritence:

// foo will use myScheduler, and its rules and rulebooks will be
// polled every turn, regardless of scope.
myScheduler: RuleScheduler;
+foo: RuleEngine
     [...]
;

// bar will use kitchen as its scheduler, and its rules and rulebooks
// will only be polled when the player is in the room's scope.
kitchen: RuleSchedulerRoom 'Kitchen'
     [...]
;
+bar: RuleEngine
     [...]
;

// baz will use toaster as its scheduler, and its rules and rulebooks
// will only be polled when the player and the toaster are in the
// same scope.
toaster: RuleSchedulerThing '(metal) toaster' 'toaster'
     [...]
;
+RuleEngine
     [...]
;

Or by defining the rule engine’s ruleScheduler property:

// define a rule engine that's only polled when the player is in the
// kitchen
RuleEngine
     ruleScheduler = kitchen
;

If neither of these methods is used, the rule engine will default to using a global singleton (accessible as gRuleScheduler) as its scheduler, and it will run independent of scope:

// Use the default scheduler, running independent of scope.
RuleEngine;

The craftingLocation inherits these behaviors, but also provides a couple additional convenience features, namely the ability to declare an optional craftingLocation on either the CraftingSystem or on individual Recipes:

// By default, everything in cookingSystem only works in the kitchen.
cookingSystem: CraftingSystem
     craftingLocation = kitchen
;

// The "bread" recipe has no declared craftingLocation, so its rules
// and rulebooks are only checked when the player is in the kitchen.
+Recipe 'bread' @Bread ->oven
     "{You/he} bake{s} some bread. ";
     [...]

// The "toast" recipe declares its craftingLocation to be the toaster,
// so its rules and rulebooks will be checked only when the player
// is in the toaster's scope (independent of whether or not that
// happens to be in the kitchen).
+ Recipe 'toast' @Toast ->toaster
     "{You/he} make{s} some toast. "
     recipeLocation = toaster
;
     [...]

Unrelated to the scope stuff, the basic logic of RuleEngine has been updated to never update if the current turn’s action is a SystemAction. This can be overwritten by instances/subclasses if necessary, but I think this is the most sensible default.

1 Like

One more update. Upon reflection, with a little jiggling of the handle all of the RuleScheduler classes mentioned above (RuleScheduler, RuleSchedulerRoom, and RuleSchedulerThing) have been merged into a single mixin class that will figure out what it’s mixed in with and do the right thing.

This does require adding the other class(-es) in the declaration, but means that you always use the same RuleScheduler class:

kitchen: RuleScheduler, Room 'Kitchen'
     [...]

toaster: RuleScheduler, Thing '(metal) toaster' 'toaster'
     [...]
;

…and so on.

3 Likes

Feature update.

You can now define crafting shortcuts with associated crafting actions. This involves adding a RecipeShortcut to the corresponding Recipe declaration, and defining a CraftingAction to serve as the shortcut verb.

Extending the earlier toaster example, we can define a >MAKE action:

DefineCraftingAction(Make);
VerbRule(Make)
        'make' singleDobj : MakeAction
        verbPhrase = 'make/making (what)'
;

…and then update the toast recipe declaration…

// This was already part of the recipe
+Recipe 'toast' @Toast ->toaster "The toaster produces a slice of toast. ";
// This is the new part
++RecipeShortcut 'toast' 'toast' ->MakeAction
        "{You/He} make{s} some toast. "
        cantCraftRecipeUnknown = '{You/He} do{es}n\'t know how to toast
                bread, amazingly. '
;
// Rest of recipe is the same

We’ll cover the syntax later, but this gives us a recipe that the player doesn’t initially know:

Void
This is a featureless void.

You see a toaster here.

>make toast
You don't know how to toast bread, amazingly.

But if the player makes the recipe “by hand” once, they can then use >MAKE TOAST as a shortcut subsequently:

Void
This is a featureless void.

You see a toaster here.

>put bread in toaster
You put the bread in the toaster.

>turn toaster on
You start the toaster.  The toaster produces a slice of toast.

>take toast
Taken.

>i
You are carrying a slice of bread, a pat of butter, and a slice of toast.

>make toast
You make some toast.

>l
Void
This is a featureless void.

You see a toaster (which contains a slice of toast) here.

>i
You are carrying a pat of butter and a slice of toast.

Looking at the shortcut declaration again:

++RecipeShortcut 'toast' 'toast' ->MakeAction
        "{You/He} make{s} some toast. "
        cantCraftRecipeUnknown = '{You/He} do{es}n\'t know how to toast
                bread, amazingly. '
;

The syntax is:

  • The RecipeShortcut keyword
  • Two single-quoted strings. These are used as the vocabWords and name for an Unthing to use with the shortcut. The format is exactly the same as in a normal Thing declaration. The Unthing is there to be a gDobj for the crafting action when a “real” instance of the thing to be crafted isn’t present. That is, an Unthing to be the “toast” in >MAKE TOAST when there’s no other “toast” object in scope.
  • The literal -> followed by the crafting action to use, in this case MakeAction.
  • A double-quoted string to display when the shortcut is (successfully) used. The normal output from the recipe being completed is suppressed. So in this case “The toaster produces a slice of toast” would be output when the recipe was completed “normally”, but only “You make some toast” will be displayed when using the shortcut.
  • Optional action messages. There are a number of crafting-specific action messages (all defined in the playerActionMessages section of craftingSystemAction.t). In order of preference the module will use an action message defined on the shortcut, on the recipe, or on playerActionMessages. In this case when the player tries to >MAKE TOAST without knowing the recipe they’ll get “You don’t know how to toast bread, amazinging” instead of the default “You can’t make that” message.

By default, no recipes are known at the start of the game and all recipes are automatically learned when the player successfully completes them. You can make recipes known at the start of the game by declaring startKnown = true on the Recipe declaration. There’s no mechanism for disabling auto-learning of recipes on completion, but auto-learning by itself doesn’t do anything unless a shortcut is explicitly declared on the recipe.

By default a shortcut requires:

  • The player has all of the ingredients in the recipe in their inventory
  • The player can touch any gear listed in the recipe, including the resultLocation for the Recipe (that’s ->toaster in the Recipe declaration in this example) and any CraftingGear instances enumerated in the recipe rules (none in this example)

The shortcut will then remove the required ingredients from the player’s inventory when it is executed. If there are multiple valid ingredients, they’ll be selected in arbitrary order.

The recipe result will end up in the resultLocation defined in the Recipe, ignoring any containment requirements on the container (you can stack multiple Toast instances in the toaster this way in the demo as written).

If any of the above isn’t what you want, you can implement your own shortcut logic by supplying a recipeAction() method in the RecipeShortcut declaration. By default, a shortcut’s recipeAction() is:

        recipeAction() {
                recipe.consumeIngredients(gActor);
                recipe.produceResult(true);
                shortcutAction();
        }

…where recipe.consumeIngredients() tells the recipe to remove the recipe’s ingredients from the given actor’s inventory; recipe.produceResult() tells the recipe to produce its result (the arg being a flag to tell it to do so silently); and shortcutAction(), which is the double-quoted string that’s displayed when the shortcut is executed (“You make some toast” in this example)…which can also be a full method instead of “just” a double-quoted string.

The biggest known issue about this at this time is that IngredientActions aren’t handled correctly in shortcuts yet.

2 Likes

A couple of more notes on shortcuts:

  • By default a CraftingAction uses two preconditions: canCraftHere and canCraft
  • canCraftHere is only used if the action’s craftingLocation property is non-nil, in which case the shortcut’s _verifyCraftingLocation() method will be called with the craftingLocation as its argument. By default RecipeShortcut._verifyCraftingLocation() will just verify that gActor is in the given location. Additional checks can be added to the shortcut by declaring a verifyCraftingLocation() method, which will be called from _verifyCraftingLocation() before any other checks.
  • canCraft calls the shortcut’s _verifyShortcut() method. Similar to the above, the shortcut can declare a verifyShortcut() method that will be called before any other checks. By default, _verifyShortcut() will check that the actor knows the recipe, that they can touch all the required gear, and that they have all the required ingredients in their inventory.

The shortcut-specific crafting messages are:

  • cantCraftHere failure message displayed when the action has a craftingLocation defined and the player attempts to use a shortcut when not in that location
  • cantCraftRecipeUnknown failure message displayed when the player attempts to use a shortcut for a recipe they don’t know
  • cantCraftThat generic failure message
  • cantCraftMissingIngredients(lst) failure message displayed when the player attempts to use a recipe shortcut and doesn’t have all the ingredients in their inventory. The argument is a List containing the missing ingredients.
  • cantCraftMissingGear(lst) failure message displayed when the player attempts to use a recipe shortcut and can’t touch all of the gear required by the recipe. The argument is a List containing the missing gear.

Each of the above messages is declared by default on playerActionMessages, and overrides can be defined on Recipe and RecipeShortcut.

2 Likes