Engine
Docs
3. A wild Emojimon appears

3. A wild Monster appears

To bring this all together we will now add the ability to generate encounters on tall grass in which the user can either capture the monster or flee the encounter.

Adding encounters and all of their functionality will serve as a review of all the concepts we've learned so far: creating schemas (i.e. components), creating and calling systems, optimistic rendering in the client, and client and contract queries.

In order to do so we will add the following features:

  1. Trigger encounters when players walk in tall grass
  2. Spawn monsters into the encounter
  3. Allow players to capture monster
  4. Allow players to flee encounters

Before continuing, try figuring out what components and systems would need to be added to get the build all these features. You could even try building them—we've already taught you all that is needed (and you can view the gif in the Introduction as a reference).

3.1. Enable tall grass to trigger encounters

Let's start by adding three new schemas to mud.config.ts.

  • encounterable → to determine whether or not an entity can engage in an encounter
  • encounter_trigger → to determine whether or not an entity can trigger an encounter when moved on by a player.
  • encounter → to associate a player with an encounter.
obelisk.config.ts
import { ObeliskConfig } from '@0xobelisk/common';
 
export default obeliskConfig = {
    name: 'constantinople',
    description: 'constantinople',
    systems: ['map_system', 'encounter_system'],
    schemas: {
        map: {
            valueType: {
                width: 'u64',
                height: 'u64',
                terrain: 'vector<vector<u8>>',
            },
            defaultValue: {
                width: 32,
                height: 27,
                terrain: [
                [0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80,80],
                [0, 0, 0, 0, 0, 0, 81, 81, 81, 81, 81, 81, 81, 0, 0, 0, 135, 136, 137, 138, 139, 0, 0, 81, 81, 81, 81, 81,81, 81, 81, 81],
                [0, 0, 0, 0, 0, 0, 22, 22, 0, 20, 20, 20, 20, 20, 20, 0, 140, 141, 142, 143, 144, 0, 0, 0, 0, 0, 0, 0, 0, 080, 80],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 145, 146, 147, 148, 149, 0, 0, 0, 0, 83, 83, 22, 0, 0,81, 81],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 150, 151, 152, 153, 154, 0, 0, 0, 0, 83, 83, 22, 0, 0,80, 80,],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 155, 156, 157, 158, 159, 161, 0, 0, 0, 0, 0, 0, 0, 0,81, 81],
                [83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [80, 80, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 22, 0, 0, 0, 0, 100, 101, 102, 103, 104, 105, 106,0, 0, 81, 81],
                [81, 81, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 107, 108, 109, 110, 111, 112, 113, 0, 0, 80, 80],
                [80, 80, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 115, 116, 117, 118, 119, 120, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 30, 31, 32, 83, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 121, 122, 123, 124, 125, 126, 127, 0, 0, 80, 80],
                [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 128, 129, 130, 131, 132, 133, 134, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 22, 72, 22, 0, 0, 0, 80, 0, 22, 22, 22, 161, 23, 0, 0, 0, 0, 80, 80 ],
                [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 22, 74, 73, 75, 22, 0, 0, 81, 0, 22, 22, 22, 0, 0, 0, 0, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 83, 20, 30, 31, 32, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 83 ],
                [80, 80, 83, 0, 0, 0, 83, 33, 34, 34, 31, 31, 31, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,31, 31, 32, 83],
                [81, 81, 83, 0, 0, 0, 83, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 43],
                [83, 83, 83, 0, 0, 0, 83, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 34, 34, 34, 37, 37, 37, 37, 37, 37, 38, 83],
                [22, 83, 0, 0, 0, 0, 83, 83, 83, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 33, 34, 35, 20, 0, 0, 0, 0, 0, 83, 83],
                [22, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 20, 36, 37, 38, 20, 80, 0, 0, 0, 0, 80, 80],
                [22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 60, 61, 62, 0, 81, 0, 0, 0, 0, 81, 81],
                [80, 80, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 0, 63, 64, 65, 0, 80, 0, 0, 22, 0, 80, 80],
                [81, 81, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 66, 67, 68, 0, 81, 0, 0, 22, 0, 81, 81],
                [80, 80, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [81, 81, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 80, 0, 80, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81, 81],
                [80, 80, 22, 0, 0, 0, 0, 0, 83, 83, 0, 0, 81, 0, 81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [81, 81, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81,81],
                ],
            },
        },
        obstruction: 'bool',
        movable: "bool",
        player: "bool",
        encounter: {
            valueType: {
                exists: 'bool',
                monster: 'address',
                catch_attempts: 'u64',
            },
        },
        encounter_trigger: 'bool',
        encounterable: 'bool',
        position: {
            valueType: {
                x: "u64",
                y: "u64",
            },
        },
}
} as ObeliskConfig;

