Effects and Preconditions
When building our simulations, there may be times when we want traits or job roles to have side-effects on characters. For example, maybe we want:
Customer service jobs to gradually increase a character’s sociability stat
A
flirtatious
trait that makes other characters more likely attracted to the flirtatious character.Require characters to have a high enough
cooking
skill before they become the owner of a restaurant.Make characters with the
shopaholic
trait to frequent shopping locations.
All these scenarios are accomplished through using Effect
and Precondition
types.
Effect
objects perform modifications to a GameObject when applied and undo their modifications when removed. Effects are used with traits and social rules to make changes to characters and relationships.
Precondition
objects check GameObjects for certain conditions. If these conditions are met, they return true.
Using effects and preconditions
Effects are specified within the effects
section of traits. Each effect type
is internally mapped to a class type of the same name that inherits from Neighborly’s Effect
base class. Within the key-value pair specification, all other key-value pairs aside from the type
key are passed to a function that constructs a new instance of that effect type.
The example below shows a trait with multiple effects. All effects are applied when a trait is attached. One of the effects adds a new location preference rule that accepts a list of preconditions. Like effects, precondition specifications require the user to provide a type: …, and all other key-value pairs are used to parameterize an instance of that precondition type. Here we specify that this location preference only applies to places that serve alcohol.
Below is an example of a drinks_too_much
trait definition as it would appear in a JSON data file.
{
"drinks_too_much": {
"display_name": "Drinks too much",
"effects": [
{
"type": "StatBuff",
"stat": "health_decay",
"amount": 0.05,
"modifier_type": "FLAT"
},
{
"type": "AddLocationPreference",
"preconditions": [
{
"type": "HasTrait",
"trait": "serves_alcohol"
}
],
"amount": 0.2
}
]
}
}
Below is the same trait defined using YAML.
drinks_too_much:
display_name: Drinks too much
effects:
- type: StatBuff
stat: health_decay
amount: 0.05
modifier_type: FLAT # <-- This is optional (defaults to FLAT)
- type: AddLocationPreference
preconditions:
- type: HasTrait
trait: serves_alcohol
amount: 0.2
Finally, this is how this trait might be defined directly within Python.
trait_lib = sim.world.resource_manager.get_resource(TraitLibrary)
trait_lib.add_definition_from_obj(
{
"definition_id": "drinks_too_much",
"display_name": "Drinks too much",
"effects": [
{
"type": "StatBuff",
"stat": "health_decay",
"amount": 0.05,
"modifier_type": "FLAT"
},
{
"type": "AddLocationPreference",
"preconditions": [
{
"type": "HasTrait",
"trait": "serves_alcohol"
}
],
"amount": 0.2
}
]
}
)
Built-in effects
Below are a list of all the currently built-in effect types. If users want to add more, they can create a new subclass of Effect
and register their effect type using.
sim.world.resource_manager.get_resource(EffectLibrary).add_effect_type(CustomEffect)
neighborly.effects.effects.StatBuff
neighborly.effects.effects.IncreaseSkill
neighborly.effects.effects.AddSocialRule
neighborly.effects.effects.AddLocationPreference
Defining new effects
users can create new event types that can be access from JSON definitions. First, you need to create a new class that inherits from the Effect
class and then register that effect by adding it to the library. See the example code below for the StatBuff
effect.
class StatBuff(Effect):
"""Add a buff to a stat."""
__slots__ = "modifier_type", "amount", "stat_id"
modifier_type: StatModifierType
"""The how the modifier amount should be applied to the stat."""
amount: float
"""The amount of buff to apply to the stat."""
stat_id: str
"""The definition ID of the stat to modify."""
def __init__(
self,
stat_id: str,
amount: float,
modifier_type: StatModifierType,
) -> None:
super().__init__()
self.stat_id = stat_id
self.modifier_type = modifier_type
self.amount = amount
@property
def description(self) -> str:
return (
f"add {self.amount}({self.modifier_type.name}) modifier to {self.stat_id}"
)
def apply(self, target: GameObject) -> None:
get_stat(target, self.stat_id).add_modifier(
StatModifier(
modifier_type=self.modifier_type,
value=self.amount,
source=self,
)
)
def remove(self, target: GameObject) -> None:
get_stat(target, self.stat_id).remove_modifiers_from_source(self)
@classmethod
def instantiate(cls, world: World, params: dict[str, Any]) -> Effect:
modifier_name: str = params.get("modifier_type", "FLAT")
amount: float = float(params["amount"])
stat_id: str = str(params["stat"])
modifier_type = StatModifierType[modifier_name.upper()]
return cls(stat_id=stat_id, amount=amount, modifier_type=modifier_type)
# Register the effect type with the library
self._world.resource_manager.get_resource(EffectLibrary).add_effect_type(
StatBuff
)
Built-in preconditions
HasTrait
: Checks if a GameObject has a traitParameters:
trait
: (str) The ID of the trait to check for
TargetHasTrait
: (For social rules only) Checks if the target of the relationship has a traitParameters:
trait
: (str) The ID of the trait to check for
SkillRequirement
: Check if the character has a skill level of at least a given levelParameters:
skill
: (str) The ID of the skill to checklevel
: (int) The required skill level
AtLeastLifeStage
: Check if a character is of at least the given life stageParameters:
life_stage
: (str) “CHILD”, “ADOLESCENT”, “YOUNG_ADULT”, “ADULT”, or “SENIOR”
TargetIsSex
: (For social rules only) Check if the target of the relationship is a given sexParameters:
sex
: (str) “MALE”, “FEMALE”, or “NOT_SPECIFIED”
TargetLifeStageLT
: (For social rules only) Check if the target of the relationships life stage is less than the given life stage.Parameters:
life_stage
: (str) “CHILD”, “ADOLESCENT”, “YOUNG_ADULT”, “ADULT”, or “SENIOR”
Defining new Preconditions
Defining new Precondition subtypes is similar to the process for creating new Effect types. Users need to create a new Python class that inherits from the Precondition
abstract class. You will need to implement all the abstract methods and finally add the class to the PreconditionLibrary
.
The follow is an example using the HasTrait
precondition.
class HasTrait(Precondition):
"""A precondition that check if a GameObject has a given trait."""
__slots__ = ("trait_id",)
trait_id: str
"""The ID of the trait to check for."""
def __init__(self, trait: str) -> None:
super().__init__()
self.trait_id = trait
@property
def description(self) -> str:
return f"has the trait {self.trait_id}"
def __call__(self, target: GameObject) -> bool:
return has_trait(target, self.trait_id)
@classmethod
def instantiate(cls, world: World, params: dict[str, Any]) -> Precondition:
trait = params["trait"]
return cls(trait)
# Add the precondition class to the library
self.world.resource_manager.get_resource(PreconditionLibrary).add_precondition_type(
HasTrait
)