Introduction
Welcome to Alacrity Book, an introductory guide to using the Alacrity library.
Alacrity is a powerful open-source engine to track and manage game entities, their attributes and the buffs that affects them. Alacrity has been designed to facilitate the development of buffs and other systems in video game servers or clients. It can be used as a standalone service that game servers or clients can query, or embedded directly into a game application as a crate. This flexibility enables Alacrity to be integrated with other languages or game engines.
It's important to note that Alacrity does not provide any service integration or
ready-to-use service code. Instead, it is up to users to create their own wrappers around
Alacrity types and systems to build their own services using its functionality. Alacrity
provides a Propagation type that enables the propagation of buffs to entities. Users can
use this type to build their systems as they see fit to meet their own specific needs.
Who Alacrity Is For
Alacrity is ideal for game (or game server) developers or game designers that want to leverage complex buffs and effects configuration and calculations in their projects using a powerful third-party library. There are a variety of reasons why Alacrity could be attractive to both video game development teams and independent video game developers.
Alacrity is Open Source
Alacrity is released under permissive dual licensing MIT or Apache 2 at your choice, making it an accessible tool for any developer who wants to use it.
Who This Book Is For
This book assumes that you have prior experience with writing Rust code and some familiarity with the Lua programming language. No prior knowledge of video game design or development is required to follow this book, although some experience in these areas would be highly beneficial.
Alacrity embeds a Luau interpreter that can be exposed and used to interact with its API, if you're not familiar with Luau, take a look at their documentation in the link above.
How to Use This Book
This book can be used both as a guide and as a reference. You can refer to any section or chapter in the book at any time, but it's generally recommended to read this book in sequence from front to back. Later chapters build upon concepts presented in earlier ones, while earlier ones may not delve deeply into the concepts they introduce, as they are revisited in later sections or chapters.
If you find yourself wondering about a specific concept or topic in a later chapter, try going back to the Alacrity Concepts chapter to refresh your memory.
Source Code
The source code used to generate this book can be found on the SourceHut Forge.
Compiling the Examples
Generating UUIDs
UUIDs are an important part of Alacrity as they are used to relate entities to each other as well as relate buffs with entities, modifiers and conditions with buffs etc.
The most common way to generate an UUID is to generate a version 4 UUID
that is -a fully randomly generated UUID- this can be done using the
rust [uuid] crate's new_v4() function or by any other means like for
example, using the uuidgen command line tool on UNIX (linux, BSD, MacOS...).
Creating random UUIDs is the most secure and easy solution and usually you will have no problems while using completely random UUIDs as buff's IDs. But the thing gets a bit more complicated when one needs to also map these IDs to some game objects that might use just numeric IDs or any other kind of identification mechanism, this is usually more problematic when trying to relate game objects to Alacrity entities, we offer some suggestions in the sections below that could provide an acceptable solution for your own use cases.
Options for Entity UUIDs mapping and storing
Different games will have different needs. If you are starting a new game from scratch and you are using Alacrity, then you might use UUIDs for your in-game objects IDs and just use them to create your Alacrity entities while if you are integrating Alacrity in your already established game you might not be able to use UUIDs to identify your game objects for whatever reason.
Just using UUIDs for all of your IDs
This is the most simple and out of the box solution, just use UUIDs for all of your game objects IDs, then you can just use the same IDs to refer to your Alacrity entities and buffs. How does this works in a real scenario?
So, for example, let's say that a new player comes through your character's
creation system, after they have configured their character selecting
whatever options that are offered to them, the system would proceed to make
a new instance of the player's character game object and persist it. At that
specific point, the system would generate a new random v4 UUID and provide
that same UUID to Alacrity when instantiating a new Entity that represents
that player's attributes and state.
This way, when you need to extract any data from your Alacrity entity in regards of the player's character you could just extract it from whatever storage solution or persistence layer you are using to store Alacrity's data using the character's game object ID as they would match.
Store the Alacrity data into the Game Object
You can also store all of the alacrity data into your game objects, this way you can use whatever system to identify your game objects and you will not care at all about which random IDs you are using for your Alacrity entity instances as you can just reach that information directly from with-in your game object directly from its allocated memory.
This is also an easy approach but that comes with some warnings. If you plan to make use of Alacrity's buffs propagation system, then you will need to put some entity to entity communication layer in top of alacrity, how this layer is implemented is up to you and your game needs. It can be just an in memory instances communication approach or a fully TCP/IP networking solution, it all will depend on your use case and implementation. If you decide to store your entities data directly into your game objects, then your game objects will need to implement a way to listen for Alacrity entity updates and that could complicate your game objects logic unnecessarily.
If you are creating a game on which buffs does not affect each other in any way then this is a perfectly safe solution that is very easy to implement and to work with.
Map your IDs and persist them
A common solution is to just create a key value map where you can just relate your game objects one-to-one to specific Alacrity entities. This can be done with an external key value storage like [redis], [JetStreams KV], [Consul KV] for example, or can be stored in plan text files or any other key value storage solution that persist its data to the local file system.
Follow a deterministic procedure to translate Game Object IDs to Alacrity IDs
This is one of the preferred methods because it does not requires of any additional key value storage system or to hardly couple Alacrity entities to anything else in your game, basically, this can be achieved using UUID version 5 also known as UUID namespace or name-based.
The RFC 4122 lists four different name space IDs that are usually available and ready to use in any UUID library implementation, those are:
NameSpace_DNS- Name string is a fully-qualified domain name, its ID is6ba7b810-9dad-11d1-80b4-00c04fd430c8NameSpace_URL- Name string is a URL, its ID is6ba7b811-9dad-11d1-80b4-00c04fd430c8NameSpace_OID- Name string is an ISO OID, its ID is6ba7b812-9dad-11d1-80b4-00c04fd430c8NameSpace_X500- Name string is an X.500 Domain Name in DER or text output format, its ID is6ba7b814-9dad-11d1-80b4-00c04fd430c8
An example of how to create a new deterministic UUID from a game object that uses sequential integer IDs for identification purposes using the DNS namespace using the Alacrity's Lua API would looks like this
local uuid = alacrity.uuid
-- assume gameObject has been created elsewhere and has an `id` field
local entity_id = uuid.new_v5(uuid.namespace_dns, "com.my_game.game_objects." .. tostring(gameObject.id))
The code above would always generate the same UUID output for a given
gameObject.id so no need map and store the map would be necessary, we could
lookup Alacrity entities in whatever registry or storage we use to persist them
just using the UUID returned by the new_v5 function.
Of course we do not have to use an already defined namespace for this, we could also create our own namespaces and use those instead. We could generate a new random UUID for our namespace and hardcode it or pass it as a config parameter or any other solution and then use that instead.
For example, lets assume that we have hardcoded a Namespace_GameObject with
a value 8c2d4d36-4635-54d9-80d8-d957ba0fb224, we could just modify the example
above and it would produce a consistent and deterministic result always that we
used the same namespace with the game object ID
local uuid = alacrity.uuid
-- assume gameObject and Namespace_GameObject are defined elsewhere
local entity_id = uuid.new_v5(Namespace_GameObject, tostring(gameObject.id))
We could even have different namespaces to refer to entities related to the given game object but that differs in their context. For example, let's imagine that we design our game to have a hero character, a weapon and a relic. Let's also imagine that we model each of these game objects to own their own Alacrity entity. We could define three different namespaces for our game like:
Namespace_Hero = "4bdcd274-7841-48fd-9290-524b96dbe524"
Namespace_Weapon = "a73a6fcd-f499-4c3b-a781-c30d76234d0f"
Namespace_Relic = "e046b816-d6e1-487e-adee-3936120698ca"
Then, we could just use those to refer different Alacrity entities just using the id of our character.
local uuid = alacrity.uuid
---@param gameObjectID integer @ID of the Game Object owning the entity
---@param nameSpace string @NameSpace to determine the entity ID
---@return string
local function resolveEntityID(gameObjectID, namespace)
local entity_id = uuid.new_v5(namespace, tostring(gameObjectID))
return entity_id
end
-- get the hero entity ID (assume gameObject has been defined elsewhere)
local heroEntityID = resolveEntityID(gameObject.id, Namespace_Hero)
-- get the hero weapon entity ID (assume gameObject has been defined elsewhere)
local weaponEntityID = resolveEntityID(gameObject.id, Namespace_Weapon)
-- get the hero relic entity ID (assume gameObject has been defined elsewhere)
local relicEntityID = resolveEntityID(gameObject.id, Namespace_Relic)
The code above would generate always the same three different IDs in a fully deterministic way. Using this technique we can refer to a myriad of Alacrity entity IDs using a single game object ID
Alacrity Concepts
In this guide, we will cover the core concepts of Alacrity that you will need to understand in order to work with it effectively.
The following sections provide a first approximation to what each of the Alacrity concepts are about.
- Entity — Entity represents an entity in the simulation
- Attributes — Attributes define a buff's modifiable value that holds the entity's state
- Properties — Properties define non buff's modifiable value that holds arbitrary data
- Values — Values represent dynamic types that can be used in a property or attribute
- Buff — A collection of attribute modifiers, rules and conditions that can be applied to entities
- Conditions — Boolean expressions that must be met for a buff or modifier to be applied
- Modifiers — Representation of a value (or values) that will modify one (or more) attributes
- Propagation Maps — Defines the set of rules a buff must met in order to propagate itself
- Tags — A key-value pair that can be attached to entities
Entity
In Alacrity, An entity represents any kind of abstract object within a simulation. It can be a character, an object, or any other kind of abstraction that could contain collections of attributes that can be track and affected by buff's modifiers. Entities have attributes, which are modifiable values that represent the entity's state, and properties, which are non-modifiable values that hold arbitrary data for the entity.
🚨 The term "modifiable" in the paragraph above makes reference to "being modifiable by buff's modifiers".
It's important to note that while attributes can be modified by buffs, properties cannot be modified directly by buffs. However, both attributes and properties can be used in conditional statements to check for certain conditions or requirements.
Entities can have buffs attached to them, which modify their attributes according to a set of rules and conditions defined by the buff. While attributes can be modified directly by buff's modifiers, properties can only be modified through the library's API. The Entities section of the documentation provides more information on working with entities in Alacrity, including how to define attributes, properties, values, and buffs.
Attributes
In Alacrity, an entity's attributes define the modifiable values that hold the
entity's state. The attribute values are dynamic types that can hold i64 or
f64 types, depending on the specific attribute. Attributes can be affected by
buffs modifiers, which modify their values according to a set of rules and
conditions defined by the buff.
Unlike properties, which are non-modifiable values that hold arbitrary data for
the entity, attributes can only hold numerical values, and are restricted to
i64 or f64 value types. However, this does not limit their flexibility,
as numerical values can represent a wide range of states and conditions for an
entity.
It's important to note that while attributes can not be directly mutated, they can be modified through buffs modifiers. This means that buffs can affect the values of an entity's attributes, based on conditions that you define in the buff. Buffs can add or subtract values, set values to specific amounts, or apply complex calculations to change the values of attributes.
The Attributes section of the Alacrity documentation provides more information on how to define attributes, their types, and how to use them in combination with buffs to create complex systems for video games.
Properties
Non-modifiable values associated with an entity that can hold arbitrary data. Properties differ from attributes in that they cannot be directly modified by buffs, but can be set and retrieved using the Alacrity APIs.
Values stored in properties can be of type i64, f64, String, or bool.
Properties can be useful for storing metadata about an entity that doesn't
affect its state, such as a display name, extended bio, or any other kind of
metadata.
One important feature of properties is that they can be used in conjunction with attributes and buffs to create more complex systems. For example, a buff might only apply to an entity if it has a certain property set to a specific value.
The Properties section of the Alacrity documentation provides more information on how to work with properties in Alacrity, including how to set and retrieve property values and how to use properties in conjunction with attributes and buffs to create complex systems.
Values
Values are immutable types that can hold a data of any of the following types;
i64, f64, String, bool. The type of the data that a Value holds into
its known as "the kind" of the Value. The kind of a value is set when a value
is instantiated at compile time.
Values are used by both Properties and Attributes they can be transform from
and into their supported kinds and can be represented as different types if
required. For example, a Value of i64 kind can be represented as a float value
if required, the Value makes the conversion safely and automatically for us.
Values can be used both in Alacrity's Rust and Lua APIs and can be passed from Rust to Lua and from Lua to Rust.
Overall, Value instances are a simple but essential building block of Alacrity's data model, allowing for flexible data representation.
Buffs
Buffs are an essential component of Alacrity, enabling dynamic modifications of entity attributes. A buff is a set of rules that determine how an entity's attributes should be modified while the buff is active.
A buff can be triggered in a variety of ways, such as when a player casts a spell or when a particular event occurs in the game. Once a buff is triggered, it can modify one or more attributes of the entity for a specified duration, until specific conditions are met or until the buff is manually deactivated.
Buffs can also define modifiers, which represents a value (or values) that will modify one or more of the attributes of an entity when a buff is applied to it. It is a way to define the actual change that should happen to the attribute's value.
Modifiers can be either additive or multiplicative. Additive modifiers add a fixed amount to the attribute's value, while multiplicative modifiers multiply the attribute's value by a fixed amount. Buffs can have multiple modifiers, which can modify different attributes of an entity in different ways.
In Alacrity, buffs can be defined using Lua scripts, which allows developers to define complex behaviors and conditions in a flexible and extensible way. The Buffs section of the documentation provides more information on working with buffs in Alacrity, including how to define buffs, modifiers, and conditions, and how to attach buffs to entities.
Modifiers
In Alacrity, a Modifier is a representation of a value (or values) that will modify one or more of the attributes of an entity when a buff is applied to it. Essentially, a modifier is a rule or condition that changes how an entity's attributes are affected by a buff.
Modifiers are defined within the context of a Buff, which is a collection of modifiers that can be applied to an entity. When a buff is applied to an entity, each of its modifiers is evaluated and applied to the appropriate attributes of the entity based on its conditions.
Modifiers can have various effects, such as increasing or decreasing an attribute's value, setting it to a specific value, or even applying a mathematical formula to it. Modifiers can also be conditional, meaning they only apply under certain circumstances, such as when the entity is in a certain state or has a certain status effect.
Overall, modifiers play a crucial role in determining how buffs affect entities in Alacrity, and understanding how to define and use them is key to creating effective and balanced buffs. The Modifiers section of the documentation extends greatly this information.
🚨 Modifiers are the bread and butter of Alacrity's buffs system, they are the most complex component of Alacrity, make sure to dedicate them the time that they deserve to fully understand how they work.
Conditions
In Alacrity, conditions are a powerful tool that allow you to specify when buffs should be applied to entities. Conditions are expressions that evaluate to either true or false, and can be used to check the values of an entity's attributes or properties, as well as other conditions such as the time of day or the presence of other entities.
When a buff is applied to an entity, its conditions are evaluated in order. If all conditions evaluate to true, the buff is applied to the entity; otherwise, it is skipped. Conditions can be used to create complex buff systems that respond dynamically to changes in the simulation.
Conditions can be expressed using the Lua programming language, which allows for a high degree of expressiveness and flexibility. You can also use built-in conditional functions provided by the Alacrity library, which provide convenient shortcuts for common conditional checks.
The Conditions section of the documentation provides more information on working with conditions in Alacrity, including how to write conditional expressions in Lua, and how to use the built-in conditional functions.
Propagation Maps
Propagation Maps are used in Alacrity to define how a buff will be propagated to other entities. When a buff is applied to an entity, it may need to be propagated to other entities in the game. Propagation Maps define the rules for how this propagation should occur.
Propagation Maps are immutable, meaning that they should not be changed after they are created. The Propagation struct includes several fields that define how the propagation should occur. These fields include the type of propagation (recursive, direct, or none), the tags to which the buff will be propagated, the tags that the propagation can traverse through (if any), and any conditions that must be met for the buff to be propagated.
Propagation Maps can be defined using the Rust programming language, and they can also be used in Lua scripts. They are a powerful tool for defining complex propagation rules that can be used to create dynamic and interesting gameplay mechanics.
Tags
In the Alacrity system, a tag is a key-value pair that can be attached to entities. The purpose of a tag is to define a numeric or string value that can be used to filter entities in queries or group them.
A tag consists of a name and a value. The name is a string that identifies
the tag, while the value can be one of the following data types: i64,
or string. The int version of the value is used on bitwise operations,
so it can represent just 64 different kind of values.
Tags can be attached to entities, and once attached, they can be used to filter or group entities. For example, entities can be grouped by their tag values, or they can be filtered based on their tag values. Tags can also be used in the definition of propagation maps, allowing buffs to propagate only to entities with certain tag values.
In Alacrity, the Tag type represents a tag, and it includes the name and value of the tag. Tags can be created and manipulated in both Rust and Lua APIs, and can be passed from Rust to Lua and from Lua to Rust. Refer to the Tags section to know more about how to use the tagging system.
Alacrity Specs
Alacrity provides several ways to create entities, buffs, modifiers, and conditions in a programmatic fashion using the Rust or Lua APIs. While this is useful for creating ad-hoc effects, there is a better way to create game specs that are loaded into the application binary at compilation time.
The following sections provide all the required information you need to know about Alacrity specs:
- Specs Path — Learn how to set up the Alacrity's Specs Path
- Templates — Learn about Alacrity's Protobuf templates
- Lua Tables — Learn how to define specs through Lua tables
Specs Path
When creating an entity in Alacrity, the library searches for the entity
definition in a set of specs tables that are embedded into your binary or
library during compilation. To achieve this, you must provide the absolute
path to where your specs definitions are located in the file system as the
value of the ALACRITY_SPECS_PATH environment variable before compiling the
sources.
🚨 It is important to note that you should only use absolute paths, as using relative paths may result in the specs not being added to the binary. This is because the Rust compiler cannot guarantee that the relative path would be accessible during the compilation phase. For more information, refer to the once_cell crate documentation..
Alacrity will lookup new entities or buffs that you try to instantiate automatically on them.
Templates
TBD
Lua Tables (to define Specs)
Alacrity allows game designers and developers to define game entities using declarative syntax in Lua. This means that game entities are defined using tables that describe their properties and behavior, rather than being hard-coded into the game's source code.
To define an entity using declarative syntax, start by creating a Lua table that contains the entity's properties. The table should have the following keys:
blueprint_id: A unique identifier for the entity in the specs.
id: An optional ID to give to any instance of this blueprint (unless explicitely override while calling `new`)
name: The name of the entity.
description: A description of the entity.
tags: A table of tags that describe the entity. Tags are used to classify entities and group them together.
attributes: A table of attributes that describe the entity's state. Attributes can be used to represent the entity's physical characteristics, such as its strength, speed or any other that should be modified by buffs.
properties: A table of properties that describe the entity's properties. Properties can be used to represent the entity's type, level, or any other property that shouldn't be modified by buffs.
buffs: A table of buffs that affect the entity. Buffs modify the entity's attributes or behavior in some way.
version: The version number of the entity definition.
Each of these keys should have a value that reflects the entity's properties. For example, the attributes key should have a table of attributes that describe the entity's sate, such as its speed or strength. Each attribute should have a name and a value.
Similarly, the properties key should have a table of properties that describe the entity's properties, such as its type or level. Each property should have a name and a value.
The tags key should have a table of tags that describe the entity. Each tag should have a name and a value.
The buffs key should have a table of buffs that affect the entity. Each buff can be either a string representing the unique identifier of a predefined buff (that is, a buff defined in the specs), or a table that describes a new embedded buff (that is, a buff that will only exist on this entity instance and doesn't exists in the specs themselves). In the case of
an embedded buffs, each one of the tables should have the following keys:
id: A unique identifier for the buff.
name: The name of the buff.
description: A description of the buff.
effect_id: The identifier of the effect that the buff applies.
modifiers: A table of attribute modifiers that the buff applies.
conditions: A table of conditions that must be met for the buff to be applied.
effect_stack_policy: The policy for stacking effects of the buff.
effect_stack_limit: The maximum number of stacked effects for the buff.
expiration: The duration of the buff in seconds. Set to 0 for a permanent buff.
stack_policy: The policy for stacking buffs.
stack_limit: The maximum number of stacked buffs.
version: The version number of the buff definition.
Once you have defined the entity using declarative syntax, you can use it in your game by just by instantiting it using its unique ID. You can then manipulate the entity and its properties using the Alacrity engine's Rust or Lua APIs.
Do no worry much about the details for now, we will extend this information in great detail in later chapters.
Alacrity Entities
In the realm of game development, managing entities is a fundamental task. Entities are the core building blocks that breathe life into your game world. In Alacrity library, entities take center stage, and understanding them is crucial for crafting immersive gaming experiences. This chapter delves deep into Alacrity entities, dissecting their structure and exploring their significance in game development.
Unveiling the Entity
At its core, an entity represents a game object within your virtual world. A player a weapon or an enemy can be modeled as an Alacrity entity. These entities are not mere static elements; they are dynamic evolving entities that can be create, updated and even destroyed as your game unfolds.
Entities serve as a canvas upon which you paint the rich tapestry of your game's interactions, attributes and behaviors. Each entity is defined by a set of properties, attributes, tags and buffs.
Anatomy of an Entity
Entities are composed of several fields that define their properties and behavior in the simulation.
| Field | Type | Mandatory? |
|---|---|---|
| id | UUID | yes |
| blueprint | String | no |
| name | String | yes |
| description | String | yes |
| tags | HashMap<String, Tag> | no |
| attributes | HashMap<String, Attribute> | yes |
| properties | HashMap<String, Property> | no |
| buffs | HashMap<Uuid, Buff> | no |
| version | String | no |
We will review all of this fields in detail below
Identification and Description
ID
Each entity has a unique ID that identifies it within the game world. IDs are UUIDs and they must be guaranteed to be unique. Users of the library are responsible from creating and providing these unique IDs, if you need guidance about how to properly achieve that take a look to the Generating UUIDs section.
An entity ID looks like this:
38a29473-1678-46b2-a7a8-ba9df7621faf
Blueprint
When an entity is instantiated from a template or blueprint, this field will contain the ID that refers to said template or blueprint.
Name
This is a human-readable name for the entity that can be used for display purposes, debugging, and other similar.
Description
A brief description of the entity, which can be used for documentation, tooltips and other similar purposes.
Version
The entity's version. It can be used to track entities versions or to update or migrate entities data when a version change is detected. Each entity blueprint has to be in an specific version (0.0.1 by default).
Tags: Classifying Entities
Tags are key-value pairs that provide additional information about the entity. They can be used to categorize entities, associate them with specific game systems or mechanics, or provide other types of metadata. They are also used by the buff's propagation system to determine if a buff must propagate from an entity to another one by looking at specific tags on propagation maps rules.
Tags values can be of any supported type by Alacrity's Value type
🚨 tags with i64 values are used in bitwise operations. That means that the only valid values they can hold are powers of 2 (1, 2, 4, 8, 16, 32...) so basically they can hold up to 64 different values. Use these as masks for bitwise comparisons
Some examples of tags could be
| Tag Key | Tag Value |
|---|---|
| controlled_by | player |
| type | enemy |
| faction | 161 |
1: This value could be assigned to a constant like FACTION_CAMELOT 1 << 4
Attributes: Defining Behavior
Attributes holds numerical values that represent the state of an entity. They are modified by buffs and are calculated to provide the entity with its final attribute's state values. Attributes can not be mutated trough any other means, they are completely immutable from the API perspective. They are always taken into account on buffs and buff's modifiers condition checks, you can not opt out them from being passed into the entity's scope when a condition is being evaluated. We will extend about this topic in the conditions section in later chapters.
Examples of attributes might include the following
| Attribute Name | Attribute Value | Type |
|---|---|---|
| health | 90 | i64 |
| speed | 200.0 | f64 |
| damage_multiplier | 1.5 | f64 |
Properties: Storing Information
Properties holds numerical, string or boolean values that represent properties or non modifiable (by buffs) state of an entity. Properties are used to store data that is less frequently accessed and/or modified during gameplay. Properties are not modified by buffs and/or buff's modifiers but they can be managed, including update, creation and deletion using both Rust and Lua APIs. Properties are taken into account for conditions checks but it can be opt out.
Examples of properties might include
| Property Name | Property Value | Type |
|---|---|---|
| color | blue | String |
| level | 10 | i64 |
| is_aggressive | true | bool |
Buffs: The Effects of Change
The "buffs" field on an entity represents the collections of buffs that can be applied to the entity it is attached to. Buffs are temporary effects that modify the attributes of an entity. They can enhance or weaken the entity's stats or abilities, alter its behavior, or introduce specific conditions for its actions.
Example of a buff spec blueprint to be used with an entity:
{
id = "5a0b8a00-ed2d-4120-839a-595df4b7fe30", -- Unique ID for the buff (all instances of this buff will share this ID)
name = "hero_strength_boost_01_001_a", -- Name of the buff
description = "Increase the entity's strength attribute by 10%", -- Human friendly description of the buff
effect_id = 65536, -- Mask for the effect associated with the buff (base 2 value for masking)
-- Modifiers define the changes to apply to the entity's attributes
modifiers = {
name = "str_base_10_percent_multiplier", -- Modifier name
description = "Multiplies the target's strength base attribute by 10%", -- Modifier description
max_stacks = 1, -- The modifier's max number of possible stacks
kind = "default", -- The modifier's kind or behavior
value = 0.1, -- The modifier's value or strength
operator = "percent", -- The modifier's operator (can be flat, percent, flatbase, percentbase, flatcalculated or percentcalculated)
-- Modifier's conditions that must need to be met for the modifier to be applied
conditions = {
{
description = "Entity class must be warrior, berserk or paladin", -- Condition's human friendly description
expression = "self.class == 'warrior' or self.class == 'berserk' or self.class == 'paladin'", -- Conditional expression
logical_operator = true, -- The outcome of the above expression must be true for the condition to be met
}
},
},
-- Conditions define the conditions that need to be met in order to activate the buff in the entity
conditions = {
{
description = "Entity level must be equal or superior than 5", -- Condition human friendly description
expression = "self.level >= 5", -- Conditional expression
logical_operator = true, -- The outcome of the above expression must be true for the condition to pass
}
},
effect_stack_policy = "no_stack", -- Impedes the buff to stack with other buffs that shares the same effect_id mask
effect_stack_limit = 1, -- Stack limit for the effect (if it could be stacked up)
expiration = 0, -- Duration of the buff in seconds (0 for permanent)
stack_policy = "no_stack", -- Stack policy for modifiers of this buff (modifiers can honor this or override with their own policies)
stack_limit = 1, -- The limit of stacks for modifiers of this buff (can also be override by modifiers)
}
Buffs are the bread and butter of Alacrity, they will be presented in great detail in the buffs section in later chapters.
Alacrity Entities in Action
Alacrity entities are not stagnant entities; they are brought to life through interactions, modification and application of various game mechanics. In the chapters that follow, we will explore how to create, modify and manage entities using both programmatic and declarative approaches.
Tagging Entities
Introduction
In Alacrity, entity tags play a crucial role in identifying and categorizing entities within a simulation or game. Tags are used to label entities based on specific characteristics, properties, or roles they possess. They provide a flexible and efficient way to filter, group, and apply actions or effects to specific subsets of entities.
Understanding Entity Tags
Entity tags are lightweight metadata attached to entities. They consist of a name-value pair that describes a certain aspect of an entity. The name represents the category or type of tag, while the value provides further details or specific identification within that category.
Tags serve as a powerful mechanism to query and manipulate entities based on common traits, relationships, or conditions. They can be utilized in various scenarios, such as filtering entities for specific behaviors, targeting entities for buffs or debuffs, applying game rules based on entity aspects, and much more.
Tags are immutable, once a tag is attached to an entity it can not be modified in any way. If one
desires to "update" a tag value, the tag must be re-attached to the entity passing the boolean
argument replace as true in both rust and lua APIs to the method add_tags.
🚨 Warning
modifying tags in a live entity could trigger buffs activations or deactivations, initiate complex and expensive buff propagation maps or trigger other behaviors that might have been setup in the game or simulation, be wary of the possible connotations of mutating tags data in live entities.
Simple Tags
Tags that doesn't contain a value are known as "simple", they are regularly used to identify the entities for some kind of filtering or grouping. This is the most common case of tagging, and it is widely used by the buff's propagation system to known to which entities a buff must be propagated or not.
Complex Tags
Tags that contains a value are known as "complex", they can be used for the same purposes as their simple counterpart but they can carry additional metadata in their value. They are also used by the buff's propagation system to apply extra conditional behavior to buffs propagations. For example a buff propagation can contain a rule that allow the buff to propagate to tags of an specific tag key but that also have an specific value like:
{
name = "humanoid", value = "centaur"
}
In the example above, the buff would only propagate to entities that contain an humanoid tag with
a value of centaur.
Different ways of tagging entities
Entities can be tagged both using the Rust or Lua APIs or tags can be included in the entity's blueprint or template definitions. The tags field in a blueprint or template contains an array of tag objects, where each object represents a specific tag associated with the entity. Each tag object consist of a name-value pair that defines the category and value of the tag.
Example using Lua declarative syntax
Here is an example of tagging entities on their definition using the blueprint's Lua declarative syntax:
local entities = {
-- Entity Dark Paladin PC
{
blueprint_id = "PC_DARK_PALADIN",
name = "pc_dark_paladin",
description = "A playable Dark Paladin Entity",
tags = {
{ name = "entity_type", value = "playable_character" },
{ name = "archetype", value = "paladin" },
{ name = "alignment", value = "evil" },
},
attributes = {
{ name = "str", value = 10 },
{ name = "cha", value = 16 },
{ name = "dex", value = 2 },
{ name = "con", vale = 18 },
{ name = "int", value = 2 },
{ name = "wis", value = 1 },
},
properties = {
{ name = "level", value = 1 },
{ name = "race", value = "human" },
}
buffs = {},
version = "0.1.0"
},
-- More entities --
}
return entities
In this example, an entity is tagged using the "entity_type" category that defines the kind of entity this entity is, a tag for the "archetype" category that defines the character's archetype or class, and a final tag for the "alignment" category that identifies the character as "evil".
The tag category can be customized based on specific context and requirements of your game or simulation.
ℹ️ Info
note that currently, tags are not accessible from buffs and/or buff's modifiers conditional so if you are planning to use them in your conditional operations you will also need to add a property that contains the data that you want to check. This might be changed in the future to allow condition scopes to have access to tag data to run in their expressions.
Example using Rust programmatic syntax
We can add tags to live entities programmatically using the Rust API. Here's an example:
use alacrity_lib::prelude::*; static BLUEPRINT_ID: &str = "ENTITY_EXAMPLE_BLUEPRINT"; fn main() { // create a new Entity instance from a blueprint ID let mut e = Entity::new(BLUEPRINT_ID, None).unwrap(); // add new tags to the entity using its rust API let tags = vec![ Tag::new("humanoid", Value::from("centaur".to_string())), ]; e.add_tags(&tags, false).unwrap(); }
If we want to replace tags that already exists in the entity and are contained in the tags
vector, we can pass true as the second argument to the add_tags method.
Example using Lua programmatic syntax
We can also add tags from Lua using the entity's Lua API:
-- create a new entity instance from a blueprint ID
local e = alacrity.Entity.instance("ENTITY_EXAMPLE_BLUEPRINT")
-- add new tags to the entity using its lua API
local tags = {
{ name = "humanoid", value = "centaur" }
}
e:add_tags(tags, false)
As in the Rust API, if we want to replace tags that already exists in the entity, we can pass
true as the second argument to the add_tags method.
Value auto conversion
When creating a Tag with a Value of a different Kind than String or Int, Alacrity
will automatically convert the value to Kind::Int or Kind::String based on the following
rules:
| Value Kind | Converted To |
|---|---|
| Bool(true) | Int(1) |
| Bool(false) | Int(0) |
| Float(x.y) | Int(x) (truncated) |
| Nil | String("") |
💡 Tip
Even though the value is auto-converted to a safe value, it is recommended to avoid creating tags with values other than string or int to prevent potential issues and unexpected complications.
Entity's Attributes
Introduction
Attributes allow you to define and manage entities characteristics that can be queried and modified during the entities life time. This section will guide you though using the attributes system in both declarative and programmatic way providing examples in both Rust and Lua along the way.
Anatomy of an Attribute
Attributes are composed of a few fields that define them.
| Field | Type | Description |
|---|---|---|
| id | UUID | The attribute unique identifier |
| name | String | The attribute name (e.g 'dex', 'str', 'level', 'class'...) |
| description | String | An human friendly attribute description |
| base_value | Alacrity::Value | The attribute base value (defined in its blueprint or while creating a new one programmatically) |
| value | Alacrity::Value | The current attribute calculated value (after applying buffs) |
| modifiers | Vec<Modifier> | A vector of active modifiers that affect the attribute |
Continue reading to find a detailed description about each of the fields in the sections below
ID
Each attribute has an unique ID, this is generated automatically by Alacrity when an Attribute is created. The ID is basically used internally by Alacrity, users normally doesn't need to worry about it. The ID is always a valid UUID.
Name
The attribute's name is a pretty important piece of information, it is used both to provide of an human-friendly short reference to the attribute and to target the attribute in Modifiers definitions. Attribute's names must always be strings.
⚠️ Alert
Alacrity will not prevent you from creating entities with repeated attribute names in their definitions but be wary that only the last one in the definition will be added to the entity and taken into account.
Also, attribute names can clash with property names, this would probably cause issues when evaluating modifiers and buffs conditions as the condition evaluation Scope is not able to differentiate from attribute or property values while evaluating their [expressions].
Description
Human friendly description of what the attribute represents for the entity.
Base Value
When an attribute is firstly instantiated by a blueprint or programmatically using
the Rust or Lua APIs, it can be assigned a base value. This value will be used in
any [Buff] calculations that modifies or uses the attribute in any avail, if no
buff's modifiers are affecting the attribute, it will return always its base_value
when we request its calculated value or it is check by conditional expressions.
The base value is an Alacrity::Value of kind Int or Float, try to use any other value
kind as an attribute value will result in an error at compilation or run time.
Value
The attribute's value is dynamically calculated by the attribute taking into account the buff's modifiers that are currently affecting the attribute. They are immutable and can be updated only by alacrity's internal routines. The attribute's value will always contain the most up to date calculated value for the attribute at any given moment in time.
Modifiers
A vector of buff's modifiers that are currently affecting the attribute.
Defining Attributes
To define attributes for an entity, you need to specify them within the entity's definition. Attributes are typically declared as key-value pairs, where the key represents the attribute name and the value represents the initial or base value of the attribute. Additionally, the attribute description can also be defined.
Here's an example of defining attributes in Lua:
local attributes = {
{ name = "max_health", value = 100, description = "Max Health parameter" },
{ name = "strength", value = 10, description = "Current strength" },
-- Additional attributes...
}
local e = alacrity.Entity.new({
name = "Entity",
description = "Some Entity",
attributes = attributes,
...
})
And here's the equivalent example in Rust:
#![allow(unused)] fn main() { use alacrity_lib::prelude::*; let attributes = vec![ Attribute::new("max_health", Value::from(100)).unwrap().with_description("Max health parameter"), Attribute::new("strength", Value::from(10)).unwrap().with_description("Current strength"), // Additional attributes ... ]; let e = EntityBuilder::default() .with_name("Entity") .with_description("Some Entity") .with_attributes(attributes) ... .build().expect("Failed to build entity"); }
In both examples, we define two attributes: "max_health" with an initial value of 100 and "strength" with an initial value of 10. You can add as many attributes as needed, each with a unique name and appropriate initial or base value.
Defining Attributes Declaratively
Attributes can be defined declaratively directly in an entity blueprint definition using Lua
local entities = {
{
-- Declare an entity
blueprint_id = "ENTITY#1",
name = "Entity",
description = "Some Entity",
attributes = {
{ name = "max_health", value = 100 },
{ name = "strength", value = 10 },
}
}
}
return entities
In the example above we are creating the exact same entity than in the programmatic examples above but using Alacrity's declarative Lua syntax.
Accessing Attribute Values
Once attributes are defined for an entity, you can access their values during runtime.
In Lua, you can access an attribute's value using the value() method that will return
the latest calculated value for a given attribute. Here's an example:
local max_health = entity:get_field_value("max_health")
print("Current max health:", max_health)
In Rust, you can also use the get_field_value method of the Entity type to retrieve
an attribute's value. Here's an example:
#![allow(unused)] fn main() { let max_health = entity.get_field_value("max_health"); println!("Current max health:", max_health) }
Both examples demonstrate how to retrieve the value of the "max_health" attribute. You can replace "max_health" with the name of any other attribute to access its corresponding value.
If the entity does not contains an attribute of the given name, an error will be returned.
💡 Tip
The
get_field_valuecan be used to retrieve values for both attributes and properties in both APIs Lua and Rust
Recalculating an attribute value
Attributes can be recalculated at any time, some operations, like buff activations, trigger
attribute recalculations automatically but you can trigger an attribute value recalculation
at will using the entity's recalculate_value in both Lua or Rust. The recalculate_value
expects an unique parameter containing the attribute to recalculate name.
If the given attribute can not be found in the entity, the method will return an
[AttributeNotFound] error.
Here is an example in Lua:
local ok, err = pcall(entity:recalculate_value("str"))
if not ok then
-- handle error
end
And here is the equivalent example in Rust:
#![allow(unused)] fn main() { if let Err(err) = entity.recalculate_value("str") { // handle error } }
Access attribute values from Conditional expressions
Entity attribute values can be accessed from conditional expressions through the virtual
object self for example, to access one attribute called str in a buff's conditional
that is attached to an entity we can use the following syntax in the condition expression:
self.str >= 10
The expression above would resolve as true only if the entity's str attribute value is
equal or higher than 10.
Attribute's Modifiers
Introduction
Attributes in Alacrity can be affected by buff modifiers, which can modify their values based on specific conditions. Buff modifiers are applied to target attributes, and a read-only copy of the modifier is pushed into the target attribute, triggering a recalculation. This modifies the attribute's nominal value.
Modifiers can affect different aspects of the final calculation of an attribute's value. An attribute's value is composed of three parts or locations used to calculate the final value.
The base value part
The base part of an attribute represents the raw value without any modifiers applied. Buff modifiers that apply a percentage over the attribute's value affect this part.
Let's take an example from the game Genshin Impact. The character Bennet has a skill called "Fantastic Voyage" that adds a percentage of Bennet's own ATK attribute to any character within its area of effect (AoE). If we were to model this effect using Alacrity, we would use the base part of Bennet's ATK value to calculate the additional ATK provided to affected players.
In Genshin Impact, a character's base ATK can be increased by leveling up the character and equipping a weapon. The ATK attribute of the weapon adds a flat value to the character's base ATK. In Alacrity, this system could be modeled using passive buffs in weapons that add flat values to the entity's ATK attribute value base part.
On the other hand, if a character receives a flat ATK bonus from Bennet's elemental burst, that bonus would not be affected by other buffs. It would be independent of subsequent calculations. This is because Bennet's bonus ATK is not considered in the calculations of other buffs. In order to model that in Alacrity we would target Bennet's bonus value into an special part of our attributes values called "calculated part". We will review this port later below.
The intermediate value part
The intermediate part of an attribute's value is used to grant bonuses that can be further augmented by final bonus modifiers (the "calculated part"). If we wanted Bennet's elemental burst ATK bonus to be affected by additional buffs, we would target this part of the attribute.
The intermediate value part has no value or meaning on its own; it is used to augment the value contained in the base part.
The calculated value part
The calculated part of an attribute's value is calculated last and is not subject to further buffs. It represents the final value of the attribute. If you want a bonus to be unaffected by other buffs (except global multipliers), you would target this part.
The calculated value part cannot be affected by any further buffs, as it is calculated last and is not part of subsequent calculations. It also hosts a special multiplier that adds a global bonus or malus to the calculated attribute's value.
The final value of an attribute is calculated using the following formula:
value = (
attribute_base_value + ( // attribute initial base value as defined in specs
(sum(modifiers.flat_base) * (1 + sum(modifiers.percent_base))) + // base part
(sum(modifiers.flat) * (1 + sum(modifiers.percent))) + // intermediate part
(sum(modifiers.flat_calculated)) // calculated part
) * (1 + sum(modifiers.percent_global)) // global multiplier
)
Each part adds to the previous part's calculation. In our example of Bennet's skill, the bonus would be pushed into the target's ATK attribute as a flat_calculated value, ensuring it is not affected by anything else except a global multiplier.
Global multipliers are used to apply a multiplier or divider to an attribute's value globally. For example, an area that unconditionally adds 20% to the character's traveling speed could be expressed as a global multiplier.
We will learn much more about modifiers and how to use them to model different effects in the Modifiers section.
Entity's Properties
Introduction
Properties allow you to define and query entities additional information that can be queried and manipulated during the entities life time. Properties allows you to attach key-value pairs of read-only metadata that enables you to customize and extend their functionality beyond core attributes.
Properties are commonly used to store metadata, configuration settings or any other relevant information associated with an entity. They provide a mechanism for adding context specific data that can be accessed and manipulated during gameplay or system interactions.
Anatomy of a Property
Properties are composed of a few fields that define them. They are pretty similar to their siblings Attributes but they differ significantly at a conceptual level.
| Field | Type | Description |
|---|---|---|
| id | UUID | The property unique identifier |
| name | String | The property name (e.g 'level', 'current_area_name', 'class'...) |
| description | String | An human friendly property description |
| value | Alacrity:Value | The property value (it can be of types i64, f64, String and bool) |
| appears_in_conditions | bool | Determines if the property will be accessible by Lua in conditional expressions |
Continue reading to find a detailed description about each of the fields in the sections below
ID
As attributes, each property has an unique ID, this is generated automatically by Alacrity when a Property is created. The ID is basically used internally by Alacrity, users normally doesn't need to worry about it.
Name
The property's name is used to both provide an human-friendly short reference to the property itself and to target the property while evaluating condition expressions. Property names must always be strings.
⚠️ Alert
Alacrity will not prevent you from creating entities with name clashes between their properties and their attributes. That would probably cause issues while evaluating modifiers and buffs conditions as the condition evaluation Scope is not able to differentiate from attribute or property values while evaluating conditional [expressions].
Description
Human friendly description of what the property represents for the entity.
Value
The property's value is set during property initialization and can be updated
programmatically from both Rust and Lua APIs. The value of a Property differs
from that of an attribute in the way that property values can contain not only
float and integer values but also boolean or String values.
Appears in Conditions
This is a boolean field that determines if the property will be accessible or not
for Lua contexts in conditional expressions. If the value of this field is true
the property will be accessible through the virtual object self inside the
conditional expression, in the other hand, if its value is false, the property
value will not be accessible by the Lua context in the expression.
This is useful to prevent leaking of sensible metadata or to prevent conditional Scopes to grow with no need.
Key Features
Dynamic and Flexible
Entity properties offer a dynamic and extensible storage for data associated with entities that needs to be accessed often or fast on runtime. You can define properties on an entity, assign values to them and retrieve or modify those values at runtime.
This flexibility allows you to adapt and customize entities based on specific requirements, scenarios or game mechanics.
Arbitrary Data
Properties can store a wider range of data types including strings, booleans, floating point and integer values. This versatility enables you to capture different aspects of an entity's state or configuration. Whether is a character's level, a quest status, or a custom gameplay flag, properties provide a generic container for various types of data.
Easy access and manipulation
Alacrity provides convenient APIs and syntax for working with entity properties. You can easily access, modify and remove property values through dedicated methods or by using the Lua's API. This streamlined access allows you to integrate properties seamlessly into your gameplay logic or system interactions.
Defining Properties
Properties can be defined in declarative or programmatic way. They can be part of an entity definition or they can be added later during runtime. This differs greatly from attributes that can be defined declaratively only and can not be attached at a later stage without updating the entity's template first.
Properties are typically declared as key-value pairs, where the key represents the property's name and a short reference to access it from both programmatic APIs and conditional expressions.
Here's an example of how to define a property in Lua:
local properties = {
{ name = "class", value = "cleric", description = "Character's class" },
{ name = "level", value = 1, description = "Character's initial level" },
-- Additional properties ...
}
local e = alacrity.Entity.new({
name = "Cleric Template",
description = "A cleric archetype initial character template",
properties = properties,
...
})
Managing Properties
Properties can be managed in runtime from both Rust or Lua APIs. Properties can be added, updated or removed from entities.
⚠️ Alert
Modify the entity properties in any way should trigger buffs checks. This is due the fact that buffs and modifier conditionals that were meet before the change could not be meet after the change or the other way around.
As a rule of thumb, perform an
activate_buffsand adeactivate_buffsin the entity after any kind of property modification if their values are used in the conditional expressions for buffs and modifiers.It might also be a good idea to trigger
recalculate_valuein specific attributes if you know a related property has changed.
Property Addition
To add a property, the add_properties method is used, for example to add a new
property to an entity we could use the following Lua code:
-- let's assume that e is an already defined and initialized entity
local ok, err = pcall(function()
e:add_properties({
{ name = "level", value = 1, description = "Character's level" }
})
end)
if not ok then
-- handle error
end
The same example in Rust would be:
#![allow(unused)] fn main() { // let's assume that e is an already defined and initialized entity if let Err(err) = e.add_properties(&[Property::new("level", Value::from(1)).with_description("Character's level")]) { // handle error } }
Property Update
Properties can be updated using the update_properties method. This method can also be
used to add properties if the optional add_if_missing flag is set to true.
💡 Tip
Use the
add_if_missingwith new properties if you need to update and add properties so you can do it with a single API call instead of two.
To update a property we can use the following Lua code:
-- let's assume that e is an already defined and initialized entity
e:update_properties({ { name = "class", value = "barbarian" } }, false)
The equivalent code using the Rust API:
#![allow(unused)] fn main() { // let's assume that e is an already defined and initialized entity e.update_properties(&[Property::new("class", Value::from("barbarian"))], false) }
The update_properties method will not return any errors in case that the property
trying to get updated does not exists and add_if_missing is false
Property Removal
Properties can also be removed from entities, there are small differences between the Lua and the Rust API for property removal as while using the Lua api we can use a property instance or the property name in order to remove it from the entity while in the Rust API only property instances are accepted by the method.
-- let's assume that e is an already defined and initialized entity
e:remove_properties({
{ name = "quest_ftue_001_status", value = nil }, -- using an instance
"player_terms_and_conditions_acceptance" -- using a property name directly as a string
})
The very same example in Rust:
#![allow(unused)] fn main() { // let's assume that e is an already defined and initialized entity e.remove_properties(&[ Property::new("quest_ftue_001_status", Value::nil()), Property::new("player_terms_and_conditions_acceptance", Value::nil()) ]) }
The remove_properties method will not raise an error if we try to remove a
property that does not exists in the entity.
Property Use in Conditionals
Properties can be opt-out from being accessible (or appearing) in conditional expressions. They will always be available by default but users can decide to hide them from conditional expressions so entity Scopes will ignore them while being constructed before a expression evaluation occurs.
Opting out while working with Rust
While working with Rust, to opt out a property from being accessible from
both buffs and modifiers condition expressions the hide_from_condition
method can be used. This method should be chained when the property is
being created.
#![allow(unused)] fn main() { let p = Property::new("some_property_name", Value::from("some value")) .hide_from_conditions(true) }
In case that we want to change the visibility of a property that already exists we can still do it ignoring the return value from the method itself.
#![allow(unused)] fn main() { // let's assume that p is an already defined and initialized property instance p.hide_from_conditions(true) }
We can also make a hidden property visible passing false as parameter to the
method instead.
Opting out while working with Lua
In similar way, to opt out a property appearance in condition expressions
users can chain the hide_from_conditions method in the newly created
property.
local p = alacrity.Property.new("some_property_name", "some value"):hide_from_conditions(true)
As in Lua we rarely create properties using this constructor but by using a
table as literal in methods that support property construction from tables
we can also opt out their condition expression appearance using an additional
field hide_from_conditions with a boolean value with our preference.
-- let's create an entity with some table literal properties
local e = alacrity.Entity.new({
name = "Some Entity",
description = "Some Entity Description",
attributes = {},
properties = {
{
name = "some_property_name",
value = "some value",
hide_from_conditions = true,
}
}
})
If this field is not present in the table, Alacrity will assume its value
is false.
ℹ️ Info
The field is called
hide_from_conditionsto maintain name consistency and make it is easier for users to remember as the field name matches the function one.
As with the Rust API, users can toggle the visibility of a property that
already exists at any time using the hide_from_conditions method in the
property instance itself.
-- let's assume that p is an already defined and initialized property
p:hide_from_conditions(true)
Attributes & Properties Differences
It might look that attributes and properties are very similar but in reality they are conceptually very different.
Attributes can not be mutated outside buffs effects
Attributes can not be changed at all outside the buffs effects system. This means they can not be mutated with arbitrary values in any way from neither Lua or Rust APIs.
Properties can not be mutated by buffs effects
In the other hand, properties are not visible from the buffs effects system beyond read access to a copy of they values on conditionals expression evaluation on runtime. Properties are only mutable through the Lua and Rust APIs.
Attributes only accept f64 and i64 values
Attribute values can only contain Value instances that are created from
values of type f64 or i64 exclusively.
Properties can accept any kind of values
Properties values can contain any Value instance created from any valid
value kind, that is f64, i64, bool, String or nil.
Attributes always appears in condition expressions
Entity's attributes are always accessible to condition expressions through
the virtual object self within the expression.
Properties can be out-out to not appear in condition expressions
Properties can be hidden from condition expressions at will.
Entity Scope
Scopes represents a Lua UserData contextual condition entity contained data information scope.
It stores the entity's attributes and properties that can be used in conditions at evaluation time.
The scope is referred as self in the condition's lua scripts.
📔 Note
Properties that are configured with their boolean field
hide_from_conditionsastruewill not be accessible from with-in entity's scopes in conditionals evaluation.
The scopes also contains information about the entity as its name, description, id, template and version.
How to create an Entity Scope
Entity scopes can be created programmatically from Lua or Rust APIs using the generate_scope method from the Entity UserData in a Lua context or by using the Scope::new method from Rust.
Field access
To access an attribute or property from with-in the scope in a conditional expression syntax, one can just reference the attribute or property name using the dot notation [.] like in the example below.
self.str >= 5
Field access missing behavior
How a scope behaves when a field that is being tried to be access is missing can be configured when we instantiate a new
scope with the parameter non_field_behavior. The possible behaviors are to return nil or to raise an error.
The value of the non_field_behavior differs depending on the API we are using.
Lua API non_field_behavior
In the Lua API, the non_field_behavior value can be a string, an integer or nil.
String values
The possible and accepted values for non_field_behavior are the following:
- error: alacrity will raise an error if the field being accessed does not exists (this is the safest option)
- nil: alacrity will return
nilif the field is not found (emulating how a Lua table would behave)
Integer values
The possible and accepted values when using an integer are the following:
- 0: alacrity will raise an error if the field being accessed does not exists (safest option)
- 1: alacrity will return
nilemulating the behavior of a Lua table
Rust API non_field_behavior
In the rust API the non_field_behavior parameter accepts one of the variants of the NonFieldBehavior enum from the
Scope type, the possible variants are Error and Nil, their names are self explanatory.
📔 Note
You don't usually need to create Entity scopes manually unless you are doing some manual conditional verifications in Lua space, generally, Alacrity will handle the creation of scopes when they are needed automatically.
Creating Entities
Alacrity provides a multitude of approaches to create new entities:
- Programmatically using the Lua or Rust APIs
- Declaratively using alacrity specs with Lua syntax.
This guide will walk you through each method, explaining the steps involved and providing examples for clarity.
Declarative
You will usually create new entities through the alacrity's specs system that makes use of the declarative Lua syntax. Unless you have the need of creating an ad-hoc entity from scratch you will have few to no need of using any other means in order to define and instance entities for your games.
Alacrity Specs
Alacrity specs allow you to define entity spec blueprints through the declarative Lua API. Here's how you can create entities using alacrity specs:
Step One
Create a new Lua spec file in your $ALACRITY_SPECS_PATH/entities
Step Two
Define a Lua table that will represent the new blueprint
local humanMageBlueprint = {
id = "PC#HUMAN#CLERIC_001",
name = "Cleric Human Playable Character 001",
description = "Blueprint defining an Human Cleric playable character",
tags = {
-- this is an humanoid type of character
{ name = "humanoid", value = "human" },
-- this is a playable character
{ name = "payable_character", value = 1 },
},
attributes = {
{ name = "max_hit_point", value = 10 },
{ name = "str", value = 5 },
{ name = "dex", value = 2 },
{ name = "con", value = 6 },
{ name = "int", value = 4 },
{ name = "wis", value = 2 },
{ name = "cha", value = 8 },
-- add more attributes as needed
},
properties = {
{ name = "mental_condition", value = "good" },
{ name = "level", value = 1 },
{ name = "class", value = "cleric" },
{ name = "ftue_completed", value = false, hide_from_conditions = true },
{ name = "current_tracking_quest", value = 0, hide_from_conditions = true },
{ name = "last_check_point", value = "none", hide_from_conditions = true },
-- add more properties as needed
},
buffs = {
-- this attached buff is defined elsewhere in the specs
"52e59fb8-0395-4425-a4d5-56d87756491a",
},
version = "0.0.1",
}
Step Three
Add the entity definition to an entities table in the same spec file
local entities = {
-- Human Cleric PC
humanMageBlueprint,
}
Step Four
Return the entities table with your entities definitions
return entities
Instantiating the Entity
With our entity in place we can now instantiate it from both Rust or Lua APIs in our game logic as we need.
-- the entity template that we want to instantiate
local entityTemplateID = "PC#HUMAN#CLERIC_001"
-- the ID that we want for the entity
local entityID = "6137c461-a3cb-4910-b501-a3db682b36a3"
-- instantiate a new Human Cleric PC entity template with the given ID
local clericEntity, ok = pcall(function()
local e = alacrity.Entity.instance(entityTemplateID, entityID)
return e
end)
if not ok then
-- handle error
end
Of course we can also instantiate the entity using the Rust API
#![allow(unused)] fn main() { let entity_template_id = "PC#HUMAN#CLERIC_001" let entity_id = "6137c461-a3cb-4910-b501-a3db682b36a3" let mut e = Entity::new(entity_template_id, Some(entity_id)); match e { Ok(e) => { // do something with the entity }, Err(e) => { // handle error } } }
Templates
Protobuf encoded objects can be used to create entity instances thorough
templates using the Entity type try_from implementation for creating
new instances from values of type alacrity_pb::tpl::EntityPrototype.
This is intended to be used from user implementations of gRPC (or other high level transportation layers) that might accept Alacrity types to be sent through the wire. An example of how to use it could be as follows:
#![allow(unused)] fn main() { fn entity_handler(entity_prototype: &alacrity_pb::tpl::EntityPrototype) -> Entity { match Entity::try_from(entity_prototype) { Ok(entity) => entity, Err(e) => panic!(err), } } }
The complete protobuf definitions can be found in the alacrity-proto more information can be found in the link above.
📔 Note
How to wrap alacrity in order to make network services around it is beyond the aim of this guide.
Programmatically
Entities can be created programmatically from both Rust and Lua APIs at any time. There is also a programmatic way of creating instances through protobuf templates but that is not something that you will really rely on as both Rust and Lua regular APIs are more convenient.
Lua Tables
To create an entity programmatically using the Lua API, you might follow these steps:
Step One
Acquire an instance of the entity module:
local entity = alacrity.Entity
Step Two
Create a new minimal entity instance using the new method:
local warrior_001 = entity.new({
id = alacrity.uuid.new_v4(),
name = "Warrior Character",
description = "Ad-Hoc instance of a Warrior Human Playable Character",
attributes = {
{ name = "max_hit_point", value = 16 },
{ name = "str", value = 8 },
{ name = "dex", value = 2 },
{ name = "con", value = 6 },
{ name = "int", value = 1 },
{ name = "wis", value = 1 },
{ name = "cha", value = 3 },
-- add more attributes as needed
},
-- we could add anything else that we needed using our familiar Lua tables syntax
version = "0.0.1",
})
Step Three
Make any optional modifications
warrior_001:add_tags({
{ name = "humanoid", value = "human "},
{ name = "playable_character", value = 1 },
})
warrior_001:add_properties({
{ name = "mental_condition", value = "good" },
{ name = "level", value = 1 },
{ name = "class", value = "cleric" },
{ name = "ftue_completed", value = false, hide_from_conditions = true },
{ name = "current_tracking_quest", value = 0, hide_from_conditions = true },
{ name = "last_check_point", value = "none", hide_from_conditions = true },
-- add more properties as needed
})
💡 Tip
Remember that attributes can not be added to entity instances after they have been created already. You must make sure your entity contains all the attributes that it needs for its specific version on instantiation / definition.
Rust
Create an entity fully programmatically in Rust might be a bit trickier as
if we don't want to rely in using the protobuf templating we must use
the EntityBuilder instead.
To create an entity programmatically using Rust you can follow these steps:
Step One
Import the necessary modules and types:
#![allow(unused)] fn main() { use alacrity_lib::prelude::*; }
Step Two
Create a new entity instance though the EntityBuilder helper:
#![allow(unused)] fn main() { let mut warrior_001 = EntityBuilder::default() .with_id(uuid::Uuid::new_v4()) .with_name("Warrior Character") .with_description("Ad-Hoc instance of a Warrior Human Playable Character") .with_attributes( [ { "max_hit_point".to_string(), Attribute::new("max_hit_point", Value::from(16)).unwrap(), }, { "str".to_string(), Attribute::new("str", Value::from(8)).unwrap(), }, { "dex".to_string(), Attribute::new("dex", Value::from(2)).unwrap(), }, { "con".to_string(), Attribute::new("con", Value::from(6)).unwrap(), }, { "int".to_string(), Attribute::new("int", Value::from(1)).unwrap(), }, { "wis".to_string(), Attribute::new("wis", Value::from(1)).unwrap(), }, { "cha".to_string(), Attribute::new("cha", Value::from(3)).unwrap(), }, ] .iter() .cloned() .collect::<HashMap<String, Attribute>>(), ) .with_version("0.0.1".to_string()) .build() .unwrap(); }
Step Three
Make any optional modifications:
#![allow(unused)] fn main() { if let Err(e) = warrior_001.add_tags(vec![ Tag::new("humanoid", Value::from("human")), Tag::new("playable_character", Value::from(1)), ], true) { // handle error } if let Err(e) = warrior_001.add_properties(vec![ Property::new("mental_condition", Value::from("good")), Property::new("level", Value::from(1)), Property::new("class", Value::from("warrior")), Property::new("ftue_completed", Value::from(false)), Property::new("current_tracking_quest", Value::from(0)).hide_from_conditions(true), Property::new("last_check_point", Value::from("none")).hide_from_conditions(true), ]) { // handle error } }
Using the Protobuf Templates API
Instantiating an entity through the protobuf templates API might be more convenient for you depending in your use case. This is regularly not recommended but there is nothing wrong in using it if you feel more comfortable doing it.
Protobuf Step One
Add the alacrity protobuf template library so we can use templating:
#![allow(unused)] fn main() { use alacrity_pb::tpl; }
Protobuf Step Two
Instantiate a new entity through the protobuf templating API:
#![allow(unused)] fn main() { let warrior_001 = Entity::try_from(&tpl::EntityPrototype { id: uuid::Uuid::new_v4().to_string(), name: "Warrior Character", description: "Ad-Hoc instance of a Warrior Human Playable Character", tags: vec![tpl::Tag { name: "humanoid".to_string(), value: Some(tpl::tag::Value::StringValue("human".to_string())), }, tpl::Tag { name: "playable_character".to_string(), value: Some(tpl::tag::Value::IntValue(1)), }], attributes: vec![tpl::entity_prototype::Attribute { name: "max_hit_point".to_string(), description: "Character's Max Hit Points".to_string(), value: 16.0, }, tpl::entity_prototype::Attribute { name: "str".to_string(), description: "Character's Strength".to_string(), value: 8.0, }, tpl::entity_prototype::Attribute { name: "dex".to_string(), description: "Character's Dexterity".to_string(), value: 2.0, }, tpl::entity_prototype::Attribute { name: "con".to_string(), description: "Character's Constitution".to_string(), value: 6.0, }, tpl::entity_prototype::Attribute { name: "int".to_string(), description: "Character's Intelligence".to_string(), value: 1.0, }, tpl::entity_prototype::Attribute { name: "wis".to_string(), description: "Character's Wisdom".to_string(), value: 1.0, }, tpl::entity_prototype::Attribute { name: "cha".to_string(), description: "Character's Charisma".to_string(), value: 3.0, }], properties: vec![tpl::entity_prototype::Property { name: "mental_condition".to_string(), description: "Character current mental condition".to_string(), appears_in_conditionals: true, value: Some(tpl::entity_prototype::property::Value::NumericValue(10.0)), }, tpl::entity_prototype::Property { name: "level".to_string(), description: "Character current level".to_string(), appears_in_conditionals: true, value: Some(tpl::entity_prototype::property::Value::NumericValue(1.0)), }, tpl::entity_prototype::Property { name: "class".to_string(), description: "Character Class".to_string(), appears_in_conditionals: true, value: Some(tpl::entity_prototype::property::Value::StringValue("warrior".to_string())), }, tpl::entity_prototype::Property { name: "ftue_completed".to_string(), description: "Character has completed the FTUE?".to_string(), appears_in_conditionals: false, value: Some(tpl::entity_prototype::property::Value::BooleanValue(false)), }, tpl::entity_prototype::Property { name: "current_tracking_quest".to_string(), description: "Character current being tracked or active quest".to_string(), appears_in_conditionals: false, value: Some(tpl::entity_prototype::property::Value::NumericValue(1.0)), }, tpl::entity_prototype::Property { name: "last_check_point".to_string(), description: "Character last check point where it will return in case of death".to_string(), appears_in_conditionals: false, value: Some(tpl::entity_prototype::property::Value::StringValue("none".to_string())), }], ..Default::default() }); match warrior_001 { Ok(warrior_001) => { // do something with warrior_001 } Err(e) => { // handle error } } }
API
To check the Rust API please refer to the docs.rs crate documentation for Alacrity Lib. If you are looking for the Lua specific API take a look at the LUA Specific API section instead.
LUA Specific API
In this section you can find a reference or guide for alacrity entities Lua APIs. We try to maintain this section up to date with the latest changes introduced to the API. If you find a mistake please, create a ticket in alacrity TODO or send a message to the alacrity discuss or alacrity devel mailing lists.
Table of Contents
- Entity's Lua UserData - Entity's Lua UserData exported type
- Fields - Entity's UserData available fields
- Methods - Entity's UserData available methods
- get_field_value - Retrieves a field (property or attribute) value
- generate_scope - Generates conditional context scopes
- get_property - Lookup and returns back a property
- add_properties - Adds one or multiple properties to the Entity
- remove_properties - Remove one or multiple properties from the Entity
- update_properties - Upgrades one or multiple properties on the Entity
- get_buff - Lookup nad retrieves the specified buff
- add_buff - Adds a buff to the Entity
- remove_buff - Removes a buff from the Entity
- activate_buffs - Activates all the buffs in the Entity that can be activated and they are not active yet
- get_tag - Lookup and retrieve a tag from the Entity
- get_tags - Lookup and retrieve one or multiple tags from the Entity
- add_tags - Adds one or multiple tags to the Entity
- remove_tags - Remove one or multiple tags from the Entity
- recalculate_attribute - Recalculates the Entity's given attribute value
- Meta Methods
Entity's Lua UserData
Alacrity exposes the Entity struct as a Lua UserData object that is accessible from any Rust module that imports and instantiate the Alacrity data structure to create a new alacrity Lua context (i.e):
import alacrity-lib::prelude::*; fn main() -> Result<(), Box<dyn std::error::Error>> { let alacrity = Alacrity::new(None)?; let lua_data = r#" local entity = alacrity.Entity.new({ ... }) "#, alacrity.load(lua_data).exec() }
Please, refer to the LUA Tables programmatically entity creation section of the documentation to learn how to create new entities in Lua contexts.
Fields
All fields for the Entity UserData are read only access, this means that you can not mutate their value assigning a new value to them. Mutation is only available through specialized methods exposed by the Entity object itself.
id
Offers read only access to the entity's unique ID
-- let's assume e is an already defined and initialized entity elsewhere
print(e.id)
-- will output something similar to: 8138674e-ca8c-458d-aa4f-c766a05fac02
blueprint
Offers read only access to the entity's blueprint identification. The blueprint identification is an arbitrary unique string.
-- let's assume e is an already defined and initialized entity elsewhere
-- let's also assume that the entity blueprint is NPC#MINOTAUR#001
print(e.blueprint)
-- will output: NPC#MINOTAUR#001
template
Template is an alias for blueprint
name
Offers read only access to the entity's name.
-- let's assume e is an already defined and initialized entity elsewhere
-- let's also assume that the entity's name is Minotaur_NPC_001
print(e.name)
-- will output: Minotaur_NPC_001
description
Offers read only access to the entity's human friendly description.
-- let's assume e is an already defined and initialized entity elsewhere
-- let's also assume that the entity's description is "Some entity description"
print(e.description)
-- will output: Some entity description
attributes
Offers read only access to the entity's attributes table.
-- let's assume e is an already defined and initialized entity elsewhere
for k, v in e.attributes do
print(k, v)
end
-- will print each attribute name and human readable representation
properties
Offers read only access to the entity's properties table.
-- let's assume e is an already defined and initialized entity elsewhere
for k, v in e.properties do
print(k, v)
end
-- will print each property name and human readable representation
buffs
Offers read only access to the entity's attached buffs as a map buff_id: buff_data
-- let's assume e is an already defined and initialized entity elsewhere
for k, v in e.buffs do
print(k, v)
end
-- will print each buff unique ID and human readable representation
version
Offers read only access to the entity's version as a string
-- let's assume e is an already defined and initialized entity elsewhere
print(e.version)
-- will output: v0.0.1 or whatever version the entity is
Methods
This section contains detailed information about each one of the available methods that the Entity UserData object exposes through its API. Please, if you find an errata or any mistakes, feel free to open a new ticket in the alacrity TODO or send a new message to any of the alacrity discuss or alacrity devel mailing lists.
get_field_value
Lookup for an attribute or property with the given name in the entity and return back its Value or return an error if it is not found
Arguments
- name - The name of the attribute or property to lookup
Returns
- Some(Value) - If the attribute or property is present in the entity
- None - If the attribute or property is not present in the entity
Signature
---@param field_name string @Name of the field to lookup
---@return Value|nil error
function Entity:get_field_value(name)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "My new Ad-Hoc entity",
description = "This is an Ad-Hoc example test entity",
attributes = {
{ name = "str", value = 10 },
{ name = "dex", value = 5 },
{ name = "int", value = 3 },
{ name = "wis", value = 1 },
{ name = "cha", value = 4 },
},
properties = {
{ name = "level", value = 1, hide_from_conditions = false },
{ name = "class", value = "knight ", hide_from_conditions = false },
{ name = "active_quest", value = 1, hide_from_conditions = true },
}
})
-- this assertions pass
assert(e:get_field_value("level").Value, 1)
assert(e:get_field_value("str").Value, 10)
assert(e:get_field_value("active_quest").Value, 1)
assert(e:get_field_value("wis").Value, 1)
ok, err = pcall(function()
local v = e:get_field_value("none")
return v
end)
assert(ok, false)
assert(e:get_field_value("none") == nil, true)
generate_scope
Generates a ready to use entity Scope from an initialized entity.
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new entity
local e = entityAPI.new({
id = id
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc test entity",
attributes = {},
properties = {
{ name = "level", value = 10, hide_from_conditions = false },
}
})
local scope = e:generate_scope()
assert(scope ~= nil)
assert(scope.id == id)
assert(scope.name == "Ad-Hoc Test Entity")
get_property
Lookup for a property with the given name in the entity properties map and returns it if it is present
Errors
- If the property is not present in the entity
- If the given name is empty
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc test entity",
attributes = {},
properties = {
{ name = "level", value = 1 },
}
})
local property = e:get_property("level")
assert(property ~= nil)
assert(property.name == "level")
assert(property.value.Value == 1)
add_properties
Adds the given properties to the entity if they are not present already
Arguments
- properties - a table of properties literals or Property UserData to be added
Errors
- Return an error if the properties list is empty
- Return an error if the property does not has a valid ID
- Return an error if the property is already present in the entity
- Return an error if the property is not valid
Signature
---@param properties table[] @Array of properties to add
function Entity:add_properties(properties)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {},
properties = {},
})
local ok, err = pcall(function()
e:add_properties({
{ name = "level", value = 10 },
})
end)
local level_property = e:get_property("level")
assert(level_property ~= nil)
assert(level_property.name == "level")
assert(level_property.value.Value == 10)
remove_properties
Removes the given properties from the entity if they are present. This function does not return errors if the properties are not present in the entity.
Arguments
- properties - A table of Property instances, property Lua tables or property names to be removed.
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {},
properties = {
{ name = "level", value = 10, hide_from_conditions = false },
{ name = "mental_condition", value = "bad", hide_from_conditions = false },
{ name = "origin", value = "earth", hide_from_conditions = false },
{ name = "last_room", value = 1000, hide_from_conditions = true },
}
})
local ok, err = pcall(function()
e:remove_properties({
"level",
})
end)
assert(ok and err == nil)
-- check that the level property is not present in the entity anymore
local ok, err = pcall(function()
e:get_property("level")
end)
assert(not ok and string.find(tostring(err), "property 'level' not found in entity"))
local ok, err = pcall(function()
e:remove_properties({
{ name = "origin", value = "" }, -- use Lua table value
alacrity.Property.new("last_room", ""), -- use Property instance
"mental_condition", -- Use just a string with the property name
})
end)
assert(ok and err == nil)
-- check that the properties table in the entity is empty
if next(e.properties) ~= nil then
error("entity.properties is not empty")
end
update_properties
Updates the given properties in the entity if they are present already.
How the method behaves when the property is not present depends on the add_if_missing flag.
Arguments
A table of Property instances, property Lua tables or property names to be removed.
Signature
---@param properties table[] @Array of properties to update
---@param add_if_missing boolean|nil @whether a missing property should be added if missing or not
function update_properties(properties, add_if_missing)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {},
properties = {
{ name = "level", value = 10 },
{ name = "class", value = "warrior" },
}
})
local ok, err = pcall(function()
e:update_properties({
{ name = "level", value = 20 },
})
end)
assert(ok and err == nil)
local level_property = e:get_property("level")
assert(level_property ~= nil)
assert(level_property.name == "level")
assert(level_property.value.Value == 20)
-- this call will idempotently do nothing
e:update_properties({
{ name = "house", value = "atreides" },
})
assert(entity.properties["house"] == nil)
local ok, err = pcall(function()
e:update_properties({
{ name = "level", value = 30 },
alacrity.Property.new("class", "mage"),
})
end)
assert(ok and err == nil)
local level_property = e:get_property("level")
local class_property = e:get_property("class")
assert(level_property ~= nil)
assert(level_property.name == "level")
assert(level_property.value.Value == 30)
assert(class_property ~= nil)
assert(class_property.name == "class")
assert(class_property.value.Value == "mage")
-- this call will also add the missing "house" property to the entity
local ok, err = pcall(function()
e:update_properties({
{ name = "house", value = "harkonnen" },
}, true)
end)
assert(ok and err == nil)
local house_property = e:get_property("house")
assert(house_property ~= nil)
assert(house_property.name == "house")
assert(house_property.value.Value == "harkonnen")
get_buff
Look up and return back the given buff ID from the entity in case that it is attached to it or an error otherwise.
Arguments
A string representing a buff UUID.
Errors
- If a buff with the given
buff_idcan not be found in the entity.
Returns
- Buff - if a buff with the given
buff_idis found in the entity
Signature
---@param buff_id string @UUID of the buff to lookup for
---@return Buff
function Entity:get_buff(buff_id)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local e = entityAPI.new({
id = id
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc test entity",
attributes = {},
properties = {
{ name = "level", value = 10, hide_from_conditions = false },
},
buffs = {
"06169055-04d9-41d9-93e5-b3746cb359fb",
}
})
-- this buff does not exists
local ok, result = pcall(function()
local buff = entity:get_buff("9949a823-50de-4501-ae85-35d97ed18562")
return buff
end)
assert(not ok and string.find(tostring(err), "not found on entity"))
-- this buff exists
local ok, result = pcall(function()
local buff = entity:get_buff("06169055-04d9-41d9-93e5-b3746cb359fb")
return buff
end)
assert(ok and result ~= nil)
assert(result.id == "9949a823-50de-4501-ae85-35d97ed18562")
add_buff
Adds the given buff to the entity.
Arguments
A table defining a buff configuration or a Buff UserData or a string representing a buff UUID
Errors
- If an invalid type is passed to the function
- If the buff definition does not have a valid UUID
Signature
---@param value Buff|table|string @UserData table or string representing a valid buff
---@return Buff
function Entity:add_buff(value)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local e = entityAPI.new({
id = id
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc test entity",
attributes = {},
properties = {
{ name = "level", value = 10, hide_from_conditions = false },
},
})
local ok, err = pcall(function()
entity:add_buff("648b13f8-2cbe-43d0-a493-6de63e8ba4ec")
end)
assert(ok)
remove_buff
Removes the given buff from the entity.
Arguments
A table defining a copy of the buff to be removed, the buff's UserData representation or a string with the buff ID of the buff to be removed from the entity.
Errors
- If the buff ID is
nilor it is invalid somehow. - If the buff is not present in the entity.
- If the buff is active (as you can't remove an active buff from an entity)
Signature
---@param value Buff|table|string @UserData table or string representing a valid buff to be removed
function Entity:remove_buff(value)
Example of Usage
-- let's assume e is a valid entity defined elsewhere
local ok, err = pcall(function()
e:remove_buff("d28452fb-2a31-42a0-8286-2901c64fd318")
end)
if not ok then
-- treat err here
end
--
activate_buffs
Activates any buff that is not active yet and can be activated.
Errors
- Returns an error if any buff cannot be activated.
Signature
---@return nil|error
function Entity:activate_buffs()
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "My new Ad-Hoc Entity",
description = "This is an Ad-Hoc entity used to test the activate_buffs method",
attributes = {
{ name = "str", value = 10 },
},
properties = {
{ name = "level", value = 1, hide_from_conditions = false },
},
buffs = {
-- assume the buff is defined elsewhere
{ "6fabe498-ac41-44fe-aa5c-d606dcbc0646" },
},
})
local ok, err = pcall(function()
e:activate_buffs()
end)
assert(ok, true)
get_tag
Lookup for the given tag name in the entity tags map and returns it if its
present, if its not present nil will be returned instead.
Arguments
- name - The name of the tag to lookup and retrieve
Signature
---@param name string @Name of the tag to lookup
---@return Tag|nil
function Entity:get_tag(name)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "My new Ad-Hoc entity",
description = "This is an Ad-Hoc example test entity",
attributes = {},
tags = {
{ name = "humanoid", value = "minotaur" },
}
})
local tag = entity:get_tag("humanoid")
assert(tag ~= nil)
assert(tag.name == "humanoid")
assert(tag.value.Value == "minotaur")
assert(entity:get_tag("undefined") == nil)
get_tags
Lookup for the given tag names in the entity tags map and returns them if they present, if they not present they will be skip. If none of the given tags are found in the entity, an empty table will be returned.
Arguments
- names - The names of the tags to lookup and retrieve as an array table
Signature
---@param names []string @Array of names of the tags to lookup
---@return []Tag
function Entity:get_tag(name)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "My new Ad-Hoc entity",
description = "This is an Ad-Hoc example test entity",
attributes = {},
tags = {
{ name = "humanoid", value = "minotaur" },
{ name = "house", value = "atreides" },
}
})
local tags = entity:get_tags({"humanoid", "house"})
assert(tags ~= nil)
assert(tags[1].name == "humanoid")
assert(tags[1].value.Value == "minotaur")
assert(tags[2].name == "house")
assert(tags[2].value.Value == "atreides")
assert(entity:get_tags("undefined") == nil)
add_tags
Adds the given tags to the entity if they are not present already. If the replace
flag argument is set to true, and the tag already exists in the entity's tags it
will be replaced with the new one.
Errors
- Return an error if the tag already exists and the replace flag is not set or set to false
- Return an error if the tag name is empty
Signature
---@params tags table[] @Array of tags to be add
---@params replace bool|nil @Replaces the tags on the entity if they already exists
---@return int|error @Number of added/replaced tags or an error
function Entity:add_tags(tags, replace)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing add_tags method",
attributes = {},
properties = {},
})
local ok, err = pcall(function()
local tags = {
{ name = "test_tag", value = "something" },
}
e:add_tags(tags)
end)
assert(ok)
assert(e:get_tag("test_tag").value:to_string() == "something")
-- this one will fail
local ok, err = pcall(function()
local tags = {
{ name = "test_tag", value = "some other value" },
}
e:add_tags(tags)
end)
assert(not ok)
assert(e:get_tag("test_tag").value:to_string() == "something")
-- this one will go through
local ok, err = pcall(function()
local tags = {
{ name = "test_tag", value = "nothing" }
}
e:add_tags(tags, true)
end)
assert(ok)
assert(e:get_tag("test_tag").value:to_string() == "nothing")
remove_tags
Removes the given tags from the entity if they are present.
Arguments
- tags - A table of [Tag] instances or literal definitions to be removed from the entity.
Signature
---@param tags []string|[]Tag|[]string|Tag @Table containing the tags to be removed
function Entity:remove_tags(tags)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {},
tags = {
{ name = "humanoid", value = "minotaur" },
{ name = "house", value = "atreides" },
}
})
local ok, err = pcall(function()
entity:remove_tags({
"humanoid",
})
end)
assert(ok and err == nil)
local humanoid_tag = entity:get_tag("humanoid")
assert(humanoid_tag == nil)
local ok, err = pcall(function()
entity:remove_tags({
{ name = "house", value = "atreides" },
})
end)
assert(ok and err == nil)
local house_tag = entity:get_tag("house")
assert(house_tag == nil)
recalculate_attribute
Recalculate the value of the given attribute name.
Arguments
- attr_name - The attribute to recalculate name as a string
Errors
- Return an error if the attribute is not present in the entity
Signature
---@param attr_name string @Name of the attribute to recalculate
function Entity:recalculate_attribute(attr_name)
Example of Usage
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {
{ name = "str", value = 10 },
},
})
local ok, err = pcall(function()
e:recalculate_attribute("dex")
end)
assert(not ok)
local ok, err = pcall(function()
e:recalculate_attribute("str")
end)
assert(ok)
Meta Methods
This section contains detailed information about each one of the implemented meta-methods that the Entity UserData object exposes through its Lua API.
new
Creates a new Entity instance using the given configuration.
Implementation
Please, refer to the LUA Tables section of entity creation documentation.
tostring
Implements the tostring meta method to make possible to pretty-print Entities from Lua.
Example of Usage
## Example of Usage
```lua
-- bind the exposed entity UserData to a local variable
local entityAPI = alacrity.Entity
-- create a new random unique user ID
local id = alacrity.uuid.new_v4()
-- create a new Entity instance
local e = entityAPI.new({
id = id,
name = "Ad-Hoc Test Entity",
description = "Ad-Hoc entity for testing",
attributes = {},
tags = {
{ name = "humanoid", value = "minotaur" },
}
})
print(e)
-- will print
--[[
Entity {
id: b5e4f8c559f64bd4be376328a2270220,
blueprint: "",
name: "Ad-Hoc Test Entity",
description: "Ad-Hoc entity for testing",
tags: {
"humanoid": Tag {
name: "humanoid",
value: Value {
kind: String(
"minotaur",
),
},
},
},
attributes: {},
properties: {},
buffs: {},
version: "0.0.1",
}
]]
Alacrity Buffs
In game development and designing, creating a dynamic and engaging gameplay often involves introducing various effects and modifications to game entities. Buffs are a fundamental concept in achieving this, allowing you to apply temporary or permanent changes to an entity's attributes, properties, or behavior. Alacrity provides a robust framework for implementing buffs seamlessly within your game.
Understanding Buffs
Buffs are essentially effects that alter attributes or other characteristics of game entities. These effects can be positive, granting benefits like increased strength or enhanced speed, or negative, imposing penalties such as reduced health or decreased accuracy. The beauty of buffs lies in their dynamic nature -they can be temporary, lasting for a set duration, or permanent until explicitly removed-.
In Alacrity, buffs are a key mechanism for introducing diversity and complexity to your game's mechanics. Whether you're designing an RPG, strategy game, or any genre when entities evolve over time, buffs play a pivotal role in shaping gameplay experiences.
Buffs in Alacrity
Alacrity's approach to buffs simplifies the implementation of these effects while providing powerful customization options. Buffs can be applied to entities using various conditions, allowing for precise control over when and how they affect gameplay. From enhancing a character's abilities during combat to applying environmental effects, buffs can be tailored to meet your game's unique requirements.
Alacrity ensures that buffs seamlessly integrate with entities, attributes and other components of your game. Buffs can modify specific attributes or trigger complex behaviors based on the defined conditions and rules. This makes possible to create intricate interaction and mechanics, all managed through Alacrity's intuitive APIs.
Buff Creation and Application
In Alacrity, creating and applying buffs is a versatile process that can be approached from different angles. Buffs can be defined programmatically using the Rust or Lua APIs, offering flexibility and control. Alternatively, buffs can be specified declaratively within Alacrity Specs using Lua syntax, streamlining the design process.
This chapter will explore both avenues of buff creation, guiding you through practical examples and clear explanations. By the end, you'll have a solid understanding of how to implement buffs effectively within the Alacrity framework.
Whether you're aiming to create a complex spell system, introduce temporary power-ups, or add strategic debuffs, Alacrity's buff system empowers you to elevate your game's mechanics and offer players an engaging and immersive experience.
Let's dive into the world of buffs in Alacrity, discovering how these dynamic effects can transform your game entities and contribute to the richness of your gameplay.
Anatomy of a Buff
Buffs are composed of several fields that define their capabilities and behavior with in the simulation.
| Field | Type | Mandatory? |
|---|---|---|
| id | UUID | yes |
| blueprint | String | no |
| name | String | yes |
| description | String | yes |
| effect_id | u32 | yes |
| modifiers | HashMap<Uuid, Modifier> | yes |
| conditions | Vec<Condition> | no |
| effect_stack_policy | EffectStackPolicy | no |
| effect_stack_limit | u32 | no |
| expiration | Expiration | no |
| activation_time | Option<Instant> | no |
| stack_policy | StackPolicy | no |
| stack_limit | usize | yes |
| current_stack | usize | no |
| propagation_map | PropagationMap | no |
| version | String | no |
We will review all of these fields in detail below
ID
Each buff must have an unique ID that identifies it within the game world. IDs are UUIDs and they must be guaranteed to be unique. Library users are responsible from creating and providing these unique IDs, this is normally done while describing the buffs in the game specs.
💡 Tip
For additional guidance about how to properly generate these IDs please, refer to the Generating UUIDs guide.
Blueprint
When a buff is instantiated from a template or blueprint, this field will contain the ID that refers to said template or blueprint.
Name
This is a human-readable name for the buff, which can be used for display purposes, debugging, and other similar actions.
Description
A brief description of the buff, which can be used for documentation, tooltips and other similar purposes.
Effect ID
The buff's effect ID is used to define the effect that this buff will apply to the entity that it is affected by it.
The effect_id is used to avoid the stack up of different buffs that could apply
a similar kind of effect to the entity (like limiting the damage multiplier to
enemies of an specific kind). This behavior can still be controlled by the
effect_stack_policy field that can be set to different values that provide
different behaviors.
Modifiers
The modifiers that this buff is holding and that will modify the entity's attributes when the buff its applied to them.
ℹ️ Info
Modifiers are reviewed extensively in their own modifiers section later in the book
Conditions
A set of conditions that will be evaluated in order to determine if the buff should be applied or not to an entity based in a conditional expression written in Lua syntax using entity scopes.
ℹ️ Info
Conditions are reviewed extensively in their own conditions section later in the book
Effect Stack Policy
Determines how the buff will stack with other buffs that define the same
effect_id that this buff and that are applied simultaneously to the entity.
The available policies are defined in the table below
| Policy | Description |
|---|---|
| NoEffectStackable | The buff will not stack with other buffs of the same effect_id |
| LastReplace | This buff will replace the last buff applied to the entity that has the same effect_id |
| HigherValueReplace | This buff will replace other buffs applied to the entity that have the same effect_id and their values are lower than the value of this buff |
| LowerValueReplace | This buff will replace other buffs applied to the entity that have the same effect_id and their values are higher than the value of this buff |
| FullStack | This buff will stack with other buffs applied to the entity that have the same effect_id until the maximum stack value is reached |
Effect Stack Limit
Defines how many times effects can be stacked up in case that the buff features
a FullStack effect stack policy. This number limits how many buffs of the same
effect_id can be stacked up into each other in the same entity.
Expiration
Defines the rules about this buff effects expiration times.
An example Expiration value could be
#![allow(unused)] fn main() { use alacrity_lib::prelude::*; // instantiate a new Expiration value that will expire in 30 minutes let exp = Expiration::new(1800); }
Activation Time
Registers the last time that the buff was activated in the entity.
Stack Policy
Defines how this buff will stack with itself if it gets re-applied to the entity after having being activated already.
The available policies are defined in the table below
| Policy | Description |
|---|---|
| NoStack | The buff or modifier can not be stacked at all |
| RefreshDuration | Stacking this buff or modifier will refresh its duration (Expiration) |
| StackDuration | Stacking this buff or modifier will stack its duration but not its value (add time to its duration) |
| StackValue | Stacking this buff or modifier will stack its value but not its duration (this is the default behavior) |
| StackValueAndRefreshDuration | Stacking this buff or modifier will stack its value and will refresh its duration |
| StackValueAndDuration | Stacking this buff or modifier will stack both its value and its duration |
Stack Limit
Defines the maximum number of times that this buff can be stacked up in case
that the buff features a FullStack stack policy or if the ame buff is
re-applied to the same entity. If the stack_limit is set to zero (that
happens to be its default value), then this buff will stack indefinitely.
Current Stack
Keeps track of the current stack count of the buff.
Propagation Map
The propagation map helps the buff to define propagation rules that must be followed for its effects to propagate to other entities.
📝 Note
Buff propagation is a very advanced topic that requires of library users to implement their own networking layer or entity-to-entity communication if needed. They are being review in more detail in their own buff propagations section.
Version
The buff's version. It can be used to track buff versions or to update their data when a version change is detected. Each buff blueprint has to be in an specific version (0.0.1 by default).
Alacrity Conditions
Conditions serve as the intricate logic gates within the Alacrity engine, shaping the behavior of buffs and modifiers in response to specific triggers and constraints. As a foundational component of Alacrity's entity and attribute management, conditions add a layer of precision and control, enabling you to create nuanced gameplay experiences.
In this chapter we delve into the technical underpinnings of conditions within the Alacrity library. We explore how conditions are defined, the diverse ways they can be employed, and their pivotal role in tailoring the impact of buffs and modifiers on game entities.
Understanding Conditions
Conditions allows you to define and model how and when buff and modifiers should be applied to entities.
At its core, a condition in Alacrity represents a logical statement or test
that evaluates to either true or false. Conditions act as filters,
allowing buffs and modifiers to affect entities only when specific criteria
are met. These criteria can encompass a wide range of factors, from attribute
values and entity tags to complex mathematical expressions.
Alacrity's flexible condition system accommodates a variety of use cases, whether you need to implement conditional abilities, environmental effects, or intricate attribute interactions. By crafting conditions with precision, you can achieve fine-grained control over how your game mechanics respond to different in-game situations.
The Triple Role of Conditions
Conditions are versatile in their application, serving three primary roles:
1. Buff Triggers
Conditions act as a gatekeepers for buffs, determining when and on which entities these effects take effect. Buffs can specify conditions that must be satisfied before they are applied. This allows you to create spells, status effects, passives or item bonuses that only activate under specific circumstances, enhancing the strategic depth of your game.
2. Modifier Filters
Modifiers, which alter attribute values, can also leverage conditions to fine tune their impact. Conditions applied to modifiers dictate when these modifiers come into play. Modifiers can target entities and attributes selectively, ensuring that attributes adjustments align with your desired gameplay mechanics and designs.
3. Propagation Trigger
Similar to Buff triggers, Propagations can make use of conditions to determine if a buff effect should propagate to other entities related with the entity that the buff is being activated on. This, together with the entity's tagging system allow you to create intricate effects that are shared with other entities in the relation tree when specific conditions are satisfied.
Conditions Expression Language
Alacrity's condition system leverages the versatile Lua language expression syntax that empowers you to craft complex logical statements. You can utilize mathematical operators, relational comparison, and logical operators to construct intricate conditions. The expressive power of Lua allows for the creation of conditions that adapt to the dynamic nature of your game.
Condition Workflow
Throughout this chapter, we will explore the lifecycle of conditions within Alacrity, from their definition to practical implementation. We will illustrate how conditions can be authored programmatically using both Rust and Lua APIs, as well as declarative using Alacrity Lua Specs.
By the end of this chapter, you will have the knowledge and tools to harness the full potential of conditions within Alacrity. You will be equipped to design sophisticated buff systems, attribute interactions, and gameplay mechanics that respond intelligently to the evolving state of your game world.
Let's embark on a technical journey into the realm of conditions within the Alacrity library, where precision meets gameplay depth.
Anatomy of a Condition
A Condition is represented by the Condition struct in Alacrity, and it is
the cornerstone of creating conditional behaviors for your buffs. Let's
dissect the components of this data structure.
| Field | Type | Mandatory? |
|---|---|---|
| id | UUID | yes |
| blueprint | String | no |
| expression | String | yes |
| description | String | yes |
| logical_operator | bool | yes |
| version | String | no |
ID
A universally unique identifier (UUID) is assigned to each Condition. This ID distinguishes one Condition from another, ensuring each condition is unique.
The ID aids in referencing and managing conditions throughout the Alacrity system.
💡 Tip
For additional guidance about how to properly generate these IDs please, refer to the Generating UUIDs guide.
Blueprint
The blueprint field holds the original blueprint of the Condition. Blueprints act as templates for creating condition instances.
Blueprints enable the reusability of conditions across multiple buffs and modifiers.
Expression
The heart of the Condition, the expression defines the logical criteria that must
be met for the condition to evaluate to either true or false.
Expressions can encompass various conditionals, such as attributes comparisons, mathematical calculations and logical operations. They determine the condition's behavior.
ℹ️ Tip
You can check the Expressions section of this chapter if you want to know more in depth details about Alacrity's conditional system expressions.
Description
The condition description provides of human readable explanation of the condition's purpose and requirements.
Descriptions enhance code readability and facilitate debugging by providing context to developers.
Logical Operator
The logical operator output field specifies whether the condition must evaluate
to true or false for its criteria to be met.
Logical operators enable you to create conditions that trigger buffs and modifiers under specific circumstances, enhancing gameplay dynamics.
ℹ️ Tip
You can check the Logical Operator section on this chapter if you want to know more in depth details about conditions logical operators.
Version
The version field denotes the current version of the condition. Version ensure compatibility and allow for updates or modifications to conditions over time.
Condition Expressions
Condition expressions are the backbone of decision-making in the Alacrity library. They form the logical core that dictates when buffs and modifiers are applied to entities. These expressions, crafted with precision, allow you to imbue your game with complex and dynamic behaviors.
In this chapter, we delve into the intricacies of condition expressions within Alacrity. We'll explore their syntax, capabilities, and practical applications. As you navigate through this technical guide, you'll gain a comprehensive understanding of how to harness the power of condition expressions to create sophisticated and responsive gameplay experiences.
The Role of Condition Expressions
At their essence, condition expressions are logical statements written in Lua. They serve as the criteria that must be satisfied for a specific action to occur. These actions can include applying a buff to an entity, triggering a modifier, or determining the outcome of an in-game event.
Lua-Powered Logic
Lua, a lightweight and efficient scripting language, is the driving force behind of Alacrity's condition expressions. With its expressive syntax and extensibility, Lua provided a robust foundation for creating complex logic.
📝 Note
Alacrity makes use of Luau a more secure and versatile Lua version developed by Roblox for Condition expressions, Lua Specs and Lua API.
Dynamic Behavior
Condition expressions allow your game to respond dynamically to in-game events and entity states. They empower you to create conditional logic that adapts to changing circumstances, enhancing player immersion and engagement.
In the following sections, we will delve into the technical details of Alacrity's condition expression language. We will explore the available operators, functions and best practices for crafting effective and efficient conditions.
With a firm grasp of condition expressions, you'll be well-equipped to implement beautiful gameplay mechanics, responsive AI behaviors and bring your game world to life within the Alacrity framework.
Expression Evaluation
Alacrity evaluates condition expressions against a given scope and determines
whether the conditions are met. The evaluation process serves to answer the
fundamental question: Should a particular action, such as applying a buff or
modifier take place?.
The Evaluation Process
The evaluation process has several steps, we are breaking down it step by step so you can understand better how a Condition is internally evaluated in Alacrity.
1. Lua Environment Initialization
Alacrity initializes a new mlua environment that serves as a workspace for
evaluating condition expressions.
2. Binding the Scope
The Scope represents the entity's current state, is bound to the Lua
environment as self. This binding allows the condition expression to access
and assess the entity's attributes and properties.
Any attribute or property attached to the entity can be accessed using the
dot [.] notation in the self special variable. For example:
self.level >= 10
3. Expression Evaluation
The method loads the condition expression and evaluates it within the Lua environment. This evaluation results in a boolean value that indicates wether the condition is met.
4. Comparison with Expected Result
The evaluated result is compared to the expected result, which is defined by the
[Logical Operator] field of the condition. If the evaluated result matches the
expected result, the condition is considered met and true is returned. Otherwise
if there is a mismatch, false is returned.
Available Operators and Functions
Alacrity's condition expressions support a range of operators and functions, empowering you to craft intricate logic. These include:
Comparison Operators
You can use operators like == (equals), ~= (not equals), < (less than),
> (greater than), <= (less than or equal to), and >= (greater than or equal)
to compare values.
Logical Operators
Logical operators such as and, or and not enable you to create complex
conditional statements.
Mathematical Operations
You can perform mathematical calculations within condition expressions using
operators like + (addition), - (subtraction), * (multiplication) and /
(division).
Functions
Any function present in Luau library can be used in the Condition expressions.
Best Practices for Efficient Conditions
Creating efficient conditions is crucial for maintaining optimal game performance. Consider the following best practices when creating Conditions.
Minimize Complexity
Keep condition expressions as simple as possible. Complex conditions can lead to performance bottlenecks.
Avoid Heavy Computation
Limit intensive calculations within conditions, as they can impact gameplay responsiveness.