We then have to make sure that players and tall grass are receiving these components properly.

First let's make sure the client is being initialized properly in deploy_hook.move.

deploy_hook.move
module constantinople::deploy_hook {
    use constantinople::world::{World, AdminCap, get_admin};
    use sui::object;
    use constantinople::map_schema;
    use std::vector;
    use constantinople::entity_key;
    use constantinople::encounter_trigger_schema;
    use constantinople::obstruction_schema;
    use constantinople::position_schema;

    /// Not the right admin for this world
    const ENotAdmin: u64 = 0;

    public entry fun run(world: &mut World, admin_cap: &AdminCap) {
        assert!( get_admin(world) == object::id(admin_cap), ENotAdmin);

        // Logic that needs to be automated once the contract is deployed
        let (width, height, terrain) = map_schema::get(world);

        let y = 0;
        while (y < height) {
            let x = 0;
            while (x < width) {
                let value = *vector::borrow(vector::borrow(&terrain, y), x);
                let entity = entity_key::from_position(x, y);
                if (value == 20) {
                    position_schema::set(world, entity, x, y);
                    encounter_trigger_schema::set(world, entity, true);
                };
                if (value >= 40) {
                    position_schema::set(world, entity, x, y);
                    obstruction_schema::set(world, entity, true);
                };
                x = x + 1;
            };
            y = y + 1;
        };
    }

    #[test_only]
    public fun deploy_hook_for_testing(world: &mut World, admin_cap: &AdminCap){
        run(world, admin_cap)
    }
}

Then let's update the register method in map_system.sol to include the encounterable schema/component.

map_system.move
    module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
        encounterable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}

Now that tall grass is an encounter trigger, we can query for an encounter trigger as we move to a new position. We'll update the map_system to handle this and also stub out a startEncounter for starting an encounter and spawning a monster.

At this point we would ideally like to implement an element of randomness for triggering encounters in tall grass. However, due to the deterministic nature of blockchains and MoveVM applications, true randomness is not currently possible. For the sake of this tutorial we will be leaving this as deterministic.

map_system.move
module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
        encounterable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, clock: &Clock, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);

        if (encounterable_schema::contains(world, player) && encounter_trigger_schema::contains(world, position)) {
            // Pass in the time as a random number
            let rand = clock::timestamp_ms(clock);
            if (rand % 2 == 0) {
                // TODO: Start encounter
            };
        };
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}

Now that we have all of the encounter logic setup we just want to take the last step of preventing movement while a player is in an encounter—this will be a modification of the move method (you should know where this is by now!)

map_system.move
module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
        encounterable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, clock: &Clock, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        // error cannot move during an encounter
        assert!(!encounter_schema::contains(world, player), ECannotMoveInEncounter);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);

        if (encounterable_schema::contains(world, player) && encounter_trigger_schema::contains(world, position)) {
            // Pass in the time as a random number
            let rand = clock::timestamp_ms(clock);
            if (rand % 2 == 0) {
            // TODO: Start encounter
            };
        };
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}

3.2. Start encounter and spawn a monster

We're almost ready start an encounter.

map_system.move
    module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
        encounterable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, clock: &Clock, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        // error cannot move during an encounter
        assert!(!encounter_schema::contains(world, player), ECannotMoveInEncounter);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);

        if (encounterable_schema::contains(world, player) && encounter_trigger_schema::contains(world, position)) {
            // Pass in the time as a random number
            let rand = clock::timestamp_ms(clock);
            if (rand % 2 == 0) {
                // Generate Monster
                let monster = entity_key::from_u256((rand as u256));
                // Start encounter
                encounter_schema::set(world, player, true, monster, 0);
            };
        };
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}

