Step 13: Custom Actions

What This Version Teaches

Custom actions let you add entirely new verbs to your game. When stdlib doesn't have the verb you need, you create your own action with the four-phase pattern.

Key Concepts

The Action Interface

Every action implements four phases:

const feedAction: Action = {
  id: 'zoo.action.feeding',
  group: 'interaction',

  // Phase 1: Can the action proceed?
  validate(context: ActionContext): ValidationResult {
    if (!hasRequiredItem) return { valid: false, error: 'no_item' };
    return { valid: true };
  },

  // Phase 2: Mutate the world (called only if valid)
  execute(context: ActionContext): void {
    context.world.setStateValue('item-used', true);
  },

  // Phase 3: Generate success events (text output)
  report(context: ActionContext): ISemanticEvent[] {
    return [context.event('action.success', { messageId: 'success_msg' })];
  },

  // Phase 4: Generate failure events (called only if invalid)
  blocked(context: ActionContext, result: ValidationResult): ISemanticEvent[] {
    return [context.event('action.blocked', { messageId: result.error })];
  },
};

Registering Custom Actions

Return your actions from getCustomActions():

getCustomActions(): any[] {
  return [feedAction, photographAction];
}

Grammar Extension

Teach the parser your new verbs in extendParser():

extendParser(parser: Parser): void {
  const grammar = parser.getStoryGrammar();
  grammar.define('feed :thing').mapsTo('zoo.action.feeding').withPriority(150).build();
  grammar.define('photograph :thing').mapsTo('zoo.action.photographing').withPriority(150).build();
}

Language Extension

Register message text in extendLanguage():

extendLanguage(language: LanguageProvider): void {
  language.addMessage('zoo.feeding.fed_goats', 'The goats eat the feed happily!');
  language.addMessage('zoo.photo.no_camera', "You don't have a camera.");
}

Passing Data Between Phases

Use context.sharedData to pass data from validate to execute/report:

validate(context) {
  context.sharedData.target = someEntity;
  return { valid: true };
},
execute(context) {
  const target = context.sharedData.target;
  // use target...
}

Commands to Try

> south / east / take feed   (get the feed)
> feed goats                 (feed the goats!)
> feed goats                 (already fed)
> photograph toucan          (no camera — blocked)
> west / west / west         (go to gift shop)
> take camera                (get the camera)
> photograph postcards       (Click! Photo taken)

Key Takeaways

The Code

See src/v13.ts for the complete, commented source.