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:
- Trigger encounters when players walk in tall grass
- Spawn monsters into the encounter
- Allow players to capture monster
- 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 encounterencounter_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.
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
.
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.
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.
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!)
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.
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
.
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:
- A component that designates whether or not a user has captured an monster.
- A new method to throw balls and catch monster.
- A way to represent the result of a catch attempt.
- 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.
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
.
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.
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.
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.
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!
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.