Then let’s query to see if the player is in an encounter and, if so, get the monster type and render the EncounterScreen. Add the code below to GameBoard.tsx.

Map.tsx
const obelisk = new Obelisk({
    networkType: NETWORK,
    packageId: PACKAGE_ID,
    metadata: contractMetadata,
});
 
let player_data = await obelisk.getEntity(WORLD_ID, 'position', obelisk.getAddress());
 
const encounter_contain = await obelisk.containEntity(WORLD_ID, 'encounter', obelisk.getAddress());

You did it! You are now able to move, start encounters, and see monster in said encounters.

With this screen setup there are two more steps to go—enabling the player to capture monster and flee the encounter. Let’s keep going!

3.3. Capture monster

What would an monster encounter be without throwing balls?

In order to have a proper capture system we will need a few new additions:

  1. A component that designates whether or not a user has captured an monster.
  2. A new method to throw balls and catch monster.
  3. A way to represent the result of a catch attempt.
  4. Showing this interaction in the client.

The first step is modifying the Obelisk config to add the necessary schemas.

owned_monsters will use a vector<address> because we use this for representing entity IDs, so one entity can own another entity by having an owned_monsters component that points to the owner entity ID.

We also need a way to represent the catch attempt. We’ll add a catch_result with the different types of results of a catch attempt (missed, caught, fled).

We’ll add catch_result as an ephemeral schema to broadcast the catch attempt to clients without storing any data on chain. This will allow the client to understand these interactions and render/animate them accordingly. You can think of ephemeral schemas like native Move events but with the same structure and encoding as regular schemas.

Go ahead and add both of these to the Obelisk config.

obelisk.config.ts
import { ObeliskConfig } from '@0xobelisk/common';
 
export default obeliskConfig = {
    name: 'constantinople',
    description: 'constantinople',
    systems: ['map_system', 'encounter_system'],
    schemas: {
        map: {
            valueType: {
                width: 'u64',
                height: 'u64',
                terrain: 'vector<vector<u8>>',
            },
            defaultValue: {
                width: 32,
                height: 27,
                terrain: [
                    [0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80,80],
                    [0, 0, 0, 0, 0, 0, 81, 81, 81, 81, 81, 81, 81, 0, 0, 0, 135, 136, 137, 138, 139, 0, 0, 81, 81, 81, 81, 81,81, 81, 81, 81],
                    [0, 0, 0, 0, 0, 0, 22, 22, 0, 20, 20, 20, 20, 20, 20, 0, 140, 141, 142, 143, 144, 0, 0, 0, 0, 0, 0, 0, 0, 080, 80],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 145, 146, 147, 148, 149, 0, 0, 0, 0, 83, 83, 22, 0, 0,81, 81],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 150, 151, 152, 153, 154, 0, 0, 0, 0, 83, 83, 22, 0, 0,80, 80,],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 155, 156, 157, 158, 159, 161, 0, 0, 0, 0, 0, 0, 0, 0,81, 81],
                    [83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [80, 80, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 22, 0, 0, 0, 0, 100, 101, 102, 103, 104, 105, 106,0, 0, 81, 81],
                    [81, 81, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 107, 108, 109, 110, 111, 112, 113, 0, 0, 80, 80],
                    [80, 80, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 115, 116, 117, 118, 119, 120, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 30, 31, 32, 83, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 121, 122, 123, 124, 125, 126, 127, 0, 0, 80, 80],
                    [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 128, 129, 130, 131, 132, 133, 134, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 22, 72, 22, 0, 0, 0, 80, 0, 22, 22, 22, 161, 23, 0, 0, 0, 0, 80, 80 ],
                    [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 22, 74, 73, 75, 22, 0, 0, 81, 0, 22, 22, 22, 0, 0, 0, 0, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 83, 20, 30, 31, 32, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 83 ],
                    [80, 80, 83, 0, 0, 0, 83, 33, 34, 34, 31, 31, 31, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,31, 31, 32, 83],
                    [81, 81, 83, 0, 0, 0, 83, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 43],
                    [83, 83, 83, 0, 0, 0, 83, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 34, 34, 34, 37, 37, 37, 37, 37, 37, 38, 83],
                    [22, 83, 0, 0, 0, 0, 83, 83, 83, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 33, 34, 35, 20, 0, 0, 0, 0, 0, 83, 83],
                    [22, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 20, 36, 37, 38, 20, 80, 0, 0, 0, 0, 80, 80],
                    [22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 60, 61, 62, 0, 81, 0, 0, 0, 0, 81, 81],
                    [80, 80, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 0, 63, 64, 65, 0, 80, 0, 0, 22, 0, 80, 80],
                    [81, 81, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 66, 67, 68, 0, 81, 0, 0, 22, 0, 81, 81],
                    [80, 80, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [81, 81, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 80, 0, 80, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81, 81],
                    [80, 80, 22, 0, 0, 0, 0, 0, 83, 83, 0, 0, 81, 0, 81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [81, 81, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81,81],
                ],
            },
        },
        obstruction: 'bool',
        movable: "bool",
        player: "bool",
        encounter: {
            valueType: {
                exists: 'bool',
                monster: 'address',
                catch_attempts: 'u64',
            },
        },
        encounter_trigger: 'bool',
        encounterable: 'bool',
        catch_result: {
            ephemeral: true,
            valueType: 'u8',
        },
        position: {
            valueType: {
                x: "u64",
                y: "u64",
            },
        },
}} as ObeliskConfig;

