Skills
Neighborly uses skills to track each character’s proficiency levels for various tasks such as cooking, swordsmanship, seduction, marksmanship, water magic, explosion magic, and salesmanship. Skills provide an alternative an alternative method of representing character stats. Every skill has a value in the range 0
to 255
, with 255
meaning that a character has maxed out that skill.
Characters can be given skills when generated, and they can acquire skills from various jobs. Skills are mainly used as preconditions for characters qualifying for specific job roles at businesses within the settlement. The Businesses wiki page explains how to perform skill checks as preconditions for job roles. Skills may also be used as event probability considerations or buffs when calculating the probability of success when performing a behavior.
How are skills represented?
At runtime, skills are represented as individual GameObjects with one Skill
component containing basic metadata (name and description). Each character’s specific skills are managed by their Skills
(note the plural ‘s’) component that contains a map of skill GameObjects to Stat
instances that track the level of the skills.
Specifying skill data
Users specify new skills in data (JSON or YAML) files. Below are examples of a few skills. Each definition starts with a definition ID that is unique to this skill. Each definition has two optional attributes, the display_name
and description
. The display name is a regular English name given to the skill. Unlike the definition IDs, display names do not need to be unique. While it may be confusing, multiple skill definitions can have the same display name. The description is a short text blurb about what the skill represents. They help when generating prose descriptions of characters and their skills.
Naming conventions for definition IDs:
Do not use spaces. Use underscores or dashes
IDs should be all lowercase letters
Defining skills in JSON:
{
"cooking": {
"display_name": "Cooking",
"description": "A measure of how good a character is at cooking delicious food."
},
"swordsmanship": {
"display_name": "Swordsmanship",
"description": "A measure of how well a character can handle a sword."
},
"explosion_magic": {
"display_name": "Explosion Magic",
"description": "A measure of how proficient a character is at using explosion magic."
}
}
Defining skills in YAML:
cooking:
display_name: Cooking
description: A measure of how good a character is at cooking delicious food.
swordsmanship:
display_name: Swordsmanship
description: A measure of how well a character can handle a sword.
explosion_magic:
display_name: Explosion Magic
description: A measure of how proficient a character is at using explosion magic.
Defining skills directly in Python:
Users can define skills using Python dicts or directly using definition classes.
skill_lib = sim.world.resource_manager.get_resource(SkillLibrary)
# Option 1: This uses a specific class to construct the definition
cooking = DefaultSkillDef(
definition_id="cooking",
display_name="Cooking",
description="A measure of how good a character is at cooking delicious food."
)
skill_lib.add_definition(cooking)
# Option 2: This uses a data dict and lets the library choose what definition class
# to use when constructing the definition. This is how data is loaded from data files
swordsmanship = {
"definition_id": "Swordsmanship"
"display_name": "Swordsmanship",
"description": "A measure of how well a character can handle a sword."
}
skill_lib.add_definition_from_obj(swordsmanship)
Using skills from Python
Skills are tracked in Skills
components attached to characters. Most of the time, if you’re writing Python code to modify skills, you will want to avoid changing this component directly. Instead, you will want to interface with them using the provided helper function(s) in the neighborly.helpers.skills
module. Below is an example of adding, retrieving, and modifying skills.
from neighborly.simulation import Simulation
from neighborly.loaders import load_characters, load_skills
from neighborly.helpers.character import create_character
from neighborly.helpers.skills import get_skill
from neighborly.components.stats import StatModifier, StatModifierType
sim = Simulation()
# Load authored data for generating characters and skills
load_characters(sim, "path/to/file")
load_skills(sim, "path/to/file")
# Instantiate the simulation to process loaded skill definitions
sim.instantiate()
# Create a new character
character = create_character(sim.world, "person")
# Add a cooking skill to the character
add_skill(character, "cooking", 0)
# Get a character's skill
cooking_skill = get_skill(character, "cooking")
# Change the base value
cooking_skill.base_value += 1
# Add stat modifiers
cooking_skill.add_modifier(
StatModifier(
modifier_type=StatModifierType.Flat,
value=25,
)
)
# Print the final calculated value
print(cooking_skill.value)
# 26
Advanced: Creating custom skill definition classes
Users who want to add fields to the skill definitions or change how skill definitions are instantiated will need to define new SkillDef
subclasses. This might be the case if you want to use a custom text generator to create skill descriptions and names. By default, Neighborly uses the DefaultSkillDef
class to store skill definitions that are loaded from external data files or definitions loaded directly into the SkillLibrary
using the add_definition_from_obj()
method. Users can supply new definition classes in Python and set a specific definition class as the default when loading new skill definition data.
Note the following terms:
“definition data”: the parameters passed to a definition class
“definition”: an instance of a definition class (constructed in Python)
“definition type/class”: the Python class definition used to create instances of definitions
Step 1: Create a new SkillDef
subclass
The first step is creating a new class that inherits directly or indirectly from SkillDef
. For this example, we will inherit from DefaultSkillDef
. All skill definition classes need to override the following two abstract methods:
neighborly.defs.base_types.SkillDef.from_obj()
neighborly.defs.base_types.SkillDef.initialize()
Below, we have Python pseudocode for defining a new definition class called CustomSkillDef
. Users can add new class instance variables directly in the function body. Skill definitions are Python data classes created using attrs. Here, we add a string variable for a large language model for name/description generation.
class CustomSkillDef(SkillDef):
"""A custom skill definition that uses an LLM to generate names and descriptions."""
llm_model_name: str
"""The name of the LLM to use for text generation."""
@classmethod
def from_obj(cls, obj: dict[str, Any]) -> SkillDef:
definition_id = obj["definition_id"]
display_name = obj.get("display_name", definition_id)
description = obj.get("description", "")
# The code below gets the llm_model_name from the dict or
# an empty string if none is provided
model_name = obj.get("llm_model", "")
return cls(
definition_id=definition_id,
display_name=display_name,
description=description,
)
def initialize(self, skill: GameObject) -> None:
if self.llm_model_name == "gpt4":
skill_name = ... # Do GPT-4 stuff
description = ... # Do GPT-4 stuff
if self.llm_model_name == "gpt3":
skill_name = ... # Do GPT-3 stuff
description = ... # Do GPT-3 stuff
else:
# Default to tracery
tracery = skill.world.resource_manager.get_resource(Tracery)
skill_name = tracery.generate(self.display_name)
description = tracery.generate(self.description)
skill.add_component(
Skill(
definition_id=self.definition_id,
display_name=skill_name,
description=description,
)
)
Step 2: Add the definition class to the library
The next step before we can use this custom definition is to add it to the SkillLibrary
using add_definition_type()
. This method makes the definition available when loading data from data files. It allows users to override the default definition class used to load definition data.
The following code should be placed inside a plugin’s load_plugin()
function. However, it can be placed anywhere after the simulation has been instantiated and before any content is loaded from external files.
skill_lib = sim.world.resource_manager.get_resource(SkillLibrary)
skill_lib.add_definition_type(
CustomSkillDef, alias="custom", set_default=True
)
Step 3: Use the definition from within a data file
The following shows how to use the custom definition type within a YAML data file. Notice that the definition supplies the definition_type
attribute. This tells Neighborly to load this definition data using the definition type saved to the given alias name. If definition_type
is not given, Neighborly will default to the last definition type added to the library with set_default=True
.
gift_of_gab:
definition_type: custom
llm_model: gpt4
display_name: Gift of Gab
# put an LLM prompt below to pass to GPT-4
description: >-
Generate a description of a "Gift of Gab" skill