Actions

Define the Rules of your World

Actions are the rules of your World. They empower developers to define, validate, and reward player actions within the World. Actions can be used to configure the rules of your World canister (aka your game server) from scratch without having to write a single line of code.

It's an optional framework, giving developers the freedom to use the no-code tool to configure Actions or instead write their own custom World canister code. This flexibility allows game devs to tailor the gaming experience to their vision.

Actions come baked into the World Template. Go to the Game Launcher website to deploy a World Template and configure Actions directly on the website in a few clicks!


How does it work?

Actions are pre-defined rules that players must follow in your game.

Example: If a player spends 100 Gold they receive 1 Sword

As you can see, Actions are the backbone of your game economy.

Action

This is what an Action looks like in code:

public type Action = {
    aid : Text;
    name : ?Text;
    description : ?Text;
    imageUrl : ?Text;
    tag : ?Text;
    actionPlugin : ?ActionPlugin;
    actionConstraint : ?ActionConstraint;
    actionResult : ActionResult;
};
  • aid: The action id that identifies this specific action

  • name: The optional name of this action

  • description: The optional description of this action

  • imageUrl: An optional image you want to show related to this action

  • tag: The optional tag that identifies this action so the client knows how to display it

  • actionPlugin: An optional plugin that holds custom config data and can run custom logic

  • actionConstraint: The optional requirements that must be validated before doing this action

  • actionResult: The result of this action which is made up of many possible outcomes

ActionPlugin

You can specify an optional plugin with custom config data and custom logic by using an ActionPlugin. The World Template comes with default plugins, but you can write your own or import plugins from other projects. Here are the default plugins:

public type ActionPlugin = 
{
    #verifyBurnNfts : { canister: Text; requiredNftMetadata : ?[Text]; };
    #verifyTransferIcp : { amt: Float; toPrincipal : Text; };
    #verifyTransferIcrc : {canister: Text; amt: Float; toPrincipal : Text; };
    #claimStakingRewardNft : { canister: Text; requiredAmount : Nat; };
    #claimStakingRewardIcp : { requiredAmount : Float;  };
    #claimStakingRewardIcrc : { canister: Text; requiredAmount : Float; };
};

ActionConstraint

You can specify optional requirements to do an Action by defining ActionConstraints:

public type ActionConstraint = {
    timeConstraint : ?{
        intervalDuration : Nat;
        actionsPerInterval : Nat;
    };
    entityConstraint : ?[{
        wid : ?TGlobal.worldId;
        gid : TGlobal.groupId;
        eid : TGlobal.entityId;
        fieldName : Text;
        validation : {
            #greaterThanNumber : Float;
            #lessThanNumber : Float;
            #greaterThanEqualToNumber : Float;
            #lessThanEqualToNumber : Float;
            #equalToNumber : Float;
            #equalToString : Text;
            #greaterThanNowTimestamp;
            #lessThanNowTimestamp;
        };
    }];
};
  • timeConstraint: Defines how many times the player can do the action in a certain time interval.

    eg. Collect gems 4 times every 24 hours. Or 1 time every 6 hours. etc.

  • entityConstraint: Defines a list of Entity requirements the player must have in order to do the action. An Entity constraint can even check Entities in another World!

    eg. Have greater than 100 Gold in Plethora to buy a Sword in Cubetopia. Or have a Luck buff that isn't expired to do a lucky dice roll. Or have a Stat with attribute that equals "Race Master" to play a secret racing map.

ActionResult

You can specify the resulting outcomes of an Action by defining the ActionResult. An ActionResult has a list of ActionOutcomes. Each ActionOutcome has a list of possibleOutcomes, meaning you can specify the likelihood of each possible ActionOutcomeOption being chosen as the ActionOutcome:

public type ActionResult = {
    outcomes: [ActionOutcome];
};

public type ActionOutcome = {
    possibleOutcomes: [ActionOutcomeOption];
};

public type ActionOutcomeOption = {
    weight : Float;
    option : {
        #mintToken : MintToken;
        #mintNft : MintNft;
        #deleteEntity : DeleteEntity;
        #renewTimestamp : RenewTimestamp;
        #setString : SetString;
        #setNumber : SetNumber;
        #decrementNumber : DecrementNumber;
        #incrementNumber : IncrementNumber;
    };
};

What is weight?

You'll notice each ActionOutcomeOption has a weight field. The weight field for each potential outcome determines its probability of being chosen. A higher weight means a higher chance of manifesting that outcome.

For example, if there are two outcomes, one with a weight of 1 and the other with a weight of 9, the latter is nine times more likely to occur, as the weights reflect their proportions in the total pool of outcomes.

If there is only one ActionOutcomeOption in the list of possibleOutcomes, it will have a 100% chance of occurring.


Using Actions to Mint NFTs and ICRC tokens

You'll notice there are fields called MintToken and MintNft in the ActionOutcomeOption. These enable you to set a probability of Tokens or NFTs being minted as an outcome. The World canister does the minting by calling the NFT canister.

Your World canister must be an admin of your NFT or ICRC canister in order to handle the minting, you can easily create NFT or ICRC collections and add admins using the Game Launcher website.

Here's what the MintToken and MintNft fields look like:

public type MintToken = 
{
    quantity : Float;
    canister : Text;
};
public type MintNft = 
{
    index : ? Nat32;
    canister : Text;
    assetId: Text;
    metadata: Text;
};

Integration with Unity

The process of integrating the Action System with your Unity game is simple. When a player wants to do an Action, call this function in your World canister:

processAction(actionArg: ActionArg)

You'll notice it takes an ActionArg parameter that gives the World more context about the Action the player wants to call. Here are examples of ActionArgs:

public type ActionArg = 
{
    #default : {actionId: Text; };
    #verifyBurnNfts : {actionId: Text; indexes: [Nat32]; };
    #verifyTransferIcp : {actionId: Text; blockIndex: Nat64; };
    #verifyTransferIcrc : {actionId: Text; blockIndex: Nat; };
    #claimStakingRewardNft : {actionId: Text; };
    #claimStakingRewardIcp : {actionId: Text; };
    #claimStakingRewardIcrc : {actionId: Text; };
};

The World canister will then lookup the ActionConfig using the actionId that is passed in ActionArg, and check for any custom ActionPlugins like verifying the ICP ledger or verifying an NFT was burned.

Then the World will call the processAction() function in UserNode:

processAction(uid : Text, aid : Text, actionConstraint : ?ActionConstraint, outcomes : [ActionOutcomeOption])

This function in UserNode will handle validating the ActionConstraint, processing the ActionResult outcomes, and apply those outcomes to their respective Entities.

processAction() will return the result as an array of Entities that were affected:

( [Entity], Text )

Then the World canister will read the tuple and handle minting any Tokens and NFTs if necessary.


Watch the Tutorial Video

Learn how to configure Actions in your World canister from the Game Launcher

Last updated