Next we’ll implement a way for the player to throw an ball and capture the monster. map_system.sol is getting crowded, and is concerned with logic that affects the map, so we can start up a new system here. Let’s call it encounter_system.sol and add the first method, throw_ball.

encounter_system.move
module constantinople::encounter_system {
    use sui::tx_context::TxContext;
    use constantinople::world::World;
    use sui::clock::Clock;

    const Caught: u8 = 0;
    const Fled: u8 = 1;
    const Missed: u8 = 2;

    public entry fun throw_ball(world: &mut World, clock: &Clock, ctx: &mut TxContext) {
        // TODO:
    }
}

But wait—we also want the monster to be able to escape if the fail throws multiple times, just like in Pokémon. This is where the actionCount on our encounter schema comes in. We’ll use that to store how many attempts we’ve made and cause the monster to flee if we’ve made too many attempts.

encounter_system.move
module constantinople::encounter_system {
    use sui::tx_context::TxContext;
    use constantinople::world::World;
    use sui::tx_context;
    use std::vector;
    use constantinople::catch_result_schema;
    use constantinople::encounter_schema;
    use constantinople::owned_monsters_schema;
    use sui::clock::Clock;
    use sui::clock;

    const Caught: u8 = 0;
    const Fled: u8 = 1;
    const Missed: u8 = 2;

    /// error not in encounter
    const ENotInEcounter: u64 = 0;

    public entry fun throw_ball(world: &mut World, clock: &Clock, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        // error not in encounter
        assert!(encounter_schema::contains(world, player), ENotInEcounter);

        let (_, monster, catch_attempts) = encounter_schema::get(world, player);
        // Pass in the time as a random number
        let rand = clock::timestamp_ms(clock);
        if (rand % 2 == 0) {
        // 50% chance to catch monster
        if(owned_monsters_schema::contains(world, player)) {
            let owned_monsters = owned_monsters_schema::get(world, player);
            vector::push_back(&mut owned_monsters, monster);
            owned_monsters_schema::set(world, player, owned_monsters);
        } else {
            owned_monsters_schema::set(world, player, vector[monster]);
        };
            encounter_schema::remove(world, player);
            catch_result_schema::emit_catch_result(Caught);
        } else if (catch_attempts >= 2) {
            // Missed 2 times, monster escapes
            encounter_schema::remove(world, player);
            catch_result_schema::emit_catch_result(Fled);
        } else {
            // Throw missed!
            encounter_schema::set_catch_attempts(world, player, catch_attempts + 1);
            catch_result_schema::emit_catch_result(Missed);
        }
    }
}

