Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
While the Item contract has full support for Merklized data structures in the backend(as yet untested), Player does not. In the interests of speed and time, adding this feature was left for future development, if and only if the developer community demands it. As of this writing, we are not fully confident such a feature set is necessary for Players
or PlayerClasses
.
Here's a pseudo-code rendering of the PlayerClass state with all the structs merged together so that you can see it "all in one".
PlayerClass
is cut into two major areas: The settings and the config. The settings mostly covers who may do what to either the class or its children. There is even a permissiveness (child_update_propagation_permissiveness
) that covers which permissions and data propagate to children!
The config focuses on what most devs classically recognize as the "definition" of a Player
. You can define BodyParts
, BasicStats
, which in their blueprint form are called BasicStatTemplates
, and the starting URI for each Player
in this class. Players
will inherit this information as their starting state.
You can describe a starting URI as some third party link, similar to the URI in Token Metadata. Maybe it's JSON, maybe it's an image, it can be anything you like. All instantiated Player
from this PlayerClass
will start with it, and it should contain richer data about the Player
that cannot be contained within BodyParts
or BasicStats
. It works via 'write-on-change' so when you make an update to Player
, you can change it to a new URI that represents a small shift away from the common starting URI all Players
of the same class share.
Any change to a PlayerClass
can be permissionlessly propagated to all Players
via a hit to the update_player endpoint on the Player
contract for each Player
. So feel free to add a new body part to the Warrior class if you need it!
Here is the Player state:
The Player
state is much smaller than the PlayerClass
and is sort of the yin to the class's yang - it contains only the stateful pieces of information that vary from Player
to Player
. For instance, there is an array of equipped items, and for each BasicStatTemplate
, a corresponding BasicStat
that represents the current state of that Stat.
You'll also notice that, as with Items, it's possible for the PlayerClass
and Player
to be either entirely separate NFTs or indeed, the SAME NFT! Due to the index system used with the PDA seeds, you could have the same NFT have a class defined at index 0, and its instance defined at index 1. This would be the Singleton pattern for those of you familiar with OO programming styles.
All PlayerClasses
define an optional vec of BasicStatTemplates
that will map 1:1 to BasicStats
in Players
when instantiated. The Template defines what kind of stat it is, as well as its name, and the actual BasicStat
contains the current state for that stat. An example BasicStat
is "health," which might have a min/max of 0/100, and could be 50 on one Player
and 60 on another.
You'll notice the use of index on both structs. This is to assist in matching across both Player
and PlayerClass
in a more stable way than name
.
The meat of a BasicStatTemplate
is the BasicStatType
, defined below:
It is an enum that defines all the information a Player
would need to know to govern a stat of a given type as that stat moves in value over time. The actual state, called a BasicStatState
, is defined thusly:
With most of the data types the state is only pointing at the current value. The tricky part comes for Integers, which need to keep track of a bunch of intermediate values to track how items, both equipped and used, change the stat over time. When the contract "does math," it uses both structs in memory to figure out where to position the current
value next, however your Player
PDA is much smaller than the PlayerClass
it inherits from because it doesn't need to keep track of mins, maxes, or all possible values.
If you need a more complex type of data field than those provided here, the stats URI provided to every Player
by its PlayerClass
may be the right fit for you. You can store any type of data you want there, but it's up to you to handle changes to it and logic with it.
Let's create a PlayerClass
, and then, for giggles, we'll have it inherit to another PlayerClass
and then we'll cover updating it.
A Player
in Raindrops is the primary primitive of the entire protocol. Just like Item
, it follows a class-inheritance system and has permissions for different functionalities. Every PlayerClass
represents a "blueprint" NFT that could be used to turn another NFT into a Player
, and once done, that NFT gains certain privileges, such as:
The right to store items and other players in their internal backpack, removing those NFTs/SFTs from your wallet, making for easy transport across your wallets. Never worry about transporting your player + 10 pieces of equipment from hot wallet to hot wallet again!
The right to equip items on various body parts
The right to have body parts!
The right to have different attributes of different data types that can change over time from outside stimuli, all on chain
The right to use items on oneself or on others per recipes encoded on chain
The underlying PlayerClass
for any Player
has permissions for different functionalities that control what a holder may do, such as:
Equipping, adding, or using items
Updating one's stats
Building new Players
Staking
We’ll cover PlayerClass
first, and then look at Player
to see how they connect, and then move on to some Live Fire exercises.
Every Player
may have BodyParts
if it so chooses. In the PlayerClass
, they are defined as:
On the actual Player
there is no corresponding state, as the only stateful part of a BodyPart
is what is equipped on it, and this is contained in the equipped_items
array. You'll notice that each BodyPart
has its own index - this, like BasicStats
, is a required unique field, to help disambiguate parts.
It's possible for a BodyPart
to equip more than one item (see total_item_spots
).
Let's Subclass the Parent with this JSON file:
The key thing to really note here is the parent block in the JSON,
If you note from the prior section, this mint and index is the mint and index used to create the parent class. By referencing it in the JSON file here for the child class, we are telling the contract to consider this class a child. This also means that when we choose an updatePermissivenessToUse
for creation here, we're actually using a permissiveness on the parent, not the child. So that tokenHolder is actually referencing the parent token, not the child. This is a nice way to get around needing to own updateAuthority on a specific NFT if you don't have it but want to make a game with it.
To create the subclass, run the same command as before:
The resulting show_player_class
command will yield the following:
The clever among you will probably be wondering where the BasicStatTemplates
from the parent class are. If it had had BasicStatTemplates
in the childUpdatePropagationPermissivenessArray
, it would have! In this case, we didn't add it in the previous section, so this child will only have the two stats it defines for itself, but if you try this example with that minor tweak to the parent, you can see the inheritance work.
However, we did set the propagation up for BodyParts
, and this resultant class does have both a Leg and an Arm.
First, let's start with a standard JSON file for a PlayerClass
. As you can see, you don't need to set every field, most are optional. In fact, a lot of the fields we are setting in this example here are optional, but this is a working example so you can play with it as needed.
There's a lot going on here. To skip understanding it and just see how it can be made to "go," let's just run this command:
With this, a PDA has been made on top of your NFT. Check it out with the show_player_class
command:
This produces:
It is not itself a Player
but a blueprint for one. If you want to use it in games, you'll actually need to either instantiate a Player
from this class, or, as we'll do in this example, subclass this PlayerClass
one PlayerClass
further, and then instantiate a Player
. So now this NFT represents a blueprint for an Abstract Warrior. We plan to have a bunch of different Warrior classes, let's move on and subclass it to make a more specific warrior called Concrete Warrior in the next section.
If you're interested in what is going on in the JSON file above, please take a look at this "Breaking down the Magic" section below.
With Players
and PlayerClasses
, you can specify how big you'd like them to be but be aware that if they aren't big enough (totalSpaceBytes
) you run into serialization errors when you equip too many items, for instance. So size appropriately for your game. You can also decide whether you want to store the mint and metadata and edition pubkeys on the PDAs, which take up a lot of space but help with indexing by your front-ends using storeMint
and storeMetadataFields
. If you yourself are not the metadataUpdateAuthority
, make sure to set it. This is a Token Metadata concept from Metaplex - meaning you have the right to update the NFT.
When creating a PlayerClass
, the contract will always use updateAuthority
even if you put, as we did above in updatePermissivenessToUse
, tokenHolder
. The only way it will not use updateAuthority
when creating a class on top of an NFT is if the PlayerClass
being created has a parent
field set in the JSON, meaning it will be inheriting from some other PlayerClass
. In that case, the updatePermissivenessToUse
you chose (in this case, tokenHolder
) is passed along to the parent
, so you'd be trying to prove you have a right to create this PlayerClass
because you are holding the token of the parent PlayerClass
and that PlayerClass
has in it's update_permissiveness
array an entry for tokenHolder
, allowing for such a permission to be used on it.
If you want to have a parent
, parent
can fit the format, if not null, of:
Also take a look at this garbage:
What is all this? What this mess of JSON is saying that any child class inherits all BodyParts
from the parent, and it's defaultCategory
, but nothing else. This system of selective inheritance allows you to decide what parts of your classes filter down to children and what don't.
The overridable
field, when present, determines whether or not the child's values will be completely overridden if present.
So for arrays like BodyParts
, if the parent has a Head and a Neck in its initial JSON file and the child has an Arm in its JSON file, and overridable
is false, then the resultant child will have a Head, Neck, and Arm. If overridable
is true, then the child, despite being created with an Arm in its JSON file, will end up with only a Head and a Neck during the creation call. For single-value types like defaultCategory
, what overridable
= true means is that even if you try to create a child class with an explicit value for defaultCategory
in the initial JSON, it will be ignored, whereas if you try to set it in the initial JSON when overridable
is false, it will be overwrite what was in the parent.
Basically, overridable
true indicates that the parent
crushes everything beneath it, and overridable
false means that the parent
will step aside for the child
if the child has a better or additional idea of what a value should be.