The encounter screen already has a “Throw” button displayed, we just need to wire it up to our client-side system calls.

PVPModal.ts
const obelisk = new Obelisk({
    networkType: NETWORK,
    packageId: PACKAGE_ID,
    metadata: contractMetadata
});
 
let tx = new TransactionBlock();
let params = [tx.pure(WORLD_ID), tx.pure('0x6')];
(await obelisk.tx.encounter_system.throw_ball(tx, params, undefined, true)) as TransactionResult;
const response = await wallet.signAndExecuteTransactionBlock({
    transactionBlock: tx,
    options: {
    showEffects: true,
    showObjectChanges: true,
},
});
console.log(response);

When you click the button, the EncounterScreen creates a pending toast and kicks off the transaction by calling our throwBall method above. We use awaitStreamValue to ensure the transaction went through and Obelisk has updated the client component data. After that, we can get the result of the catch attempt and return it so that the EncounterScreen can render the proper message in the toast.

3.4. Flee encounters

Last but not least, players should be able to flee encounters. We can add this with a flee method in encounter_system.move as well. To keep it simple we’ll guarantee that the player can run away safely.

encounter_system.move
module constantinople::encounter_system {
    use sui::tx_context::TxContext;
    use constantinople::world::World;
    use sui::tx_context;
    use std::vector;
    use constantinople::catch_result_schema;
    use constantinople::encounter_schema;
    use constantinople::owned_monsters_schema;
    use sui::clock::Clock;
    use sui::clock;

    const Caught: u8 = 0;
    const Fled: u8 = 1;
    const Missed: u8 = 2;

    /// error not in encounter
    const ENotInEcounter: u64 = 0;

    public entry fun throw_ball(world: &mut World, clock: &Clock, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        // error not in encounter
        assert!(encounter_schema::contains(world, player), ENotInEcounter);

        let (_, monster, catch_attempts) = encounter_schema::get(world, player);
        // Pass in the time as a random number
        let rand = clock::timestamp_ms(clock);
        if (rand % 2 == 0) {
        // 50% chance to catch monster
        if(owned_monsters_schema::contains(world, player)) {
            let owned_monsters = owned_monsters_schema::get(world, player);
            vector::push_back(&mut owned_monsters, monster);
            owned_monsters_schema::set(world, player, owned_monsters);
        } else {
            owned_monsters_schema::set(world, player, vector[monster]);
        };
            encounter_schema::remove(world, player);
            catch_result_schema::emit_catch_result(Caught);
        } else if (catch_attempts >= 2) {
            // Missed 2 times, monster escapes
            encounter_schema::remove(world, player);
            catch_result_schema::emit_catch_result(Fled);
        } else {
            // Throw missed!
            encounter_schema::set_catch_attempts(world, player, catch_attempts + 1);
            catch_result_schema::emit_catch_result(Missed);
        }
    }

    public fun flee(world: &mut World, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        // error not in encounter
        assert!(encounter_schema::contains(world, player), ENotInEcounter);

        encounter_schema::remove(world, player);
    }
}

Since our flee system always allows you to run away, we technically don't need to listen for system call updates to determine the outcome. But doing so will help our UI and toasts stay in sync with component updates.

Because the encounter screen is shown only when you're in an encounter, you'll see that it will automatically disappear when you run away. This is the nice thing about Obelisk and declarative, responsive UI!

createSystemCalls.ts
const obelisk = new Obelisk({
    networkType: NETWORK,
    packageId: PACKAGE_ID,
    metadata: contractMetadata,
});
 
let tx = new TransactionBlock();
let params = [tx.pure(WORLD_ID)];
(await obelisk.tx.encounter_system.flee(tx, params, undefined, true)) as TransactionResult;
 
const response = await wallet.signAndExecuteTransactionBlock({
    transactionBlock: tx,
    options: {
    showEffects: true,
    showObjectChanges: true,
},
});
console.log(response);

You could probably add optimistic rendering here, but we’ll skip that for now.