Repository: ShiJbey/neighborly (language: python)

Issue:
Builder api



Discussion / hints:

Fix patch:
diff --git a/.idea/neighborly.iml b/.idea/neighborly.iml
index 014b52a..32aa871 100644
--- a/.idea/neighborly.iml
+++ b/.idea/neighborly.iml
@@ -11,6 +11,9 @@
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>
+  <component name="PackageRequirementsSettings">
+    <option name="requirementsPath" value="" />
+  </component>
   <component name="PyDocumentationSettings">
     <option name="format" value="NUMPY" />
     <option name="myDocStringFormat" value="NumPy" />
@@ -22,6 +25,9 @@
       </list>
     </option>
   </component>
+  <component name="ReSTService">
+    <option name="DOC_DIR" value="$MODULE_DIR$/docs" />
+  </component>
   <component name="TestRunnerService">
     <option name="PROJECT_TEST_RUNNER" value="py.test" />
   </component>
diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml
index c4246bd..fe64103 100644
--- a/.idea/watcherTasks.xml
+++ b/.idea/watcherTasks.xml
@@ -41,13 +41,13 @@
       <option name="workingDir" value="$ProjectFileDir$" />
       <envs />
     </TaskOptions>
-    <TaskOptions isEnabled="true">
+    <TaskOptions isEnabled="false">
       <option name="arguments" value="$FilePath$" />
       <option name="checkSyntaxErrors" value="true" />
       <option name="description" />
       <option name="exitCodeBehavior" value="NEVER" />
       <option name="fileExtension" value="py" />
-      <option name="immediateSync" value="true" />
+      <option name="immediateSync" value="false" />
       <option name="name" value="Flake8" />
       <option name="output" value="" />
       <option name="outputFilters">
@@ -66,7 +66,7 @@
       </option>
       <option name="outputFromStdout" value="false" />
       <option name="program" value="$PyInterpreterDirectory$/flake8" />
-      <option name="runOnExternalChanges" value="true" />
+      <option name="runOnExternalChanges" value="false" />
       <option name="scopeName" value="Project Files" />
       <option name="trackOnlyRoot" value="false" />
       <option name="workingDir" value="" />
diff --git a/MANIFEST.in b/MANIFEST.in
index ad9e7c3..0a8e826 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,4 @@
 include LICENSE
 include CODE_OF_CONDUCT.md
 recursive-include src/neighborly/plugins/default_plugin/data *
+include src/neighborly/plugins/talktown/data.yaml
diff --git a/README.md b/README.md
index 650b1df..442e3d3 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
     src="https://user-images.githubusercontent.com/11076525/165836171-9ffdea6e-1633-440c-be06-b46e1e3e4e04.png"
   >
   <br>
-  Neighborly (W.I.P.)
+  Neighborly
 </h1>
 
 <h3 align="center"><b>Social simulation engine for procedurally generating towns of characters</b></h2>
@@ -17,8 +17,6 @@
   <img src="https://img.shields.io/pypi/pyversions/neighborly">
 </p>
 
-![pygame_sample_screenshot](https://user-images.githubusercontent.com/11076525/166522030-598a1fcd-3f9e-4203-ab50-8fa04722bd50.png)
-
 # Overview
 
 Neighborly is a social simulation engine for procedurally generating towns of characters. It simulates
@@ -81,16 +79,17 @@ python -m pip install -e "."
 
 # Running the Tests
 
-I am still working on providing a decent test suite, but the codebase
-changes so frequently that it hasn't been worth the time. As modules
-become more established, I will add proper tests for them. Feel free
-to contribute tests. Remember to fork the repo, add your test(s), and
-submit a pull request with a description of your test cases. Your commits
+The tests are currently out-of-date and may refer to systems
+and logic that no longer exists in Neighborly. The codebase
+changes so frequently that it hasn't been worth the time. 
+As modules  become more established, I will add proper tests for them. 
+Feel free to contribute tests by forking the repo, adding your test(s), and
+submitting a pull request with a description of your test cases. Your commits
 should only contain changes to files within the `tests` directory. If you
 change any files in other parts of the project, your PR will be rejected.
 
-Please follow the steps for how to use then run the following to download
-the dependencies for running tests.
+Please follow the steps below to run Neighborly's test suite. Neighborly uses
+[PyTest](https://docs.pytest.org/en/7.1.x/) to handle unit testing.
 
 ```bash
 # Step 1: Install dependencies for tests
@@ -102,8 +101,9 @@ pytest
 
 # Running the Samples
 
-Please follow the steps for how to use then run the following to download the
-dependencies for running the samples.
+Please follow the steps below to run the sample simulations.
+We also have examples for using Neighborly in a IPython
+notebook and with PyGame.
 
 ```bash
 # Step 1: Install dependencies for samples
@@ -115,7 +115,8 @@ python ./samples/<sample_name>.py
 
 # Documentation
 
-Neighborly uses Numpy-style docstrings and we generate the documentation using Sphinx.
+Neighborly uses Numpy-style docstrings in code and generates the full 
+documentation using Sphinx.
 
 When adding docstrings for existing or new bits of code please use the following
 references for how to format your contributions:
@@ -141,7 +142,7 @@ python -m http.server 8080 --directory _build/html
 
 If you are interested in contributing to Neighborly, feel free to fork this repository, make your changes, and submit a pull-request. Please keep in mind that this project is a tool for creativity and learning. We have a [code of conduct](./CODE_OF_CONDUCT.md) to encourage healthy collaboration, and will enforce it if we need to.
 
-**WARNING::** This repository's structure in in high flux. Parts of the code get shifted to make the API's cleaner for use.
+**WARNING::** This repository's structure in high flux. Parts of the code get shifted to make the APIs cleaner for use.
 
 Here are some ways that people can contribute to Neighborly:
 
@@ -150,7 +151,7 @@ Here are some ways that people can contribute to Neighborly:
 3. Providing optimizations
 4. Fixing typos
 5. Filing issues
-6. Contributing tutorials/how-to's to the wiki
+6. Contributing tutorials/how-tos to the wiki
 7. Fixing grammar and spelling in the wiki
 8. Creating new samples
 
diff --git a/docs/character_behavior.rst b/docs/character_behavior.rst
new file mode 100644
index 0000000..ebc36c4
--- /dev/null
+++ b/docs/character_behavior.rst
@@ -0,0 +1,38 @@
+Character Behavior
+------------------
+
+Character behavior is executed two ways.
+
+The first and primary way is life events. Life events are triggered when
+there are characters or other GameObjects that fit the specifications
+for the roles defined by the event. You can think of Roles like roles in
+a theatrical performance. GameObjects are cast into these roles if they
+meet the requirements of the role.
+
+When all roles are successfully casted, the event may trigger some additional
+code to run that makes changes to the state of the simulation. This code
+
+When an event fires, the simulation makes a record of it and the GameObjects
+casted into the roles. We can use this information later for story sifting or
+debugging.
+
+What is the difference between behaviors and events?
+====================================================
+
+Events immediately create a new event that is recorded in the
+simulation's history. They assume that when all the roles have
+been casted, there are no additional checks that could veto the
+event or directly trigger cascading events.
+
+Behaviors are an abstraction built on behavior trees. Like events,
+they cast an initial set of characters before executing the tree
+logic. These characters are stored in a roles array within the
+behavior tree's blackboard. Each leaf node in the tree
+receives the current blackboard and world instances an parameters.
+Nodes are responsible for performing checks and executing **Actions**.
+
+**Reserved Blackboard Keys**
+
+* "roles" - List of roles cast before and during execution
+* "trigger" - Previous BehaviorTree/Action that triggered this (optional)
+* "" -
diff --git a/docs/modules.rst b/docs/modules.rst
deleted file mode 100644
index a817a50..0000000
--- a/docs/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-neighborly
-==========
-
-.. toctree::
-   :maxdepth: 4
-
-   neighborly
diff --git a/docs/neighborly.ai.rst b/docs/neighborly.ai.rst
deleted file mode 100644
index 391511b..0000000
--- a/docs/neighborly.ai.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-neighborly.ai package
-=====================
-
-Submodules
-----------
-
-neighborly.ai.behavior\_tree module
------------------------------------
-
-.. automodule:: neighborly.ai.behavior_tree
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.ai.behavior\_utils module
-------------------------------------
-
-.. automodule:: neighborly.ai.behavior_utils
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.ai.character\_behavior module
-----------------------------------------
-
-.. automodule:: neighborly.ai.character_behavior
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.ai.loader module
----------------------------
-
-.. automodule:: neighborly.ai.loader
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.ai
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.core.character.rst b/docs/neighborly.core.character.rst
deleted file mode 100644
index c1fbc83..0000000
--- a/docs/neighborly.core.character.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-neighborly.core.character package
-=================================
-
-Submodules
-----------
-
-neighborly.core.character.character module
-------------------------------------------
-
-.. automodule:: neighborly.core.character.character
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.character.values module
----------------------------------------
-
-.. automodule:: neighborly.core.character.values
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.core.character
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.core.rst b/docs/neighborly.core.rst
deleted file mode 100644
index d44348d..0000000
--- a/docs/neighborly.core.rst
+++ /dev/null
@@ -1,158 +0,0 @@
-neighborly.core package
-=======================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   neighborly.core.character
-   neighborly.core.utils
-
-Submodules
-----------
-
-neighborly.core.activity module
--------------------------------
-
-.. automodule:: neighborly.core.activity
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.business module
--------------------------------
-
-.. automodule:: neighborly.core.business
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.ecs module
---------------------------
-
-.. automodule:: neighborly.core.ecs
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.engine module
------------------------------
-
-.. automodule:: neighborly.core.engine
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.life\_event module
-----------------------------------
-
-.. automodule:: neighborly.core.life_event
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.location module
--------------------------------
-
-.. automodule:: neighborly.core.location
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.name\_generation module
----------------------------------------
-
-.. automodule:: neighborly.core.name_generation
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.position module
--------------------------------
-
-.. automodule:: neighborly.core.position
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.processors module
----------------------------------
-
-.. automodule:: neighborly.core.processors
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.relationship module
------------------------------------
-
-.. automodule:: neighborly.core.relationship
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.residence module
---------------------------------
-
-.. automodule:: neighborly.core.residence
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.rng module
---------------------------
-
-.. automodule:: neighborly.core.rng
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.routine module
-------------------------------
-
-.. automodule:: neighborly.core.routine
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.social\_network module
---------------------------------------
-
-.. automodule:: neighborly.core.social_network
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.status module
------------------------------
-
-.. automodule:: neighborly.core.status
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.time module
----------------------------
-
-.. automodule:: neighborly.core.time
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.core.town module
----------------------------
-
-.. automodule:: neighborly.core.town
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.core
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.core.utils.rst b/docs/neighborly.core.utils.rst
deleted file mode 100644
index 43e30a7..0000000
--- a/docs/neighborly.core.utils.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-neighborly.core.utils package
-=============================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   neighborly.core.utils.tracery
-
-Submodules
-----------
-
-neighborly.core.utils.utilities module
---------------------------------------
-
-.. automodule:: neighborly.core.utils.utilities
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.core.utils
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.core.utils.tracery.rst b/docs/neighborly.core.utils.tracery.rst
deleted file mode 100644
index 17015d0..0000000
--- a/docs/neighborly.core.utils.tracery.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-neighborly.core.utils.tracery package
-=====================================
-
-Submodules
-----------
-
-neighborly.core.utils.tracery.modifiers module
-----------------------------------------------
-
-.. automodule:: neighborly.core.utils.tracery.modifiers
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.core.utils.tracery
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.data.rst b/docs/neighborly.data.rst
deleted file mode 100644
index d503b0a..0000000
--- a/docs/neighborly.data.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-neighborly.data package
-=======================
-
-Submodules
-----------
-
-neighborly.data.businesses module
----------------------------------
-
-.. automodule:: neighborly.data.businesses
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.data.events module
------------------------------
-
-.. automodule:: neighborly.data.events
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.data.relationship\_tags module
------------------------------------------
-
-.. automodule:: neighborly.data.relationship_tags
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.data.statuses module
--------------------------------
-
-.. automodule:: neighborly.data.statuses
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.data
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.plugins.default_plugin.rst b/docs/neighborly.plugins.default_plugin.rst
deleted file mode 100644
index 2028ca8..0000000
--- a/docs/neighborly.plugins.default_plugin.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-neighborly.plugins.default\_plugin package
-==========================================
-
-Module contents
----------------
-
-.. automodule:: neighborly.plugins.default_plugin
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.plugins.rst b/docs/neighborly.plugins.rst
deleted file mode 100644
index fd29bcf..0000000
--- a/docs/neighborly.plugins.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-neighborly.plugins package
-==========================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   neighborly.plugins.default_plugin
-
-Submodules
-----------
-
-neighborly.plugins.weather\_plugin module
------------------------------------------
-
-.. automodule:: neighborly.plugins.weather_plugin
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly.plugins
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/neighborly.rst b/docs/neighborly.rst
deleted file mode 100644
index 804d98b..0000000
--- a/docs/neighborly.rst
+++ /dev/null
@@ -1,48 +0,0 @@
-neighborly package
-==================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   neighborly.ai
-   neighborly.core
-   neighborly.data
-   neighborly.plugins
-
-Submodules
-----------
-
-neighborly.exporter module
---------------------------
-
-.. automodule:: neighborly.exporter
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.loaders module
--------------------------
-
-.. automodule:: neighborly.loaders
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-neighborly.simulation module
-----------------------------
-
-.. automodule:: neighborly.simulation
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: neighborly
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/pyproject.toml b/pyproject.toml
index 7149e21..374b58c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,6 +4,3 @@ requires = [
     "wheel"
 ]
 build-backend = "setuptools.build_meta"
-
-[tool.black]
-target-version = ['py38']
diff --git a/samples/generating_characters.py b/samples/generating_characters.py
deleted file mode 100644
index ff792b1..0000000
--- a/samples/generating_characters.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import random
-from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import GameObject
-from neighborly.loaders import YamlDataLoader
-from neighborly.simulation import NeighborlyConfig, Simulation, SimulationConfig
-from neighborly.core.life_event import (
-    EventCallbackDatabase,
-    LifeEvent,
-    event_effect,
-    handle_gameobject_effects,
-)
-
-CHARACTER_DATA = """
-CharacterDefinitions:
-  - name: Human
-    generation:
-      first_name: "#first_name#"
-      last_name: "#last_name#"
-      family:
-        probability_spouse: 0.5
-        probability_children: 0.5
-        num_children: "0-2"
-    lifecycle:
-      lifespan: 85
-      life_stages:
-        child: 0
-        adolescent: 13
-        young_adult: 18
-        adult: 30
-        elder: 85
-    events:
-      marriage:
-        effects: [on_married_behavior]
-      child-birth:
-        effects: [on_child_birth_behavior]
-      move:
-        effects: [on_move_behavior]
-      custom-event:
-        effects: [on_custom_event]
-Characters:
-  - name: Human
-    components:
-      - type: GameCharacter
-        options:
-          character_def: Human
-"""
-
-DEFAULT_NEIGHBORLY_CONFIG = NeighborlyConfig(
-    simulation=SimulationConfig(
-        seed=random.randint(0, 999999),
-        hours_per_timestep=6,
-        start_date="0000-00-00T00:00.000z",
-        end_date="0100-00-00T00:00.000z",
-    ),
-    plugins=["neighborly.plugins.default_plugin"],
-)
-
-
-def on_married_behavior(gameobject: GameObject, event: LifeEvent) -> bool:
-    print("I'm Got Married!")
-    return True
-
-
-def on_child_birth_behavior(gameobject: GameObject, event: LifeEvent) -> bool:
-    print("I had a child!")
-    return True
-
-
-def on_move_behavior(gameobject: GameObject, event: LifeEvent) -> bool:
-    print("I moved to a new house!")
-    return True
-
-
-@event_effect("on_custom_event")
-def on_custom_event(gameobject: GameObject, event: LifeEvent) -> bool:
-    if not isinstance(event, CustomLifeEvent):
-        raise TypeError(f"Expected CustomLifeEvent but was {type(event)}")
-    print(str(event))
-    return True
-
-
-def setup(sim: Simulation):
-    EventCallbackDatabase.register_effect("on_married_behavior", on_married_behavior)
-    EventCallbackDatabase.register_effect(
-        "on_child_birth_behavior", on_child_birth_behavior
-    )
-    EventCallbackDatabase.register_effect("on_move_behavior", on_move_behavior)
-    YamlDataLoader(str_data=CHARACTER_DATA).load(sim.get_engine())
-
-
-class CustomLifeEvent(LifeEvent):
-    event_type = "custom-event"
-
-    def __init__(self, timestamp: str, character_id: int, character_name: str) -> None:
-        super().__init__(timestamp)
-        self.character_id: int = character_id
-        self.character_name: str = character_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def __str__(self) -> str:
-        return f"({self.timestamp}): Custom event happened to {self.character_name}"
-
-
-def main():
-    sim = Simulation(DEFAULT_NEIGHBORLY_CONFIG)
-
-    setup(sim)
-
-    character = sim.get_engine().create_character("Human")
-
-    print(character.get_component(GameCharacter))
-
-    handle_gameobject_effects(
-        character,
-        CustomLifeEvent(
-            "May 26, 2022",
-            character.id,
-            str(character.get_component(GameCharacter).name),
-        ),
-    )
-    # print(create_family(BASE_CHARACTER_TYPE, DefaultRNG()))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/samples/john_wick/john_wick.py b/samples/john_wick/john_wick.py
index 00575f8..cad28b4 100644
--- a/samples/john_wick/john_wick.py
+++ b/samples/john_wick/john_wick.py
@@ -47,7 +47,7 @@ def main():
     name_gen.register_rule("hitman_last_name", ["Wick", "47"])
 
     print(sim.get_engine().create_character("Assassin"))
-    print(sim.get_engine().create_place("The Continental Hotel"))
+    print(sim.get_engine().spawn_place("The Continental Hotel"))
 
 
 if __name__ == "__main__":
diff --git a/samples/lod_generator.py b/samples/lod_generator.py
deleted file mode 100644
index 2f4aa7a..0000000
--- a/samples/lod_generator.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import random
-from typing import Optional, Generator
-from neighborly.core.time import SimDateTime, TimeDelta, DAYS_PER_YEAR
-
-
-def lod_generator(
-    current_date: SimDateTime,
-    high_lod_step: int,
-    days_per_year: int,
-    end_date: Optional[SimDateTime] = None,
-) -> Generator[int, None, None]:
-    """
-    Yields time step increments based on the current date,
-    switching between low and high levels-of-detail.
-    """
-    while True:
-        one_year_ahead = current_date.to_ordinal() + DAYS_PER_YEAR
-
-        # Sample the fully simulated days for the next year
-        high_res_days = set(
-            random.sample(
-                range(current_date.to_ordinal(), one_year_ahead),
-                days_per_year,
-            )
-        )
-
-        while current_date.to_ordinal() < one_year_ahead:
-            if current_date.to_ordinal() in high_res_days:
-                current_date += TimeDelta(hours=high_lod_step)
-                yield high_lod_step
-            else:
-                current_date += TimeDelta(hours=24)
-                yield 24
-
-            if end_date and current_date > end_date:
-                return
-
-
-class SimpleSim:
-    def __init__(self) -> None:
-        self.x: int = 0
-        self.y: int = 0
-        self.z: int = 0
-
-    def __repr__(self) -> str:
-        return f"{self.x=} {self.y=} {self.z=}"
-
-    def step(self) -> Generator[None, None, None]:
-        while True:
-            self.x += 1
-            self.y += 3
-            self.z -= 2
-            yield
-
-
-if __name__ == "__main__":
-
-    # current_date = SimDateTime()
-    # end_date = SimDateTime(year=1)
-
-    # for delta_time in lod_generator(current_date, 6, 10, end_date):
-    #     print(current_date.to_date_str())
-
-    sim = SimpleSim()
-
-    for _ in sim.step():
-        print(sim)
diff --git a/samples/notebooks/gui.py b/samples/notebooks/gui.py
index d58509f..d5a17c6 100644
--- a/samples/notebooks/gui.py
+++ b/samples/notebooks/gui.py
@@ -10,23 +10,25 @@
 
 
 class SimulationGUIWidget(Protocol):
-
     def update(self, *args, **kwargs) -> None:
         raise NotImplementedError()
 
 
 class GameCharacterWidget:
     """Displays information about a GameCharacter instance"""
+
     ...
 
 
 class LocationWidget:
     """Displays information about a Location instance"""
+
     ...
 
 
 class RoutineWidget:
     """Displays information about a Routine instance"""
+
     ...
 
 
@@ -36,12 +38,17 @@ class RelationshipWidget:
 
 class SimulationState(Enum):
     """Tracks if the simulation is running or paused"""
+
     PAUSED = 0
     STEPPING = 1
     RUNNING = 2
     STOPPED = 3
 
 
+def create_character_tab():
+    """Create the GUI tab that"""
+
+
 class SimulationGUI:
     """Ipywidget GUI that displays information about a Neighborly Simulation instance"""
 
@@ -57,43 +64,48 @@ def __init__(self, simulation: Simulation) -> None:
         self.date_text = widgets.Text(
             value=self.simulation.get_time().to_date_str(),
             disabled=True,
-            layout=widgets.Layout(width='100%'),
+            layout=widgets.Layout(width="100%"),
         )
 
         self.play_button = widgets.Button(
-            description='Play',
+            description="Play",
             disabled=False,
         )
         self.play_button.on_click(self.play_simulation)
 
         self.step_button = widgets.Button(
-            description='Step',
+            description="Step",
             disabled=False,
         )
         self.step_button.on_click(self.step_simulation)
 
         self.pause_button = widgets.Button(
-            description='Pause',
+            description="Pause",
             disabled=True,
         )
         self.pause_button.on_click(self.pause_simulation)
 
         self.characters_tab = widgets.VBox(
-            [widgets.HTML(value=f"<h1>Characters</h1>")])  # type: ignore
+            [widgets.HTML(value=f"<h1>Characters</h1>")]
+        )  # type: ignore
         self.places_tab = widgets.VBox(
-            [widgets.HTML(value=f"<h1>Places</h1>")])  # type: ignore
+            [widgets.HTML(value=f"<h1>Places</h1>")]
+        )  # type: ignore
 
-        tabs = widgets.Tab(
-            [self.characters_tab, self.places_tab])  # type: ignore
+        tabs = widgets.Tab([self.characters_tab, self.places_tab])  # type: ignore
         tabs.set_title(0, "Characters")
         tabs.set_title(1, "Places")
 
-        self.root_widget = widgets.VBox([
-            widgets.HBox([self.play_button, self.step_button,
-                         self.pause_button]),  # type: ignore
-            self.date_text,
-            tabs,
-        ], layout=widgets.Layout(width='80%', padding='12px'),)  # type: ignore
+        self.root_widget = widgets.VBox(
+            [
+                widgets.HBox(
+                    [self.play_button, self.step_button, self.pause_button]
+                ),  # type: ignore
+                self.date_text,
+                tabs,
+            ],
+            layout=widgets.Layout(width="80%", padding="12px"),
+        )  # type: ignore
 
     def update_gui(self):
         self.date_text.value = self.simulation.get_time().to_date_str()
diff --git a/samples/notebooks/sample.ipynb b/samples/notebooks/sample.ipynb
index 82c5e93..6013081 100644
--- a/samples/notebooks/sample.ipynb
+++ b/samples/notebooks/sample.ipynb
@@ -14,10 +14,8 @@
    "outputs": [],
    "source": [
     "from gui import SimulationGUI\n",
-    "from neighborly.simulation import Simulation, SimulationConfig\n",
-    "from neighborly.core.character import CharacterConfig\n",
-    "import neighborly.theming.plugins.default as default_plugin\n",
-    "from neighborly.core.ecs_manager import register_character_config"
+    "import random\n",
+    "from neighborly.simulation import NeighborlyConfig, Simulation"
    ]
   },
   {
@@ -25,10 +23,17 @@
    "execution_count": 2,
    "metadata": {},
    "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Sun, 00/00/0001 @ 00:00\n"
+     ]
+    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "b53f62b3a2864c918db8687b3969fbb7",
+       "model_id": "38b2dd4e7c5549329d8d31f1316eeaf4",
        "version_major": 2,
        "version_minor": 0
       },
@@ -41,15 +46,23 @@
     }
    ],
    "source": [
-    "default_plugin.initialize_plugin()\n",
-    "\n",
-    "register_character_config('default', CharacterConfig())\n",
+    "NEIGHBORLY_CONFIG = NeighborlyConfig(\n",
+    "    **{\n",
+    "        \"simulation\": {\n",
+    "            \"seed\": random.randint(0, 999999),\n",
+    "            \"hours_per_timestep\": 6,\n",
+    "            \"start_date\": \"0000-00-00T00:00.000z\",\n",
+    "            \"end_date\": \"0001-00-00T00:00.000z\",\n",
+    "            \"days_per_year\": 10,\n",
+    "        },\n",
+    "        \"plugins\": [\"neighborly.plugins.default_plugin\", \"neighborly.plugins.talktown\"],\n",
+    "    }\n",
+    ")\n",
     "\n",
-    "config = SimulationConfig(seed='sample_01')\n",
-    "sim = Simulation(config)\n",
+    "sim = Simulation(NEIGHBORLY_CONFIG)\n",
+    "sim.establish_setting()\n",
     "\n",
     "sim_gui = SimulationGUI(sim)\n",
-    "\n",
     "sim_gui.run()"
    ]
   }
@@ -73,7 +86,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.7"
+   "version": "3.9.12"
   }
  },
  "nbformat": 4,
diff --git a/samples/patterns.py b/samples/patterns.py
deleted file mode 100644
index 7810485..0000000
--- a/samples/patterns.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""
-In this file, I am messing around with using generator functions
-to handle things like pattern matching in LifeEventRules and
-timestep sizes when handling level-of-detail changes
-"""
-from abc import abstractmethod
-from typing import Generator, List, Protocol, Tuple
-from dataclasses import dataclass
-from neighborly.core.ecs import GameObject, Component
-from neighborly.core.life_event import (
-    ILifeEventListener,
-    LifeEvent,
-    check_gameobject_preconditions,
-    handle_gameobject_effects,
-)
-
-
-class IPatternFn(Protocol):
-    @abstractmethod
-    def __call__(
-        self, world: List[GameObject]
-    ) -> Generator[Tuple[LifeEvent, Tuple[GameObject, ...]], None, None]:
-        raise NotImplementedError()
-
-
-class TestEvent(LifeEvent):
-
-    event_type: str = "test-event"
-
-    def __init__(self, timestamp: str) -> None:
-        super().__init__(timestamp)
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-
-def strength_greater_than(value: int) -> IPatternFn:
-    def pattern(
-        world: List[GameObject],
-    ):
-        for g in world:
-            if g.has_component(A) and g.get_component(A).strength > value:
-                yield TestEvent("now"), (g,)
-
-    return pattern
-
-
-strength_greater_than_10 = strength_greater_than(10)
-
-
-@dataclass
-class A(Component, ILifeEventListener):
-    strength: int = 0
-
-    def handle_event(self, event: LifeEvent) -> bool:
-        if event.get_type() == "test-event":
-            print("Handling works")
-        return True
-
-    def check_preconditions(self, event: LifeEvent) -> bool:
-        print("Pizza")
-        return True
-
-
-@dataclass
-class B(Component):
-    health: int = 0
-
-
-def main():
-    world = [
-        GameObject(components=[A()]),
-        GameObject(components=[A(11), B(10)]),
-        GameObject(components=[A(25)]),
-    ]
-
-    for event, participants in strength_greater_than_10(world):
-        preconditions_pass = all(
-            [check_gameobject_preconditions(g, event) for g in participants]
-        )
-        if preconditions_pass:
-            for g in participants:
-                handle_gameobject_effects(g, event)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/samples/pygame/data.yaml b/samples/pygame/data.yaml
new file mode 100644
index 0000000..5a843a3
--- /dev/null
+++ b/samples/pygame/data.yaml
@@ -0,0 +1,27 @@
+Places:
+  - name: Space Casino
+    components:
+      - type: Location
+        options:
+          activities: ["Gambling", "Drinking", "Eating", "Socializing"]
+  - name: Mars
+    components:
+      - type: Location
+        options:
+          activities: ["Reading", "Relaxing"]
+  - name: Kamino
+    components:
+      - type: Location
+        options:
+          activities: ["Recreation", "Studying"]
+Residences:
+  - name: House
+    inherits: House
+    components:
+      - type: Location
+        options:
+          activities: ["Rest"]
+      - type: Building
+        options:
+          size: small
+      - type: Position2D
diff --git a/samples/pygame/pygame_sample.py b/samples/pygame/pygame_sample.py
index bc63788..574402a 100644
--- a/samples/pygame/pygame_sample.py
+++ b/samples/pygame/pygame_sample.py
@@ -1,15 +1,19 @@
+import os
+import random
 from dataclasses import dataclass
-from typing import List, Optional, Tuple, Set, Union, Sequence, cast, Iterable, Dict
+from pathlib import Path
+from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union, cast
 
 import pygame
 import pygame_gui
 
 from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import GameObject, Component
+from neighborly.core.ecs import Component, GameObject, World
 from neighborly.core.engine import AbstractFactory, ComponentDefinition
 from neighborly.core.location import Location
 from neighborly.core.position import Position2D
-from neighborly.simulation import Simulation, NeighborlyConfig
+from neighborly.loaders import YamlDataLoader
+from neighborly.simulation import NeighborlyConfig, Simulation
 
 # COMMON COLORS
 SKY_BLUE = (153, 218, 232)
@@ -62,8 +66,8 @@ class BuildingFactory(AbstractFactory):
     def __init__(self):
         super().__init__("Building")
 
-    def create(self, spec: ComponentDefinition, **kwargs) -> Building:
-        return Building(size=spec.get_attributes().get("size", "small"))
+    def create(self, world: World, **kwargs) -> Building:
+        return Building(kwargs.get("size", "small"))
 
 
 class CharacterInfoWindow(pygame_gui.elements.UIWindow):
@@ -78,7 +82,7 @@ def __init__(
         super().__init__(
             pygame.Rect((10, 10), (320, 240)),
             ui_manager,
-            window_display_title=character.name,
+            window_display_title=str(character.get_component(GameCharacter).name),
             object_id=f"{character.id}",
         )
         self.ui_manager = ui_manager
@@ -293,18 +297,18 @@ def __init__(
         self.rect.topleft = (round(self.position.x), round(self.position.y))
 
         self.font = pygame.font.SysFont("Arial", 12)
-        self.textSurf = self.font.render(
-            place.name, True, (255, 255, 255), (0, 0, 0, 255)
-        )
-        text_width = self.textSurf.get_width()
-        text_height = self.textSurf.get_height()
-        self.image.blit(
-            self.textSurf,
-            [
-                (width * CHARACTER_SIZE) / 2 - text_width / 2,
-                (height * CHARACTER_SIZE) / 2 - text_height / 2,
-            ],
-        )
+        # self.textSurf = self.font.render(
+        #     place.name, True, (255, 255, 255), (0, 0, 0, 255)
+        # )
+        # text_width = self.textSurf.get_width()
+        # text_height = self.textSurf.get_height()
+        # self.image.blit(
+        #     self.textSurf,
+        #     [
+        #         (width * CHARACTER_SIZE) / 2 - text_width / 2,
+        #         (height * CHARACTER_SIZE) / 2 - text_height / 2,
+        #     ],
+        # )
 
         self.occupancy: OccupancyGrid = OccupancyGrid(width, height)
 
@@ -440,7 +444,10 @@ def __init__(
 
         self._create_background(
             self.background_group,
-            (self.sim.config.town.width, self.sim.config.town.length),
+            (
+                self.sim.config.simulation.town.width,
+                self.sim.config.simulation.town.length,
+            ),
         )
 
         self.ui_elements = {
@@ -484,43 +491,24 @@ def __init__(
 
     @staticmethod
     def _init_sim() -> Simulation:
-
-        structure_data = """
-        Places:
-        - name: Space Casino
-          components:
-            - type: Location
-              options:
-                activities: ["Gambling", "Drinking", "Eating", "Socializing"]
-        - name: Mars
-          components:
-            - type: Location
-              options:
-                activities: ["Reading", "Relaxing"]
-        - name: Kamino
-          components:
-            - type: Location
-              options:
-                activities: ["Recreation", "Studying"]
-        - name: House
-          inherits: House
-          components:
-            - type: Location
-              options:
-                activities: ["Rest"]
-            - type: Building
-              options:
-                size: small
-            - type: Position2D
-        """
-
-        sim = Simulation.create(
-            SimulationConfig(
-                hours_per_timestep=4, town=TownConfig(town_width=5, town_length=5)
-            )
+        NEIGHBORLY_CONFIG = NeighborlyConfig(
+            **{
+                "simulation": {
+                    "seed": random.randint(0, 999999),
+                    "hours_per_timestep": 6,
+                    "start_date": "0000-00-00T00:00.000z",
+                    "end_date": "0060-00-00T00:00.000z",
+                    "days_per_year": 10,
+                },
+                "plugins": [
+                    "neighborly.plugins.talktown",
+                ],
+            }
         )
-        default_plugin.initialize_plugin(sim.get_engine())
-        YamlDataLoader(str_data=structure_data).load(sim.get_engine())
+        sim = Simulation(NEIGHBORLY_CONFIG)
+        YamlDataLoader(
+            filepath=Path(os.path.abspath(__file__)).parent / "data.yaml"
+        ).load(sim.get_engine())
         sim.get_engine().add_component_factory(BuildingFactory())
 
         return sim
diff --git a/samples/sample_01.py b/samples/sample_01.py
deleted file mode 100644
index ffe8bde..0000000
--- a/samples/sample_01.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from neighborly.core.town import TownConfig
-from neighborly.loaders import YamlDataLoader
-from neighborly.plugins import NeighborlyPlugin, PluginContext
-from neighborly.plugins.default_plugin import DefaultPlugin
-from neighborly.simulation import Simulation, SimulationConfig, NeighborlyConfig
-
-
-class CustomPlugin(NeighborlyPlugin):
-
-    def apply(self, ctx: PluginContext, **kwargs) -> None:
-        places = \
-            """
-            Places:
-                - name: Space Casino
-                  components:
-                    - type: Location
-                      options:
-                        activities: ["Gambling", "Drinking", "Eating", "Socializing"]
-                - name: Mars
-                  components:
-                    - type: Location
-                      options:
-                        activities: ["Reading", "Relaxing"]
-                - name: Kamino
-                  components:
-                    - type: Location
-                      options:
-                        activities: ["Recreation", "Studying"]
-            """
-        YamlDataLoader(str_data=places).load(ctx.engine)
-
-
-def main():
-    config = NeighborlyConfig(
-        simulation=SimulationConfig(
-            hours_per_timestep=12,
-            town=TownConfig(
-                width=5,
-                length=1
-            )
-        ),
-        plugins=[
-            CustomPlugin(),
-            DefaultPlugin(),
-        ]
-    )
-
-    _sim = Simulation.from_config(config)
-
-    _sim.run()
-
-    return _sim
-
-
-if __name__ == "__main__":
-    sim = main()
diff --git a/samples/sample_02.py b/samples/sample_02.py
deleted file mode 100644
index f494fd6..0000000
--- a/samples/sample_02.py
+++ /dev/null
@@ -1,352 +0,0 @@
-"""
-In this sample, I am adjusting the event system to be more ergonomic
-"""
-from dataclasses import dataclass
-from pprint import pprint
-import random
-from typing import Dict, Any, Tuple
-
-from neighborly.core.ecs import GameObject, Component
-from neighborly.core.life_event import (
-    LifeEvent,
-    event_precondition,
-    event_effect,
-    ILifeEventCallback,
-)
-from neighborly.core.relationship import RelationshipTag
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.character import (
-    CharacterDefinition,
-    CharacterName,
-    FamilyGenerationOptions,
-    GameCharacter,
-    GenerationConfig,
-    LifeCycleConfig,
-)
-from neighborly.core.business import Occupation
-from neighborly.simulation import NeighborlyConfig, Simulation, SimulationConfig
-
-# ==============================
-# HELPER PRECONDITION FUNCTIONS
-# ==============================
-
-
-def always_accept(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Event precondition function that always returns True"""
-    return True
-
-
-def reject(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Event precondition function that always returns False"""
-    return True
-
-
-def friendship_greater_than(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    friendship value for a character to another is greater than
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).friendship
-                > value
-            )
-        return False
-
-    return fn
-
-
-def friendship_less_than(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    friendship value for a character to another is less than
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).friendship
-                < value
-            )
-        return False
-
-    return fn
-
-
-def friendship_equal_to(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    friendship value for a character to another is equal to
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).friendship
-                == value
-            )
-        return False
-
-    return fn
-
-
-def romance_greater_than(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    romance value for a character to another is greater than
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).romance > value
-            )
-        return False
-
-    return fn
-
-
-def romance_less_than(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    romance value for a character to another is less than
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).romance < value
-            )
-        return False
-
-    return fn
-
-
-def romance_equal_to(value: int) -> ILifeEventCallback:
-    """
-    Returns a precondition function that checks if the
-    romance value for a character to another is equal to
-    a given value
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return (
-                relationship_net.get_connection(gameobject.id, other.id).romance
-                == value
-            )
-        return False
-
-    return fn
-
-
-def relationship_has_tag(tag: str) -> ILifeEventCallback:
-    """
-    Return true if the relationship between this character and
-    another has the given tag
-    """
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            return relationship_net.get_connection(gameobject.id, other.id).has_tags(
-                RelationshipTag[tag]
-            )
-        return False
-
-    return fn
-
-
-def is_single(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Return True if this character has no relationships tagged as significant others"""
-    relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-    significant_other_relationships = relationship_net.get_all_relationships_with_tags(
-        gameobject.id, RelationshipTag.SignificantOther
-    )
-    return bool(significant_other_relationships)
-
-
-def is_unemployed(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Returns True if this character does not have a job"""
-    return not gameobject.has_component(Occupation)
-
-
-def is_employed(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Returns True if this character has a job"""
-    return gameobject.has_component(Occupation)
-
-
-# ==============================
-# HELPER EFFECT FUNCTIONS
-# ==============================
-
-
-def add_relationship_tag(tag: str) -> ILifeEventCallback:
-    """Add the given tag from another character for the given character"""
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            relationship_net.get_connection(gameobject.id, other.id).add_tags(
-                RelationshipTag[tag]
-            )
-        return True
-
-    return fn
-
-
-def remove_relationship_tag(tag: str) -> ILifeEventCallback:
-    """Remove the given tag from another character for the given character"""
-
-    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
-        other: GameObject = event.data["other"]
-        relationship_net = gameobject.world.get_resource(RelationshipNetwork)
-        if relationship_net.has_connection(gameobject.id, other.id):
-            relationship_net.get_connection(gameobject.id, other.id).remove_tags(
-                RelationshipTag[tag]
-            )
-        return True
-
-    return fn
-
-
-@event_precondition("test")
-def test_event_precondition(gameobject: GameObject, event: LifeEvent) -> bool:
-    print("Running precondition")
-    return True
-
-
-@event_effect("test")
-def test_event_effect(gameobject: GameObject, event: LifeEvent) -> bool:
-    print(f"Running post effect: {event.data}")
-    return True
-
-
-BASE_CHARACTER_DEF = CharacterDefinition(
-    name="BaseCharacter",
-    lifecycle=LifeCycleConfig(
-        romantic_feelings_age=13,
-        life_stages={
-            "child": 0,
-            "adolescent": 13,
-            "young_adult": 18,
-            "adult": 30,
-            "senior": 65,
-        },
-    ),
-    generation=GenerationConfig(
-        first_name="#first_name#",
-        last_name="#last_name#",
-        family=FamilyGenerationOptions(
-            probability_spouse=0.5, probability_children=0.3, num_children=(0, 3)
-        ),
-    ),
-)
-
-HUMAN_CHARACTER_DEF = CharacterDefinition.create({"name": "Human"}, BASE_CHARACTER_DEF)
-
-ANDROID_CHARACTER_DEF = CharacterDefinition.create(
-    {"name": "Android"}, BASE_CHARACTER_DEF
-)
-
-
-DEFAULT_NEIGHBORLY_CONFIG = NeighborlyConfig(
-    simulation=SimulationConfig(
-        seed=random.randint(0, 999999),
-        hours_per_timestep=6,
-        start_date="0000-00-00T00:00.000z",
-        end_date="0100-00-00T00:00.000z",
-    ),
-    plugins=["neighborly.plugins.default_plugin"],
-)
-
-
-@dataclass
-class DemonSlayer(Component):
-    breathing_style: Tuple[str, ...]
-    demons_slain: int
-    power_level: int
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "breathing_style": self.breathing_style,
-            "demons_slain": self.demons_slain,
-            "power_level": self.power_level,
-        }
-
-
-def main():
-    sim = Simulation(DEFAULT_NEIGHBORLY_CONFIG)
-
-    delores_a = GameObject(
-        components=[
-            GameCharacter(
-                ANDROID_CHARACTER_DEF,
-                CharacterName("Delores", "Abernathy"),
-                30,
-                events={
-                    "become-friends": {
-                        "preconditions": [friendship_greater_than(0)],
-                        "effects": [add_relationship_tag("Friend")],
-                    },
-                    "become-enemies": {
-                        "preconditions": [reject],
-                    },
-                },
-            ),
-        ]
-    )
-
-    pprint(delores_a.to_dict())
-
-    sim.world.add_gameobject(delores_a)
-
-    kamado_t = GameObject(
-        components=[
-            GameCharacter(
-                HUMAN_CHARACTER_DEF,
-                CharacterName("Kamado", "Tanjiro"),
-                13,
-                events={
-                    "become-friends": {
-                        "preconditions": [friendship_greater_than(0)],
-                        "effects": [add_relationship_tag("Friend")],
-                    },
-                    "become-enemies": {
-                        "preconditions": [reject],
-                    },
-                },
-            ),
-            DemonSlayer(("sun", "water"), 0, 10),
-        ],
-    )
-
-    pprint(kamado_t.to_dict())
-
-    sim.world.add_gameobject(kamado_t)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/samples/sample_03.py b/samples/sample_03.py
deleted file mode 100644
index 2ae5391..0000000
--- a/samples/sample_03.py
+++ /dev/null
@@ -1,322 +0,0 @@
-import random
-from typing import List, Protocol, TypeVar, Generator
-from pprint import pprint
-
-from neighborly.core.character import (
-    CharacterDefinition,
-    LifeCycleConfig,
-    GenerationConfig,
-    FamilyGenerationOptions,
-)
-from neighborly.core.name_generation import get_name
-from neighborly.simulation import Simulation, NeighborlyConfig, SimulationConfig
-from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.ecs import GameObject, World
-from neighborly.core.relationship import RelationshipModifier
-from neighborly.core.social_network import RelationshipNetwork, Relationship
-from neighborly.core.time import SimDateTime
-from neighborly.core.character import GameCharacter, CharacterName
-from neighborly.core.tags import Tag
-from neighborly.core.life_event import (
-    LifeEvent,
-    ILifeEventCallback,
-    check_gameobject_preconditions,
-    handle_gameobject_effects,
-)
-
-BASE_CHARACTER_DEF = CharacterDefinition(
-    name="BaseCharacter",
-    lifecycle=LifeCycleConfig(
-        lifespan=85,
-        life_stages={
-            "child": 0,
-            "adolescent": 13,
-            "young_adult": 18,
-            "adult": 30,
-            "elder": 65,
-        },
-    ),
-    generation=GenerationConfig(
-        first_name="#first_name#",
-        last_name="#last_name#",
-        family=FamilyGenerationOptions(
-            probability_spouse=0.5, probability_children=0.3, num_children=(0, 3)
-        ),
-    ),
-)
-
-HUMAN_CHARACTER_DEF = CharacterDefinition.create({"name": "Human"}, BASE_CHARACTER_DEF)
-ANDROID_CHARACTER_DEF = CharacterDefinition.create(
-    {"name": "Android"}, BASE_CHARACTER_DEF
-)
-
-
-class LifeEvenRulePatternFn(Protocol):
-    def __call__(self, world: World) -> LifeEvent:
-        raise NotImplementedError
-
-
-class LifeEvenRulePreconditionFn(Protocol):
-    def __call__(self, event: LifeEvent) -> bool:
-        raise NotImplementedError
-
-
-class LifeEventRuleEffectFn(Protocol):
-    def __call__(self, event: LifeEvent) -> None:
-        raise NotImplementedError
-
-
-class LifeEventRule:
-
-    __slots__ = (
-        "name",
-        "description",
-        "pattern_fn",
-        "probability",
-        "precondition_fn",
-        "effect_fn",
-    )
-
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        probability: float,
-        pattern_fn: LifeEvenRulePatternFn,
-        precondition_fn: LifeEvenRulePreconditionFn,
-        effect_fn: LifeEventRuleEffectFn,
-    ) -> None:
-        self.name: str = name
-        self.description: str = description
-        self.probability: float = probability
-        self.pattern_fn: LifeEvenRulePatternFn = pattern_fn
-        self.precondition_fn: LifeEvenRulePreconditionFn = precondition_fn
-        self.effect_fn: LifeEventRuleEffectFn = effect_fn
-
-
-def say_hello(gameobject: GameObject, event: LifeEvent) -> bool:
-    print("Hello, World")
-    return True
-
-
-def add_friendship_buff(value: int) -> ILifeEventCallback:
-    def cb(gameobject: GameObject, event: LifeEvent) -> bool:
-        relationship: Relationship = event.data["relationship"]
-        relationship.add_modifier(
-            RelationshipModifier(
-                "Friendly",
-                "This character is friendly to others",
-                friendship_boost=value,
-            )
-        )
-        print("Added relationship buff")
-        return True
-
-    return cb
-
-
-def reduce_friendliness_to_women(value: int) -> ILifeEventCallback:
-    def cb(gameobject: GameObject, event: LifeEvent) -> bool:
-        relationship: Relationship = event.data["relationship"]
-
-        other = relationship.target
-
-        target = gameobject.world.get_gameobject(other).get_component(GameCharacter)
-
-        if "Woman" in target.tags:
-            relationship.add_modifier(
-                RelationshipModifier(
-                    "Misogynist",
-                    "This character is does not like women",
-                    friendship_boost=value,
-                )
-            )
-            print("Added relationship buff against women")
-        return True
-
-    return cb
-
-
-always_friendly_tag = Tag(
-    name="Always Friendly",
-    description="This character is always friends to everyone even enemies",
-    event_effects={"createRelationship": add_friendship_buff(10)},
-)
-
-woman_tag = Tag(name="Woman", description="This character is a woman")
-
-misogynist = Tag(
-    name="Misogynist",
-    description="This character is not friendly to women",
-    event_effects={"createRelationship": reduce_friendliness_to_women(-10)},
-)
-
-
-def handle_socialize(gameobject: GameObject, event: LifeEvent) -> bool:
-    # Get the other character
-    characters: list[GameObject] = [*event.data["characters"]]
-    characters.remove(gameobject)
-    other = characters[0]
-
-    # Check if there is an existing relationship
-    relationship_network = gameobject.world.get_resource(RelationshipNetwork)
-    if not relationship_network.has_connection(gameobject.id, other.id):
-        relationship = Relationship(gameobject.id, other.id)
-        relationship_network.add_connection(gameobject.id, other.id, relationship)
-        handle_gameobject_effects(
-            gameobject,
-            LifeEvent(
-                event_type="create-relationship",
-                timestamp=gameobject.world.get_resource(SimDateTime).to_iso_str(),
-                relationship=relationship,
-            ),
-        )
-
-    return True
-
-
-_T = TypeVar("_T")
-
-
-def chunk_list(lst: List[_T], n: int) -> Generator[List[_T], None, None]:
-    """Yield successive n-sized chunks from lst."""
-    for i in range(0, len(lst), n):
-        yield lst[i : i + n]
-
-
-def socialize_system(world: World, **kwargs) -> None:
-    for pair in chunk_list(world.get_component(GameCharacter), 2):
-        character_0 = pair[0][1].gameobject
-        character_1 = pair[1][1].gameobject
-
-        # choose an interaction type
-        interaction_type = random.choice(
-            ["neutral", "friendly", "flirty", "bad", "boring", "good", "exciting"]
-        )
-
-        socialize_event = LifeEvent(
-            event_type="Socialize",
-            timestamp=world.get_resource(SimDateTime).to_iso_str(),
-            interaction=interaction_type,
-            characters=[character_0, character_1],
-        )
-
-        character_0_consent = check_gameobject_preconditions(
-            character_0, socialize_event
-        )
-        character_1_consent = check_gameobject_preconditions(
-            character_1, socialize_event
-        )
-
-        if character_0_consent and character_1_consent:
-            handle_gameobject_effects(character_0, socialize_event)
-            handle_gameobject_effects(character_1, socialize_event)
-
-
-def create_child(
-    engine: NeighborlyEngine, pregnant_parent: GameObject, other_parent: GameObject
-) -> GameObject:
-
-    # Choose which parent to inherit last name from
-    last_name = pregnant_parent.get_component(GameCharacter).name.surname
-
-    # Choose which parent to inherit archetype / definition from
-    character_def = (
-        engine.get_rng()
-        .choice([pregnant_parent, other_parent])
-        .get_component(GameCharacter)
-        .character_def
-    )
-
-    first_name = get_name(character_def.generation.first_name)
-
-    baby = GameObject(
-        components=[
-            GameCharacter(
-                character_def,
-                CharacterName(first_name, last_name),
-                0,
-            )
-        ]
-    )
-
-    return baby
-
-
-def main():
-
-    DEFAULT_NEIGHBORLY_CONFIG = NeighborlyConfig(
-        simulation=SimulationConfig(
-            seed=random.randint(0, 999999),
-            hours_per_timestep=6,
-            start_date="0000-00-00T00:00.000z",
-            end_date="0100-00-00T00:00.000z",
-        ),
-        plugins=["neighborly.plugins.default_plugin"],
-    )
-
-    sim = Simulation(DEFAULT_NEIGHBORLY_CONFIG)
-    world = sim.world
-    world.add_system(socialize_system)
-
-    tanjiro = GameObject(
-        components=[
-            GameCharacter(
-                HUMAN_CHARACTER_DEF,
-                CharacterName("Tanjiro", "Kamado"),
-                13,
-                tags=[misogynist],
-                events={
-                    "greet": {
-                        "effects": [say_hello],
-                    },
-                    "socialize": {
-                        "effects": [handle_socialize],
-                    },
-                },
-            ),
-        ]
-    )
-
-    world.add_gameobject(tanjiro)
-
-    pprint(tanjiro.to_dict())
-
-    delores = GameObject(
-        components=[
-            GameCharacter(
-                ANDROID_CHARACTER_DEF,
-                CharacterName("Delores", "Abernathy"),
-                30,
-                tags=[woman_tag],
-                events={
-                    "greet": {
-                        "effects": [say_hello],
-                    },
-                    "socialize": {
-                        "effects": [handle_socialize],
-                    },
-                },
-            )
-        ]
-    )
-
-    world.add_gameobject(delores)
-
-    event = LifeEvent(event_type="greet", timestamp="")
-
-    handle_gameobject_effects(delores, event)
-    handle_gameobject_effects(tanjiro, event)
-
-    for _ in range(10):
-        print(
-            create_child(world.get_resource(NeighborlyEngine), delores, tanjiro)
-            .get_component(GameCharacter)
-            .name
-        )
-
-    print("Done")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/samples/talktown.py b/samples/talktown.py
index 6536435..7f136d8 100644
--- a/samples/talktown.py
+++ b/samples/talktown.py
@@ -1,22 +1,37 @@
-import random
 import logging
-from neighborly.simulation import NeighborlyConfig, Simulation
-from neighborly.inspection_tools import *
+import random
+import time
 
-NEIGHBORLY_CONFIG = NeighborlyConfig(
-    **{
-        "simulation": {
-            "seed": random.randint(0, 999999),
-            "hours_per_timestep": 6,
-            "start_date": "0000-00-00T00:00.000z",
-            "end_date": "0001-00-00T00:00.000z",
-        },
-        "plugins": ["neighborly.plugins.default_plugin", "neighborly.plugins.talktown"],
-    }
+from neighborly.core.life_event import LifeEventLog
+from neighborly.core.time import SimDateTime
+from neighborly.plugins.default_plugin import (
+    DefaultLifeEventPlugin,
+    DefaultNameDataPlugin,
 )
-
+from neighborly.plugins.talktown import TalkOfTheTownPlugin
+from neighborly.plugins.weather_plugin import WeatherPlugin
+from neighborly.simulation import Simulation
 
 if __name__ == "__main__":
-    # logging.basicConfig(filename="neighborly.log", filemode="w", level=logging.DEBUG)
-    sim = Simulation(NEIGHBORLY_CONFIG)
-    sim.run()
+    logging.basicConfig(level=logging.DEBUG)
+
+    sim = (
+        Simulation.create(
+            seed=random.randint(0, 999999),
+            world_gen_start=SimDateTime(year=1839, month=8, day=19),
+            world_gen_end=SimDateTime(year=1979, month=8, day=19),
+        )
+        .add_plugin(DefaultNameDataPlugin())
+        .add_plugin(DefaultLifeEventPlugin())
+        .add_plugin(WeatherPlugin())
+        .add_plugin(TalkOfTheTownPlugin())
+    )
+
+    sim.world.get_resource(LifeEventLog).subscribe(lambda e: print(str(e)))
+
+    st = time.time()
+    sim.establish_setting()
+    elapsed_time = time.time() - st
+
+    print(f"World Date: {sim.get_time().to_iso_str()}")
+    print("Execution time: ", elapsed_time, "seconds")
diff --git a/setup.cfg b/setup.cfg
index 24fb2ef..4dec052 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,7 +10,7 @@ url = https://github.com/ShiJbey/neighborly
 project_urls =
     Bug Tracker = https://github.com/ShiJbey/neighborly/issues
 license = MIT
-keywords = social simulation, emergent narrative
+keywords = social simulation, emergent narrative, life simulator framework, multi-agent,
 classifiers =
     Development Status :: 4 - Beta
     Programming Language :: Python :: 3.9
@@ -34,7 +34,7 @@ install_requires =
     pydantic ==1.8.2
     numpy
     PyYAML
-    cityhash
+    esper
 
 [options.packages.find]
 where = src
@@ -56,7 +56,7 @@ samples =
 [flake8]
 exclude = build,.git,docs,tests,.vscode,.idea,
 extend-ignore = E203
-max-line-length = 88
+max-line-length = 120
 max-complexity = 10
 
 [isort]
diff --git a/src/neighborly/__main__.py b/src/neighborly/__main__.py
index d9e54f9..08acb5f 100644
--- a/src/neighborly/__main__.py
+++ b/src/neighborly/__main__.py
@@ -5,14 +5,18 @@
 import pathlib
 import random
 import sys
+import importlib
 from typing import Any, Dict, Optional
 
 import yaml
+from pydantic import BaseModel
 
 import neighborly
 from neighborly.exporter import NeighborlyJsonExporter
 from neighborly.loaders import UnsupportedFileType
-from neighborly.simulation import NeighborlyConfig, Simulation, SimulationConfig
+from neighborly.simulation import Simulation, PluginSetupError, Plugin
+from neighborly.server import NeighborlyServer
+import neighborly.core.utils.utilities as utilities
 
 logger = logging.getLogger(__name__)
 
@@ -21,12 +25,127 @@
         seed=random.randint(0, 999999),
         hours_per_timestep=6,
         start_date="0000-00-00T00:00.000z",
-        end_date="0100-00-00T00:00.000z",
+        end_date="0001-00-00T00:00.000z",
+        days_per_year=10,
     ),
-    plugins=["neighborly.plugins.default_plugin"],
+    plugins=["neighborly.plugins.default_plugin", "neighborly.plugins.talktown"],
 )
 
 
+class SimulationConfig(BaseModel):
+    """Configuration parameters for a Neighborly instance
+
+    Attributes
+    ----------
+    seed: int
+        The seed provided to the random number factory
+    hours_per_timestep: int
+        How many in-simulation hours elapse every simulation tic
+    town: TownConfig
+        Configuration settings for town creation
+    """
+
+    seed: int
+    hours_per_timestep: int
+    start_date: str
+    end_date: str
+    days_per_year: int
+
+
+class PluginConfig(BaseModel):
+    """
+    Settings for loading and constructing a plugin
+
+    Attributes
+    ----------
+    name: str
+        Name of the plugin to load
+    options: Dict[str, Any]
+        Parameters to pass to the plugin when constructing
+        and loading it
+    """
+
+    name: str
+    path: Optional[str] = None
+    options: Dict[str, Any] = Field(default_factory=dict)
+
+
+class NeighborlyConfig(BaseModel):
+    """
+    Static configuration setting for the Neighborly
+
+    Attributes
+    ----------
+    simulation: SimulationConfig
+        Static configuration settings specifically for
+        the simulation instance
+    plugins: List[Union[str, PluginConfig]]
+        Names of plugins to load or names combined with
+        instantiation parameters
+    path: str
+        Path to the config file
+    """
+
+    quiet: bool = False
+    simulation: SimulationConfig
+    plugins: List[Union[str, PluginConfig]] = Field(default_factory=list)
+    path: str = "."
+
+    @classmethod
+    def from_partial(
+        cls, data: Dict[str, Any], defaults: NeighborlyConfig
+    ) -> NeighborlyConfig:
+        """Construct new config from a default config and a partial set of parameters"""
+        return cls(**utilities.merge(data, defaults.dict()))
+
+
+def add_plugin(self, plugin: Union[str, PluginConfig, Plugin]) -> Simulation:
+    """Add plugin to simulation"""
+    if isinstance(plugin, str):
+        self._dynamic_load_plugin(plugin)
+    elif isinstance(plugin, PluginConfig):
+        self._dynamic_load_plugin(plugin.name, plugin.path, **plugin.options)
+    else:
+        plugin.setup(self)
+        logger.debug(f"Successfully loaded plugin: {plugin.get_name()}")
+    return self
+
+
+def _dynamic_load_plugin(
+    self, module_name: str, path: Optional[str] = None, **kwargs
+) -> None:
+    """
+    Load a plugin
+
+    Parameters
+    ----------
+    module_name: str
+        Name of module to load
+    """
+    path_prepended = False
+
+    if path:
+        path_prepended = True
+        plugin_abs_path = os.path.abspath(path)
+        sys.path.insert(0, plugin_abs_path)
+        logger.debug(f"Temporarily added plugin path '{plugin_abs_path}' to sys.path")
+
+    try:
+        plugin_module = importlib.import_module(module_name)
+        plugin: Plugin = plugin_module.__dict__["get_plugin"]()
+        plugin.setup(self, **kwargs)
+        logger.debug(f"Successfully loaded plugin: {plugin.get_name()}")
+    except KeyError:
+        raise PluginSetupError(
+            f"'get_plugin' function not found for plugin: {module_name}"
+        )
+    finally:
+        # Remove the given plugin path from the front
+        # of the system path to prevent module resolution bugs
+        if path_prepended:
+            sys.path.pop(0)
+
+
 def load_config_from_path(config_path: str) -> Dict[str, Any]:
     """
     This function loads the configuration file at the given path
@@ -103,6 +222,9 @@ def get_arg_parser() -> argparse.ArgumentParser:
         action="store_true",
         help="Disable all printing to stdout",
     )
+    parser.add_argument(
+        "--server", default=False, action="store_true", help="Run web server UI"
+    )
     return parser
 
 
@@ -134,20 +256,35 @@ def main():
 
     sim = Simulation(config)
 
-    sim.run()
+    if args.server:
+        server = NeighborlyServer(sim)
+        server.run()
+    else:
+        sim.establish_setting()
 
-    if not args.no_emit:
-        output_path = (
-            args.output
-            if args.output
-            else f"{sim.config.simulation.seed}_{sim.get_town().name.replace(' ', '_')}.json"
-        )
+        if not args.no_emit:
+            output_path = (
+                args.output
+                if args.output
+                else f"{sim.config.simulation.seed}_{sim.get_town().name.replace(' ', '_')}.json"
+            )
 
-        with open(output_path, "w") as f:
-            data = NeighborlyJsonExporter().export(sim.world)
-            f.write(data)
-            logger.debug(f"Simulation data written to: '{output_path}'")
+            with open(output_path, "w") as f:
+                data = NeighborlyJsonExporter().export(sim.world)
+                f.write(data)
+                logger.debug(f"Simulation data written to: '{output_path}'")
 
 
 if __name__ == "__main__":
     main()
+
+
+# Load Plugins
+# for plugin in config.plugins:
+#     if isinstance(plugin, str):
+#         self._load_plugin(plugin)
+#     else:
+#         plugin_path = os.path.join(
+#             pathlib.Path(config.path).parent, plugin.path if plugin.path else ""
+#         )
+#         self._load_plugin(plugin.name, plugin_path, **plugin.options)
diff --git a/src/neighborly/core/builtin/__init__.py b/src/neighborly/builtin/__init__.py
similarity index 100%
rename from src/neighborly/core/builtin/__init__.py
rename to src/neighborly/builtin/__init__.py
diff --git a/src/neighborly/builtin/events.py b/src/neighborly/builtin/events.py
new file mode 100644
index 0000000..302b589
--- /dev/null
+++ b/src/neighborly/builtin/events.py
@@ -0,0 +1,1380 @@
+from __future__ import annotations
+
+from typing import List
+
+from neighborly.builtin.helpers import is_single
+from neighborly.builtin.statuses import (
+    Adult,
+    Dating,
+    Elder,
+    Married,
+    Pregnant,
+    Retired,
+    Unemployed,
+)
+from neighborly.core.business import Business, Occupation, OccupationType
+from neighborly.core.character import GameCharacter
+from neighborly.core.ecs import GameObject, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import (
+    EventRoles,
+    EventRoleType,
+    LifeEvent,
+    LifeEventType,
+    join_filters,
+)
+from neighborly.core.relationship import RelationshipGraph, RelationshipTag
+from neighborly.core.time import SimDateTime
+
+
+def single_person_role_type() -> EventRoleType:
+    def can_get_married(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+        if len(
+            rel_graph.get_all_relationships_with_tags(
+                gameobject.id, RelationshipTag.SignificantOther
+            )
+        ):
+            return False
+        return True
+
+    return EventRoleType(
+        "SinglePerson", [GameCharacter, Adult], filter_fn=can_get_married
+    )
+
+
+def potential_spouse_role() -> EventRoleType:
+    def filter_fn(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+        is_single = (
+            len(
+                rel_graph.get_all_relationships_with_tags(
+                    gameobject.id, RelationshipTag.SignificantOther
+                )
+            )
+            == 0
+        )
+
+        if not rel_graph.has_connection(gameobject.id, event["SinglePerson"]):
+            return False
+
+        in_love = (
+            rel_graph.get_connection(gameobject.id, event["SinglePerson"]).romance > 45
+        )
+        return is_single and in_love
+
+    return EventRoleType("PotentialSpouse", [GameCharacter, Adult], filter_fn=filter_fn)
+
+
+def hiring_business_role() -> EventRoleType:
+    """Defines a Role for a business with job opening"""
+
+    def help_wanted(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        business = gameobject.get_component(Business)
+        return len(business.get_open_positions()) > 0
+
+    return EventRoleType("HiringBusiness", components=[Business], filter_fn=help_wanted)
+
+
+def unemployed_role() -> EventRoleType:
+    """Defines event Role for a character without a job"""
+    return EventRoleType("Unemployed", components=[Unemployed])
+
+
+def hire_employee_event() -> LifeEventType:
+    def cb(world: World, event: LifeEvent):
+        engine = world.get_resource(NeighborlyEngine)
+        character = world.get_gameobject(event["Unemployed"]).get_component(
+            GameCharacter
+        )
+        business = world.get_gameobject(event["HiringBusiness"]).get_component(Business)
+
+        position = engine.rng.choice(business.get_open_positions())
+
+        character.gameobject.remove_component(Unemployed)
+
+        business.add_employee(character.gameobject.id, position)
+        character.gameobject.add_component(
+            Occupation(
+                OccupationTypeLibrary.get(position),
+                business.gameobject.id,
+            )
+        )
+
+    return LifeEventType(
+        "HiredAtBusiness",
+        roles=[EventRoles.get("HiringBusiness"), EventRoles.get("Unemployed")],
+        execute_fn=cb,
+    )
+
+
+# class ChildBirthEvent(LifeEvent):
+#     event_type: str = "child-birth"
+#
+#     __slots__ = "parent_names", "parent_ids", "child_name", "child_id"
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         parent_names: Tuple[str, str],
+#         parent_ids: Tuple[int, int],
+#         child_name: str,
+#         child_id: int,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.parent_names: Tuple[str, str] = parent_names
+#         self.parent_ids: Tuple[int, int] = parent_ids
+#         self.child_name: str = child_name
+#         self.child_id: int = child_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "parent_names": self.parent_names,
+#             "parent_ids": self.parent_ids,
+#             "child_name": self.child_name,
+#             "child_id": self.child_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} born to parents {} and {}.".format(
+#             self.timestamp, self.child_name, self.parent_names[0], self.parent_names[1]
+#         )
+#
+#
+# class OpenBusinessEvent(LifeEvent):
+#     event_type: str = "open-business"
+#
+#     __slots__ = "business_id", "business_name", "owner_id", "owner_name"
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         owner_id: int,
+#         owner_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.owner_name: str = owner_name
+#         self.owner_id: int = owner_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "owner_name": self.owner_name,
+#             "owner_id": self.owner_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} opened new business, {}.".format(
+#             self.timestamp, self.owner_name, self.business_name
+#         )
+#
+#
+# class CloseBusinessEvent(LifeEvent):
+#     event_type: str = "close-business"
+#
+#     __slots__ = "business_id", "business_name", "owner_id", "owner_name"
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         owner_id: int,
+#         owner_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.owner_name: str = owner_name
+#         self.owner_id: int = owner_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "owner_name": self.owner_name,
+#             "owner_id": self.owner_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {}'s {} goes out of business.".format(
+#             self.timestamp, self.owner_name, self.business_name
+#         )
+#
+#
+# class DepartureEvent(LifeEvent):
+#     event_type: str = "departure"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} departed from the town.".format(
+#             self.timestamp, self.character_name
+#         )
+#
+#
+# class MarriageEvent(LifeEvent):
+#     event_type: str = "marriage"
+#
+#     __slots__ = (
+#         "character_ids",
+#         "character_names",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_ids: Tuple[int, int],
+#         character_names: Tuple[str, str],
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_names: Tuple[str, str] = character_names
+#         self.character_ids: Tuple[int, int] = character_ids
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_names": self.character_names,
+#             "character_ids": self.character_ids,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} and {} got married.".format(
+#             self.timestamp, self.character_names[0], self.character_names[1]
+#         )
+#
+#
+# class DivorceEvent(LifeEvent):
+#     event_type: str = "marriage"
+#
+#     __slots__ = (
+#         "character_ids",
+#         "character_names",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_ids: Tuple[int, int],
+#         character_names: Tuple[str, str],
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_names: Tuple[str, str] = character_names
+#         self.character_ids: Tuple[int, int] = character_ids
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_names": self.character_names,
+#             "character_ids": self.character_ids,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} and {} got divorced.".format(
+#             self.timestamp, self.character_names[0], self.character_names[1]
+#         )
+#
+#
+# class JobHiringEvent(LifeEvent):
+#     event_type: str = "job-hiring"
+#
+#     __slots__ = (
+#         "business_id",
+#         "business_name",
+#         "character_id",
+#         "character_name",
+#         "occupation_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         character_id: int,
+#         character_name: str,
+#         occupation_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.occupation_name: str = occupation_name
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "occupation_name": self.occupation_name,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} got hired as a {} at {}.".format(
+#             self.timestamp,
+#             self.character_name,
+#             self.occupation_name,
+#             self.business_name,
+#         )
+#
+#
+# class JobLayoffEvent(LifeEvent):
+#     event_type: str = "job-layoff"
+#
+#     __slots__ = (
+#         "business_id",
+#         "business_name",
+#         "character_id",
+#         "character_name",
+#         "occupation_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         character_id: int,
+#         character_name: str,
+#         occupation_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.occupation_name: str = occupation_name
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "occupation_name": self.occupation_name,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} got laid-off as a {} at {}.".format(
+#             self.timestamp,
+#             self.character_name,
+#             self.occupation_name,
+#             self.business_name,
+#         )
+#
+#
+# class JobPromotionEvent(LifeEvent):
+#     event_type: str = "job-promotion"
+#
+#     __slots__ = (
+#         "business_id",
+#         "business_name",
+#         "character_id",
+#         "character_name",
+#         "occupation_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         character_id: int,
+#         character_name: str,
+#         occupation_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.occupation_name: str = occupation_name
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "occupation_name": self.occupation_name,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} got promoted to a {} at {}.".format(
+#             self.timestamp,
+#             self.character_name,
+#             self.occupation_name,
+#             self.business_name,
+#         )
+#
+#
+# class LeaveJobEvent(LifeEvent):
+#     event_type: str = "leave-job"
+#
+#     __slots__ = (
+#         "business_id",
+#         "business_name",
+#         "character_id",
+#         "character_name",
+#         "occupation_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         character_id: int,
+#         character_name: str,
+#         occupation_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.occupation_name: str = occupation_name
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "occupation_name": self.occupation_name,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} left their job as a {} at {}.".format(
+#             self.timestamp,
+#             self.character_name,
+#             self.occupation_name,
+#             self.business_name,
+#         )
+#
+#
+# class RetirementEvent(LifeEvent):
+#     event_type: str = "retire"
+#
+#     __slots__ = (
+#         "business_id",
+#         "business_name",
+#         "character_id",
+#         "character_name",
+#         "occupation_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         business_id: int,
+#         business_name: str,
+#         character_id: int,
+#         character_name: str,
+#         occupation_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.business_id: int = business_id
+#         self.business_name: str = business_name
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.occupation_name: str = occupation_name
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "business_name": self.business_name,
+#             "business_id": self.business_id,
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "occupation_name": self.occupation_name,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} retired from being a {} at {}.".format(
+#             self.timestamp,
+#             self.character_name,
+#             self.occupation_name,
+#             self.business_name,
+#         )
+#
+#
+# class MoveResidenceEvent(LifeEvent):
+#     event_type: str = "move-residence"
+#
+#     __slots__ = ("character_id", "character_name", "residence_id")
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#         residence_id: int,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.residence_id: int = residence_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "residence_id": self.residence_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} moved to a new residence.".format(
+#             self.timestamp, self.character_name
+#         )
+#
+#
+# class DeathEvent(LifeEvent):
+#     event_type: str = "death"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} died.".format(self.timestamp, self.character_name)
+#
+#
+# class DeathInFamilyEvent(LifeEvent):
+#     event_type: str = "death-in-family"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+
+#
+# class BecomeAdolescentEvent(LifeEvent):
+#     event_type: str = "become-teen"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} became an teen.".format(self.timestamp, self.character_name)
+#
+#
+# class BecomeYoungAdultEvent(LifeEvent):
+#     event_type: str = "become-young-adult"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} became an young adult.".format(
+#             self.timestamp, self.character_name
+#         )
+#
+#
+# class BecomeAdultEvent(LifeEvent):
+#     event_type: str = "become-adult"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} became an adult.".format(self.timestamp, self.character_name)
+#
+#
+# class BecomeElderEvent(LifeEvent):
+#     event_type: str = "become-elder"
+#
+#     __slots__ = (
+#         "character_id",
+#         "character_name",
+#     )
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} became an elder.".format(self.timestamp, self.character_name)
+#
+#
+# class SocializeEvent(LifeEvent):
+#     event_type: str = "socialize"
+#
+#     __slots__ = ("character_ids", "character_names", "interaction_type")
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_ids: Tuple[int, int],
+#         character_names: Tuple[str, str],
+#         interaction_type: str,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_names: Tuple[str, str] = character_names
+#         self.character_ids: Tuple[int, int] = character_ids
+#         self.interaction_type: str = interaction_type
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_names": self.character_names,
+#             "character_ids": self.character_ids,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} and {} socialized.".format(
+#             self.timestamp, self.character_names[0], self.character_names[1]
+#         )
+#
+#
+# class HomePurchaseEvent(LifeEvent):
+#     event_type: str = "home-purchase"
+#
+#     __slots__ = ("character_id", "character_name", "residence_id")
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_id: int,
+#         character_name: str,
+#         residence_id: int,
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_name: str = character_name
+#         self.character_id: int = character_id
+#         self.residence_id: int = residence_id
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_name": self.character_name,
+#             "character_id": self.character_id,
+#             "residence_id": self.residence_id,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) {} purchased a house.".format(self.timestamp, self.character_name)
+#
+#
+# class BecomeFriendsEvent(LifeEvent):
+#     """Two characters become friends"""
+#
+#     event_type: str = "become-friends"
+#
+#     __slots__ = ("character_ids", "character_names")
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_ids: Tuple[int, int],
+#         character_names: Tuple[str, str],
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_names: Tuple[str, str] = character_names
+#         self.character_ids: Tuple[int, int] = character_ids
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_names": self.character_names,
+#             "character_ids": self.character_ids,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) became friends.".format(self.timestamp, self.character_names)
+#
+#
+# class BecomeEnemiesEvent(LifeEvent):
+#     """Two characters become friends"""
+#
+#     event_type: str = "become-enemies"
+#
+#     __slots__ = ("character_ids", "character_names")
+#
+#     def __init__(
+#         self,
+#         timestamp: str,
+#         character_ids: Tuple[int, int],
+#         character_names: Tuple[str, str],
+#     ) -> None:
+#         super().__init__(timestamp)
+#         self.character_names: Tuple[str, str] = character_names
+#         self.character_ids: Tuple[int, int] = character_ids
+#
+#     @classmethod
+#     def get_type(cls) -> str:
+#         return cls.event_type
+#
+#     def to_dict(self) -> Dict[str, Any]:
+#         return {
+#             **super().to_dict(),
+#             "character_names": self.character_names,
+#             "character_ids": self.character_ids,
+#         }
+#
+#     def __str__(self) -> str:
+#         return "({}) became enemies.".format(self.timestamp, self.character_names)
+def become_friends_event(
+    threshold: int = 25, probability: float = 1.0
+) -> LifeEventType:
+    """Defines an event where two characters become friends"""
+
+    def bind_potential_friend(world: World, event: LifeEvent):
+        """
+        Return a Character that has a mutual friendship score
+        with PersonA that is above a given threshold
+        """
+        rel_graph = world.get_resource(RelationshipGraph)
+        engine = world.get_resource(NeighborlyEngine)
+        person_a_relationships = rel_graph.get_relationships(event["PersonA"])
+        eligible_characters: List[GameObject] = []
+        for rel in person_a_relationships:
+            if (
+                world.has_gameobject(rel.target)
+                and rel_graph.has_connection(rel.target, event["PersonA"])
+                and (
+                    rel_graph.get_connection(rel.target, event["PersonA"]).friendship
+                    >= threshold
+                )
+                and not rel_graph.get_connection(rel.target, event["PersonA"]).has_tags(
+                    RelationshipTag.Friend
+                )
+                and not rel.has_tags(RelationshipTag.Friend)
+                and rel.friendship >= threshold
+            ):
+                eligible_characters.append(world.get_gameobject(rel.target))
+
+        if eligible_characters:
+            return engine.rng.choice(eligible_characters)
+        return None
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).add_tags(
+            RelationshipTag.Friend
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).add_tags(
+            RelationshipTag.Friend
+        )
+
+    return LifeEventType(
+        name="BecomeFriends",
+        roles=[
+            EventRoleType(name="PersonA", components=[GameCharacter]),
+            EventRoleType(name="PersonB", binder_fn=bind_potential_friend),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def become_enemies_event(
+    threshold: int = -25, probability: float = 1.0
+) -> LifeEventType:
+    """Defines an event where two characters become friends"""
+
+    def potential_enemy_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(
+            event["PersonA"], gameobject.id
+        ) and rel_graph.has_connection(gameobject.id, event["PersonA"]):
+            return (
+                rel_graph.get_connection(event["PersonA"], gameobject.id).friendship
+                <= threshold
+                and rel_graph.get_connection(gameobject.id, event["PersonA"]).friendship
+                <= threshold
+                and not rel_graph.get_connection(
+                    event["PersonA"], gameobject.id
+                ).has_tags(RelationshipTag.Enemy)
+            )
+        else:
+            return False
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).add_tags(
+            RelationshipTag.Enemy
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).add_tags(
+            RelationshipTag.Enemy
+        )
+
+    return LifeEventType(
+        name="BecomeEnemies",
+        roles=[
+            EventRoleType(name="PersonA", components=[GameCharacter]),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=potential_enemy_filter,
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def start_dating_event(threshold: int = 25, probability: float = 0.5) -> LifeEventType:
+    """Defines an event where two characters become friends"""
+
+    def potential_partner_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(
+            event["PersonA"], gameobject.id
+        ) and rel_graph.has_connection(gameobject.id, event["PersonA"]):
+            return (
+                rel_graph.get_connection(event["PersonA"], gameobject.id).romance
+                >= threshold
+                and rel_graph.get_connection(gameobject.id, event["PersonA"]).romance
+                >= threshold
+            )
+        else:
+            return False
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).add_tags(
+            RelationshipTag.SignificantOther
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).add_tags(
+            RelationshipTag.SignificantOther
+        )
+
+        person_a = world.get_gameobject(event["PersonA"])
+        person_b = world.get_gameobject(event["PersonB"])
+
+        person_a.add_component(
+            Dating(
+                partner_id=person_b.id,
+                partner_name=str(person_b.get_component(GameCharacter).name),
+            )
+        )
+        person_b.add_component(
+            Dating(
+                partner_id=person_a.id,
+                partner_name=str(person_a.get_component(GameCharacter).name),
+            )
+        )
+
+    return LifeEventType(
+        name="StartDating",
+        roles=[
+            EventRoleType(
+                name="PersonA", components=[GameCharacter], filter_fn=is_single
+            ),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=join_filters(potential_partner_filter, is_single),
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def dating_break_up_event(
+    threshold: int = -15, probability: float = 0.5
+) -> LifeEventType:
+    """Defines an event where two characters stop dating"""
+
+    def current_partner_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        if gameobject.has_component(Dating):
+            if gameobject.get_component(Dating).partner_id == event["PersonA"]:
+                return (
+                    rel_graph.get_connection(event["PersonA"], gameobject.id).romance
+                    < threshold
+                )
+
+        return False
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).remove_tags(
+            RelationshipTag.SignificantOther
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).remove_tags(
+            RelationshipTag.SignificantOther
+        )
+
+        world.get_gameobject(event["PersonA"]).remove_component(Dating)
+        world.get_gameobject(event["PersonB"]).remove_component(Dating)
+
+    return LifeEventType(
+        name="DatingBreakUp",
+        roles=[
+            EventRoleType(name="PersonA", components=[GameCharacter]),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=current_partner_filter,
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def divorce_event(threshold: int = -25, probability: float = 0.5) -> LifeEventType:
+    """Defines an event where two characters stop dating"""
+
+    def current_partner_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        if gameobject.has_component(Married):
+            if gameobject.get_component(Married).partner_id == event["PersonA"]:
+                return (
+                    rel_graph.get_connection(event["PersonA"], gameobject.id).romance
+                    < threshold
+                )
+
+        return False
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).remove_tags(
+            RelationshipTag.SignificantOther | RelationshipTag.Spouse
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).remove_tags(
+            RelationshipTag.SignificantOther | RelationshipTag.Spouse
+        )
+
+        world.get_gameobject(event["PersonA"]).remove_component(Married)
+        world.get_gameobject(event["PersonB"]).remove_component(Married)
+
+    return LifeEventType(
+        name="GotDivorced",
+        roles=[
+            EventRoleType(name="PersonA", components=[GameCharacter]),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=current_partner_filter,
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def marriage_event(threshold: int = 35, probability: float = 0.5) -> LifeEventType:
+    """Defines an event where two characters become friends"""
+
+    def potential_partner_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        rel_graph = world.get_resource(RelationshipGraph)
+        if (
+            gameobject.has_component(Dating)
+            and gameobject.get_component(Dating).partner_id == event["PersonA"]
+        ):
+            if rel_graph.has_connection(
+                event["PersonA"], gameobject.id
+            ) and rel_graph.has_connection(gameobject.id, event["PersonA"]):
+                return (
+                    rel_graph.get_connection(event["PersonA"], gameobject.id).romance
+                    >= threshold
+                    and rel_graph.get_connection(
+                        gameobject.id, event["PersonA"]
+                    ).romance
+                    >= threshold
+                )
+
+        return False
+
+    def execute(world: World, event: LifeEvent) -> None:
+        rel_graph = world.get_resource(RelationshipGraph)
+
+        rel_graph.get_connection(event["PersonA"], event["PersonB"]).add_tags(
+            RelationshipTag.SignificantOther | RelationshipTag.Spouse
+        )
+        rel_graph.get_connection(event["PersonB"], event["PersonA"]).add_tags(
+            RelationshipTag.SignificantOther | RelationshipTag.Spouse
+        )
+
+        person_a = world.get_gameobject(event["PersonA"])
+        person_b = world.get_gameobject(event["PersonB"])
+
+        person_a.remove_component(Dating)
+        person_b.remove_component(Dating)
+
+        person_a.add_component(
+            Married(
+                partner_id=person_b.id,
+                partner_name=str(person_b.get_component(GameCharacter).name),
+            )
+        )
+        person_b.add_component(
+            Married(
+                partner_id=person_a.id,
+                partner_name=str(person_a.get_component(GameCharacter).name),
+            )
+        )
+
+    return LifeEventType(
+        name="GotMarried",
+        roles=[
+            EventRoleType(
+                name="PersonA", components=[GameCharacter], filter_fn=is_single
+            ),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=join_filters(potential_partner_filter, is_single),
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def depart_due_to_unemployment() -> LifeEventType:
+    def bind_unemployed_character(world: World, event: LifeEvent):
+        eligible_characters: List[GameObject] = []
+        for _, unemployed in world.get_component(Unemployed):
+            if unemployed.duration_days > 30:
+                eligible_characters.append(unemployed.gameobject)
+        if eligible_characters:
+            return world.get_resource(NeighborlyEngine).rng.choice(eligible_characters)
+        return None
+
+    def effect(world: World, event: LifeEvent):
+        world.delete_gameobject(event["Person"])
+
+    return LifeEventType(
+        name="DepartTown",
+        roles=[EventRoleType(name="Person", binder_fn=bind_unemployed_character)],
+        execute_fn=effect,
+    )
+
+
+def pregnancy_event(probability: float = 0.3) -> LifeEventType:
+    """Defines an event where two characters stop dating"""
+
+    def can_get_pregnant_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        return gameobject.get_component(
+            GameCharacter
+        ).can_get_pregnant and not gameobject.has_component(Pregnant)
+
+    def current_partner_filter(
+        world: World, event: LifeEvent, gameobject: GameObject
+    ) -> bool:
+        return (
+            gameobject.has_component(Married)
+            and gameobject.get_component(Married).partner_id == event["PersonA"]
+        )
+
+    def execute(world: World, event: LifeEvent) -> None:
+        due_date = SimDateTime.from_iso_str(
+            world.get_resource(SimDateTime).to_iso_str()
+        )
+        due_date.increment(months=9)
+
+        world.get_gameobject(event["PersonA"]).add_component(
+            Pregnant(
+                partner_name=str(
+                    world.get_gameobject(event["PersonB"])
+                    .get_component(GameCharacter)
+                    .name
+                ),
+                partner_id=event["PersonB"],
+                due_date=due_date,
+            )
+        )
+
+    return LifeEventType(
+        name="GotDivorced",
+        roles=[
+            EventRoleType(
+                name="PersonA",
+                components=[GameCharacter],
+                filter_fn=can_get_pregnant_filter,
+            ),
+            EventRoleType(
+                name="PersonB",
+                components=[GameCharacter],
+                filter_fn=current_partner_filter,
+            ),
+        ],
+        execute_fn=execute,
+        probability=probability,
+    )
+
+
+def retire_event(probability: float = 0.4) -> LifeEventType:
+    """
+    Event for characters retiring from working after reaching elder status
+
+    Parameters
+    ----------
+    probability: float
+        Probability that a character will retire from their job
+        when they are an elder
+
+    Returns
+    -------
+    LifeEventType
+        LifeEventType instance with all configuration defined
+    """
+
+    def bind_retiree(world: World, event: LifeEvent):
+        eligible_characters: List[GameObject] = []
+        for gid, _ in world.get_components(Elder, Occupation):
+            gameobject = world.get_gameobject(gid)
+            if not gameobject.has_component(Retired):
+                eligible_characters.append(gameobject)
+        if eligible_characters:
+            return world.get_resource(NeighborlyEngine).rng.choice(eligible_characters)
+        return None
+
+    def execute(world: World, event: LifeEvent):
+        retiree = world.get_gameobject(event["Retiree"])
+        retiree.remove_component(Occupation)
+        retiree.add_component(Retired())
+
+    return LifeEventType(
+        name="Retire",
+        roles=[EventRoleType(name="Retiree", binder_fn=bind_retiree)],
+        execute_fn=execute,
+        probability=probability,
+    )
diff --git a/src/neighborly/builtin/helpers.py b/src/neighborly/builtin/helpers.py
new file mode 100644
index 0000000..b4dbbe8
--- /dev/null
+++ b/src/neighborly/builtin/helpers.py
@@ -0,0 +1,441 @@
+from __future__ import annotations
+
+import logging
+from typing import Any, List, Literal, Optional, Tuple, cast
+
+import numpy as np
+
+import neighborly.core.behavior_tree as bt
+from neighborly.builtin.statuses import (
+    Adult,
+    Child,
+    CollegeGraduate,
+    Elder,
+    Female,
+    Male,
+    NonBinary,
+    Teen,
+    Unemployed,
+    YoungAdult,
+)
+from neighborly.core.activity import ActivityLibrary
+from neighborly.core.behavior_tree import BehaviorNode
+from neighborly.core.business import Business, IOccupationPreconditionFn, WorkHistory
+from neighborly.core.character import CharacterArchetype, GameCharacter
+from neighborly.core.ecs import GameObject, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import EventRole, LifeEvent, LifeEventLog
+from neighborly.core.location import Location
+from neighborly.core.personal_values import PersonalValues
+from neighborly.core.relationship import (
+    Relationship,
+    RelationshipGraph,
+    RelationshipTag,
+)
+from neighborly.core.residence import Residence, Resident
+from neighborly.core.time import SimDateTime
+
+logger = logging.getLogger(__name__)
+
+############################################
+# Occupation Precondition Functions
+############################################
+
+
+def before_year(year: int) -> IOccupationPreconditionFn:
+    """Return precondition function that checks if the date is before a given year"""
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return world.get_resource(SimDateTime).year < year
+
+    return fn
+
+
+def after_year(year: int) -> IOccupationPreconditionFn:
+    """Return precondition function that checks if the date is after a given year"""
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return world.get_resource(SimDateTime).year > year
+
+    return fn
+
+
+def is_gender(
+    gender: Literal["male", "female", "non-binary"]
+) -> IOccupationPreconditionFn:
+    """Return precondition function that checks if a character is a given gender"""
+    gender_component_types = {"male": Male, "female": Female, "non-binary": NonBinary}
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return gameobject.has_component(gender_component_types[gender])
+
+    return fn
+
+
+def has_any_work_experience() -> IOccupationPreconditionFn:
+    """Return True if the character has any work experience at all"""
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return len(gameobject.get_component(WorkHistory)) > 0
+
+    return fn
+
+
+def has_experience_as_a(
+    occupation_type: str, years_experience: int = 0
+) -> IOccupationPreconditionFn:
+    """
+    Returns Precondition function that returns true if the character
+    has experience as a given occupation type.
+
+    Parameters
+    ----------
+    occupation_type: str
+        The name of the occupation to check for
+    years_experience: int
+        The number of years of experience the character needs to have
+
+    Returns
+    -------
+    IOccupationPreconditionFn
+        The precondition function used when filling the occupation
+    """
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        work_history = gameobject.get_component(WorkHistory)
+        return (
+            work_history.has_experience_as_a(occupation_type)
+            and work_history.total_experience_as_a(occupation_type) >= years_experience
+        )
+
+    return fn
+
+
+def is_college_graduate() -> IOccupationPreconditionFn:
+    """Return True if the character is a college graduate"""
+
+    def fn(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return gameobject.has_component(CollegeGraduate)
+
+    return fn
+
+
+############################################
+# Role Filter Functions
+############################################
+
+
+def is_single(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+    rel_graph = world.get_resource(RelationshipGraph)
+    return (
+        len(
+            rel_graph.get_all_relationships_with_tags(
+                gameobject.id, RelationshipTag.SignificantOther
+            )
+        )
+        == 0
+    )
+
+
+def get_top_activities(character_values: PersonalValues, n: int = 3) -> Tuple[str, ...]:
+    """Return the top activities a character would enjoy given their values"""
+
+    scores: List[Tuple[int, str]] = []
+
+    for activity in ActivityLibrary.get_all_activities():
+        score: int = int(np.dot(character_values.traits, activity.personal_values))
+        scores.append((score, activity.name))
+
+    return tuple(
+        [
+            activity_score[1]
+            for activity_score in sorted(scores, key=lambda s: s[0], reverse=True)
+        ][:n]
+    )
+
+
+def find_places_with_activities(world: World, *activities: str) -> List[int]:
+    """Return a list of entity ID for locations that have the given activities"""
+    locations = world.get_component(Location)
+
+    matches: List[int] = []
+
+    for location_id, location in locations:
+        if location.has_flags(*ActivityLibrary.get_activity_flags(*activities)):
+            matches.append(location_id)
+
+    return matches
+
+
+def find_places_with_any_activities(world: World, *activities: str) -> List[int]:
+    """Return a list of entity ID for locations that have any of the given activities
+
+    Results are sorted by how many activities they match
+    """
+
+    flags = ActivityLibrary.get_activity_flags(*activities)
+
+    def score_location(loc: Location) -> int:
+        location_score: int = 0
+        for flag in flags:
+            if loc.activity_flags & flag > 0:
+                location_score += 1
+        return location_score
+
+    locations = world.get_component(Location)
+
+    matches: List[Tuple[int, int]] = []
+
+    for location_id, location in locations:
+        score = score_location(location)
+        if score > 0:
+            matches.append((score, location_id))
+
+    return [match[1] for match in sorted(matches, key=lambda m: m[0], reverse=True)]
+
+
+def add_coworkers(character: GameObject, business: Business) -> None:
+    """Add coworker tags to current coworkers in relationship network"""
+
+    world: World = character.world
+    rel_graph = world.get_resource(RelationshipGraph)
+
+    for employee_id in business.get_employees():
+        if employee_id == character.id:
+            continue
+
+        if not rel_graph.has_connection(character.id, employee_id):
+            rel_graph.add_relationship(Relationship(character.id, employee_id))
+
+        if not rel_graph.has_connection(employee_id, character.id):
+            rel_graph.add_relationship(Relationship(employee_id, character.id))
+
+        rel_graph.get_connection(character.id, employee_id).add_tags(
+            RelationshipTag.Coworker
+        )
+
+        rel_graph.get_connection(employee_id, character.id).add_tags(
+            RelationshipTag.Coworker
+        )
+
+
+def remove_coworkers(character: GameObject, business: Business) -> None:
+    """Remove coworker tags from current coworkers in relationship network"""
+    world = character.world
+    rel_graph = world.get_resource(RelationshipGraph)
+
+    for employee_id in business.get_employees():
+        if employee_id == character.id:
+            continue
+
+        if rel_graph.has_connection(character.id, employee_id):
+            rel_graph.get_connection(character.id, employee_id).remove_tags(
+                RelationshipTag.Coworker
+            )
+
+        if rel_graph.has_connection(employee_id, character.id):
+            rel_graph.get_connection(employee_id, character.id).remove_tags(
+                RelationshipTag.Coworker
+            )
+
+
+def move_to_location(
+    world: World, character: GameCharacter, destination: Location
+) -> None:
+    if destination.gameobject.id == character.location:
+        return
+
+    if character.location is not None:
+        current_location: Location = world.get_gameobject(
+            character.location
+        ).get_component(Location)
+        current_location.remove_character(character.gameobject.id)
+
+    destination.add_character(character.gameobject.id)
+    character.location = destination.gameobject.id
+
+
+def get_locations(world: World) -> List[Tuple[int, Location]]:
+    return sorted(
+        cast(List[Tuple[int, Location]], world.get_component(Location)),
+        key=lambda pair: pair[0],
+    )
+
+
+def move_residence(character: GameCharacter, new_residence: Residence) -> None:
+    """Move a character into a residence"""
+
+    world = character.gameobject.world
+
+    # Move out of the old residence
+    if "home" in character.location_aliases:
+        old_residence = world.get_gameobject(
+            character.location_aliases["home"]
+        ).get_component(Residence)
+        old_residence.remove_resident(character.gameobject.id)
+        if old_residence.is_owner(character.gameobject.id):
+            old_residence.remove_owner(character.gameobject.id)
+        old_residence.gameobject.get_component(Location).whitelist.remove(
+            character.gameobject.id
+        )
+
+    # Move into new residence
+    new_residence.add_resident(character.gameobject.id)
+    character.location_aliases["home"] = new_residence.gameobject.id
+    new_residence.gameobject.get_component(Location).whitelist.add(
+        character.gameobject.id
+    )
+    character.gameobject.add_component(Resident(new_residence.gameobject.id))
+
+
+############################################
+# GENERATING CHARACTERS OF DIFFERENT AGES
+############################################
+
+
+def generate_child_character(
+    world: World, engine: NeighborlyEngine, archetype: CharacterArchetype
+) -> GameObject:
+    character = world.spawn_archetype(archetype)
+    character.add_component(Child())
+    return character
+
+
+def generate_teen_character(
+    world: World, engine: NeighborlyEngine, archetype: CharacterArchetype
+) -> GameObject:
+    character = world.spawn_archetype(archetype)
+    character.add_component(Teen())
+    return character
+
+
+def generate_young_adult_character(
+    world: World, engine: NeighborlyEngine, archetype: CharacterArchetype
+) -> GameObject:
+    """
+    Create a new Young-adult-aged character
+
+    Parameters
+    ----------
+    world: World
+        The world to spawn the character into
+    engine: NeighborlyEngine
+        The engine instance that holds the character archetypes
+    archetype: Optional[str]
+        The name of the archetype to generate. A random archetype
+        will be generated if not provided
+
+    Returns
+    -------
+    GameObject
+        The final generated gameobject
+    """
+    character = world.spawn_archetype(archetype)
+    character.add_component(Unemployed())
+    character.add_component(YoungAdult())
+    return character
+
+
+def generate_adult_character(
+    world: World, engine: NeighborlyEngine, archetype: CharacterArchetype
+) -> GameObject:
+    character = world.spawn_archetype(archetype)
+    character.add_component(Unemployed())
+    character.add_component(Adult())
+    return character
+
+
+def generate_elderly_character(
+    world: World, engine: NeighborlyEngine, archetype: CharacterArchetype
+) -> GameObject:
+    character = world.spawn_archetype(archetype)
+    character.add_component(Elder())
+    return character
+
+
+############################################
+# Actions
+############################################
+
+
+def become_adult_behavior(chance_depart: float) -> bt.BehaviorNode:
+    return bt.sequence(chance_node(chance_depart), depart_action)
+
+
+def chance_node(chance: float) -> bt.BehaviorNode:
+    """Returns BehaviorTree node that returns True with a given probability"""
+
+    def fn(world: World, event: LifeEvent, **kwargs) -> bool:
+        return world.get_resource(NeighborlyEngine).rng.random() < chance
+
+    return fn
+
+
+def action_node(fn) -> bt.BehaviorNode:
+    def wrapper(world: World, event: LifeEvent, **kwargs) -> bool:
+        fn(world, event, **kwargs)
+        return True
+
+    return wrapper
+
+
+@action_node
+def go_to_college(world: World, event: LifeEvent, **kwargs) -> None:
+    gameobject = world.get_gameobject(event["Unemployed"])
+    gameobject.add_component(CollegeGraduate())
+    # Reset the unemployment counter since they graduate from school
+    gameobject.get_component(Unemployed).duration_days = 0
+
+    world.get_resource(LifeEventLog).record_event(
+        LifeEvent(
+            name="GraduatedCollege",
+            roles=[EventRole("Graduate", gameobject.id)],
+            timestamp=world.get_resource(SimDateTime).to_iso_str(),
+        )
+    )
+
+
+@action_node
+def death_action(world: World, event: LifeEvent, **kwargs) -> None:
+    gameobject = world.get_gameobject(event["Deceased"])
+    gameobject.archive()
+
+    world.get_resource(LifeEventLog).record_event(
+        LifeEvent(
+            name="Death",
+            roles=[EventRole("Deceased", gameobject.id)],
+            timestamp=world.get_resource(SimDateTime).to_iso_str(),
+        )
+    )
+
+
+@action_node
+def depart_action(world: World, event: LifeEvent, **kwargs) -> None:
+    gameobject = world.get_gameobject(event["Departee"])
+    gameobject.archive()
+
+    # Get the character's dependent nuclear family
+    rel_graph = world.get_resource(RelationshipGraph)
+
+    spouse_rel = rel_graph.get_all_relationships_with_tags(
+        gameobject.id, RelationshipTag.Spouse
+    )
+
+    if spouse_rel:
+        world.get_gameobject(spouse_rel[0].target).archive()
+        event.roles.append(EventRole("Departee", spouse_rel[0].target))
+
+    children = rel_graph.get_all_relationships_with_tags(
+        gameobject.id, RelationshipTag.Child | RelationshipTag.NuclearFamily
+    )
+    for child_rel in children:
+        world.get_gameobject(child_rel.target).archive()
+        event.roles.append(EventRole("Departee", child_rel.target))
+
+    world.get_resource(LifeEventLog).record_event(
+        LifeEvent(
+            name="Depart",
+            roles=event.roles,
+            timestamp=world.get_resource(SimDateTime).to_iso_str(),
+        )
+    )
diff --git a/src/neighborly/builtin/role_filters.py b/src/neighborly/builtin/role_filters.py
new file mode 100644
index 0000000..512b53e
--- /dev/null
+++ b/src/neighborly/builtin/role_filters.py
@@ -0,0 +1,175 @@
+from neighborly.builtin.statuses import Male
+from neighborly.core.character import GameCharacter
+from neighborly.core.ecs import GameObject, World
+from neighborly.core.life_event import LifeEvent
+from neighborly.core.time import SimDateTime
+
+
+def over_age(age: int) -> RoleFilterFn:
+    def fn(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        return gameobject.get_component(GameCharacter).age > age
+
+    return fn
+
+
+def before_year(year: int):
+    """
+    Returns precondition function that checks if the
+    current year is less than the given year
+    """
+
+    def precondition_fn(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        return world.get_resource(SimDateTime).year < year
+
+    return precondition_fn
+
+
+def is_man(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+    """Return true if GameObject is a man"""
+    return gameobject.has_component(Male)
+
+
+def older_than(age: int):
+    def precondition_fn(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        return gameobject.get_component(GameCharacter).age > age
+
+    return precondition_fn
+
+
+def friendship_greater_than(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    friendship value for a character to another is greater than
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).friendship > value
+        return False
+
+    return fn
+
+
+def friendship_less_than(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    friendship value for a character to another is less than
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).friendship < value
+        return False
+
+    return fn
+
+
+def friendship_equal_to(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    friendship value for a character to another is equal to
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).friendship == value
+        return False
+
+    return fn
+
+
+def romance_greater_than(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    romance value for a character to another is greater than
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).romance > value
+        return False
+
+    return fn
+
+
+def romance_less_than(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    romance value for a character to another is less than
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).romance < value
+        return False
+
+    return fn
+
+
+def romance_equal_to(value: int) -> ILifeEventCallback:
+    """
+    Returns a precondition function that checks if the
+    romance value for a character to another is equal to
+    a given value
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).romance == value
+        return False
+
+    return fn
+
+
+def relationship_has_tag(tag: str) -> ILifeEventCallback:
+    """
+    Return true if the relationship between this character and
+    another has the given tag
+    """
+
+    def fn(gameobject: GameObject, event: LifeEvent) -> bool:
+        other: GameObject = event.data["other"]
+        rel_graph = gameobject.world.get_resource(RelationshipGraph)
+        if rel_graph.has_connection(gameobject.id, other.id):
+            return rel_graph.get_connection(gameobject.id, other.id).has_tags(
+                RelationshipTag[tag]
+            )
+        return False
+
+    return fn
+
+
+def is_single(gameobject: GameObject, event: LifeEvent) -> bool:
+    """Return True if this character has no relationships tagged as significant others"""
+    rel_graph = gameobject.world.get_resource(RelationshipGraph)
+    significant_other_relationships = rel_graph.get_all_relationships_with_tags(
+        gameobject.id, RelationshipTag.SignificantOther
+    )
+    return bool(significant_other_relationships)
+
+
+def is_unemployed(gameobject: GameObject, event: LifeEvent) -> bool:
+    """Returns True if this character does not have a job"""
+    return not gameobject.has_component(Occupation)
+
+
+def is_employed(gameobject: GameObject, event: LifeEvent) -> bool:
+    """Returns True if this character has a job"""
+    return gameobject.has_component(Occupation)
diff --git a/src/neighborly/builtin/statuses.py b/src/neighborly/builtin/statuses.py
new file mode 100644
index 0000000..00375ca
--- /dev/null
+++ b/src/neighborly/builtin/statuses.py
@@ -0,0 +1,193 @@
+from neighborly.core.business import Business
+from neighborly.core.ecs import Component, World
+from neighborly.core.status import Status
+from neighborly.core.time import SimDateTime
+
+
+class Child(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "child",
+            "Character is seen as a child in the eyes of society",
+        )
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls()
+
+
+class Teen(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Adolescent",
+            "Character is seen as an adolescent in the eyes of society",
+        )
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls()
+
+
+class YoungAdult(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Young Adult",
+            "Character is seen as a young adult in the eyes of society",
+        )
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls()
+
+
+class Adult(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Adult",
+            "Character is seen as an adult in the eyes of society",
+        )
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls()
+
+
+class Elder(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Senior",
+            "Character is seen as a senior in the eyes of society",
+        )
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls()
+
+
+class Deceased(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Deceased",
+            "This character is dead",
+        )
+
+
+class Retired(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "Retired",
+            "This character retired from their last occupation",
+        )
+
+
+class Dependent(Status):
+    def __init__(self) -> None:
+        super().__init__("Dependent", "This character is dependent on their parents")
+
+
+class Unemployed(Status):
+    __slots__ = "duration_days"
+
+    def __init__(self) -> None:
+        super().__init__(
+            "Unemployed",
+            "Character doesn't have a job",
+        )
+        self.duration_days: float = 0
+
+
+class Dating(Status):
+    __slots__ = "duration_years", "partner_id", "partner_name"
+
+    def __init__(self, partner_id: int, partner_name: str) -> None:
+        super().__init__(
+            "Dating",
+            "This character is in a relationship with another",
+        )
+        self.duration_years: float = 0.0
+        self.partner_id: int = partner_id
+        self.partner_name: str = partner_name
+
+
+class Married(Status):
+    __slots__ = "duration_years", "partner_id", "partner_name"
+
+    def __init__(self, partner_id: int, partner_name: str) -> None:
+        super().__init__(
+            "Married",
+            "This character is married to another",
+        )
+        self.duration_years = 0.0
+        self.partner_id: int = partner_id
+        self.partner_name: str = partner_name
+
+
+class InRelationship(Status):
+    __slots__ = "duration_years", "partner_id", "partner_name", "relationship_type"
+
+    def __init__(
+        self, relationship_type: str, partner_id: int, partner_name: str
+    ) -> None:
+        super().__init__(
+            "Married",
+            "This character is married to another",
+        )
+        self.relationship_type: str = relationship_type
+        self.duration_years = 0.0
+        self.partner_id: int = partner_id
+        self.partner_name: str = partner_name
+
+
+class BusinessOwner(Status):
+    __slots__ = "duration", "business_id", "business_name"
+
+    def __init__(self, business_id: int, business_name: str) -> None:
+        super().__init__(
+            "Business Owner",
+            "This character owns a business",
+        )
+        self.duration = 0.0
+        self.business_id: int = business_id
+        self.business_name: str = business_name
+
+
+class Pregnant(Status):
+    def __init__(
+        self, partner_name: str, partner_id: int, due_date: SimDateTime
+    ) -> None:
+        super().__init__("Pregnant", "This character is pregnant")
+        self.partner_name: str = partner_name
+        self.partner_id: int = partner_id
+        self.due_date: SimDateTime = due_date
+
+    @classmethod
+    def create(cls, world, **kwargs) -> Component:
+        return cls(**kwargs)
+
+
+class Male(Status):
+    def __init__(self):
+        super().__init__("Male", "This character is perceived as masculine.")
+
+
+class Female(Status):
+    def __init__(self):
+        super().__init__("Female", "This character is perceived as feminine.")
+
+
+class NonBinary(Status):
+    def __init__(self):
+        super().__init__("NonBinary", "This character is perceived as non-binary.")
+
+
+class CollegeGraduate(Status):
+    def __init__(self) -> None:
+        super().__init__("College Graduate", "This character graduated from college.")
+
+
+class InTheWorkforce(Status):
+    def __init__(self) -> None:
+        super().__init__(
+            "In the Workforce",
+            "This Character is eligible for employment opportunities.",
+        )
diff --git a/src/neighborly/builtin/systems.py b/src/neighborly/builtin/systems.py
new file mode 100644
index 0000000..f4f10e1
--- /dev/null
+++ b/src/neighborly/builtin/systems.py
@@ -0,0 +1,1068 @@
+from __future__ import annotations
+
+import itertools
+import logging
+from typing import List, Optional, Set, cast
+
+from neighborly.builtin.helpers import (
+    generate_child_character,
+    generate_young_adult_character,
+    get_locations,
+    move_residence,
+    move_to_location,
+)
+from neighborly.builtin.statuses import (
+    Adult,
+    BusinessOwner,
+    Child,
+    Dating,
+    Deceased,
+    Elder,
+    InRelationship,
+    InTheWorkforce,
+    Married,
+    Pregnant,
+    Teen,
+    Unemployed,
+    YoungAdult,
+)
+from neighborly.core.business import (
+    Business,
+    BusinessArchetype,
+    BusinessArchetypeLibrary,
+    BusinessStatus,
+    Occupation,
+    OccupationTypeLibrary,
+)
+from neighborly.core.character import (
+    CharacterArchetype,
+    CharacterArchetypeLibrary,
+    CharacterName,
+    GameCharacter,
+)
+from neighborly.core.ecs import GameObject, ISystem, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import EventRole, LifeEvent, LifeEventLog
+from neighborly.core.location import Location
+from neighborly.core.personal_values import PersonalValues
+from neighborly.core.position import Position2D
+from neighborly.core.relationship import (
+    Relationship,
+    RelationshipGraph,
+    RelationshipTag,
+)
+from neighborly.core.residence import (
+    Residence,
+    ResidenceArchetype,
+    ResidenceArchetypeLibrary,
+)
+from neighborly.core.rng import DefaultRNG
+from neighborly.core.routine import Routine
+from neighborly.core.time import DAYS_PER_YEAR, HOURS_PER_YEAR, SimDateTime, TimeDelta
+from neighborly.core.town import LandGrid, Town
+
+logger = logging.getLogger(__name__)
+
+
+class RoutineSystem(ISystem):
+    """
+    Moves characters based on their Routine component
+    characters with routines move to positions
+    """
+
+    def process(self, *args, **kwargs) -> None:
+        date = self.world.get_resource(SimDateTime)
+        engine = self.world.get_resource(NeighborlyEngine)
+
+        for _, (character, routine) in self.world.get_components(
+            GameCharacter, Routine
+        ):
+            character = cast(GameCharacter, character)
+            routine = cast(Routine, routine)
+
+            routine_entries = routine.get_entries(date.weekday_str, date.hour)
+
+            if routine_entries:
+                chosen_entry = engine.rng.choice(routine_entries)
+                location_id = (
+                    character.location_aliases[str(chosen_entry.location)]
+                    if isinstance(chosen_entry.location, str)
+                    else chosen_entry.location
+                )
+                move_to_location(
+                    self.world,
+                    character,
+                    self.world.get_gameobject(location_id).get_component(Location),
+                )
+            else:
+
+                potential_locations = get_locations(self.world)
+
+                if potential_locations:
+                    _, location = engine.rng.choice(potential_locations)
+                    move_to_location(self.world, character, location)
+
+
+class LinearTimeSystem(ISystem):
+    """
+    Advances simulation time using a linear time increment
+    """
+
+    __slots__ = "increment"
+
+    def __init__(self, increment: TimeDelta) -> None:
+        super().__init__()
+        self.increment: TimeDelta = increment
+
+    def process(self, *args, **kwargs) -> None:
+        """Advance time"""
+        current_date = self.world.get_resource(SimDateTime)
+        current_date += self.increment
+
+
+class DynamicLoDTimeSystem(ISystem):
+    """
+    Updates the current date/time in the simulation
+    using a variable level-of-detail where a subset
+    of the days during a year receive more simulation
+    ticks
+
+    Attributes
+    ----------
+    days_per_year: int
+        How many high LoD days to simulate per year
+
+    """
+
+    def __init__(self, days_per_year: int) -> None:
+        super().__init__()
+        self._low_lod_delta_time: int = 6
+        self._days_per_year: int = days_per_year
+        self._high_lod_days_for_year: Set[int] = set()
+        self._current_year: int = -1
+
+        assert (
+            days_per_year < DAYS_PER_YEAR
+        ), f"Days per year is greater than max: {DAYS_PER_YEAR}"
+        assert days_per_year > 0, "Days per year must be greater than 0"
+
+    def process(self, *args, **kwargs):
+        current_date = self.world.get_resource(SimDateTime)
+        rng = self.world.get_resource(NeighborlyEngine).rng
+
+        if self._current_year != current_date.year:
+            # Generate a new set of days to simulate this year
+            self._high_lod_days_for_year = set(
+                rng.sample(
+                    range(
+                        current_date.to_ordinal(),
+                        current_date.to_ordinal() + DAYS_PER_YEAR,
+                    ),
+                    self._days_per_year,
+                )
+            )
+            self._current_year = current_date.year
+
+        if current_date.to_ordinal() in self._high_lod_days_for_year:
+            # Increment the time using a smaller time increment (High LoD)
+            current_date.increment(hours=self._low_lod_delta_time)
+            return
+        else:
+            # Increment by one whole day (Low LoD)
+            current_date.increment(hours=24)
+
+    @staticmethod
+    def _generate_sample_days(
+        world: World, start_date: SimDateTime, end_date: SimDateTime, n: int
+    ) -> List[int]:
+        """Samples n days from each year between the start and end dates"""
+        ordinal_start_date: int = start_date.to_ordinal()
+        ordinal_end_date: int = end_date.to_ordinal()
+
+        sampled_ordinal_dates: List[int] = []
+
+        current_date = ordinal_start_date
+
+        while current_date < ordinal_end_date:
+            sampled_dates = world.get_resource(DefaultRNG).sample(
+                range(current_date, current_date + DAYS_PER_YEAR), n
+            )
+            sampled_ordinal_dates.extend(sorted(sampled_dates))
+            current_date = min(current_date + DAYS_PER_YEAR, ordinal_end_date)
+
+        return sampled_ordinal_dates
+
+
+class FindBusinessOwnerSystem(ISystem):
+    def process(self, *args, **kwargs) -> None:
+        unemployed_characters: List[GameCharacter] = list(
+            map(lambda x: x[1][0], self.world.get_components(GameCharacter, Unemployed))
+        )
+
+        engine = self.world.get_resource(NeighborlyEngine)
+        event_log = self.world.get_resource(LifeEventLog)
+        date = self.world.get_resource(SimDateTime)
+
+        for _, business in self.world.get_component(Business):
+            if not business.needs_owner():
+                continue
+
+            if len(unemployed_characters) == 0:
+                break
+
+            character = engine.rng.choice(unemployed_characters)
+            character.gameobject.add_component(
+                BusinessOwner(business.gameobject.id, business.name)
+            )
+            character.gameobject.remove_component(Unemployed)
+            character.gameobject.add_component(
+                Occupation(
+                    OccupationTypeLibrary.get(business.owner_type),
+                    business.gameobject.id,
+                )
+            )
+            business.owner = character.gameobject.id
+            unemployed_characters.remove(character)
+
+            event_log.record_event(
+                LifeEvent(
+                    name="BecameBusinessOwner",
+                    timestamp=date.to_iso_str(),
+                    roles=[
+                        EventRole("Business", business.gameobject.id),
+                        EventRole("Owner", character.gameobject.id),
+                    ],
+                    position=business.owner_type,
+                )
+            )
+
+
+class UnemploymentSystem(ISystem):
+    """
+    Handles updating the amount of time that a character
+    has been unemployed. If a character has been unemployed
+    for longer than a specified amount of time, they will
+    depart from the simulation.
+
+    Attributes
+    ----------
+    days_to_departure: int
+        The number of days a character is allowed to be
+        unemployed before they choose to depart from
+        the simulation
+    """
+
+    __slots__ = "days_to_departure"
+
+    def __init__(self, days_to_departure: int = 30) -> None:
+        super().__init__()
+        self.days_to_departure: int = days_to_departure
+
+    def process(self, *args, **kwargs):
+        date = self.world.get_resource(SimDateTime)
+        for _, unemployed in self.world.get_component(Unemployed):
+            # Convert delta time from hours to days
+            unemployed.duration_days += date.delta_time / 24
+
+            # Trigger the DepartAction and cast this character
+            # as the departee
+            if unemployed.duration_days >= self.days_to_departure:
+                pass  # TODO: Add DepartAction constructor and execute
+
+
+class RelationshipStatusSystem(ISystem):
+    def process(self, *args, **kwargs):
+        date = self.world.get_resource(SimDateTime)
+
+        for _, marriage in self.world.get_component(Married):
+            # Convert delta time from hours to days
+            marriage.duration_years += date.delta_time / HOURS_PER_YEAR
+
+        for _, dating in self.world.get_component(Dating):
+            # Convert delta time from hours to days
+            dating.duration_years += date.delta_time / HOURS_PER_YEAR
+
+        for _, relationship in self.world.get_component(InRelationship):
+            # Convert delta time from hours to days
+            relationship.duration_years += date.delta_time / HOURS_PER_YEAR
+
+
+class FindEmployeesSystem(ISystem):
+    def process(self, *args, **kwargs) -> None:
+        unemployed_characters: List[GameCharacter] = list(
+            map(lambda x: x[1][0], self.world.get_components(GameCharacter, Unemployed))
+        )
+
+        engine = self.world.get_resource(NeighborlyEngine)
+        date = self.world.get_resource(SimDateTime)
+        event_log = self.world.get_resource(LifeEventLog)
+
+        for _, business in self.world.get_component(Business):
+            open_positions = business.get_open_positions()
+
+            for position in open_positions:
+                if len(unemployed_characters) == 0:
+                    break
+                character = engine.rng.choice(unemployed_characters)
+                character.gameobject.remove_component(Unemployed)
+
+                business.add_employee(character.gameobject.id, position)
+                character.gameobject.add_component(
+                    Occupation(
+                        OccupationTypeLibrary.get(position),
+                        business.gameobject.id,
+                    )
+                )
+                unemployed_characters.remove(character)
+
+                event_log.record_event(
+                    LifeEvent(
+                        "HiredAtBusiness",
+                        date.to_iso_str(),
+                        roles=[
+                            EventRole("Business", business.gameobject.id),
+                            EventRole("Employee", character.gameobject.id),
+                        ],
+                        position=position,
+                    )
+                )
+
+
+class SocializeSystem(ISystem):
+    """
+    Every timestep, characters interact with other characters
+    at the same location. Characters meeting for the first time
+    form relationships based on the compatibility of their
+    personal values.
+    """
+
+    @staticmethod
+    def get_compatibility(character_a: GameObject, character_b: GameObject) -> float:
+        """Return value [-1.0, 1.0] representing the compatibility of two characters"""
+        return PersonalValues.compatibility(
+            character_a.get_component(PersonalValues),
+            character_b.get_component(PersonalValues),
+        )
+
+    def process(self, *args, **kwargs) -> None:
+
+        engine = self.world.get_resource(NeighborlyEngine)
+        rel_graph = self.world.get_resource(RelationshipGraph)
+
+        for _, location in self.world.get_component(Location):
+            for character_id, other_character_id in itertools.combinations(
+                location.characters_present, 2
+            ):
+                assert character_id != other_character_id
+                character = self.world.get_gameobject(character_id)
+                other_character = self.world.get_gameobject(other_character_id)
+
+                if (
+                    character.get_component(GameCharacter).age < 3
+                    or other_character.get_component(GameCharacter).age < 3
+                ):
+                    continue
+
+                chance_of_interaction: float = 0.7
+
+                if engine.rng.random() >= chance_of_interaction:
+                    continue
+
+                compatibility: float = SocializeSystem.get_compatibility(
+                    character, other_character
+                )
+
+                # This should be replaced with something more intelligent that allows
+                # for other decision-making systems to decide how to interact
+                friendship_score = (
+                    engine.rng.uniform(0, 2) * (1 + compatibility) - 1.0
+                ) * 10
+                romance_score = (
+                    engine.rng.uniform(0, 2) * (1 + compatibility) - 1.0
+                ) * 10
+
+                if not rel_graph.has_connection(character_id, other_character_id):
+                    rel_graph.add_relationship(
+                        Relationship(
+                            character_id,
+                            other_character_id,
+                            compatibility=compatibility,
+                        )
+                    )
+
+                rel_graph.get_connection(
+                    character_id, other_character_id
+                ).increment_romance(romance_score)
+                rel_graph.get_connection(
+                    character_id, other_character_id
+                ).increment_friendship(friendship_score)
+
+                if not rel_graph.has_connection(other_character_id, character_id):
+                    rel_graph.add_relationship(
+                        Relationship(
+                            other_character_id,
+                            character_id,
+                            compatibility=compatibility,
+                        ),
+                    )
+                rel_graph.get_connection(
+                    other_character_id, character_id
+                ).increment_romance(romance_score)
+                rel_graph.get_connection(
+                    other_character_id, character_id
+                ).increment_friendship(friendship_score)
+
+
+class BuildResidenceSystem(ISystem):
+    __slots__ = "chance_of_build", "interval", "next_trigger"
+
+    def __init__(
+        self, chance_of_build: float = 0.5, interval: TimeDelta = None
+    ) -> None:
+        super().__init__()
+        self.chance_of_build: float = chance_of_build
+        self.interval: TimeDelta = interval if interval else TimeDelta()
+        self.next_trigger: SimDateTime = SimDateTime()
+
+    def choose_random_archetype(
+        self, engine: NeighborlyEngine
+    ) -> Optional[ResidenceArchetype]:
+        archetype_choices: List[ResidenceArchetype] = []
+        archetype_weights: List[int] = []
+        for archetype in ResidenceArchetypeLibrary.get_all():
+            archetype_choices.append(archetype)
+            archetype_weights.append(archetype.spawn_multiplier)
+
+        if archetype_choices:
+            # Choose an archetype at random
+            archetype: ResidenceArchetype = engine.rng.choices(
+                population=archetype_choices, weights=archetype_weights, k=1
+            )[0]
+
+            return archetype
+
+        return None
+
+    def process(self, *args, **kwargs) -> None:
+        """Build a new residence when there is space"""
+        date = self.world.get_resource(SimDateTime)
+
+        if date < self.next_trigger:
+            return
+        else:
+            self.next_trigger = date.copy() + self.interval
+
+        land_grid = self.world.get_resource(LandGrid)
+        engine = self.world.get_resource(NeighborlyEngine)
+        event_log = self.world.get_resource(LifeEventLog)
+
+        # Return early if the random-roll is not sufficient
+        if engine.rng.random() > self.chance_of_build:
+            return
+
+        # Return early if there is nowhere to build
+        if not land_grid.has_vacancy():
+            return
+
+        # Pick a random lot from those available
+        lot = engine.rng.choice(land_grid.get_vacancies())
+
+        archetype = self.choose_random_archetype(engine)
+
+        if archetype is None:
+            return None
+
+        # Construct a random residence archetype
+        residence = self.world.spawn_archetype(archetype)
+
+        # Reserve the space
+        land_grid.reserve_space(lot, residence.id)
+
+        # Set the position of the residence
+        residence.add_component(Position2D(lot[0], lot[1]))
+
+        event_log.record_event(
+            LifeEvent(
+                "NewResidenceBuilt",
+                date.to_iso_str(),
+                roles=[EventRole("Residence", residence.id)],
+            )
+        )
+
+
+class BuildBusinessSystem(ISystem):
+    __slots__ = "chance_of_build", "interval", "next_trigger"
+
+    def __init__(
+        self, chance_of_build: float = 0.5, interval: TimeDelta = None
+    ) -> None:
+        super().__init__()
+        self.chance_of_build: float = chance_of_build
+        self.interval: TimeDelta = interval if interval else TimeDelta()
+        self.next_trigger: SimDateTime = SimDateTime()
+
+    def choose_random_eligible_business(
+        self, engine: NeighborlyEngine
+    ) -> Optional[BusinessArchetype]:
+        """
+        Return all business archetypes that may be built
+        given the state of the simulation
+        """
+        town = self.world.get_resource(Town)
+        date = self.world.get_resource(SimDateTime)
+
+        archetype_choices: List[BusinessArchetype] = []
+        archetype_weights: List[int] = []
+
+        for archetype in BusinessArchetypeLibrary.get_all():
+            if (
+                archetype.instances < archetype.max_instances
+                and town.population >= archetype.min_population
+                and archetype.year_available <= date.year < archetype.year_obsolete
+            ):
+                archetype_choices.append(archetype)
+                archetype_weights.append(archetype.spawn_multiplier)
+
+        if archetype_choices:
+            # Choose an archetype at random
+            archetype: BusinessArchetype = engine.rng.choices(
+                population=archetype_choices, weights=archetype_weights, k=1
+            )[0]
+
+            return archetype
+
+        return None
+
+    def process(self, *args, **kwargs) -> None:
+        """Build a new business when there is space"""
+        date = self.world.get_resource(SimDateTime)
+
+        if date < self.next_trigger:
+            return
+        else:
+            self.next_trigger = date.copy() + self.interval
+
+        land_grid = self.world.get_resource(LandGrid)
+        engine = self.world.get_resource(NeighborlyEngine)
+        event_log = self.world.get_resource(LifeEventLog)
+
+        # Return early if the random-roll is not sufficient
+        if engine.rng.random() > self.chance_of_build:
+            return
+
+        # Return early if there is nowhere to build
+        if not land_grid.has_vacancy():
+            return
+
+        # Pick a random lot from those available
+        lot = engine.rng.choice(land_grid.get_vacancies())
+
+        # Pick random eligible business archetype
+        archetype = self.choose_random_eligible_business(engine)
+
+        if archetype is None:
+            return
+
+        # Build a random business archetype
+        business = self.world.spawn_archetype(archetype)
+
+        # Reserve the space
+        land_grid.reserve_space(lot, business.id)
+
+        # Set the position of the residence
+        business.get_component(Position2D).x = lot[0]
+        business.get_component(Position2D).y = lot[1]
+
+        event_log.record_event(
+            LifeEvent(
+                "NewBusinessBuilt",
+                date.to_iso_str(),
+                roles=[EventRole("Business", business.id)],
+            )
+        )
+
+
+class SpawnResidentSystem(ISystem):
+    """Adds new characters to the simulation"""
+
+    __slots__ = "chance_spawn", "chance_married", "max_kids", "interval", "next_trigger"
+
+    def __init__(
+        self,
+        chance_spawn: float = 0.5,
+        chance_married: float = 0.5,
+        max_kids: int = 3,
+        interval: TimeDelta = None,
+    ) -> None:
+        super().__init__()
+        self.chance_spawn: float = chance_spawn
+        self.chance_married: float = chance_married
+        self.max_kids: int = max_kids
+        self.interval: TimeDelta = interval if interval else TimeDelta()
+        self.next_trigger: SimDateTime = SimDateTime()
+
+    def choose_random_character_archetype(self, engine: NeighborlyEngine):
+        archetype_choices: List[CharacterArchetype] = []
+        archetype_weights: List[int] = []
+        for archetype in CharacterArchetypeLibrary.get_all():
+            archetype_choices.append(archetype)
+            archetype_weights.append(archetype.spawn_multiplier)
+
+        if archetype_choices:
+            # Choose an archetype at random
+            archetype: CharacterArchetype = engine.rng.choices(
+                population=archetype_choices, weights=archetype_weights, k=1
+            )[0]
+
+            return archetype
+        else:
+            return None
+
+    def process(self, *args, **kwargs) -> None:
+
+        date = self.world.get_resource(SimDateTime)
+
+        if date < self.next_trigger:
+            return
+        else:
+            self.next_trigger = date.copy() + self.interval
+
+        town = self.world.get_resource(Town)
+        engine = self.world.get_resource(NeighborlyEngine)
+        rel_graph = self.world.get_resource(RelationshipGraph)
+        event_logger = self.world.get_resource(LifeEventLog)
+
+        for _, residence in self.world.get_component(Residence):
+            # Skip occupied residences
+            if not residence.is_vacant():
+                continue
+
+            # Return early if the random-roll is not sufficient
+            if engine.rng.random() > self.chance_spawn:
+                return
+
+            archetype = self.choose_random_character_archetype(engine)
+
+            if archetype is None:
+                return
+
+            # Create a new character
+            character = generate_young_adult_character(self.world, engine, archetype)
+            residence.add_owner(character.id)
+            town.population += 1
+
+            move_residence(character.get_component(GameCharacter), residence)
+            move_to_location(
+                self.world,
+                character.get_component(GameCharacter),
+                residence.gameobject.get_component(Location),
+            )
+
+            spouse: Optional[GameObject] = None
+            # Potentially generate a spouse for this character
+            if engine.rng.random() < self.chance_married:
+                spouse = generate_young_adult_character(self.world, engine, archetype)
+                spouse.get_component(
+                    GameCharacter
+                ).name.surname = character.get_component(GameCharacter).name.surname
+                residence.add_owner(spouse.id)
+                town.population += 1
+
+                move_residence(spouse.get_component(GameCharacter), residence)
+                move_to_location(
+                    self.world,
+                    spouse.get_component(GameCharacter),
+                    residence.gameobject.get_component(Location),
+                )
+
+                character.add_component(
+                    Married(
+                        partner_id=spouse.id,
+                        partner_name=str(spouse.get_component(GameCharacter).name),
+                    )
+                )
+
+                spouse.add_component(
+                    Married(
+                        partner_id=character.id,
+                        partner_name=str(character.get_component(GameCharacter).name),
+                    )
+                )
+
+                # character to spouse
+                rel_graph.add_relationship(
+                    Relationship(
+                        character.id,
+                        spouse.id,
+                        base_friendship=30,
+                        base_romance=50,
+                        tags=(
+                            RelationshipTag.Friend
+                            | RelationshipTag.Spouse
+                            | RelationshipTag.SignificantOther
+                        ),
+                    )
+                )
+
+                # spouse to character
+                rel_graph.add_relationship(
+                    Relationship(
+                        spouse.id,
+                        character.id,
+                        base_friendship=30,
+                        base_romance=50,
+                        tags=(
+                            RelationshipTag.Friend
+                            | RelationshipTag.Spouse
+                            | RelationshipTag.SignificantOther
+                        ),
+                    )
+                )
+
+            # Note: Characters can spawn as single parents with kids
+            num_kids = engine.rng.randint(0, self.max_kids)
+            children: List[GameObject] = []
+            for _ in range(num_kids):
+                child = generate_child_character(self.world, engine, archetype)
+                child.get_component(
+                    GameCharacter
+                ).name.surname = character.get_component(GameCharacter).name.surname
+                town.population += 1
+
+                move_residence(child.get_component(GameCharacter), residence)
+                move_to_location(
+                    self.world,
+                    child.get_component(GameCharacter),
+                    residence.gameobject.get_component(Location),
+                )
+                children.append(child)
+
+                # child to character
+                rel_graph.add_relationship(
+                    Relationship(
+                        child.id,
+                        character.id,
+                        base_friendship=20,
+                        tags=RelationshipTag.Parent,
+                    )
+                )
+
+                # character to child
+                rel_graph.add_relationship(
+                    Relationship(
+                        character.id,
+                        child.id,
+                        base_friendship=20,
+                        tags=RelationshipTag.Child,
+                    )
+                )
+
+                if spouse:
+                    # child to spouse
+                    rel_graph.add_relationship(
+                        Relationship(
+                            child.id,
+                            spouse.id,
+                            base_friendship=20,
+                            tags=RelationshipTag.Parent,
+                        )
+                    )
+
+                    # spouse to child
+                    rel_graph.add_relationship(
+                        Relationship(
+                            spouse.id,
+                            child.id,
+                            base_friendship=20,
+                            tags=RelationshipTag.Child,
+                        )
+                    )
+
+                for sibling in children:
+                    # child to sibling
+                    rel_graph.add_relationship(
+                        Relationship(
+                            child.id,
+                            sibling.id,
+                            base_friendship=20,
+                            tags=RelationshipTag.Sibling,
+                        )
+                    )
+
+                    # sibling to child
+                    rel_graph.add_relationship(
+                        Relationship(
+                            sibling.id,
+                            child.id,
+                            base_friendship=20,
+                            tags=RelationshipTag.Sibling,
+                        )
+                    )
+
+            # Record a life event
+            event_logger.record_event(
+                LifeEvent(
+                    name="MoveIntoTown",
+                    timestamp=date.to_iso_str(),
+                    roles=[
+                        EventRole("resident", r.id)
+                        for r in [character, spouse, *children]
+                        if r is not None
+                    ],
+                )
+            )
+
+
+class BusinessUpdateSystem(ISystem):
+    def process(self, *args, **kwargs) -> None:
+        time = self.world.get_resource(SimDateTime)
+        rng = self.world.get_resource(NeighborlyEngine).rng
+        for _, business in self.world.get_component(Business):
+            if business.status == BusinessStatus.OpenForBusiness:
+                # Increment the age of the business
+                business.increment_years_in_business(time.delta_time / HOURS_PER_YEAR)
+
+                # Check if this business is going to close
+                if rng.random() < 0.3:
+                    # Go Out of business
+                    business.set_business_status(BusinessStatus.ClosedForBusiness)
+                    business.owner = None
+                    for employee in business.get_employees():
+                        business.remove_employee(employee)
+                        self.world.get_gameobject(employee).remove_component(Occupation)
+                    business.gameobject.archive()
+
+                # Attempt to hire characters for open job positions
+                for position in business.get_open_positions():
+                    OccupationTypeLibrary.get(position)
+
+
+class CharacterAgingSystem(ISystem):
+    """
+    Updates the ages of characters, adds/removes life
+    stage components (Adult, Child, Elder, ...), and
+    handles character deaths.
+
+    Notes
+    -----
+    This system runs every time step
+    """
+
+    def process(self, *args, **kwargs) -> None:
+        date_time = self.world.get_resource(SimDateTime)
+        engine = self.world.get_resource(NeighborlyEngine)
+        event_log = self.world.get_resource(LifeEventLog)
+
+        for _, character in self.world.get_component(GameCharacter):
+            if character.gameobject.has_component(Deceased):
+                continue
+
+            character.age += float(date_time.delta_time) / HOURS_PER_YEAR
+
+            if (
+                character.gameobject.has_component(Child)
+                and character.age >= character.character_def.life_stages["teen"]
+            ):
+                character.gameobject.remove_component(Child)
+                character.gameobject.add_component(Teen())
+                event_log.record_event(
+                    LifeEvent(
+                        name="BecomeTeen",
+                        timestamp=date_time.to_iso_str(),
+                        roles=[
+                            EventRole("Character", character.gameobject.id),
+                        ],
+                    )
+                )
+
+            elif (
+                character.gameobject.has_component(Teen)
+                and character.age >= character.character_def.life_stages["young_adult"]
+            ):
+                character.gameobject.remove_component(Teen)
+                character.gameobject.add_component(YoungAdult())
+                character.gameobject.add_component(Adult())
+
+                if not character.gameobject.has_component(Occupation):
+                    character.gameobject.add_component(Unemployed())
+
+                event_log.record_event(
+                    LifeEvent(
+                        name="BecomeYoungAdult",
+                        timestamp=date_time.to_iso_str(),
+                        roles=[
+                            EventRole("Character", character.gameobject.id),
+                        ],
+                    )
+                )
+
+            elif (
+                character.gameobject.has_component(YoungAdult)
+                and character.age >= character.character_def.life_stages["adult"]
+            ):
+                character.gameobject.remove_component(YoungAdult)
+                event_log.record_event(
+                    LifeEvent(
+                        name="BecomeAdult",
+                        timestamp=date_time.to_iso_str(),
+                        roles=[
+                            EventRole("Character", character.gameobject.id),
+                        ],
+                    )
+                )
+
+            elif (
+                character.gameobject.has_component(Adult)
+                and not character.gameobject.has_component(Elder)
+                and character.age >= character.character_def.life_stages["elder"]
+            ):
+                character.gameobject.add_component(Elder())
+                event_log.record_event(
+                    LifeEvent(
+                        name="BecomeElder",
+                        timestamp=date_time.to_iso_str(),
+                        roles=[
+                            EventRole("Character", character.gameobject.id),
+                        ],
+                    )
+                )
+
+            if (
+                character.age >= character.character_def.lifespan
+                and engine.rng.random() < 0.8
+            ):
+                character.gameobject.add_component(Deceased())
+                event_log.record_event(
+                    LifeEvent(
+                        name="Death",
+                        timestamp=date_time.to_iso_str(),
+                        roles=[
+                            EventRole("Character", character.gameobject.id),
+                        ],
+                    )
+                )
+
+                # Archive GameObject instead of removing it
+                character.gameobject.archive()
+                # self.world.delete_gameobject(character.gameobject.id)
+
+
+class PregnancySystem(ISystem):
+    """
+    Pregnancy system is responsible for managing ChildBirth events.
+    It checks if the current date is after pregnant characters'
+    due dates, and triggers childbirths if it is.
+    """
+
+    def process(self, *args, **kwargs):
+        current_date = self.world.get_resource(SimDateTime)
+        rel_graph = self.world.get_resource(RelationshipGraph)
+        event_logger = self.world.get_resource(LifeEventLog)
+
+        for _, (character, pregnancy) in self.world.get_components(
+            GameCharacter, Pregnant
+        ):
+            if current_date > pregnancy.due_date:
+                continue
+
+            baby = self.world.spawn_archetype(character.gameobject.archetype)
+
+            baby.get_component(GameCharacter).age = (
+                current_date - pregnancy.due_date
+            ).hours / HOURS_PER_YEAR
+
+            baby.get_component(GameCharacter).name = CharacterName(
+                baby.get_component(GameCharacter).name.firstname, character.name.surname
+            )
+
+            move_to_location(
+                self.world,
+                baby.get_component(GameCharacter),
+                self.world.get_gameobject(character.location).get_component(Location),
+            )
+
+            # Birthing parent to child
+            rel_graph.add_relationship(
+                Relationship(
+                    character.gameobject.id,
+                    baby.id,
+                    tags=RelationshipTag.Child,
+                )
+            )
+
+            # Child to birthing parent
+            rel_graph.add_relationship(
+                Relationship(
+                    baby.id,
+                    character.gameobject.id,
+                    tags=RelationshipTag.Parent,
+                ),
+            )
+
+            # Other parent to child
+            rel_graph.add_relationship(
+                Relationship(
+                    pregnancy.partner_id,
+                    baby.id,
+                    tags=RelationshipTag.Child,
+                )
+            )
+
+            # Child to other parent
+            rel_graph.add_relationship(
+                Relationship(
+                    baby.id,
+                    pregnancy.partner_id,
+                    tags=RelationshipTag.Parent,
+                ),
+            )
+
+            # Create relationships with children of birthing parent
+            for rel in rel_graph.get_all_relationships_with_tags(
+                character.gameobject.id, RelationshipTag.Child
+            ):
+                if rel.target == baby.id:
+                    continue
+                # Baby to sibling
+                rel_graph.add_relationship(
+                    Relationship(baby.id, rel.target, tags=RelationshipTag.Sibling)
+                )
+                # Sibling to baby
+                rel_graph.add_relationship(
+                    Relationship(rel.target, baby.id, tags=RelationshipTag.Sibling)
+                )
+
+            # Create relationships with children of other parent
+            for rel in rel_graph.get_all_relationships_with_tags(
+                pregnancy.partner_id, RelationshipTag.Child
+            ):
+                if rel.target == baby.id:
+                    continue
+                # Baby to sibling
+                rel_graph.add_relationship(
+                    Relationship(baby.id, rel.target, tags=RelationshipTag.Sibling)
+                )
+                # Sibling to baby
+                rel_graph.add_relationship(
+                    Relationship(rel.target, baby.id, tags=RelationshipTag.Sibling)
+                )
+
+            # Pregnancy event dates are retconned to be the actual date that the
+            # child was due.
+            event_logger.record_event(
+                LifeEvent(
+                    name="ChildBirth",
+                    timestamp=pregnancy.due_date.to_iso_str(),
+                    roles=[
+                        EventRole("Parent", character.gameobject.id),
+                        EventRole("Parent", pregnancy.partner_id),
+                        EventRole("Child", baby.id),
+                    ],
+                )
+            )
+
+            character.gameobject.remove_component(Pregnant)
+            character.gameobject.remove_component(InTheWorkforce)
+            character.gameobject.remove_component(Occupation)
+
+            # TODO: Birthing parent should also leave their job
diff --git a/src/neighborly/core/activity.py b/src/neighborly/core/activity.py
new file mode 100644
index 0000000..314ba1f
--- /dev/null
+++ b/src/neighborly/core/activity.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from typing import Dict, List, Tuple
+
+import numpy as np
+
+from neighborly.core.personal_values import PersonalValues
+
+
+class Activity:
+    """Activities that a character can do at a location in the town
+
+    Attributes
+    ----------
+    name: str
+        The name of the activity
+    trait_names: Tuple[str, ...]
+        Character values that associated with this activity
+    personal_values: PersonalValues
+        The list of trait_names encoded as a vector of 0's and 1's
+        for non-applicable and applicable character values respectively.
+    """
+
+    __slots__ = "name", "trait_names", "personal_values"
+
+    def __init__(self, name: str, trait_names: List[str]) -> None:
+        self.name: str = name
+        self.trait_names: List[str] = trait_names
+        self.personal_values: np.array = PersonalValues(
+            {name: 1 for name in self.trait_names}, default=0
+        ).traits
+
+
+class ActivityLibrary:
+
+    _activity_registry: Dict[str, Activity] = {}
+    _activity_flags: Dict[str, int] = {}
+
+    @classmethod
+    def register_activity(cls, activity: Activity) -> None:
+        """Registers an activity instance for use in other places"""
+        next_flag = 1 << len(cls._activity_registry.keys())
+        cls._activity_registry[activity.name] = activity
+        cls._activity_flags[activity.name] = next_flag
+
+    @classmethod
+    def get_activity_flags(cls, *activities: str) -> Tuple[int, ...]:
+        """Return flags corresponding to given activities"""
+        return tuple([cls._activity_flags[activity] for activity in activities])
+
+    @classmethod
+    def get_activity(cls, activity: str) -> Activity:
+        """Return Activity instance corresponding to a given string"""
+        return cls._activity_registry[activity]
+
+    @classmethod
+    def get_all_activities(cls) -> List[Activity]:
+        """Return all activity instances in the registry"""
+        return list(cls._activity_registry.values())
diff --git a/src/neighborly/core/behavior_tree.py b/src/neighborly/core/behavior_tree.py
new file mode 100644
index 0000000..c226d57
--- /dev/null
+++ b/src/neighborly/core/behavior_tree.py
@@ -0,0 +1,58 @@
+from abc import abstractmethod
+from typing import Protocol
+
+from neighborly.core.ecs import World
+from neighborly.core.life_event import LifeEvent
+
+
+class BehaviorNode(Protocol):
+    """A single node in the behavior tree"""
+
+    @abstractmethod
+    def __call__(self, world: World, event: LifeEvent, **kwargs) -> bool:
+        """Evaluate the behavior tree node"""
+        raise NotImplemented
+
+
+def invert(node: BehaviorNode) -> BehaviorNode:
+    """
+    Returns precondition function that checks if the
+    current year is less than the given year
+    """
+
+    def fn(world: World, event: LifeEvent, **kwargs) -> bool:
+        return not node(world, event, **kwargs)
+
+    return fn
+
+
+def selector(*nodes: BehaviorNode) -> BehaviorNode:
+    """
+    Returns precondition function that checks if the
+    current year is less than the given year
+    """
+
+    def fn(world: World, event: LifeEvent, **kwargs) -> bool:
+        for node in nodes:
+            res = node(world, event, **kwargs)
+            if res is True:
+                return True
+        return False
+
+    return fn
+
+
+def sequence(*nodes: BehaviorNode) -> BehaviorNode:
+    """
+    Returns precondition function that checks if the
+    current year is less than the given year
+    """
+
+    def fn(world: World, event: LifeEvent, **kwargs) -> bool:
+        for node in nodes:
+            res = node(world, event, **kwargs)
+            if res is False:
+                return False
+        return True
+
+    return fn
diff --git a/src/neighborly/core/builtin/event_rules.py b/src/neighborly/core/builtin/event_rules.py
deleted file mode 100644
index 740bace..0000000
--- a/src/neighborly/core/builtin/event_rules.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from typing import cast
-from neighborly.core.ecs import World
-from neighborly.core.life_event import LifeEventRule
-from neighborly.core.builtin.statuses import ElderStatus, AdultStatus
-from neighborly.core.builtin.events import DeathEvent
-from neighborly.core.time import SimDateTime
-from neighborly.core.character import GameCharacter
-
-
-def get_elder_characters(world: World, **kwargs):
-    timestamp = world.get_resource(SimDateTime).to_iso_str()
-    for gid, (character, _) in world.get_components(GameCharacter, ElderStatus):
-        character = cast(GameCharacter, character)
-        yield DeathEvent(timestamp, gid, str(character.name)), (character.gameobject,)
-
-
-def get_adult_characters(world: World, **kwargs):
-    timestamp = world.get_resource(SimDateTime).to_iso_str()
-    for gid, (character, _) in world.get_components(GameCharacter, AdultStatus):
-        character = cast(GameCharacter, character)
-        yield DeathEvent(timestamp, gid, str(character.name)), (character.gameobject,)
-
-
-death_event_rule = LifeEventRule(
-    "Death",
-    pattern_fn=get_elder_characters,
-    probability=0.8,
-)
-
-depart_town_rule = LifeEventRule(
-    "Depart",
-    pattern_fn=get_adult_characters,
-    probability=0.3,
-)
diff --git a/src/neighborly/core/builtin/events.py b/src/neighborly/core/builtin/events.py
deleted file mode 100644
index 584996d..0000000
--- a/src/neighborly/core/builtin/events.py
+++ /dev/null
@@ -1,793 +0,0 @@
-from typing import Any, Dict, Tuple
-from neighborly.core.life_event import LifeEvent
-from neighborly.core.relationship import Relationship
-
-
-class ChildBirthEvent(LifeEvent):
-
-    event_type: str = "child-birth"
-
-    __slots__ = "parent_names", "parent_ids", "child_name", "child_id"
-
-    def __init__(
-        self,
-        timestamp: str,
-        parent_names: Tuple[str, str],
-        parent_ids: Tuple[int, int],
-        child_name: str,
-        child_id: int,
-    ) -> None:
-        super().__init__(timestamp)
-        self.parent_names: Tuple[str, str] = parent_names
-        self.parent_ids: Tuple[int, int] = parent_ids
-        self.child_name: str = child_name
-        self.child_id: int = child_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "parent_names": self.parent_names,
-            "parent_ids": self.parent_ids,
-            "child_name": self.child_name,
-            "child_id": self.child_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} born to parents {} and {}.".format(
-            self.timestamp, self.child_name, self.parent_names[0], self.parent_names[1]
-        )
-
-
-class OpenBusinessEvent(LifeEvent):
-    event_type: str = "open-business"
-
-    __slots__ = "business_id", "business_name", "owner_id", "owner_name"
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        owner_id: int,
-        owner_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.owner_name: str = owner_name
-        self.owner_id: int = owner_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "owner_name": self.owner_name,
-            "owner_id": self.owner_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} opened new business, {}.".format(
-            self.timestamp, self.owner_name, self.business_name
-        )
-
-
-class CloseBusinessEvent(LifeEvent):
-    event_type: str = "close-business"
-
-    __slots__ = "business_id", "business_name", "owner_id", "owner_name"
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        owner_id: int,
-        owner_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.owner_name: str = owner_name
-        self.owner_id: int = owner_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "owner_name": self.owner_name,
-            "owner_id": self.owner_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {}'s {} goes out of business.".format(
-            self.timestamp, self.owner_name, self.business_name
-        )
-
-
-class DepartureEvent(LifeEvent):
-    event_type: str = "departure"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} departed from the town.".format(
-            self.timestamp, self.character_name
-        )
-
-
-class MarriageEvent(LifeEvent):
-    event_type: str = "marriage"
-
-    __slots__ = (
-        "character_ids",
-        "character_names",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_ids: Tuple[int, int],
-        character_names: Tuple[str, str],
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_names: Tuple[str, str] = character_names
-        self.character_ids: Tuple[int, int] = character_ids
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_names": self.character_names,
-            "character_ids": self.character_ids,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} and {} got married.".format(
-            self.timestamp, self.character_names[0], self.character_names[1]
-        )
-
-
-class DivorceEvent(LifeEvent):
-    event_type: str = "marriage"
-
-    __slots__ = (
-        "character_ids",
-        "character_names",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_ids: Tuple[int, int],
-        character_names: Tuple[str, str],
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_names: Tuple[str, str] = character_names
-        self.character_ids: Tuple[int, int] = character_ids
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_names": self.character_names,
-            "character_ids": self.character_ids,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} and {} got divorced.".format(
-            self.timestamp, self.character_names[0], self.character_names[1]
-        )
-
-
-class JobHiringEvent(LifeEvent):
-    event_type: str = "job-hiring"
-
-    __slots__ = (
-        "business_id",
-        "business_name",
-        "character_id",
-        "character_name",
-        "occupation_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        character_id: int,
-        character_name: str,
-        occupation_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-        self.occupation_name: str = occupation_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-            "occupation_name": self.occupation_name,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} got hired as a {} at {}.".format(
-            self.timestamp,
-            self.character_name,
-            self.occupation_name,
-            self.business_name,
-        )
-
-
-class JobLayoffEvent(LifeEvent):
-    event_type: str = "job-layoff"
-
-    __slots__ = (
-        "business_id",
-        "business_name",
-        "character_id",
-        "character_name",
-        "occupation_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        character_id: int,
-        character_name: str,
-        occupation_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-        self.occupation_name: str = occupation_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-            "occupation_name": self.occupation_name,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} got layed-off as a {} at {}.".format(
-            self.timestamp,
-            self.character_name,
-            self.occupation_name,
-            self.business_name,
-        )
-
-
-class JobPromotionEvent(LifeEvent):
-    event_type: str = "job-promotion"
-
-    __slots__ = (
-        "business_id",
-        "business_name",
-        "character_id",
-        "character_name",
-        "occupation_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        character_id: int,
-        character_name: str,
-        occupation_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-        self.occupation_name: str = occupation_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-            "occupation_name": self.occupation_name,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} got promototed to a {} at {}.".format(
-            self.timestamp,
-            self.character_name,
-            self.occupation_name,
-            self.business_name,
-        )
-
-
-class LeaveJobEvent(LifeEvent):
-    event_type: str = "leave-job"
-
-    __slots__ = (
-        "business_id",
-        "business_name",
-        "character_id",
-        "character_name",
-        "occupation_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        character_id: int,
-        character_name: str,
-        occupation_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-        self.occupation_name: str = occupation_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-            "occupation_name": self.occupation_name,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} left their job as a {} at {}.".format(
-            self.timestamp,
-            self.character_name,
-            self.occupation_name,
-            self.business_name,
-        )
-
-
-class RetirementEvent(LifeEvent):
-    event_type: str = "retire"
-
-    __slots__ = (
-        "business_id",
-        "business_name",
-        "character_id",
-        "character_name",
-        "occupation_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        business_id: int,
-        business_name: str,
-        character_id: int,
-        character_name: str,
-        occupation_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.business_id: int = business_id
-        self.business_name: str = business_name
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-        self.occupation_name: str = occupation_name
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "business_name": self.business_name,
-            "business_id": self.business_id,
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-            "occupation_name": self.occupation_name,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} retired from being a {} at {}.".format(
-            self.timestamp,
-            self.character_name,
-            self.occupation_name,
-            self.business_name,
-        )
-
-
-class MoveResidenceEvent(LifeEvent):
-    event_type: str = "move-residence"
-
-    __slots__ = ("character_ids", "character_names", "residence_id")
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_ids: Tuple[int, ...],
-        character_names: Tuple[str, ...],
-        residence_id: int,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_names: Tuple[str, ...] = character_names
-        self.character_ids: Tuple[int, ...] = character_ids
-        self.residence_id: int = residence_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_names": self.character_names,
-            "character_ids": self.character_ids,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} moved to a new residence.".format(
-            self.timestamp, self.character_names
-        )
-
-
-class DeathEvent(LifeEvent):
-    event_type: str = "death"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} died.".format(self.timestamp, self.character_name)
-
-
-class DeathInFamilyEvent(LifeEvent):
-    event_type: str = "death-in-family"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-
-class CreateRelationshipEvent(LifeEvent):
-    event_type: str = "create-relationship"
-
-    __slots__ = "relationship"
-
-    def __init__(
-        self,
-        timestamp: str,
-        relationship: Relationship,
-    ) -> None:
-        super().__init__(timestamp)
-        self.relationship: Relationship = relationship
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {**super().to_dict(), "relationship": self.relationship}
-
-
-class BecomeAdolescentEvent(LifeEvent):
-    event_type: str = "become-adolescent"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} became an adolescent.".format(
-            self.timestamp, self.character_name
-        )
-
-
-class BecomeYoungAdultEvent(LifeEvent):
-    event_type: str = "become-young-adult"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} became an young adult.".format(
-            self.timestamp, self.character_name
-        )
-
-
-class BecomeAdultEvent(LifeEvent):
-    event_type: str = "become-adult"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} became an adult.".format(self.timestamp, self.character_name)
-
-
-class BecomeElderEvent(LifeEvent):
-    event_type: str = "become-elder"
-
-    __slots__ = (
-        "character_id",
-        "character_name",
-    )
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_id: int,
-        character_name: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_name: str = character_name
-        self.character_id: int = character_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_name": self.character_name,
-            "character_id": self.character_id,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} became an elder.".format(self.timestamp, self.character_name)
-
-
-class SocializeEvent(LifeEvent):
-    event_type: str = "socialize"
-
-    __slots__ = ("character_ids", "character_names", "interaction_type")
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_ids: Tuple[int, int],
-        character_names: Tuple[str, str],
-        interaction_type: str,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_names: Tuple[str, str] = character_names
-        self.character_ids: Tuple[int, int] = character_ids
-        self.interaction_type: str = interaction_type
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_names": self.character_names,
-            "character_ids": self.character_ids,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} and {} socialized.".format(
-            self.timestamp, self.character_names[0], self.character_names[1]
-        )
-
-
-class HomePurchaseEvent(LifeEvent):
-    event_type: str = "home-purchase"
-
-    __slots__ = ("character_ids", "character_names", "residence_id")
-
-    def __init__(
-        self,
-        timestamp: str,
-        character_ids: Tuple[int, int],
-        character_names: Tuple[str, str],
-        residence_id: int,
-    ) -> None:
-        super().__init__(timestamp)
-        self.character_names: Tuple[str, str] = character_names
-        self.character_ids: Tuple[int, int] = character_ids
-        self.residence_id: int = residence_id
-
-    @classmethod
-    def get_type(cls) -> str:
-        return cls.event_type
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            **super().to_dict(),
-            "character_names": self.character_names,
-            "character_ids": self.character_ids,
-        }
-
-    def __str__(self) -> str:
-        return "({}) {} purchased a house.".format(self.timestamp, self.character_names)
diff --git a/src/neighborly/core/builtin/statuses.py b/src/neighborly/core/builtin/statuses.py
deleted file mode 100644
index 6a78936..0000000
--- a/src/neighborly/core/builtin/statuses.py
+++ /dev/null
@@ -1,123 +0,0 @@
-from neighborly.core.ecs import World
-from neighborly.core.status import Status
-
-
-class ChildStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "child",
-            "Character is seen as a child in the eyes of society",
-        )
-
-
-class AdolescentStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "Adolescent",
-            "Character is seen as an adolescent in the eyes of society",
-        )
-
-
-class YoungAdultStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "Young Adult",
-            "Character is seen as a young adult in the eyes of society",
-        )
-
-
-class AdultStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "Adult",
-            "Character is seen as an adult in the eyes of society",
-        )
-
-
-class ElderStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "Senior",
-            "Character is seen as a senior in the eyes of society",
-        )
-
-
-class RetiredStatus(Status):
-    def __init__(self) -> None:
-        super().__init__(
-            "Retired",
-            "This character retired from their last occupation",
-        )
-
-
-class ResidentStatus(Status):
-    __slots__ = "duration", "town"
-
-    def __init__(self, town: str) -> None:
-        super().__init__(
-            "Resident",
-            "This character is a resident of a town",
-        )
-        self.duration: float = 0
-        self.town: str = town
-
-    @staticmethod
-    def system_fn(world: World, **kwargs) -> None:
-        delta_time: float = kwargs["delta_time"]
-        for _, resident_status in world.get_component(ResidentStatus):
-            resident_status.duration += delta_time
-
-
-class UnemployedStatus(Status):
-    __slots__ = "duration"
-
-    def __init__(self) -> None:
-        super().__init__(
-            "Unemployed",
-            "Character doesn't have a job",
-        )
-        self.duration: float = 0
-
-    @staticmethod
-    def system_fn(world: World, **kwargs) -> None:
-        delta_time: float = kwargs["delta_time"]
-        for _, unemployed_status in world.get_component(UnemployedStatus):
-            unemployed_status.duration += delta_time
-
-
-class DatingStatus(Status):
-    __slots__ = "duration", "partner_id", "partner_name"
-
-    def __init__(self, partner_id: int, partner_name: str) -> None:
-        super().__init__(
-            "Dating",
-            "This character is in a relationship with another",
-        )
-        self.duration: float = 0.0
-        self.partner_id: int = partner_id
-        self.partner_name: str = partner_name
-
-    @staticmethod
-    def system_fn(world: World, **kwargs) -> None:
-        delta_time: float = kwargs["delta_time"]
-        for _, dating_status in world.get_component(DatingStatus):
-            dating_status.duration += delta_time
-
-
-class MarriedStatus(Status):
-    __slots__ = "duration", "partner_id", "partner_name"
-
-    def __init__(self, partner_id: int, partner_name: str) -> None:
-        super().__init__(
-            "Married",
-            "This character is married to another",
-        )
-        self.duration = 0.0
-        self.partner_id: int = partner_id
-        self.partner_name: str = partner_name
-
-    @staticmethod
-    def system_fn(world: World, **kwargs) -> None:
-        delta_time: float = kwargs["delta_time"]
-        for _, married_status in world.get_component(MarriedStatus):
-            married_status.duration += delta_time
diff --git a/src/neighborly/core/business.py b/src/neighborly/core/business.py
index 4afb0f8..54c8f6c 100644
--- a/src/neighborly/core/business.py
+++ b/src/neighborly/core/business.py
@@ -1,12 +1,23 @@
+from __future__ import annotations
+
+import functools
+import logging
 import math
-from dataclasses import dataclass, field
-from enum import IntFlag, auto
-from typing import Any, ClassVar, Dict, List, Optional, Protocol, Tuple
+from dataclasses import dataclass
+from enum import Enum, IntFlag
+from typing import Any, ClassVar, Dict, List, Optional, Protocol, Tuple, Type
+
+from neighborly.core.character import GameCharacter
+from neighborly.core.ecs import Component, EntityArchetype, GameObject, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import LifeEvent
+from neighborly.core.location import Location
+from neighborly.core.position import Position2D
+from neighborly.core.residence import Resident
+from neighborly.core.routine import RoutineEntry, RoutinePriority
+from neighborly.core.time import SimDateTime
 
-from neighborly.core import name_generation as name_gen
-from neighborly.core.ecs import Component, GameObject
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
-from neighborly.core.routine import RoutineEntry, RoutinePriority, parse_schedule_str
+logger = logging.getLogger(__name__)
 
 
 class IOccupationPreconditionFn(Protocol):
@@ -21,15 +32,17 @@ class IOccupationPreconditionFn(Protocol):
     does not have proper support for **kwargs
     """
 
-    def __call__(self, gameobject: GameObject, **kwargs: Any) -> bool:
+    def __call__(self, world: World, gameobject: GameObject, **kwargs: Any) -> bool:
         """
         A function that must evaluate to True for a character to
         be eligible to hold the occupation.
 
         Arguments
         ---------
+        world: World
+            The simulation's world instance
         gameobject: GameObject
-            GameObject to evaluate
+            The GameObject to evaluate for the position
 
         Returns
         -------
@@ -39,50 +52,97 @@ def __call__(self, gameobject: GameObject, **kwargs: Any) -> bool:
         raise NotImplementedError()
 
 
+def join_preconditions(
+    *preconditions: IOccupationPreconditionFn,
+) -> IOccupationPreconditionFn:
+    """Join multiple occupation precondition functions into a single function"""
+
+    def wrapper(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        return all([p(world, gameobject, **kwargs) for p in preconditions])
+
+    return wrapper
+
+
+def or_preconditions(
+    *preconditions: IOccupationPreconditionFn,
+) -> IOccupationPreconditionFn:
+    """Only one of the given preconditions has to pass to return True"""
+
+    def wrapper(world: World, gameobject: GameObject, **kwargs: Any) -> bool:
+        for p in preconditions:
+            if p(world, gameobject, **kwargs):
+                return True
+        return False
+
+    return wrapper
+
+
 @dataclass
-class OccupationDefinition:
+class OccupationType:
     """
     Shared information about all occupations with this type
 
     Attributes
     ----------
-    _name: str
+    name: str
         Name of the position
-    _level: int
+    level: int
         Prestige or socioeconomic status associated with the position
-    _preconditions: List[OccupationPreconditionFn]
-        Preconditions functions that need to pass for a character
-        to qualify for this occupation type
-
-    Class Attributes
-    ----------------
-    _type_registry: Dict[str, OccupationType]
-        Registered instances of OccupationTypes
-    _precondition_registry: Dict[str, OccupationPreconditionFn]
-        Registered preconditions used to determine
-        if a character qualifies for a position
+    precondition: Optional[IOccupationPreconditionFn]
+        Function that determines of a candidate gameobject meets th requirements
+        of the occupation
     """
 
-    _type_registry: ClassVar[Dict[str, "OccupationDefinition"]] = {}
-    _precondition_registry: ClassVar[Dict[str, IOccupationPreconditionFn]] = {}
-
     name: str
-    level: int
-    preconditions: List[IOccupationPreconditionFn] = field(default_factory=list)
+    level: int = 1
+    precondition: Optional[IOccupationPreconditionFn] = None
 
-    @classmethod
-    def register_type(cls, occupation_type: "OccupationDefinition") -> None:
-        cls._type_registry[occupation_type.name] = occupation_type
+    def fill_role(self, world: World, business: Business) -> Optional[Occupation]:
+        """
+        Attempt to find a component character that meets the preconditions
+        for this occupation
+        """
+        candidate_list: List[GameObject] = list(
+            filter(
+                lambda g: self.precondition(world, g) if self.precondition else True,
+                map(
+                    lambda res: world.get_gameobject(res[0]),
+                    world.get_components(GameCharacter, Resident),
+                ),
+            )
+        )
 
-    @classmethod
-    def get_registered_type(cls, name: str) -> "OccupationDefinition":
-        return cls._type_registry[name]
+        if any(candidate_list):
+            chosen_candidate = world.get_resource(NeighborlyEngine).rng.choice(
+                candidate_list
+            )
+            occupation = Occupation(self, business.id)
+            chosen_candidate.add_component(occupation)
+            return occupation
+        return None
+
+
+class OccupationTypeLibrary:
+    """Stores OccupationType instances mapped to strings for lookup at runtime"""
+
+    _registry: ClassVar[Dict[str, OccupationType]] = {}
 
     @classmethod
-    def register_precondition(
-        cls, name: str, precondition: IOccupationPreconditionFn
+    def add(
+        cls,
+        occupation_type: OccupationType,
+        name: str = None,
+        overwrite_ok: bool = False,
     ) -> None:
-        cls._precondition_registry[name] = precondition
+        entry_key = name if name else occupation_type.name
+        if entry_key in cls._registry and not overwrite_ok:
+            logger.warning(f"Attempted to overwrite OcuppationType: ({entry_key})")
+            return
+        cls._registry[occupation_type.name] = occupation_type
+
+    @classmethod
+    def get(cls, name: str) -> OccupationType:
+        return cls._registry[name]
 
 
 class Occupation(Component):
@@ -92,13 +152,15 @@ class Occupation(Component):
 
     __slots__ = "_occupation_def", "_years_held", "_business"
 
+    _definition_registry: Dict[str, OccupationType] = {}
+
     def __init__(
         self,
-        occupation_type: OccupationDefinition,
+        occupation_type: OccupationType,
         business: int,
     ) -> None:
         super().__init__()
-        self._occupation_def: OccupationDefinition = occupation_type
+        self._occupation_def: OccupationType = occupation_type
         self._business: int = business
         self._years_held: float = 0.0
 
@@ -110,7 +172,7 @@ def to_dict(self) -> Dict[str, Any]:
             "years_held": self.get_years_held(),
         }
 
-    def get_type(self) -> OccupationDefinition:
+    def get_type(self) -> OccupationType:
         return self._occupation_def
 
     def get_business(self) -> int:
@@ -122,143 +184,282 @@ def get_years_held(self) -> int:
     def increment_years_held(self, years: float) -> None:
         self._years_held += years
 
+    @classmethod
+    def create(cls, world, business: int = -1, **kwargs) -> Occupation:
+        type_name = kwargs["name"]
+        level = kwargs.get("level", 1)
+        preconditions = kwargs.get("preconditions", [])
+        return Occupation(
+            cls.get_occupation_definition(type_name, level, preconditions),
+            business=business,
+        )
 
-class BusinessServiceFlag(IntFlag):
-    NONE = 0
-    ALCOHOL = auto()
-    BANKING = auto()
-    COLLEGE_EDUCATION = auto()
-    CONSTRUCTION = auto()
-    COSMETICS = auto()
-    CLOTHING = auto()
-    FIRE_EMERGENCY = auto()
-    FOOD = auto()
-    HARDWARE = auto()
-    HOME_IMPROVEMENT = auto()
-    HOUSING = auto()
-    LEGAL = auto()
-    MEDICAL_EMERGENCY = auto()
-    MORTICIAN = auto()
-    RECREATION = auto()
-    PUBLIC_SERVICE = auto()
-    PRIMARY_EDUCATION = auto()
-    REALTY = auto()
-    SECONDARY_EDUCATION = auto()
-    SHOPPING = auto()
-    SOCIALIZING = auto()
+    @classmethod
+    def get_occupation_definition(
+        cls, name: str, level: int, precondition: IOccupationPreconditionFn
+    ) -> OccupationType:
+        if name not in cls._definition_registry:
+            cls._definition_registry[name] = OccupationType(
+                name=name, level=level, precondition=precondition
+            )
+        return cls._definition_registry[name]
+
+    def on_remove(self) -> None:
+        """Run when the component is removed from the GameObject"""
+        world = self.gameobject.world
+        workplace = world.get_gameobject(self._business).get_component(Business)
+        if workplace.owner != self.gameobject.id:
+            workplace.remove_employee(self.gameobject.id)
+        else:
+            workplace.owner = None
+
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
 
 
 @dataclass
-class BusinessDefinition:
+class WorkHistoryEntry:
+    """Record of a job held by a character"""
+
+    occupation_type: str
+    business: int
+    start_date: SimDateTime
+    end_date: SimDateTime
+    reason_for_leaving: Optional[LifeEvent] = None
+
+    @property
+    def years_held(self) -> int:
+        return (self.start_date - self.end_date).years
+
+    def to_dict(self) -> Dict[str, Any]:
+        """Return dictionary representation for serialization"""
+
+        ret = {
+            "occupation_type": self.occupation_type,
+            "business": self.business,
+            "start_date": self.start_date.to_iso_str(),
+            "end_date": self.end_date.to_iso_str(),
+        }
+
+        if self.reason_for_leaving:
+            # This should probably point to a unique ID for the
+            # event, but we will leave it as the name of the event for now
+            ret["reason_for_leaving"] = self.reason_for_leaving.name
+
+        return ret
+
+
+class WorkHistory(Component):
     """
-    Shared information about all businesses that
-    have this type
+    Stores information about all the jobs that a character
+    has held
+
+    Attributes
+    ----------
+    _chronological_history: List[WorkHistoryEntry]
+        List of previous job in order from oldest to most recent
     """
 
-    _type_registry: ClassVar[Dict[str, "BusinessDefinition"]] = {}
+    __slots__ = "_chronological_history", "_categorical_history"
 
-    name: str
-    hours: str
-    name_pattern: Optional[str] = None
-    owner_type: Optional[str] = None
-    max_instances: int = 9999
-    instances: int = 0
-    min_population: int = 0
-    employees: Dict[str, int] = field(default_factory=dict)
-    services: List[str] = field(default_factory=list)
-    service_flags: BusinessServiceFlag = field(init=False)
-
-    def __post_init__(self) -> None:
-        self.service_flags = BusinessServiceFlag.NONE
-        for service_name in self.services:
-            self.service_flags |= BusinessServiceFlag[
-                service_name.strip().upper().replace(" ", "_")
-            ]
+    def __init__(self) -> None:
+        super().__init__()
+        self._chronological_history: List[WorkHistoryEntry] = []
+        self._categorical_history: Dict[str, List[WorkHistoryEntry]] = {}
 
-    @classmethod
-    def register_type(cls, business_type: "BusinessDefinition") -> None:
-        """Register one or more business types for later use"""
-        cls._type_registry[business_type.name] = business_type
+    def add_entry(
+        self,
+        occupation_type: str,
+        business: int,
+        start_date: SimDateTime,
+        end_date: SimDateTime,
+        reason_for_leaving: Optional[LifeEvent] = None,
+    ) -> None:
+        """Add an entry to the work history"""
+        entry = WorkHistoryEntry(
+            occupation_type=occupation_type,
+            business=business,
+            start_date=start_date,
+            end_date=end_date,
+            reason_for_leaving=reason_for_leaving,
+        )
 
-    @classmethod
-    def get_registered_type(cls, name: str) -> "BusinessDefinition":
-        return cls._type_registry[name]
+        self._chronological_history.append(entry)
 
-    @classmethod
-    def get_all_types(cls) -> List["BusinessDefinition"]:
-        return list(cls._type_registry.values())
+        if occupation_type not in self._categorical_history:
+            self._categorical_history[occupation_type] = []
+
+        self._categorical_history[occupation_type].append(entry)
+
+    def has_experience_as_a(self, occupation_type: str) -> bool:
+        """Return True if the work history has an entry for a given occupation type"""
+        return occupation_type in self._categorical_history
+
+    def total_experience_as_a(self, occupation_type: str) -> int:
+        """Return the total years of experience someone has as a given occupation type"""
+        return functools.reduce(
+            lambda _sum, _entry: _sum + _entry.years_held,
+            self._categorical_history.get(occupation_type, []),
+            0,
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            **super().to_dict(),
+            "history": [entry.to_dict() for entry in self._chronological_history],
+        }
+
+    def __len__(self) -> int:
+        return len(self._chronological_history)
+
+
+class BusinessService(IntFlag):
+    NONE = 0
+    DRINKING = 1 << 0
+    BANKING = 1 << 1
+    COLLEGE_EDUCATION = 1 << 2
+    CONSTRUCTION = 1 << 3
+    COSMETICS = 1 << 4
+    CLOTHING = 1 << 5
+    FIRE_EMERGENCY = 1 << 6
+    FOOD = 1 << 7
+    HARDWARE = 1 << 8
+    HOME_IMPROVEMENT = 1 << 9
+    HOUSING = 1 << 10
+    LEGAL = 1 << 11
+    MEDICAL_EMERGENCY = 1 << 12
+    MORTICIAN = 1 << 13
+    RECREATION = 1 << 14
+    PUBLIC_SERVICE = 1 << 15
+    PRIMARY_EDUCATION = 1 << 16
+    REALTY = 1 << 17
+    SECONDARY_EDUCATION = 1 << 18
+    SHOPPING = 1 << 19
+    SOCIALIZING = 1 << 20
+    ERRANDS = 1 << 21
+
+
+class BusinessStatus(Enum):
+    PendingOpening = 0
+    OpenForBusiness = 1
+    ClosedForBusiness = 2
 
 
 class Business(Component):
     __slots__ = (
-        "_business_def",
-        "_name",
+        "business_type",
+        "name",
         "_years_in_business",
-        "_operating_hours",
+        "operating_hours",
         "_employees",
         "_open_positions",
-        "_owner",
+        "owner",
+        "owner_type",
+        "status",
+        "services",
     )
 
     def __init__(
-        self, business_def: BusinessDefinition, name: str, owner: Optional[int] = None
+        self,
+        business_type: str,
+        name: str,
+        owner_type: str = None,
+        owner: int = None,
+        open_positions: Dict[str, int] = None,
+        operating_hours: Dict[str, List[RoutineEntry]] = None,
+        services: BusinessService = BusinessService.NONE,
     ) -> None:
         super().__init__()
-        self._business_def: BusinessDefinition = business_def
-        self._name: str = name
-        self._operating_hours: Dict[str, List[RoutineEntry]] = self._create_routines(
-            parse_schedule_str(business_def.hours)
+        self.business_type: str = business_type
+        self.owner_type: Optional[str] = owner_type
+        self.name: str = name
+        # self._operating_hours: Dict[str, List[RoutineEntry]] = self._create_routines(
+        #     parse_schedule_str(business_def.hours)
+        # )
+        self.operating_hours: Dict[str, List[RoutineEntry]] = (
+            operating_hours if operating_hours else {}
         )
-        self._open_positions: Dict[str, int] = business_def.employees
-        self._employees: List[Tuple[str, int]] = []
-        self._owner: Optional[int] = owner
+        self._open_positions: Dict[str, int] = open_positions if open_positions else {}
+        self._employees: Dict[int, str] = {}
+        self.owner: Optional[int] = owner
+        self.status: BusinessStatus = BusinessStatus.PendingOpening
+        self._years_in_business: float = 0.0
+        self.services: BusinessService = services
+
+    @property
+    def years_in_business(self) -> int:
+        return math.floor(self._years_in_business)
 
     def to_dict(self) -> Dict[str, Any]:
         return {
             **super().to_dict(),
-            "business_def": self._business_def.name,
-            "name": self._name,
-            "operating_hours": self._business_def.hours,
-            "open_positions": self._open_positions,
-            "employees": self._employees,
-            "owner": self._owner if self._owner else -1,
+            "business_type": self.business_type,
+            "name": self.name,
+            "operating_hours": self.operating_hours,
+            "open_positions": self.operating_hours,
+            "employees": self.get_employees(),
+            "owner": self.owner if self.owner is not None else -1,
         }
 
     def needs_owner(self) -> bool:
-        return self._owner is None and self.get_type().owner_type is not None
+        return self.owner is None and self.owner_type is not None
 
     def get_open_positions(self) -> List[str]:
         return [title for title, n in self._open_positions.items() if n > 0]
 
-    def get_type(self) -> BusinessDefinition:
-        return self._business_def
-
-    def get_name(self) -> str:
-        return self._name
-
     def get_employees(self) -> List[int]:
-        return list(map(lambda x: x[1], self._employees))
+        """Return a list of IDs for current employees"""
+        return list(self._employees.keys())
+
+    def add_employee(self, character: int, position: str) -> None:
+        """Add character to employees and remove a vacant position"""
+        self._employees[character] = position
+        self._open_positions[position] -= 1
 
-    def hire_owner(self, character: int) -> None:
-        self._owner = character
+    def remove_employee(self, character: int) -> None:
+        """Remove a character as an employee and add a vacant position"""
+        position = self._employees[character]
+        del self._employees[character]
+        self._open_positions[position] += 1
 
-    def on_start(self) -> None:
-        self.get_type().instances = self.get_type().instances + 1
+    def set_business_status(self, status: BusinessStatus) -> None:
+        self.status = status
 
-    def on_destroy(self) -> None:
-        self.get_type().instances = self.get_type().instances - 1
+    def increment_years_in_business(self, years: float) -> None:
+        self._years_in_business += years
 
     def __repr__(self) -> str:
         """Return printable representation"""
-        return "{}(type='{}', name='{}', owner={}, employees={}, openings={})".format(
-            self.__class__.__name__,
-            self._business_def.name,
-            self._name,
-            self._owner,
+        return "Business(type='{}', name='{}', owner={}, employees={}, openings={})".format(
+            self.business_type,
+            self.name,
+            self.owner,
             self._employees,
             self._open_positions,
         )
 
+    @classmethod
+    def create(cls, world: World, **kwargs) -> Business:
+        engine = world.get_resource(NeighborlyEngine)
+
+        business_type: str = kwargs["business_type"]
+        business_name: str = engine.name_generator.get_name(
+            kwargs.get("name_format", business_type)
+        )
+        owner_type: Optional[str] = kwargs.get("owner_type")
+        employee_types: Dict[str, int] = kwargs.get("employee_types", {})
+        services: BusinessService = kwargs.get("services", BusinessService.NONE)
+        operating_hours: List[RoutineEntry] = []
+
+        return Business(
+            business_type=business_type,
+            name=business_name,
+            open_positions=employee_types,
+            services=services,
+            owner_type=owner_type,
+            # operating_hours=operating_hours,
+        )
+
     @staticmethod
     def _create_routines(
         times: Dict[str, Tuple[int, int]]
@@ -310,24 +511,139 @@ def _create_routines(
         return schedules
 
 
-class BusinessFactory(AbstractFactory):
-    """Create instances of the default business component"""
+class BusinessArchetype(EntityArchetype):
+    """
+    Shared information about all businesses that
+    have this type
+    """
 
-    def __init__(self) -> None:
-        super().__init__("Business")
+    __slots__ = (
+        "hours",
+        "name_format",
+        "owner_type",
+        "max_instances",
+        "min_population",
+        "employee_types",
+        "services",
+        "service_flags",
+        "spawn_multiplier",
+        "year_available",
+        "year_obsolete",
+    )
+
+    def __init__(
+        self,
+        name: str,
+        hours: List[str] = None,
+        name_format: str = None,
+        owner_type: str = None,
+        max_instances: int = 9999,
+        min_population: int = 0,
+        employee_types: Dict[str, int] = None,
+        services: List[str] = None,
+        spawn_multiplier: int = 1,
+        extra_components: Dict[Type[Component], Dict[str, Any]] = None,
+        year_available: int = -1,
+        year_obsolete: int = 9999,
+    ) -> None:
+        super().__init__(name)
+        self.hours: List[str] = hours if hours else ["day"]
+        self.name_format: str = name_format if name_format else name
+        self.owner_type: Optional[str] = owner_type
+        self.max_instances: int = max_instances
+        self.min_population: int = min_population
+        self.employee_types: Dict[str, int] = employee_types if employee_types else {}
+        self.services: List[str] = services if services else {}
+        self.service_flags: BusinessService = BusinessService.NONE
+        self.spawn_multiplier: int = spawn_multiplier
+        for service_name in self.services:
+            self.service_flags |= BusinessService[
+                service_name.strip().upper().replace(" ", "_")
+            ]
+        self.year_available: int = year_available
+        self.year_obsolete: int = year_obsolete
+
+        self.add(
+            Business,
+            business_type=self.name,
+            name_format=self.name_format,
+            hours=self.hours,
+            owner_type=self.owner_type,
+            employee_types=self.employee_types,
+            services=self.service_flags,
+        )
+        self.add(Location)
+        self.add(Position2D)
 
-    def create(self, spec: ComponentDefinition, **kwargs) -> Business:
-        type_name: Optional[str] = spec.get_attribute("business_type")
+        if extra_components:
+            for component_type, params in extra_components.items():
+                self.add(component_type, **params)
 
-        if type_name is None:
-            raise TypeError("Expected to fund business_type str but was None")
 
-        business_type = BusinessDefinition.get_registered_type(type_name)
+class BusinessArchetypeLibrary:
+    _registry: Dict[str, BusinessArchetype] = {}
 
-        name = (
-            name_gen.get_name(business_type.name_pattern)
-            if business_type.name_pattern
-            else type_name
-        )
+    @classmethod
+    def add(
+        cls, archetype: BusinessArchetype, name: str = None, overwrite_ok: bool = False
+    ) -> None:
+        """Register a new LifeEventType mapped to a name"""
+        entry_key = name if name else archetype.name
+        if entry_key in cls._registry and not overwrite_ok:
+            logger.warning(f"Attempted to overwrite BusinessArchetype: ({entry_key})")
+            return
+        cls._registry[entry_key] = archetype
 
-        return Business(business_type, name)
+    @classmethod
+    def get_all(cls) -> List[BusinessArchetype]:
+        return list(cls._registry.values())
+
+    @classmethod
+    def get(cls, name: str) -> BusinessArchetype:
+        """Get a LifeEventType using a name"""
+        return cls._registry[name]
+
+
+def tod_to_operating_hours(
+    str_hours: List[str], business_id: int
+) -> List[RoutineEntry]:
+    """
+    Convert a list of string with times of day and convert
+    them to a list of RoutineEntries for when employees
+    should report to work and when a business is open to the
+    public.
+
+    Parameters
+    ----------
+    str_hours: List[str]
+        The times of day that this business is open
+    business_id: int
+        Unique ID for the business associated with these entries
+
+    Returns
+    -------
+    List[RoutineEntry]
+        Routine entries for when this business is operational
+    """
+    times_to_intervals = {
+        "morning": (5, 12),
+        "late-morning": (11, 12),
+        "early-morning": (5, 8),
+        "day": (8, 11),
+        "afternoon": (12, 5),
+        "evening": (5, 9),
+        "night": (9, 12),
+    }
+
+    routines: List[RoutineEntry] = []
+
+    for time_of_day in str_hours:
+        try:
+            start, end = times_to_intervals[time_of_day]
+            routines.append(
+                RoutineEntry(start, end, location=business_id, activity="work")
+            )
+        except KeyError:
+            raise ValueError(f"{time_of_day} is not a valid time of day.")
+
+    return routines
diff --git a/src/neighborly/core/character.py b/src/neighborly/core/character.py
index ff151af..1d4f5f9 100644
--- a/src/neighborly/core/character.py
+++ b/src/neighborly/core/character.py
@@ -1,86 +1,29 @@
 from __future__ import annotations
-from collections import defaultdict
 
-from typing import Any, ClassVar, NamedTuple, Optional, Union, Tuple, List, Dict
-from typing_extensions import TypedDict
+from dataclasses import dataclass
+from typing import Any, ClassVar, Dict, List, Optional, Type
 
-from pydantic import BaseModel, Field, validator
+from typing_extensions import TypedDict
 
-from neighborly.core import name_generation as name_gen
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
-from neighborly.core.rng import IRandNumGenerator
-from neighborly.core.tags import Tag
-from neighborly.core.life_event import (
-    LifeEvent,
-    ILifeEventCallback,
-    LifeEventHandler,
-    ILifeEventListener,
-    EventCallbackDatabase,
-)
-from neighborly.core.utils.utilities import parse_number_range
+from neighborly.core.ecs import Component, EntityArchetype, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.location import Location
+from neighborly.core.personal_values import PersonalValues
+from neighborly.core.routine import Routine
 
 
 class LifeStages(TypedDict):
     """Ages when characters are in certain stages of their lives"""
 
     child: int
-    adolescent: int
+    teen: int
     young_adult: int
     adult: int
     elder: int
 
 
-class LifeCycleConfig(BaseModel):
-    """Configuration parameters for a characters lifecycle
-
-    Fields
-    ------
-    can_age: bool
-        Will his character's age change during th simulation
-    can_die: bool
-        Can this character die when it reaches its max age
-    lifespan_mean: int
-        Average lifespan of characters with this configuration
-    lifespan_std: int
-        Standard deviation of the lifespans of characters with this
-        configuration
-    adult_age: int
-        Age that characters with thi config are considered adults
-        in society
-    romantic_feelings_age: int
-        The age that characters start to develop romantic feelings
-        for other characters
-    """
-
-    life_stages: LifeStages
-    lifespan: int
-
-
-class FamilyGenerationOptions(BaseModel):
-    """Options used when generating a family for a new character entering the town"""
-
-    probability_spouse: float
-    probability_children: float
-    num_children: Tuple[int, int]
-
-    @validator("num_children", pre=True)
-    def convert_age_ranges_to_tuples(cls, v):
-        if isinstance(v, str):
-            return parse_number_range(v)
-        return v
-
-
-class GenerationConfig(BaseModel):
-    """Parameters for generating new characters with this type"""
-
-    first_name: str
-    last_name: str
-    family: FamilyGenerationOptions
-    gender_weights: Dict[str, int] = Field(default_factory=dict)
-
-
-class CharacterDefinition(BaseModel):
+@dataclass
+class CharacterDefinition:
     """Configuration parameters for characters
 
     Fields
@@ -91,39 +34,15 @@ class CharacterDefinition(BaseModel):
 
     _type_registry: ClassVar[Dict[str, CharacterDefinition]] = {}
 
-    name: str
-    lifecycle: LifeCycleConfig
-    generation: GenerationConfig
-    gender_overrides: Dict[str, Any] = Field(default_factory=dict)
-    events: Dict[str, Dict[str, List[str]]] = Field(default_factory=dict)
-
-    @classmethod
-    def create(
-        cls, options: Dict[str, Any], base: Optional[CharacterDefinition] = None
-    ) -> CharacterDefinition:
-        """Create a new Character type
-
-        Parameters
-        ----------
-        options: Dict[str, Any]
-            data used to instantiate the CharacterType
-        base: CharacterType
-            Character type to base the new type off of
-        """
-
-        character_def = {}
-
-        if base:
-            character_def.update(base.dict())
-
-        character_def.update(options)
-
-        return CharacterDefinition(**character_def)
+    type_name: str
+    life_stages: LifeStages
+    lifespan: int
+    chance_can_get_pregnant: float = 0.5
 
     @classmethod
     def register_type(cls, type_config: CharacterDefinition) -> None:
         """Registers a character config with the shared registry"""
-        cls._type_registry[type_config.name] = type_config
+        cls._type_registry[type_config.type_name] = type_config
 
     @classmethod
     def get_type(cls, name: str) -> CharacterDefinition:
@@ -131,15 +50,21 @@ def get_type(cls, name: str) -> CharacterDefinition:
         return cls._type_registry[name]
 
 
-class CharacterName(NamedTuple):
-    firstname: str
-    surname: str
+class CharacterName:
+    __slots__ = "firstname", "surname"
+
+    def __init__(self, firstname: str, surname: str) -> None:
+        self.firstname: str = firstname
+        self.surname: str = surname
+
+    def __repr__(self) -> str:
+        return f"{self.firstname} {self.surname}"
 
     def __str__(self) -> str:
         return f"{self.firstname} {self.surname}"
 
 
-class GameCharacter(Component, ILifeEventListener):
+class GameCharacter(Component):
     """
     The state of a single character within the world
 
@@ -155,10 +80,6 @@ class GameCharacter(Component, ILifeEventListener):
         Entity ID of the location where this character current is
     location_aliases: Dict[str, int]
         Maps string names to entity IDs of locations in the world
-    tags: Dict[str, Tag]
-        Tags attached to this character
-    _event_handlers: Dict[str, LifeEventHandler]
-        Event handlers for the GameCharacter component
     """
 
     __slots__ = (
@@ -167,17 +88,17 @@ class GameCharacter(Component, ILifeEventListener):
         "age",
         "location",
         "location_aliases",
-        "tags",
-        "_event_handlers",
+        "can_get_pregnant",
     )
 
+    character_def_registry: Dict[str, CharacterDefinition] = {}
+
     def __init__(
         self,
         character_def: CharacterDefinition,
         name: CharacterName,
         age: float,
-        tags: Optional[List[Tag]] = None,
-        events: Optional[Dict[str, Dict[str, List[ILifeEventCallback]]]] = None,
+        can_get_pregnant: bool = False,
     ) -> None:
         super().__init__()
         self.character_def = character_def
@@ -185,150 +106,143 @@ def __init__(
         self.age: float = age
         self.location: Optional[int] = None
         self.location_aliases: Dict[str, int] = {}
-        self.tags: Dict[str, Tag] = {}
-        self._event_handlers: defaultdict[str, LifeEventHandler] = defaultdict(
-            LifeEventHandler
+        self.can_get_pregnant: bool = can_get_pregnant
+
+    def on_remove(self) -> None:
+        if self.location:
+            location = self.gameobject.world.get_gameobject(self.location)
+            location.get_component(Location).remove_character(self.gameobject.id)
+        self.location = None
+
+    @classmethod
+    def create(cls, world: World, **kwargs) -> GameCharacter:
+        """Create a new instance of a character"""
+        engine: NeighborlyEngine = world.get_resource(NeighborlyEngine)
+
+        character_type_name = kwargs["character_type"]
+        first_name, surname = kwargs.get(
+            "name_format", "#first_name# #family_name#"
+        ).split(" ")
+        lifespan: int = kwargs["lifespan"]
+        life_stages: LifeStages = kwargs["life_stages"]
+        chance_can_get_pregnant: float = kwargs.get("chance_can_get_pregnant", 0.5)
+
+        character_def = cls.get_character_def(
+            CharacterDefinition(
+                type_name=character_type_name,
+                lifespan=lifespan,
+                life_stages=life_stages,
+            )
         )
 
-        if tags:
-            self.tags.update({tag.name: tag for tag in tags})
+        name = CharacterName(
+            engine.name_generator.get_name(first_name),
+            engine.name_generator.get_name(surname),
+        )
 
-        if events:
-            for event_name, callbacks in events.items():
-                preconditions = callbacks.get("preconditions", [])
-                for fn in preconditions:
-                    self._event_handlers[event_name].add_precondition(fn)
-                effects = callbacks.get("effects", [])
-                for fn in effects:
-                    self._event_handlers[event_name].add_effect(fn)
+        can_get_pregnant = engine.rng.random() < chance_can_get_pregnant
+
+        # generate an age
+        age = engine.rng.randint(
+            character_def.life_stages["young_adult"],
+            character_def.life_stages["elder"] - 1,
+        )
+
+        return GameCharacter(
+            character_def=character_def,
+            name=name,
+            age=float(age),
+            can_get_pregnant=can_get_pregnant,
+        )
+
+    @classmethod
+    def get_character_def(
+        cls, character_def: CharacterDefinition
+    ) -> CharacterDefinition:
+        """Returns an existing CharacterDefinition with the same name or creates a new one"""
+        if character_def.type_name not in cls.character_def_registry:
+            cls.character_def_registry[character_def.type_name] = character_def
+        return cls.character_def_registry[character_def.type_name]
 
     def to_dict(self) -> Dict[str, Any]:
         return {
             **super().to_dict(),
-            "character_def": self.character_def.name,
+            "character_def": self.character_def.type_name,
             "name": str(self.name),
             "age": self.age,
             "location": self.location,
             "location_aliases": self.location_aliases,
-            "tags": list(self.tags.keys()),
+            "can_get_pregnant": self.can_get_pregnant,
         }
 
-    def check_preconditions(self, event: LifeEvent) -> bool:
-        """
-        Check the preconditions for this event type to see if they pass
-
-        Returns
-        -------
-        bool
-            True if the event passes all the preconditions
-        """
-        self._event_handlers[event.get_type()].check_preconditions(
-            self.gameobject, event
-        )
-
-        if self.tags:
-            for tag in self.tags.values():
-                precondition = tag.event_preconditions.get(event.get_type())
-                if (
-                    precondition is not None
-                    and precondition(self.gameobject, event) is False
-                ):
-                    return False
-        return True
-
-    def handle_event(self, event: LifeEvent) -> bool:
-        """
-        Perform logic when an event occurs
-
-        Returns
-        -------
-        bool
-            True if the event was handled successfully
-        """
-        self._event_handlers[event.get_type()].handle_event(self.gameobject, event)
-
-        for tag in self.tags.values():
-            effect = tag.event_effects.get(event.get_type())
-            if effect is not None and effect(self.gameobject, event) is False:
-                return False
-        return True
+    def __str__(self) -> str:
+        return f"{str(self.name)}({self.gameobject.id})"
 
     def __repr__(self) -> str:
         """Return printable representation"""
-        return "{}(name={}, age={}, location={}, location_aliases={})".format(
+        return "{}(name={}, age={}, location={}, location_aliases={}, can_get_pregnant={})".format(
             self.__class__.__name__,
             str(self.name),
             round(self.age),
             self.location,
             self.location_aliases,
+            self.can_get_pregnant,
         )
 
 
-def generate_character(
-    character_def: CharacterDefinition, rng: IRandNumGenerator, **kwargs
-) -> GameCharacter:
-    first_name = kwargs.get(
-        "first_name", name_gen.get_name(character_def.generation.first_name)
-    )
-    last_name = kwargs.get(
-        "last_name", name_gen.get_name(character_def.generation.last_name)
-    )
-    name = CharacterName(first_name, last_name)
-
-    # generate an age
-    age_range: Optional[Union[str, tuple[int, int]]] = kwargs.get("age_range")
-
-    if isinstance(age_range, str):
-        lower_bound: int = character_def.lifecycle.life_stages[age_range]
+class CharacterArchetype(EntityArchetype):
+    """
+    Archetype subclass for building characters
+    """
 
-        potential_upper_bounds = {
-            "child": character_def.lifecycle.life_stages["adolescent"],
-            "adolescent": character_def.lifecycle.life_stages["young_adult"],
-            "young_adult": character_def.lifecycle.life_stages["adult"],
-            "adult": character_def.lifecycle.life_stages["elder"],
-            "elder": character_def.lifecycle.lifespan,
-        }
+    __slots__ = "name_format", "spawn_multiplier"
 
-        age = rng.randint(lower_bound, potential_upper_bounds[age_range] - 1)
-    elif isinstance(age_range, tuple):
-        age = rng.randint(age_range[0], age_range[1])
-    else:
-        age = rng.randint(
-            character_def.lifecycle.life_stages["young_adult"],
-            character_def.lifecycle.life_stages["adult"] - 1,
+    def __init__(
+        self,
+        name: str,
+        lifespan: int,
+        life_stages: LifeStages,
+        name_format: "#first_name# #family_name#",
+        chance_can_get_pregnant: float = 0.5,
+        spawn_multiplier: int = 1,
+        extra_components: Dict[Type[Component], Dict[str, Any]] = None,
+    ) -> None:
+        super().__init__(name)
+        self.name_format: str = name_format
+        self.spawn_multiplier: int = spawn_multiplier
+
+        self.add(
+            GameCharacter,
+            character_type=name,
+            name_format=name_format,
+            lifespan=lifespan,
+            life_stages=life_stages,
+            chance_can_get_pregnant=chance_can_get_pregnant,
         )
 
-    event_handlers: Dict[str, Dict[str, List[ILifeEventCallback]]] = {}
+        self.add(Routine)
+        self.add(PersonalValues)
 
-    for event_name, handler_config in character_def.events.items():
-        if event_name not in event_handlers:
-            event_handlers[event_name] = {"preconditions": [], "effects": []}
-
-        for callback_name in handler_config.get("preconditions", []):
-            event_handlers[event_name]["preconditions"].append(
-                EventCallbackDatabase.get_precondition(callback_name)
-            )
-
-        for callback_name in handler_config.get("effects", []):
-            event_handlers[event_name]["effects"].append(
-                EventCallbackDatabase.get_effect(callback_name)
-            )
+        if extra_components:
+            for component_type, params in extra_components.items():
+                self.add(component_type, **params)
 
-    return GameCharacter(
-        character_def=character_def, name=name, age=float(age), events=event_handlers
-    )
 
+class CharacterArchetypeLibrary:
+    _registry: Dict[str, CharacterArchetype] = {}
 
-class GameCharacterFactory(AbstractFactory):
-    """
-    Default factory for constructing instances of GameCharacters.
-    """
+    @classmethod
+    def register(
+        cls, archetype: CharacterArchetype, name: Optional[str] = None
+    ) -> None:
+        """Register a new LifeEventType mapped to a name"""
+        cls._registry[name if name else archetype.name] = archetype
 
-    def __init__(self) -> None:
-        super().__init__("GameCharacter")
+    @classmethod
+    def get_all(cls) -> List[CharacterArchetype]:
+        return list(cls._registry.values())
 
-    def create(self, spec: ComponentDefinition, **kwargs) -> GameCharacter:
-        """Create a new instance of a character"""
-        return generate_character(
-            character_def=CharacterDefinition.get_type(spec["character_def"]), **kwargs
-        )
+    @classmethod
+    def get(cls, name: str) -> CharacterArchetype:
+        """Get a LifeEventType using a name"""
+        return cls._registry[name]
diff --git a/src/neighborly/core/ecs.py b/src/neighborly/core/ecs.py
index d8b195e..0c4568e 100644
--- a/src/neighborly/core/ecs.py
+++ b/src/neighborly/core/ecs.py
@@ -1,63 +1,67 @@
-"""Tuple
-Custom Entity component implementation that blends
-the Unity-style workflow with the logic from the
-Python esper library.
+"""
+Custom Entity-Component implementation that blends
+the Unity-style GameObjects with the ECS logic from the
+Python esper library and the Bevy Game Engine.
 
 Sources:
+https://docs.unity3d.com/ScriptReference/GameObject.html
 https://github.com/benmoran56/esper
+https://github.com/bevyengine/bevy
 """
 from __future__ import annotations
 
 import hashlib
-from abc import ABC, abstractmethod
-from typing import (
-    Any,
-    Iterable,
-    List,
-    Dict,
-    Tuple,
-    Optional,
-    Protocol,
-    Type,
-    TypeVar,
-    cast,
-)
+import logging
+from abc import ABC
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, TypeVar
 from uuid import uuid1
 
+import esper
+
+logger = logging.getLogger(__name__)
+
 _CT = TypeVar("_CT", bound="Component")
 _RT = TypeVar("_RT", bound="Any")
 
 
 class GameObject:
-    """The core class on which all entities are built
+    """
+    Collections of components that share are unique identifier
+    and represent entities within the game world
 
     Attributes
     ----------
     id: int
         unique identifier
-    tags: List[string]
-        Associated tag strings
+    name: str
+        name of the GameObject
     world: World
         the World instance this GameObject belongs to
-    _components: Dict[Type, Components]
+    components: List[Components]
         Components attached to this GameObject
     """
 
-    __slots__ = "_id", "_tags", "_components", "_world", "_active", "_archetype_name"
+    __slots__ = (
+        "_id",
+        "_name",
+        "_components",
+        "_world",
+        "_archetype",
+    )
 
     def __init__(
         self,
-        tags: Iterable[str] = (),
+        unique_id: Optional[int] = None,
+        name: str = "GameObject",
         components: Iterable[Component] = (),
         world: Optional[World] = None,
-        archetype_name: Optional[str] = None,
+        archetype: Optional[EntityArchetype] = None,
     ) -> None:
-        self._id: int = self.generate_id()
-        self._active: bool = True
-        self._tags: set[str] = set(tags)
+        self._name: str = name
+        self._id: int = unique_id if unique_id else self.generate_id()
         self._world: Optional[World] = world
-        self._components: Dict[str, Component] = {}
-        self._archetype_name: Optional[str] = archetype_name
+        self._components: Dict[Type[_CT], Component] = {}
+        self._archetype: Optional[EntityArchetype] = archetype
 
         if components:
             for component in components:
@@ -69,13 +73,14 @@ def id(self) -> int:
         return self._id
 
     @property
-    def tags(self) -> set[str]:
-        """Return tags associated with this GameObject"""
-        return self._tags
+    def name(self) -> str:
+        """Get the name of the GameObject"""
+        return self._name
 
     @property
-    def archetype_name(self) -> Optional[str]:
-        return self._archetype_name
+    def archetype(self) -> Optional[EntityArchetype]:
+        """Return the name of the archetype for creating this GameObject"""
+        return self._archetype
 
     @property
     def world(self) -> World:
@@ -85,90 +90,57 @@ def world(self) -> World:
         raise TypeError("World is None for GameObject")
 
     @property
-    def active(self) -> bool:
-        return self._active
+    def components(self) -> List[Component]:
+        return list(self._components.values())
 
     def set_world(self, world: Optional[World]) -> None:
         """set the world instance"""
         self._world = world
 
-    def set_active(self, is_active: bool) -> None:
-        self._active = is_active
-
-    def add_tags(self, *tags: str) -> None:
-        """Add tags to this GameObject"""
-        for tag in tags:
-            self._tags.add(tag)
-
-    def remove_tags(self, *tags: str) -> None:
-        """Remove tags to this GameObject"""
-        for tag in tags:
-            self._tags.remove(tag)
-
-    def has_tags(self, *tags: str) -> bool:
-        """Add tags to this GameObject"""
-        for tag in tags:
-            if tag not in self._tags:
-                return False
-        return True
-
-    def add_component(self, component: Component) -> None:
+    def add_component(self, component: Component) -> GameObject:
         """Add a component to this GameObject"""
         component.set_gameobject(self)
+        self._components[type(component)] = component
+        self.world.ecs.add_component(self.id, component)
         component.on_add()
-        self._components[type(component).__name__] = component
+        return self
 
-    def remove_component(self, component: Component) -> None:
+    def remove_component(self, component_type: Type[_CT]) -> None:
         """Add a component to this GameObject"""
+        component = self._components[component_type]
         component.on_remove()
-        component.set_gameobject(None)
-        del self._components[type(component).__name__]
+        self.world.ecs.remove_component(self.id, component_type)
+        del self._components[component_type]
 
     def get_component(self, component_type: Type[_CT]) -> _CT:
-        return cast(_CT, self._components[component_type.__name__])
+        return self._components[component_type]
 
-    def has_component(self, component_type: Type[_CT]) -> bool:
-        return component_type.__name__ in self._components
+    def has_component(self, *component_type: Type[_CT]) -> bool:
+        return all([ct in self._components for ct in component_type])
 
     def try_component(self, component_type: Type[_CT]) -> Optional[_CT]:
-        return cast(Optional[_CT], self._components.get(component_type.__name__))
+        return self._components.get(component_type)
 
-    def get_component_with_name(self, name: str) -> Component:
-        return self._components[name]
+    def archive(self) -> None:
+        """
+        Deactivates the GameObject by removing excess components.
 
-    def has_component_with_name(self, name: str) -> bool:
-        return name in self._components
-
-    def try_component_with_name(self, name: str) -> Optional[Component]:
-        return self._components.get(name)
-
-    def get_components(self) -> List[Component]:
-        return list(self._components.values())
-
-    def start(self) -> None:
-        for component in self._components.values():
-            component.on_start()
-
-    def destroy(self) -> None:
-        """Callback for when this GameObject is destroyed"""
-        for component in self._components.values():
-            component.on_destroy()
+        The GameObject stays in the ECS though.
+        """
+        for component in self.components:
+            component.on_archive()
 
     def to_dict(self) -> Dict[str, Any]:
         return {
-            "id": self._id,
-            "tags": sorted([*self._tags]),
+            "id": self.id,
+            "name": self.name,
             "components": [c.to_dict() for c in self._components.values()],
+            "archetype": self.archetype.name if self.archetype else "",
         }
 
     def __hash__(self) -> int:
         return self._id
 
-    def __repr__(self) -> str:
-        return "GameObject(id={}, tags={}, components={})".format(
-            self.id, self.tags, tuple(self._components.keys())
-        )
-
     @staticmethod
     def generate_id() -> int:
         """Create a new unique int ID"""
@@ -176,16 +148,24 @@ def generate_id() -> int:
 
 
 class Component(ABC):
-    """Component attached to a game object"""
+    """
+    Components are collections of related data attached to GameObjects.
+
+    Attributes
+    ----------
+    _gameobject: Optional[GameObject]
+        Reference to the gameobject this component is attached to
+    """
 
     __slots__ = "_gameobject"
 
-    def __init__(self) -> None:
+    def __init__(self, *args, **kwargs) -> None:
         super().__init__()
         self._gameobject: Optional[GameObject] = None
 
     @property
     def gameobject(self) -> GameObject:
+        """Returns the GameObject this component is attached to"""
         if self._gameobject is None:
             raise TypeError("Component's GameObject is None")
         return self._gameobject
@@ -194,50 +174,102 @@ def set_gameobject(self, gameobject: Optional[GameObject]) -> None:
         """set the gameobject instance for this component"""
         self._gameobject = gameobject
 
-    def to_dict(self) -> Dict[str, Any]:
-        return {"type": self.__class__.__name__}
-
     def on_add(self) -> None:
-        """Callback for when the component is added to a GameObject"""
-        pass
+        """Run when the component is added to the GameObject"""
+        return
 
     def on_remove(self) -> None:
-        """Callback for when the component is removed to a GameObject"""
-        pass
+        """Run when the component is removed from the GameObject"""
+        return
 
-    def on_destroy(self) -> None:
-        """Callback for when the GameObject's on_destroy method is called"""
-        pass
+    def on_archive(self) -> None:
+        """Run when the GameObject this is connected to is archived"""
+        return
 
-    def on_start(self) -> None:
-        """Callback for when the GameObject's start method is called"""
-        pass
+    @classmethod
+    def create(cls, world: World, **kwargs) -> Component:
+        """Create an instance of the component using a reference to the World object and additional parameters"""
+        return cls(**kwargs)
 
+    def to_dict(self) -> Dict[str, Any]:
+        """Serialize the component to a dict"""
+        return {"type": self.__class__.__name__}
 
-class ISystem(Protocol):
-    """Abstract base class for ECS systems"""
 
-    @abstractmethod
-    def __call__(self, world: World, **kwargs) -> None:
-        raise NotImplementedError()
+class ISystem(ABC, esper.Processor):
+    world: World
+
+    def __init__(self):
+        super().__init__()
 
 
 class World:
-    """Collection of GameObjects"""
+    """
+    Manages Gameobjects, Systems, and resources for the simulation
+
+    Attributes
+    ----------
+    _ecs: esper.World
+        Esper ECS instance used for efficiency
+    _gameobjects: Dict[int, GameObject]
+        Mapping of GameObjects to unique identifiers
+    _dead_gameobjects: List[int]
+        List of identifiers for GameObject to remove after
+        the latest time step
+    _resources: Dict[Type, Any]
+        Global resources shared by systems in the ECS
+    """
 
-    __slots__ = "_gameobjects", "_dead_gameobjects", "_systems", "_resources"
+    __slots__ = (
+        "_ecs",
+        "_gameobjects",
+        "_dead_gameobjects",
+        "_resources",
+    )
 
     def __init__(self) -> None:
+        self._ecs: esper.World = esper.World()
         self._gameobjects: Dict[int, GameObject] = {}
         self._dead_gameobjects: List[int] = []
-        self._systems: List[Tuple[int, ISystem]] = []
         self._resources: Dict[str, Any] = {}
 
-    def add_gameobject(self, gameobject: GameObject) -> None:
-        """Add gameobject to the world"""
+    @property
+    def ecs(self) -> esper.World:
+        return self._ecs
+
+    def spawn_gameobject(
+        self, *components: Component, name: Optional[str] = None
+    ) -> GameObject:
+        """Create a new gameobject and attach any given component instances"""
+        entity_id = self._ecs.create_entity(*components)
+        gameobject = GameObject(
+            unique_id=entity_id,
+            components=components,
+            world=self,
+            name=(name if name else f"GameObject({entity_id})"),
+        )
         self._gameobjects[gameobject.id] = gameobject
-        gameobject.set_world(self)
-        gameobject.start()
+        return gameobject
+
+    def spawn_archetype(self, archetype: EntityArchetype) -> GameObject:
+        component_instances: List[Component] = []
+        for component_type, options in archetype.components.items():
+            component_instances.append(component_type.create(self, **options))
+
+        entity_id = self._ecs.create_entity(*component_instances)
+        gameobject = GameObject(
+            unique_id=entity_id,
+            components=component_instances,
+            world=self,
+            name=f"{archetype.name}({entity_id})",
+            archetype=archetype,
+        )
+
+        archetype.increment_instances()
+
+        self._gameobjects[gameobject.id] = gameobject
+
+        return gameobject
 
     def get_gameobject(self, gid: int) -> GameObject:
         """Retrieve the GameObject with the given id"""
@@ -257,82 +289,133 @@ def try_gameobject(self, gid: int) -> Optional[GameObject]:
 
     def delete_gameobject(self, gid: int) -> None:
         """Remove gameobject from world"""
+        self._ecs.delete_entity(gid)
         self._dead_gameobjects.append(gid)
 
     def get_component(self, component_type: Type[_CT]) -> List[Tuple[int, _CT]]:
         """Get all the gameobjects that have a given component type"""
-        components: List[Tuple[int, _CT]] = []
-        for gid, gameobject in self._gameobjects.items():
-            component = gameobject.try_component(component_type)
-            if component is not None:
-                components.append((gid, component))
-        return components
-
-    def get_component_by_name(self, name: str) -> List[Tuple[int, Component]]:
-        components: List[Tuple[int, Component]] = []
-        for gid, gameobject in self._gameobjects.items():
-            component = gameobject.try_component_with_name(name)
-            if component is not None:
-                components.append((gid, component))
-        return components
+        return self._ecs.get_component(component_type)
 
     def get_components(
         self, *component_types: Type[_CT]
-    ) -> List[Tuple[int, Tuple[_CT, ...]]]:
+    ) -> List[Tuple[int, List[_CT, ...]]]:
         """Get all game objects with the given components"""
-        components: List[Tuple[int, Tuple[_CT, ...]]] = []
-        for gid, gameobject in self._gameobjects.items():
-            try:
-                components.append(
-                    (gid, tuple([gameobject.get_component(c) for c in component_types]))
-                )
-            except KeyError:
-                continue
-        return components
+        return self._ecs.get_components(*component_types)
+
+    def try_components(
+        self, entity: int, *component_types: Type[_CT]
+    ) -> Optional[List[List[_CT]]]:
+        """Try to get a multiple component types for a GameObject."""
+        return self._ecs.try_components(entity, *component_types)
 
     def _clear_dead_gameobjects(self) -> None:
         """Delete gameobjects that were removed from the world"""
         for gameobject_id in self._dead_gameobjects:
-            self._gameobjects[gameobject_id].destroy()
-            self._gameobjects[gameobject_id].set_world(None)
+            gameobject = self._gameobjects[gameobject_id]
+            for component in gameobject.components:
+                component.on_remove()
+            if gameobject.archetype:
+                gameobject.archetype.decrement_instances()
+            gameobject.set_world(None)
             del self._gameobjects[gameobject_id]
 
         self._dead_gameobjects.clear()
 
-    def add_system(self, system: ISystem, priority=0) -> None:
+    def add_system(self, system: ISystem, priority: int = 0) -> None:
         """Add a System instance to the World"""
-        self._systems.append((priority, system))
-        self._systems.sort(key=lambda pair: pair[0], reverse=True)
+        self._ecs.add_processor(system, priority=priority)
+        system.world = self
 
-    def remove_system(self, system: ISystem) -> None:
+    def remove_system(self, system: Type[ISystem]) -> None:
         """Remove a System from the World"""
-        for entry in self._systems:
-            _, s = entry
-            if s == system:
-                self._systems.remove(entry)
+        self._ecs.remove_processor(system)
+        system.world = None
 
-    def process(self, **kwargs) -> None:
+    def step(self, **kwargs) -> None:
         """Call the process method on all systems"""
         self._clear_dead_gameobjects()
-        for _, system in self._systems:
-            system(self, **kwargs)
+        self._ecs.process(**kwargs)
 
     def add_resource(self, resource: Any) -> None:
         """Add a global resource to the world"""
-        self._resources[type(resource).__name__] = resource
+        resource_type = type(resource)
+        if resource_type in self._resources:
+            logger.warning(f"Replacing existing resource of type: {resource_type}")
+        self._resources[resource_type] = resource
 
-    def remove_resource(self, resource: Type[_RT]) -> None:
+    def remove_resource(self, resource_type: Any) -> None:
         """remove a global resource to the world"""
-        del self._resources[resource.__name__]
+        del self._resources[resource_type]
 
-    def get_resource(self, resource: Type[_RT]) -> _RT:
+    def get_resource(self, resource_type: Type[_RT]) -> _RT:
         """Add a global resource to the world"""
-        return self._resources[resource.__name__]
+        return self._resources[resource_type]
 
-    def has_resource(self, resource: Type[_RT]) -> bool:
+    def has_resource(self, resource_type: Any) -> bool:
         """Return true if the world has the given resource"""
-        return resource.__name__ in self._resources
+        return resource_type in self._resources
+
+    def __repr__(self) -> str:
+        return "World(gameobjects={}, resources={})".format(
+            len(self._gameobjects),
+            list(self._resources.values()),
+        )
+
+
+class EntityArchetype:
+    """
+    Organizes information for constructing components that compose GameObjects.
+
+    Attributes
+    ----------
+    _name: str
+        (Read-only) The name of the entity archetype
+    _components: Dict[Type[Component], Dict[str, Any]]
+        Dict of components used to construct this archetype
+    """
+
+    __slots__ = "_name", "_components", "_instances"
+
+    def __init__(self, name: str) -> None:
+        self._name: str = name
+        self._components: Dict[Type[Component], Dict[str, Any]] = {}
+        self._instances: int = 0
+
+    @property
+    def name(self) -> str:
+        """Returns the name of this archetype"""
+        return self._name
+
+    @property
+    def components(self) -> Dict[Type[Component], Dict[str, Any]]:
+        """Returns a list of components in this archetype"""
+        return {**self._components}
 
-    def try_resource(self, resource: Type[_RT]) -> Optional[_RT]:
-        """Return the resource if it exists, None otherwise"""
-        return self._resources.get(resource.__name__)
+    @property
+    def instances(self) -> int:
+        return self._instances
+
+    def add(self, component_type: Type[Component], **kwargs: Any) -> EntityArchetype:
+        """
+        Add a component to this archetype
+
+        Parameters
+        ----------
+        component_type: subclass of neighborly.core.ecs.Component
+            The component type to add to the entity archetype
+        **kwargs: Dict[str, Any]
+            Attribute overrides to pass to the component
+        """
+        self._components[component_type] = {**kwargs}
+        return self
+
+    def increment_instances(self) -> None:
+        self._instances += 1
+
+    def decrement_instances(self) -> None:
+        self._instances -= 1
+
+    def __repr__(self) -> str:
+        return "{}(name={}, components={})".format(
+            self.__class__.__name__, self._name, self._components
+        )
diff --git a/src/neighborly/core/engine.py b/src/neighborly/core/engine.py
index 4373a64..bf92d35 100644
--- a/src/neighborly/core/engine.py
+++ b/src/neighborly/core/engine.py
@@ -1,248 +1,58 @@
-from abc import ABC
-from typing import Any, Optional, Protocol, Dict, List
+from __future__ import annotations
 
-from neighborly.core.ecs import Component, GameObject
-from neighborly.core.rng import IRandNumGenerator
-from neighborly.core.utils.tracery import set_grammar_rng
+import random
+from typing import Dict, Optional, Type
 
+import neighborly.core.utils.tracery as tracery
+from neighborly.core.ecs import Component
+from neighborly.core.name_generation import TraceryNameFactory
 
-class AbstractFactory(ABC):
-    """Abstract class for factory instances
 
-    Attributes
-    ----------
-    _type: str
-        The name of the type of component the factory instantiates
-    """
-
-    __slots__ = "_type"
-
-    def __init__(self, name: str) -> None:
-        self._type = name
-
-    def get_type(self) -> str:
-        """Return the name of the type this factory creates"""
-        return self._type
-
-
-class ComponentDefinition:
-    """
-    Collection of Key-Value pairs passed to factories when to
-    instantiating instances of components
-    """
-
-    __slots__ = "_component_type", "_attributes"
-
-    def __init__(
-        self,
-        component_type: str,
-        attributes: Optional[Dict[str, Any]] = None,
-    ) -> None:
-        self._component_type: str = component_type
-        self._attributes: Dict[str, Any] = attributes if attributes else {}
-
-    def get_type(self) -> str:
-        """Returns the name of the type of component this spec is for"""
-        return self._component_type
-
-    def get_attributes(self) -> Dict[str, Any]:
-        """Get all the attributes associated with this definition"""
-        return self._attributes
-
-    def get_attribute(self, key: str, default: Any = None) -> Any:
-        """Get a specific attribute associated with this definition"""
-        return self._attributes.get(key, default)
-
-    def update(self, new_attributes: Dict[str, Any]) -> None:
-        """Update the attributes using a given Dict of values"""
-        self._attributes.update(new_attributes)
-
-    def __getitem__(self, key: str) -> Any:
-        """Set an attribute using a str key"""
-        return self._attributes[key]
-
-    def __setitem__(self, key: str, value: Any) -> None:
-        """Set an attribute using a str key"""
-        self._attributes[key] = value
-
-
-class EntityArchetypeDefinition:
-    """Collection of Component specs used to instantiate instances of entities"""
-
-    __slots__ = (
-        "_name",
-        "_components",
-        "_attributes",
-        "_is_template",
-    )
-
-    def __init__(
-        self,
-        name: str,
-        components: Optional[Dict[str, ComponentDefinition]] = None,
-        is_template: bool = False,
-    ) -> None:
-        self._name: str = name
-        self._is_template: bool = is_template
-        self._components: Dict[str, ComponentDefinition] = (
-            components if components else {}
-        )
-
-    def is_template(self) -> bool:
-        return self._is_template
-
-    def get_components(self) -> Dict[str, ComponentDefinition]:
-        """Return the ComponentSpecs that make up this archetype"""
-        return self._components
-
-    def try_component(self, name: str) -> Optional[ComponentDefinition]:
-        """Try to get a component definition by name"""
-        return self._components.get(name, None)
-
-    def get_name(self) -> str:
-        """Get the type of archetype this is"""
-        return self._name
-
-    def get_attributes(self) -> Dict[str, Any]:
-        """Get the type of archetype this is"""
-        return self._attributes
+class ArchetypeNotFoundError(Exception):
+    """Error thrown when an archetype is not found in the engine"""
 
-    def add_component(self, node: ComponentDefinition) -> None:
-        """Add (or overwrites) a component spec attached to this archetype"""
-        self._components[node.get_type()] = node
+    def __init__(self, archetype_name: str) -> None:
+        super().__init__()
+        self.archetype_name: str = archetype_name
+        self.message: str = f"Could not find archetype with name '{archetype_name}'"
 
-    def has_component(self, *components: str) -> bool:
-        """Return True if this archetype has the given components"""
-        return all([c in self._components for c in components])
-
-    def __getitem__(self, attribute: str) -> Any:
-        """Get a value from this archetype's attributes"""
-        return self._attributes[attribute]
-
-
-class IComponentFactory(Protocol):
-    """Interface for Factory classes that are used to construct entity components"""
-
-    def get_type(self) -> str:
-        """Return the name of the type this factory creates"""
-        raise NotImplementedError()
-
-    def create(self, spec: ComponentDefinition, **kwargs) -> Component:
-        """Create component instance"""
-        raise NotImplementedError()
+    def __str__(self) -> str:
+        return self.message
 
 
 class NeighborlyEngine:
-    """Manages all the factories for creating entities and archetypes
+    """
+    An engine stores and instantiates entity archetypes for characters, businesses,
+    residences, and other places.
 
-    Attributes
-    ----------
-    _component_factories: Dict[str, AbstractFactory[Any]]
-        Map of component class names to factories that produce them
-    _character_archetypes: Dict[str, Dict[str, Any]]
-        Map of archetype names to their specification data
-    _place_archetypes: Dict[str, Dict[str, Any]]
-        Map of archetype names to their specification data
+    There should only be one NeighborlyEngine instance per simulation. It is designed
+    to handle the internal logic of determining when to create certain entities. For
+    example, the engine creates characters entities using spawn multipliers associated
+    with each archetype.
     """
 
     __slots__ = (
-        "_component_factories",
-        "_character_archetypes",
-        "_place_archetypes",
-        "_business_archetypes",
-        "_residence_archetypes",
         "_rng",
+        "_name_generator",
+        "_component_types",
     )
 
-    def __init__(self, rng: IRandNumGenerator) -> None:
-        self._component_factories: Dict[str, IComponentFactory] = {}
-        self._character_archetypes: Dict[str, EntityArchetypeDefinition] = {}
-        self._business_archetypes: Dict[str, EntityArchetypeDefinition] = {}
-        self._residence_archetypes: Dict[str, EntityArchetypeDefinition] = {}
-        self._place_archetypes: Dict[str, EntityArchetypeDefinition] = {}
-        self._rng: IRandNumGenerator = rng
-        set_grammar_rng(self._rng)
+    def __init__(self, seed: Optional[int] = None) -> None:
+        self._rng: random.Random = random.Random(seed)
+        self._name_generator: TraceryNameFactory = TraceryNameFactory()
+        tracery.set_grammar_rng(self._rng)
+        self._component_types: Dict[str, Type[Component]] = {}
 
-    def get_rng(self) -> IRandNumGenerator:
+    @property
+    def rng(self) -> random.Random:
         return self._rng
 
-    def add_character_archetype(self, archetype: EntityArchetypeDefinition) -> None:
-        self._character_archetypes[archetype.get_name()] = archetype
-
-    def get_character_archetype(self, archetype_name: str) -> EntityArchetypeDefinition:
-        return self._character_archetypes[archetype_name]
-
-    def get_character_archetypes(self) -> List[EntityArchetypeDefinition]:
-        return list(self._character_archetypes.values())
-
-    def add_place_archetype(self, archetype: EntityArchetypeDefinition) -> None:
-        self._place_archetypes[archetype.get_name()] = archetype
-
-    def add_residence_archetype(self, archetype: EntityArchetypeDefinition) -> None:
-        self._residence_archetypes[archetype.get_name()] = archetype
-
-    def add_business_archetype(self, archetype: EntityArchetypeDefinition) -> None:
-        self._business_archetypes[archetype.get_name()] = archetype
-
-    def get_place_archetype(self, archetype_name: str) -> EntityArchetypeDefinition:
-        return self._place_archetypes[archetype_name]
-
-    def get_business_archetype(self, archetype_name: str) -> EntityArchetypeDefinition:
-        return self._business_archetypes[archetype_name]
-
-    def get_residence_archetype(self, archetype_name: str) -> EntityArchetypeDefinition:
-        return self._residence_archetypes[archetype_name]
-
-    def get_residence_archetypes(self) -> List[EntityArchetypeDefinition]:
-        return list(self._residence_archetypes.values())
-
-    def filter_place_archetypes(self, options: Dict[str, Any]) -> List[str]:
-        """Retrieve a set of place archetypes based on given options"""
-        results: List[str] = []
-
-        include: List[str] = options.get("include", [])
-        exclude: List[str] = options.get("exclude", [])
-
-        for _, spec in self._place_archetypes.items():
-            if spec.has_component(*include) and not spec.has_component(*exclude):
-                results.append(spec.get_name())
-
-        return results
-
-    def add_component_factory(self, factory: IComponentFactory) -> None:
-        self._component_factories[factory.get_type()] = factory
-
-    def get_component_factory(self, type_name: str) -> IComponentFactory:
-        return self._component_factories[type_name]
-
-    def create_character(self, archetype_name: str, **kwargs) -> GameObject:
-        archetype = self._character_archetypes[archetype_name]
-        return self.create_entity(archetype, **kwargs)
-
-    def create_place(self, archetype_name: str, **kwargs) -> GameObject:
-        archetype = self._place_archetypes[archetype_name]
-        return self.create_entity(archetype, **kwargs)
-
-    def create_business(self, archetype_name: str, **kwargs) -> GameObject:
-        archetype = self._business_archetypes[archetype_name]
-        return self.create_entity(archetype, **kwargs)
-
-    def create_residence(self, archetype_name: str, **kwargs) -> GameObject:
-        archetype = self._residence_archetypes[archetype_name]
-        return self.create_entity(archetype, **kwargs)
-
-    def create_entity(
-        self, archetype: EntityArchetypeDefinition, **kwargs
-    ) -> GameObject:
-        """Create a new GameObject and attach the components in the spec"""
-        components: List[Component] = []
-
-        for name, spec in archetype.get_components().items():
-            factory = self.get_component_factory(name)
-            components.append(factory.create(spec, **{**kwargs, "rng": self.get_rng()}))
+    @property
+    def name_generator(self) -> TraceryNameFactory:
+        return self._name_generator
 
-        gameobject = GameObject(
-            archetype_name=archetype.get_name(), components=components
-        )
+    def get_component(self, component_name: str) -> Type[Component]:
+        return self._component_types[component_name]
 
-        return gameobject
+    def add_component(self, component_type: Type[Component]) -> None:
+        self._component_types[component_type.__name__] = component_type
diff --git a/src/neighborly/core/helpers.py b/src/neighborly/core/helpers.py
deleted file mode 100644
index b01756f..0000000
--- a/src/neighborly/core/helpers.py
+++ /dev/null
@@ -1,187 +0,0 @@
-from typing import Any, Dict, cast, List, Tuple, Optional
-
-from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import GameObject, World
-from neighborly.core.business import Business
-from neighborly.core.location import Location
-from neighborly.core.relationship import Relationship, RelationshipTag
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.tags import Tag
-from neighborly.core.engine import NeighborlyEngine
-
-
-def hire_character_at_business(character: GameObject, **kwargs) -> None:
-    """Hire a character at a given business"""
-    ...
-
-
-def add_employee(business: GameObject, **kwargs) -> None:
-    """Add an employee to the given business"""
-    ...
-
-
-def remove_employee(business: GameObject, **kwargs) -> None:
-    """Add an employee to the given business"""
-    ...
-
-
-def add_coworkers(character: GameObject, **kwargs) -> None:
-    """Add coworker tags to current coworkers in relationship network"""
-    business: GameObject = kwargs["business"]
-    business_comp = business.get_component(Business)
-    world: World = kwargs["world"]
-
-    relationship_net = world.get_resource(RelationshipNetwork)
-
-    for employee_id in business_comp.get_employees():
-        if employee_id == character.id:
-            continue
-
-        if not relationship_net.has_connection(character.id, employee_id):
-            relationship_net.add_connection(
-                character.id, employee_id, Relationship(character.id, employee_id)
-            )
-
-        relationship_net.get_connection(character.id, employee_id).add_tags(
-            RelationshipTag.Coworker
-        )
-
-
-def remove_coworkers(character: GameObject, **kwargs) -> None:
-    """Remove coworker tags from current coworkers in relationship network"""
-    business: GameObject = kwargs["business"]
-    business_comp = business.get_component(Business)
-    world: World = kwargs["world"]
-
-    relationship_net = world.get_resource(RelationshipNetwork)
-
-    for employee_id in business_comp.get_employees():
-        if employee_id == character.id:
-            continue
-
-        if relationship_net.has_connection(character.id, employee_id):
-            relationship_net.get_connection(character.id, employee_id).remove_tags(
-                RelationshipTag.Coworker
-            )
-
-
-def add_tag(character: GameCharacter, tag: Tag) -> None:
-    if tag.name in character.tags:
-        raise RuntimeError("Cannot add duplicate tags")
-    character.tags[tag.name] = tag
-
-
-def remove_tag(character: GameCharacter, tag: Tag) -> None:
-    if tag.name in character.tags:
-        raise RuntimeError("Cannot add duplicate tags")
-    del character.tags[tag.name]
-
-
-def move_character(world: World, character_id: int, location_id: int) -> None:
-    destination: Location = world.get_gameobject(location_id).get_component(Location)
-    character: GameCharacter = world.get_gameobject(character_id).get_component(
-        GameCharacter
-    )
-
-    if location_id == character.location:
-        return
-
-    if character.location is not None:
-        current_location: Location = world.get_gameobject(
-            character.location
-        ).get_component(Location)
-        current_location.remove_character(character_id)
-
-    destination.add_character(character_id)
-    character.location = location_id
-
-
-def get_locations(world: World) -> List[Tuple[int, Location]]:
-    return sorted(
-        cast(List[Tuple[int, Location]], world.get_component(Location)),
-        key=lambda pair: pair[0],
-    )
-
-
-def add_relationship_tag(
-    world: World, owner_id: int, target_id: int, tag: RelationshipTag
-) -> None:
-    """Add a relationship modifier on the subject's relationship to the target"""
-    world.get_resource(RelationshipNetwork).get_connection(
-        owner_id, target_id
-    ).add_tags(tag)
-
-
-def try_generate_family(
-    engine: NeighborlyEngine, gameobject: GameObject
-) -> Dict[str, Any]:
-    """For the given character, try to generate a spouse and children"""
-
-    # If the character does not have an archetype, throw an error
-    if gameobject.archetype_name is None:
-        raise TypeError("Character's GameObject does not have an archetype name")
-
-    result = {"spouse": None, "children": []}
-
-    character = gameobject.get_component(GameCharacter)
-
-    has_spouse = (
-        engine.get_rng().random()
-        < character.character_def.generation.family.probability_spouse
-    )
-
-    spouse: Optional[GameObject] = None
-
-    if has_spouse:
-        spouse = engine.create_character(
-            gameobject.archetype_name,
-            last_name=character.name.surname,
-            age_range="young_adult",
-        )
-        result["spouse"] = spouse
-
-    has_children = (
-        engine.get_rng().random()
-        < character.character_def.generation.family.probability_children
-    )
-
-    if has_children:
-        result["children"] = create_children(
-            character,
-            engine,
-            gameobject.archetype_name,
-            spouse.get_component(GameCharacter) if spouse else None,
-        )
-
-    return result
-
-
-def create_children(
-    character: GameCharacter,
-    engine: NeighborlyEngine,
-    archetype_name: str,
-    spouse: Optional[GameCharacter] = None,
-) -> List[GameObject]:
-
-    n_children = engine.get_rng().randint(
-        character.character_def.generation.family.num_children[0],
-        character.character_def.generation.family.num_children[1],
-    )
-
-    spouse_age = spouse.age if spouse else 999
-    min_parent_age = min(spouse_age, character.age)
-    child_age_max = (
-        min_parent_age - character.character_def.lifecycle.life_stages["young_adult"]
-    )
-
-    children = []
-
-    for _ in range(n_children):
-        child = engine.create_character(
-            archetype_name,
-            age_range=(0, child_age_max),
-            last_name=character.name.surname,
-        )
-        children.append(child)
-
-    return children
diff --git a/src/neighborly/core/life_event.py b/src/neighborly/core/life_event.py
index 6c7253f..d51e03c 100644
--- a/src/neighborly/core/life_event.py
+++ b/src/neighborly/core/life_event.py
@@ -1,382 +1,384 @@
 from __future__ import annotations
-import logging
 
-from collections import defaultdict
-from dataclasses import dataclass
-from abc import ABC, abstractclassmethod, abstractmethod
-from typing import (
-    Any,
-    Callable,
-    Generator,
-    Dict,
-    Tuple,
-    List,
-    Generic,
-    Iterable,
-    Optional,
-    Protocol,
-    TypeVar,
-)
+from abc import ABC, abstractmethod
+from typing import Any, Callable, Dict, List, Optional, Protocol, Type
 
-from neighborly.core.ecs import GameObject, World
+from neighborly.core.ecs import Component, GameObject, ISystem, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.time import SimDateTime, TimeDelta
 
-logger = logging.getLogger(__name__)
 
-_T = TypeVar("_T")
-
-
-class IProbabilityFn(Protocol):
-    """Callable that produces a probability [0.0, 1.0] of a event firing"""
-
-    def __call__(self, gameobject: GameObject, **kwargs) -> float:
-        raise NotImplementedError()
-
-
-class ILifeEventCallback(Protocol):
-    def __call__(self, gameobject: GameObject, event: LifeEvent) -> bool:
-        raise NotImplementedError
-
-
-@dataclass
-class _EventCallbackDbEntry(Generic[_T]):
+class LifeEvent:
     """
-    A single entry within the event callback database
+    LifeEvents contain information about occurrences that
+    happened in the story world.
 
     Attributes
     ----------
-    fn: _T
-        Either a Precondition, Effect, or Probability function
-    description: str
-        Short description of what the function does
+    name: str
+        Name of the event
+    timestamp: str
+        Timestamp for when the event occurred
+    roles: List[EventRole]
+        Roles that were involved in this event
     """
 
-    fn: _T
-    description: str
+    __slots__ = "timestamp", "name", "roles", "metadata"
 
+    def __init__(
+        self, name: str, timestamp: str, roles: List[EventRole], **kwargs
+    ) -> None:
+        self.name: str = name
+        self.timestamp: str = timestamp
+        self.roles: List[EventRole] = [*roles]
+        self.metadata: Dict[str, Any] = {**kwargs}
 
-class EventCallbackDatabase:
-    """Manages the various callback functions available when constructing events"""
+    def get_type(self) -> str:
+        """Return the type of this event"""
+        return self.name
 
-    _precondition_db: Dict[str, _EventCallbackDbEntry[ILifeEventCallback]] = {}
-    _effect_db: Dict[str, _EventCallbackDbEntry[ILifeEventCallback]] = {}
-    _probability_db: Dict[str, _EventCallbackDbEntry[IProbabilityFn]] = {}
+    def to_dict(self) -> Dict[str, Any]:
+        """Serialize this LifeEvent to a dictionary"""
+        return {
+            "name": self.name,
+            "timestamp": self.timestamp,
+            "roles": {role.name: role.gid for role in self.roles},
+        }
+
+    def __getitem__(self, role_name: str) -> int:
+        for role in self.roles:
+            if role.name == role_name:
+                return role.gid
+        raise KeyError(role_name)
 
-    @classmethod
-    def register_precondition(
-        cls, name: str, fn: ILifeEventCallback, description: Optional[str] = None
-    ) -> None:
-        """Adds function to the global registry of event preconditions"""
-        cls._precondition_db[name] = _EventCallbackDbEntry(
-            fn, description if description else ""
-        )
+    def __repr__(self) -> str:
+        return f"LifeEvent(name={self.name}, timestamp={self.timestamp}, roles=[{self.roles}], metadata={self.metadata})"
 
-    @classmethod
-    def register_effect(
-        cls, name: str, fn: ILifeEventCallback, description: Optional[str] = None
-    ) -> None:
-        """Adds a function to the global registry of event effects"""
-        cls._effect_db[name] = _EventCallbackDbEntry(
-            fn, description if description else ""
-        )
+    def __str__(self) -> str:
+        return f"{self.name} [at {self.timestamp}] : {', '.join(map(lambda r: f'{r.name}:{r.gid}', self.roles))}"
 
-    @classmethod
-    def register_probability(
-        cls, name: str, fn: IProbabilityFn, description: Optional[str] = None
-    ) -> None:
-        """Adds a function to the global registry of event probability functions"""
-        cls._probability_db[name] = _EventCallbackDbEntry(
-            fn, description if description else ""
-        )
 
-    @classmethod
-    def get_precondition(cls, name: str) -> ILifeEventCallback:
-        try:
-            return cls._precondition_db[name].fn
-        except KeyError:
-            raise EventPreconditionNotFoundError(name)
+class EventRole:
+    """
+    EventRole is a role that has a GameObject bound to it.
+    It does not contain any information about filtering for
+    the role.
 
-    @classmethod
-    def get_effect(cls, name: str) -> ILifeEventCallback:
-        try:
-            return cls._effect_db[name].fn
-        except KeyError:
-            raise EventEffectNotFoundError(name)
+    Attributes
+    ----------
+    name: str
+        Name of the role
+    gid: int
+        Unique identifier for the GameObject bound to this role
+    """
 
-    @classmethod
-    def get_probability(cls, name: str) -> IProbabilityFn:
-        try:
-            return cls._probability_db[name].fn
-        except KeyError:
-            raise EventProbabilityNotFoundError(name)
+    __slots__ = "name", "gid"
 
+    def __init__(self, name: str, gid: int) -> None:
+        self.name: str = name
+        self.gid: int = gid
 
-def event_precondition(name: str, description: Optional[str] = None):
-    """Decorator registers a precondition function"""
 
-    def wrapper(fn: ILifeEventCallback):
-        EventCallbackDatabase.register_precondition(name, fn, description)
-        return fn
+class RoleBinderFn(Protocol):
+    """Callable that returns a GameObject that meets requirements for a given Role"""
 
-    return wrapper
+    def __call__(self, world: World, event: LifeEvent) -> Optional[GameObject]:
+        raise NotImplementedError
 
 
-def event_effect(name: str, description: Optional[str] = None):
-    """Decorator that registers an effect function"""
+class RoleFilterFn(Protocol):
+    """Function that filters GameObjects for an EventRole"""
 
-    def wrapper(fn: ILifeEventCallback):
-        EventCallbackDatabase.register_effect(name, fn, description)
-        return fn
+    def __call__(self, world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        raise NotImplementedError
 
-    return wrapper
 
+def join_filters(*filters: RoleFilterFn) -> RoleFilterFn:
+    """Joins a bunch of filters into one function"""
 
-def event_probability(name: str, description: Optional[str] = None):
-    """Decorator that registers an probability function"""
+    def fn(world: World, event: LifeEvent, gameobject: GameObject) -> bool:
+        return all([f(world, event, gameobject) for f in filters])
 
-    def wrapper(fn: IProbabilityFn):
-        EventCallbackDatabase.register_probability(name, fn, description)
-        return fn
+    return fn
 
-    return wrapper
 
+class AbstractEventRoleType(ABC):
+    """
+    Abstract base class for defining roles that
+    GameObjects can be bound to when executing
+    LifeEvents
+    """
 
-class EventPreconditionNotFoundError(Exception):
-    """Event Raised when a precondition's name is not found in the library"""
+    __slots__ = "name"
 
     def __init__(self, name: str) -> None:
-        super().__init__(f"Precondition with name '{name}' not found")
-        self.message: str = f"Precondition with name '{name}' not found"
+        self.name: str = name
 
-    def __str__(self) -> str:
-        return self.message
+    @abstractmethod
+    def fill_role(self, world: World, event: LifeEvent) -> Optional[EventRole]:
+        """Find a GameObject to bind to this role given the event"""
+        raise NotImplementedError
 
-    def __repr__(self) -> str:
-        return self.message
+    @abstractmethod
+    def fill_role_with(
+        self, world: World, event: LifeEvent, candidate: GameObject
+    ) -> Optional[EventRole]:
+        """Attempt to bind the candidate GameObject to this role given the event"""
+        raise NotImplementedError
 
 
-class EventEffectNotFoundError(Exception):
-    """Event Raised when a post-effect's name is not found in the library"""
+class EventRoleType(AbstractEventRoleType):
+    """
+    Information about a role within a LifeEvent, and logic
+    for how to filter which gameobjects can be bound to the
+    role.
+    """
 
-    def __init__(self, name: str) -> None:
-        super().__init__(f"Post-Effect with name '{name}' not found")
-        self.message: str = f"Post-Effect with name '{name}' not found"
+    __slots__ = "binder_fn", "components", "filter_fn"
 
-    def __str__(self) -> str:
-        return self.message
+    def __init__(
+        self,
+        name: str,
+        components: List[Type[Component]] = None,
+        filter_fn: Optional[RoleFilterFn] = None,
+        binder_fn: Optional[RoleBinderFn] = None,
+    ) -> None:
+        super().__init__(name)
+        self.components: List[Type[Component]] = components if components else []
+        self.filter_fn: RoleFilterFn = filter_fn if filter_fn else lambda w, e, g: True
+        self.binder_fn: Optional[RoleBinderFn] = binder_fn
 
-    def __repr__(self) -> str:
-        return self.message
+    def fill_role(
+        self,
+        world: World,
+        event: LifeEvent,
+    ) -> Optional[EventRole]:
+
+        if self.binder_fn is not None:
+            obj = self.binder_fn(world, event)
+            return EventRole(self.name, obj.id) if obj is not None else None
+
+        candidate_list: List[int] = list(
+            map(
+                lambda entry: entry[0],
+                filter(
+                    lambda res: self.filter_fn(
+                        world, event, world.get_gameobject(res[0])
+                    ),
+                    world.get_components(*self.components),
+                ),
+            )
+        )
 
+        if any(candidate_list):
+            chosen_candidate = world.get_resource(NeighborlyEngine).rng.choice(
+                candidate_list
+            )
+            return EventRole(self.name, chosen_candidate)
 
-class EventProbabilityNotFoundError(Exception):
-    """Event Raised when a probability function is not found in the library"""
+        return None
 
-    def __init__(self, name: str) -> None:
-        super().__init__(f"Probability function with name '{name}' not found")
-        self.message: str = f"Probability function with name '{name}' not found"
+    def fill_role_with(
+        self,
+        world: World,
+        event: LifeEvent,
+        candidate: GameObject,
+    ) -> Optional[EventRole]:
+        if candidate.has_component(*self.components) and self.filter_fn(
+            world, event, candidate
+        ):
+            return EventRole(self.name, candidate.id)
 
-    def __str__(self) -> str:
-        return self.message
+        return None
 
-    def __repr__(self) -> str:
-        return self.message
 
+class LifeEventEffectFn(Protocol):
+    """Callback function called when a life event is executed"""
 
-class LifeEvent(ABC):
-    """
-    Events are things that happen in the story world that GameObjects
-    can react to.
+    def __call__(self, world: World, event: LifeEvent) -> None:
+        raise NotImplementedError
 
 
-    Events allow neighborly to decouple the simulation logic from any
-    GameObject-specific logic that should run as a response. Events are
-    blocking. They are handled immediately by event handlers when available.
+class AbstractLifeEventType(ABC):
+    """
+    Abstract base class for defining LifeEventTypes
     """
 
-    __slots__ = "timestamp"
-
-    def __init__(self, timestamp: str) -> None:
-        super().__init__()
-        self.timestamp: str = timestamp
-
-    @abstractclassmethod
-    def get_type(cls) -> str:
-        """Return the type of this event"""
-        raise NotImplementedError()
-
-    def to_dict(self) -> Dict[str, Any]:
-        """Serialize this LifeEvent to a dictionary"""
-        return {"event_type": self.get_type(), "timestamp": self.timestamp}
+    __slots__ = "name", "probability", "roles"
 
+    def __init__(
+        self, name: str, roles: List[EventRoleType], frequency: float = 1.0
+    ) -> None:
+        self.name: str = name
+        self.roles: List[EventRoleType] = roles
+        self.probability: float = frequency
 
-class IPatternFn(Protocol):
-    @abstractmethod
-    def __call__(
-        self, world: World, **kwargs
-    ) -> Generator[Tuple[LifeEvent, Tuple[GameObject, ...]], None, None]:
-        raise NotImplementedError()
+    def instantiate(self, world: World, **kwargs: GameObject) -> Optional[LifeEvent]:
+        """
+        Attempts to create a new LifeEvent instance
 
+        **Do Not Override this method unless absolutely necessary**
+        """
+        life_event = LifeEvent(
+            self.name, world.get_resource(SimDateTime).to_iso_str(), []
+        )
 
-class LifeEventRule:
+        for role_type in self.roles:
+            bound_object = kwargs.get(role_type.name)
+            if bound_object is not None:
+                temp = role_type.fill_role_with(
+                    world, life_event, candidate=bound_object
+                )
+                if temp is not None:
+                    life_event.roles.append(temp)
+                else:
+                    # Return none if the role candidate is not a fit
+                    return None
+            else:
+                temp = role_type.fill_role(world, life_event)
+                if temp is not None:
+                    life_event.roles.append(temp)
+                else:
+                    # Return None if there are no available entities to fill
+                    # the current role
+                    return None
+
+        return life_event
+
+    def execute(self, world: World, event: LifeEvent) -> None:
+        return
+
+
+class LifeEventType(AbstractLifeEventType):
     """
-    An event that happens to a gameobject in the world. This may be used to represent
-    life events for characters, social interactions, business events, etc.
+    User-facing class for implementing behaviors around life events
+
+    This is adapted from:
+    https://github.com/ianhorswill/CitySimulator/blob/master/Assets/Codes/Action/Actions/ActionType.cs
 
     Attributes
     ----------
     name: str
-        Name of the event used when checking event handlers
-    description: str
-        A short description of what this event is
-    pattern_fn: IPatternFn
-        A precondition function that needs to return True for the event to fire
-    effect: LifeEventCallback
-        An effect function that runs if the event fires
-    probability: float
-        A functions that returns the probability of the event firing
+        Name of the LifeEventType and the LifeEvent it instantiates
+    roles: List[EventRoleType]
+        The roles that need to be cast for this event to be executed
+    probability: int (default: 1)
+        The relative frequency of this event compared to other events
+    execute_fn: Callable[..., None]
+        Function that executes changes to the world state base don the event
     """
 
-    __slots__ = (
-        "name",
-        "pattern_fn",
-        "probability",
-        "effect_fn",
-        "description",
-    )
+    __slots__ = "execute_fn"
 
     def __init__(
         self,
         name: str,
-        pattern_fn: IPatternFn,
-        effect_fn: Optional[Callable[[World], None]] = None,
+        roles: List[EventRoleType],
         probability: float = 1.0,
-        description: str = "",
+        execute_fn: Optional[LifeEventEffectFn] = None,
     ) -> None:
-        self.name: str = name
-        self.pattern_fn: IPatternFn = pattern_fn
-        self.effect_fn: Optional[Callable[[World], None]] = effect_fn
-        self.probability: float = probability
-        self.description: str = description
-
-
-class LifeEventHandler:
-    """
-    Collection of precondition and effect callback associated with a specific event type
-
-    Attributes
-    ----------
-    preconditions: list[LifeEventCallback]
-        precondition callback functions
-    effects: list[LifeEventCallback]
-        effect callback functions
-    """
-
-    __slots__ = "preconditions", "effects"
-
-    def __init__(self) -> None:
-        self.preconditions: list[ILifeEventCallback] = []
-        self.effects: list[ILifeEventCallback] = []
-
-    def check_preconditions(self, gameobject: GameObject, event: LifeEvent) -> bool:
-        """Return True if all the preconditions for an event pass"""
-        if self.preconditions:
-            return all([fn(gameobject, event) for fn in self.preconditions])
-        return True
+        super().__init__(name, roles, probability)
+        self.execute_fn: Optional[LifeEventEffectFn] = execute_fn
 
-    def handle_event(self, gameobject: GameObject, event: LifeEvent) -> None:
-        """Run the effect callbacks associated with this event"""
-        for fn in self.effects:
-            fn(gameobject, event)
+    def execute(self, world: World, event: LifeEvent) -> None:
+        self.execute_fn(world, event)
 
-    def add_precondition(self, fn: ILifeEventCallback) -> None:
-        """Add one or more preconditions to the given event"""
-        self.preconditions.append(fn)
 
-    def add_effect(self, fn: ILifeEventCallback) -> None:
-        """Add one or more effects to the given event"""
-        self.effects.append(fn)
+class EventRoles:
+    _registry: Dict[str, EventRoleType] = {}
 
+    @classmethod
+    def register(cls, name: str, event_role_type: EventRoleType) -> None:
+        """Register a new LifeEventType mapped to a name"""
+        cls._registry[name] = event_role_type
 
-class LifeEventLogger:
-    """Records a collection of life events associated with GameObjects"""
-
-    __slots__ = "_events", "_game_objects", "_next_event_id", "_subscribers"
+    @classmethod
+    def roles(cls) -> List[EventRoleType]:
+        return list(cls._registry.values())
 
-    def __init__(self) -> None:
-        self._next_event_id: int = 0
-        self._events: list[LifeEvent] = []
-        self._game_objects: defaultdict[int, list[int]] = defaultdict(lambda: list())
-        self._subscribers: Dict[int, List[Callable[[int, LifeEvent], None]]] = {}
+    @classmethod
+    def get(cls, name: str) -> EventRoleType:
+        """Get a LifeEventType using a name"""
+        return cls._registry[name]
 
-    def log_event(self, event: LifeEvent, gameobject_ids: Iterable[int]) -> None:
-        """Add an event to the record of all events and associate it with character"""
-        self._events.append(event)
 
-        for gid in gameobject_ids:
-            self._game_objects[gid].append(self._next_event_id)
-            if gid in self._subscribers:
-                map(lambda f: f(gid, event), self._subscribers[gid])
+class LifeEvents:
+    """
+    Static class used to store instances of LifeEventTypes for
+    use at runtime.
+    """
 
-        logger.debug(str(event))
+    _registry: Dict[str, LifeEventType] = {}
 
-        self._next_event_id += 1
+    @classmethod
+    def register(cls, life_event_type: LifeEventType, name: str = None) -> None:
+        """Register a new LifeEventType mapped to a name"""
+        cls._registry[name if name else life_event_type.name] = life_event_type
 
-    def get_all_events(self) -> List[LifeEvent]:
-        """Get all recorded events"""
-        return self._events
+    @classmethod
+    def events(cls) -> List[LifeEventType]:
+        return list(cls._registry.values())
 
-    def get_events_for(self, gid: int) -> List[LifeEvent]:
-        """Get all the life events for a game object"""
-        return [self._events[i] for i in self._game_objects[gid]]
+    @classmethod
+    def get(cls, name: str) -> LifeEventType:
+        """Get a LifeEventType using a name"""
+        return cls._registry[name]
 
-    def subscribe_to_gameobject(
-        self, gid: int, fn: Callable[[int, LifeEvent], None]
-    ) -> None:
-        if gid not in self._subscribers:
-            self._subscribers[gid] = []
-        self._subscribers[gid].append(fn)
 
+class LifeEventLog:
+    """
+    Global resource for storing and accessing LifeEvents
+    """
 
-class ILifeEventListener(ABC):
-    """Abstract interface that components inherit from when they want to listen for events"""
+    __slots__ = "event_history", "_subscribers"
 
-    @abstractmethod
-    def check_preconditions(self, event: LifeEvent) -> bool:
-        """
-        Check the preconditions for this event type to see if they pass
+    def __init__(self) -> None:
+        self.event_history: List[LifeEvent] = []
+        self._subscribers: List[Callable[[LifeEvent], None]] = []
 
-        Returns
-        -------
-        bool
-            True if the event passes all the preconditions
-        """
-        raise NotImplementedError()
+    def record_event(self, event: LifeEvent) -> None:
+        self.event_history.append(event)
+        for cb in self._subscribers:
+            cb(event)
 
-    @abstractmethod
-    def handle_event(self, event: LifeEvent) -> bool:
-        """
-        Perform logic when an event occurs
+    def subscribe(self, cb: Callable[[LifeEvent], None]) -> None:
+        self._subscribers.append(cb)
 
-        Returns
-        -------
-        bool
-            True if the event was handled successfully
-        """
-        raise NotImplementedError()
+    def unsubscribe(self, cb: Callable[[LifeEvent], None]) -> None:
+        self._subscribers.remove(cb)
 
 
-def check_gameobject_preconditions(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Return True if the given gameobject accepts the event"""
-    for c in gameobject.get_components():
-        if isinstance(c, ILifeEventListener):
-            if not c.check_preconditions(event):
-                return False
-    return True
+class LifeEventSimulator(ISystem):
+    """
+    LifeEventSimulator handles firing LifeEvents for characters
+    and performing character behaviors
+    """
 
+    __slots__ = "interval", "next_trigger"
 
-def handle_gameobject_effects(gameobject: GameObject, event: LifeEvent) -> bool:
-    """Return True if the given gameobject accepts the event"""
-    for c in gameobject.get_components():
-        if isinstance(c, ILifeEventListener):
-            c.handle_event(event)
-    return True
+    def __init__(self, interval: TimeDelta = None) -> None:
+        super().__init__()
+        self.interval: TimeDelta = interval if interval else TimeDelta()
+        self.next_trigger: SimDateTime = SimDateTime()
+
+    def try_execute_event(self, world: World, event_type: LifeEventType) -> None:
+        """Execute the given LifeEventType if successfully instantiated"""
+        event: LifeEvent = event_type.instantiate(world)
+        if event is not None:
+            if event_type.execute_fn is not None:
+                event_type.execute_fn(world, event)
+            world.get_resource(LifeEventLog).record_event(event)
+
+    def process(self, *args, **kwargs) -> None:
+        """Simulate LifeEvents for characters"""
+        date = self.world.get_resource(SimDateTime)
+
+        if date < self.next_trigger:
+            return
+        else:
+            self.next_trigger = date.copy() + self.interval
+
+        rng = self.world.get_resource(NeighborlyEngine).rng
+        for event_type in LifeEvents.events():
+            if rng.random() < event_type.probability:
+                self.try_execute_event(self.world, event_type)
diff --git a/src/neighborly/core/location.py b/src/neighborly/core/location.py
index 6c4bb9b..b6e35f1 100644
--- a/src/neighborly/core/location.py
+++ b/src/neighborly/core/location.py
@@ -1,28 +1,55 @@
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional, Set
 
 from ordered_set import OrderedSet
 
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
+from neighborly.core.ecs import Component, World
 
 
 class Location(Component):
     """Anywhere where game characters may be"""
 
-    __slots__ = "characters_present", "max_capacity"
+    __slots__ = (
+        "characters_present",
+        "max_capacity",
+        "name",
+        "whitelist",
+        "is_private",
+        "activity_flags",
+    )
 
-    def __init__(self, max_capacity: int) -> None:
+    def __init__(
+        self,
+        max_capacity: int,
+        name: str = "",
+        whitelist: Optional[List[int]] = None,
+        is_private: bool = False,
+    ) -> None:
         super().__init__()
+        self.name: str = name
         self.max_capacity: int = max_capacity
         self.characters_present: OrderedSet[int] = OrderedSet([])
+        self.whitelist: Set[int] = set(whitelist if whitelist else [])
+        self.is_private: bool = is_private
+        self.activity_flags: int = 0
 
     def to_dict(self) -> Dict[str, Any]:
         return {
             **super().to_dict(),
             "max_capacity": self.max_capacity,
             "characters_present": list(self.characters_present),
+            "whitelist": list(self.whitelist),
+            "is_private": self.is_private,
         }
 
+    def can_enter(self, character_id: int) -> bool:
+        """Return true if the given character is allowed to enter this location"""
+        if self.is_private is False:
+            return True
+
+        return character_id in self.whitelist
+
     def add_character(self, character: int) -> None:
         self.characters_present.append(character)
 
@@ -32,17 +59,22 @@ def remove_character(self, character: int) -> None:
     def has_character(self, character: int) -> bool:
         return character in self.characters_present
 
+    def has_flags(self, *flags: int):
+        return all([self.activity_flags & f for f in flags])
+
     def __repr__(self) -> str:
-        return "{}(present={}, max_capacity={})".format(
+        return "{}(name={}, present={}, max_capacity={}, whitelist={}, is_private={})".format(
             self.__class__.__name__,
+            self.name,
             self.characters_present,
             self.max_capacity,
+            self.whitelist,
+            self.is_private,
         )
 
+    @classmethod
+    def create(cls, world: World, **kwargs) -> Location:
+        return cls(max_capacity=kwargs.get("max capacity", 9999))
 
-class LocationFactory(AbstractFactory):
-    def __init__(self):
-        super().__init__("Location")
-
-    def create(self, spec: ComponentDefinition, **kwargs) -> Location:
-        return Location(max_capacity=spec.get_attribute("max capacity", 9999))
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
diff --git a/src/neighborly/core/name_generation.py b/src/neighborly/core/name_generation.py
index f504459..12e11b8 100644
--- a/src/neighborly/core/name_generation.py
+++ b/src/neighborly/core/name_generation.py
@@ -1,20 +1,60 @@
-from typing import Dict, List, Union
+from pathlib import Path
+from typing import Dict, List, Optional, Union
 
 import neighborly.core.utils.tracery as tracery
-from neighborly.core.utils.tracery.modifiers import base_english
+import neighborly.core.utils.tracery.modifiers as tracery_modifiers
 
 _all_name_rules: Dict[str, Union[str, List[str]]] = {}
 _grammar: tracery.Grammar = tracery.Grammar(_all_name_rules)
 
+AnyPath = Union[str, Path]
 
-def register_rule(name: str, rule: Union[str, List[str]]) -> None:
+
+def register_rule(self, name: str, rule: Union[str, List[str]]) -> None:
     """Add a rule to the name factory"""
-    global _grammar, _all_name_rules
+    global _all_name_rules, _grammar
     _all_name_rules[name] = rule
-    _grammar = tracery.Grammar(_all_name_rules)
-    _grammar.add_modifiers(base_english)
+    _grammar = tracery.Grammar(self._all_name_rules)
+    _grammar.add_modifiers(tracery_modifiers.base_english)
 
 
-def get_name(seed_str: str) -> str:
+def get_name(self, seed_str: str) -> str:
     """Return a name generated using the grammar rules"""
-    return _grammar.flatten(seed_str)
+    return self._grammar.flatten(seed_str)
+
+
+class TraceryNameFactory:
+    """
+    Generates town names using Tracery
+    """
+
+    def __init__(self) -> None:
+        self._all_name_rules: Dict[str, Union[str, List[str]]] = {}
+        self._grammar: tracery.Grammar = tracery.Grammar(self._all_name_rules)
+
+    def register_rule(self, name: str, rule: Union[str, List[str]]) -> None:
+        """Add a rule to the name factory"""
+        self._all_name_rules[name] = rule
+        self._grammar = tracery.Grammar(self._all_name_rules)
+        self._grammar.add_modifiers(tracery_modifiers.base_english)
+
+    def get_name(self, seed_str: str) -> str:
+        """Return a name generated using the grammar rules"""
+        return self._grammar.flatten(seed_str)
+
+    def load_names(
+        self,
+        rule_name: str,
+        names: Optional[List[str]] = None,
+        filepath: Optional[AnyPath] = None,
+    ) -> None:
+        """Load names a list of names from a text file or given list"""
+        if names:
+            self.register_rule(rule_name, names)
+        elif filepath:
+            with open(filepath, "r") as f:
+                self.register_rule(rule_name, f.read().splitlines())
+        else:
+            raise ValueError(
+                "Need to supply names list or path to file containing names"
+            )
diff --git a/src/neighborly/plugins/default_plugin/character_values.py b/src/neighborly/core/personal_values.py
similarity index 65%
rename from src/neighborly/plugins/default_plugin/character_values.py
rename to src/neighborly/core/personal_values.py
index c18f229..3ca7d7b 100755
--- a/src/neighborly/plugins/default_plugin/character_values.py
+++ b/src/neighborly/core/personal_values.py
@@ -1,11 +1,13 @@
+from __future__ import annotations
+
 from enum import Enum
 from typing import Dict, List, Optional
 
 import numpy as np
 import numpy.typing as npt
 
-from neighborly.core.ecs import Component
-from neighborly.core.rng import IRandNumGenerator
+from neighborly.core.ecs import Component, World
+from neighborly.core.engine import NeighborlyEngine
 
 TRAIT_MAX = 50
 TRAIT_MIN = -50
@@ -45,7 +47,7 @@ class ValueTrait(Enum):
 }
 
 
-class CharacterValues(Component):
+class PersonalValues(Component):
     """
     Values are what a character believes in. They are used
     for decision-making and relationship compatibility among
@@ -63,6 +65,7 @@ class CharacterValues(Component):
     def __init__(
         self, overrides: Optional[Dict[str, int]] = None, default: int = 0
     ) -> None:
+        super().__init__()
         self._traits: npt.NDArray[np.int32] = np.array(
             [default] * len(_VALUE_INDICES.keys()), dtype=np.int32
         )
@@ -78,18 +81,18 @@ def traits(self) -> npt.NDArray[np.int32]:
         return self._traits
 
     @staticmethod
-    def calculate_compatibility(
-        traits_a: "CharacterValues", traits_b: "CharacterValues"
+    def compatibility(
+        character_a: PersonalValues, character_b: PersonalValues
     ) -> float:
         # Cosine similarity is a value between -1 and 1
-        cos_sim: float = np.dot(traits_a.traits, traits_b.traits) / (
-            np.linalg.norm(traits_a.traits) * np.linalg.norm(traits_b.traits)
+        cos_sim: float = np.dot(character_a.traits, character_b.traits) / (
+            np.linalg.norm(character_a.traits) * np.linalg.norm(character_b.traits)
         )
 
         return cos_sim
 
     def get_high_values(self, n=3) -> List[str]:
-        """Return the value names associated with the to n values"""
+        """Return the value names associated with the n values"""
         # This code is adapted from https://stackoverflow.com/a/23734295
 
         ind = np.argpartition(self.traits, -n)[-n:]
@@ -110,29 +113,32 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return "{}({})".format(self.__class__.__name__, self._traits.__repr__())
 
+    @classmethod
+    def create(cls, world: World, **kwargs) -> Component:
+        engine = world.get_resource(NeighborlyEngine)
+        n_likes: int = kwargs.get("n_likes", 3)
+        n_dislikes: int = kwargs.get("n_dislikes", 3)
 
-def generate_character_values(
-    rng: IRandNumGenerator, n_likes: int = 3, n_dislikes: int = 3
-) -> CharacterValues:
-    """Generate a new set of character values"""
-    # Select Traits
-    total_traits: int = n_likes + n_dislikes
-    traits = [str(trait.value) for trait in rng.sample(list(ValueTrait), total_traits)]
+        total_traits: int = n_likes + n_dislikes
+        traits = [
+            str(trait.value)
+            for trait in engine.rng.sample(list(ValueTrait), total_traits)
+        ]
 
-    # select likes and dislikes
-    high_values = rng.sample(traits, n_likes)
+        # select likes and dislikes
+        high_values = engine.rng.sample(traits, n_likes)
 
-    map(lambda t: traits.remove(t), high_values)
+        map(lambda t: traits.remove(t), high_values)
 
-    low_values = [*traits]
+        low_values = [*traits]
 
-    # Generate values for each ([30,50] for high values, [-50,-30] for dislikes)
-    values_overrides: Dict[str, int] = {}
+        # Generate values for each ([30,50] for high values, [-50,-30] for dislikes)
+        values_overrides: Dict[str, int] = {}
 
-    for trait in high_values:
-        values_overrides[trait] = rng.randint(30, 50)
+        for trait in high_values:
+            values_overrides[trait] = engine.rng.randint(30, 50)
 
-    for trait in low_values:
-        values_overrides[trait] = rng.randint(-50, -30)
+        for trait in low_values:
+            values_overrides[trait] = engine.rng.randint(-50, -30)
 
-    return CharacterValues(values_overrides)
+        return PersonalValues(values_overrides)
diff --git a/src/neighborly/core/position.py b/src/neighborly/core/position.py
index 2a4d165..3009152 100644
--- a/src/neighborly/core/position.py
+++ b/src/neighborly/core/position.py
@@ -1,8 +1,9 @@
+from __future__ import annotations
+
 from dataclasses import dataclass
 from typing import Any, Dict
 
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
+from neighborly.core.ecs import Component, World
 
 
 @dataclass
@@ -13,10 +14,9 @@ class Position2D(Component):
     def to_dict(self) -> Dict[str, Any]:
         return {**super().to_dict(), "x": self.x, "y": self.y}
 
+    @classmethod
+    def create(cls, world: World, **kwargs) -> Position2D:
+        return Position2D(x=kwargs.get("x", 0.0), y=kwargs.get("y", 0.0))
 
-class Position2DFactory(AbstractFactory):
-    def __init__(self):
-        super().__init__("Position2D")
-
-    def create(self, spec: ComponentDefinition, **kwargs) -> Position2D:
-        return Position2D()
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
diff --git a/src/neighborly/core/relationship.py b/src/neighborly/core/relationship.py
index 5559caf..a9b2fa2 100644
--- a/src/neighborly/core/relationship.py
+++ b/src/neighborly/core/relationship.py
@@ -1,17 +1,17 @@
+from __future__ import annotations
+
 from dataclasses import dataclass
-from enum import IntFlag, auto
+from enum import IntFlag
 from typing import Any, ClassVar, Dict, List
 
+from neighborly.core.utils.graph import DirectedGraph
+
 FRIENDSHIP_MAX: float = 50
 FRIENDSHIP_MIN: float = -50
 
 ROMANCE_MAX: float = 50
 ROMANCE_MIN: float = -50
 
-SALIENCE_MAX: float = 100
-SALIENCE_MIN: float = 0
-SALIENCE_INCREMENT: float = 1
-
 
 def clamp(value: float, minimum: float, maximum: float) -> float:
     """Clamp a floating point value within a min,max range"""
@@ -21,28 +21,29 @@ def clamp(value: float, minimum: float, maximum: float) -> float:
 class RelationshipTag(IntFlag):
     """Relationship Tags are bitwise flags that indicate certain relationship types"""
 
-    NONE = 0
-    Father = auto()
-    Mother = auto()
-    Parent = auto()
-    Brother = auto()
-    Sister = auto()
-    Sibling = auto()
-    Son = auto()
-    Daughter = auto()
-    Coworker = auto()
-    Boss = auto()
-    Spouse = auto()
-    Friend = auto()
-    Enemy = auto()
-    BestFriend = auto()
-    WorstEnemy = auto()
-    Rival = auto()
-    Acquaintance = auto()
-    BiologicalFamily = auto()
-    Neighbors = auto()
-    SignificantOther = auto()
-    LoveInterest = auto()
+    Acquaintance = 0
+    Father = 1 << 0
+    Mother = 1 << 1
+    Parent = 1 << 2
+    Brother = 1 << 3
+    Sister = 1 << 4
+    Sibling = 1 << 5
+    Son = 1 << 6
+    Daughter = 1 << 7
+    Coworker = 1 << 8
+    Boss = 1 << 9
+    Spouse = 1 << 10
+    Friend = 1 << 11
+    Enemy = 1 << 12
+    BestFriend = 1 << 13
+    WorstEnemy = 1 << 14
+    Rival = 1 << 15
+    BiologicalFamily = 1 << 16
+    Neighbors = 1 << 17
+    SignificantOther = 1 << 18
+    LoveInterest = 1 << 19
+    Child = 1 << 20
+    NuclearFamily = 1 << 21
 
 
 @dataclass
@@ -72,7 +73,7 @@ class RelationshipModifier:
         Flat value to apply when incrementing the salience value
     """
 
-    _tag_registry: ClassVar[Dict[str, "RelationshipModifier"]] = {}
+    _tag_registry: ClassVar[Dict[str, RelationshipModifier]] = {}
 
     name: str
     description: str = ""
@@ -84,12 +85,12 @@ class RelationshipModifier:
     salience_increment: float = 0
 
     @classmethod
-    def register_tag(cls, tag: "RelationshipModifier") -> None:
+    def register_tag(cls, tag: RelationshipModifier) -> None:
         """Add a tag to the internal registry for finding later"""
         cls._tag_registry[tag.name] = tag
 
     @classmethod
-    def get_tag(cls, name: str) -> "RelationshipModifier":
+    def get_tag(cls, name: str) -> RelationshipModifier:
         """Retrieve a tag from the internal registry of RelationshipModifiers"""
         return cls._tag_registry[name]
 
@@ -115,7 +116,7 @@ class Relationship:
         The character who this relationship is directed toward
     _friendship: float
         Friendship score on the scale [FRIENDSHIP_MIN, FRIENDSHIP_MAX]
-        where a max is best friends and the min is worst enemies
+        where a max means best friends and min means worst-enemies
     _friendship_base: float
         Friendship score without any modifiers from tags
     _friendship_increment: float
@@ -127,20 +128,12 @@ class Relationship:
         Romance score without any modifiers from tags
     _romance_increment: float
         Amount to increment the romance score by after each interaction
-    _salience: float
-        Salience score on the scale [SALIENCE_MIN, SALIENCE_MAX]
-        where a max is I know them well and the min is "I've never seen this
-        person in my life"
-    _salience_base: float
-        Salience score without any modifiers from tags
-    _salience_increment: float
-        Amount to increment the salience score by after each interaction
     _is_dirty: bool
         Used internally to mark when score need to be recalculated after a change
     _modifiers: Dict[str, RelationshipModifier]
         All the modifiers active on the Relationship instance
     _tags: RelationshipTag
-        All the tags that that are attached to this
+        All the tags that are attached to this
     """
 
     __slots__ = (
@@ -149,36 +142,37 @@ class Relationship:
         "_friendship",
         "_friendship_base",
         "_friendship_increment",
+        "_compatibility",
         "_romance",
         "_romance_base",
         "_romance_increment",
-        "_salience",
-        "_salience_base",
-        "_salience_increment",
         "_is_dirty",
         "_modifiers",
         "_tags",
+        "_romance_disabled",
     )
 
     def __init__(
         self,
         owner: int,
         target: int,
+        base_friendship: int = 0,
+        base_romance: int = 0,
+        tags: RelationshipTag = RelationshipTag.Acquaintance,
+        compatibility: float = 0.1,
+        romance_disabled: bool = False,
     ) -> None:
         self._owner: int = owner
         self._target: int = target
-        self._friendship: float = 0
+        self._friendship: float = base_friendship
         self._romance: float = 0
-        self._salience: float = 0
         self._friendship_base: float = 0
-        self._romance_base: float = 0
-        self._salience_base: float = 0
-        self._friendship_increment: float = 0
-        self._romance_increment: float = 0
-        self._salience_increment: float = 0
+        self._romance_base: float = base_romance
         self._is_dirty: bool = True
         self._modifiers: Dict[str, RelationshipModifier] = {}
-        self._tags: RelationshipTag = RelationshipTag.NONE
+        self._tags: RelationshipTag = tags
+        self._compatibility: float = compatibility
+        self._romance_disabled: bool = romance_disabled
 
     @property
     def target(self) -> int:
@@ -200,11 +194,16 @@ def romance(self) -> float:
             self._recalculate_stats()
         return self._romance
 
-    @property
-    def salience(self) -> float:
-        if self._is_dirty:
-            self._recalculate_stats()
-        return self._salience
+    def set_compatibility(self, value: float) -> None:
+        self._compatibility = value
+
+    def increment_friendship(self, value: float) -> None:
+        self._friendship_base += value
+        self._is_dirty = True
+
+    def increment_romance(self, value: float) -> None:
+        self._romance_base += value
+        self._is_dirty = True
 
     def add_tags(self, tags: RelationshipTag) -> None:
         """Return add a tag to this Relationship"""
@@ -241,7 +240,6 @@ def update(self) -> None:
         if self._is_dirty:
             self._recalculate_stats()
 
-        self._salience_base += self._salience_increment
         self._romance_base += self._romance_increment
         self._friendship_base += self._friendship_increment
 
@@ -249,9 +247,10 @@ def update(self) -> None:
 
     def to_dict(self) -> Dict[str, Any]:
         return {
+            "owner": self.owner,
+            "target": self.target,
             "friendship": self.friendship,
             "romance": self.romance,
-            "salience": self.salience,
             "tags": self._tags,
             "modifiers": [m for m in self._modifiers.keys()],
         }
@@ -261,38 +260,68 @@ def _recalculate_stats(self) -> None:
         # Reset the increments
         self._romance_increment = 0.0
         self._friendship_increment = 0.0
-        self._salience_increment = SALIENCE_INCREMENT
 
         # Reset final values back to base values
         self._friendship = self._friendship_base
         self._romance = self._romance_base
-        self._salience = self._salience_base
 
         # Apply modifiers in tags
         for modifier in self._modifiers.values():
             # Apply boosts to relationship scores
-            self._salience += modifier.salience_boost
             self._romance += modifier.romance_boost
             self._friendship += modifier.friendship_boost
             # Apply increment boosts
             self._romance_increment += modifier.romance_increment
-            self._salience_increment += modifier.salience_increment
             self._friendship_increment += modifier.friendship_increment
 
-        self._salience = clamp(self._salience, SALIENCE_MIN, SALIENCE_MAX)
         self._romance = clamp(self._romance, ROMANCE_MIN, ROMANCE_MAX)
         self._friendship = clamp(self._friendship, FRIENDSHIP_MIN, FRIENDSHIP_MAX)
 
         self._is_dirty = False
 
     def __repr__(self) -> str:
-        return "{}(owner={}, target={}, romance={}, friendship={}, salience={}, tags={}, modifiers={})".format(
+        return "{}(owner={}, target={}, romance={}, friendship={}, tags={}, modifiers={})".format(
             self.__class__.__name__,
             self.owner,
             self.target,
             self.romance,
             self.friendship,
-            self.salience,
             self._tags,
             list(self._modifiers.keys()),
         )
+
+
+class RelationshipGraph(DirectedGraph[Relationship]):
+    def __init__(self) -> None:
+        super().__init__()
+
+    def add_relationship(self, relationship: Relationship) -> None:
+        """Add a new relationship to the graph"""
+        self.add_connection(relationship.owner, relationship.target, relationship)
+
+    def get_relationships(self, owner: int) -> List[Relationship]:
+        """Get all the outgoing relationships for this character"""
+        owner_node = self._nodes[owner]
+        return [self._edges[owner, target] for target in owner_node.outgoing]
+
+    def get_all_relationships_with_tags(
+        self, owner: int, tags: RelationshipTag
+    ) -> List[Relationship]:
+        owner_node = self._nodes[owner]
+
+        return list(
+            filter(
+                lambda rel: rel.has_tags(tags),
+                [self._edges[owner, target] for target in owner_node.outgoing],
+            )
+        )
+
+    def to_dict(self) -> Dict[int, Dict[int, Dict[str, Any]]]:
+        network_dict: Dict[int, Dict[int, Dict[str, Any]]] = {}
+
+        for character_id in self._nodes.keys():
+            network_dict[character_id] = {}
+            for relationship in self.get_relationships(character_id):
+                network_dict[character_id][relationship.target] = relationship.to_dict()
+
+        return network_dict
diff --git a/src/neighborly/core/residence.py b/src/neighborly/core/residence.py
index f2430c6..0b3bcfc 100644
--- a/src/neighborly/core/residence.py
+++ b/src/neighborly/core/residence.py
@@ -1,12 +1,17 @@
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any, Dict, List, Type
 
 from ordered_set import OrderedSet
 
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
+from neighborly.core.ecs import Component, EntityArchetype, World
+from neighborly.core.location import Location
+from neighborly.core.position import Position2D
 
 
 class Residence(Component):
+    """Residence is a place where characters live"""
+
     __slots__ = "owners", "former_owners", "residents", "former_residents", "_vacant"
 
     def __init__(self) -> None:
@@ -17,6 +22,9 @@ def __init__(self) -> None:
         self.former_residents: OrderedSet[int] = OrderedSet([])
         self._vacant: bool = True
 
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
+
     def to_dict(self) -> Dict[str, Any]:
         return {
             **super().to_dict(),
@@ -27,29 +35,96 @@ def to_dict(self) -> Dict[str, Any]:
             "vacant": self._vacant,
         }
 
-    def add_tenant(self, person: int, is_owner: bool = False) -> None:
+    def add_owner(self, owner: int) -> None:
+        """Add owner to the residence"""
+        self.owners.add(owner)
+
+    def remove_owner(self, owner: int) -> None:
+        """Remove owner from residence"""
+        self.owners.remove(owner)
+
+    def is_owner(self, character: int) -> bool:
+        """Return True if the character is an owner of this residence"""
+        return character in self.owners
+
+    def add_resident(self, resident: int) -> None:
         """Add a tenant to this residence"""
-        self.residents.add(person)
-        if is_owner:
-            self.owners.add(person)
+        self.residents.add(resident)
         self._vacant = False
 
-    def remove_tenant(self, person: int) -> None:
+    def remove_resident(self, resident: int) -> None:
         """Remove a tenant rom this residence"""
-        self.residents.remove(person)
-        self.former_residents.add(person)
-        if person in self.owners:
-            self.owners.remove(person)
-            self.former_owners.add(person)
+        self.residents.remove(resident)
+        self.former_residents.add(resident)
         self._vacant = len(self.residents) == 0
 
+    def is_resident(self, character: int) -> bool:
+        """Return True if the given character is a resident"""
+        return character in self.residents
+
     def is_vacant(self) -> bool:
+        """Return True if the residence is vacant"""
         return self._vacant
 
 
-class ResidenceFactory(AbstractFactory):
-    def __init__(self):
-        super().__init__("Residence")
+class Resident(Component):
+    """Component attached to characters indicating that they live in the town"""
+
+    __slots__ = "residence"
+
+    def __init__(self, residence: int) -> None:
+        super().__init__()
+        self.residence: int = residence
+
+    def on_remove(self) -> None:
+        world = self.gameobject.world
+        residence = world.get_gameobject(self.residence).get_component(Residence)
+        residence.remove_resident(self.gameobject.id)
+        if residence.is_owner(self.gameobject.id):
+            residence.remove_owner(self.gameobject.id)
+
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
+
+
+class ResidenceArchetype(EntityArchetype):
+    __slots__ = ("spawn_multiplier",)
+
+    def __init__(
+        self,
+        name: str,
+        spawn_multiplier: int = 1,
+        extra_components: Dict[Type[Component], Dict[str, Any]] = None,
+    ) -> None:
+        super().__init__(name)
+        self.spawn_multiplier: int = spawn_multiplier
+
+        self.add(Residence)
+        self.add(Location)
+        self.add(Position2D)
+
+        if extra_components:
+            for component_type, params in extra_components.items():
+                self.add(component_type, **params)
+
+
+class ResidenceArchetypeLibrary:
+    _registry: Dict[str, ResidenceArchetype] = {}
+
+    @classmethod
+    def register(
+        cls,
+        archetype: ResidenceArchetype,
+        name: str = None,
+    ) -> None:
+        """Register a new LifeEventType mapped to a name"""
+        cls._registry[name if name else archetype.name] = archetype
+
+    @classmethod
+    def get_all(cls) -> List[ResidenceArchetype]:
+        return list(cls._registry.values())
 
-    def create(self, spec: ComponentDefinition, **kwargs) -> Residence:
-        return Residence()
+    @classmethod
+    def get(cls, name: str) -> ResidenceArchetype:
+        """Get a LifeEventType using a name"""
+        return cls._registry[name]
diff --git a/src/neighborly/core/routine.py b/src/neighborly/core/routine.py
index 10d1d7a..0944f31 100644
--- a/src/neighborly/core/routine.py
+++ b/src/neighborly/core/routine.py
@@ -1,9 +1,9 @@
-from enum import IntEnum
-from typing import Optional, Union, List, Dict, Tuple
+from __future__ import annotations
 
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
+from enum import IntEnum
+from typing import Dict, List, Optional, Tuple, Union
 
+from neighborly.core.ecs import Component, World
 
 TIME_ALIAS = {
     "early morning": "02:00",
@@ -174,13 +174,8 @@ def remove_entries(self, days: List[str], *entries: RoutineEntry) -> None:
         for day in days:
             self._daily_routines[day].remove_entries(*entries)
 
-
-class RoutineFactory(AbstractFactory):
-    def __init__(self):
-        super().__init__("Routine")
-
-    def create(self, spec: ComponentDefinition, **kwargs) -> Routine:
-        return Routine()
+    def on_archive(self) -> None:
+        self.gameobject.remove_component(type(self))
 
 
 def time_str_to_int(s: str) -> int:
diff --git a/src/neighborly/core/systems.py b/src/neighborly/core/systems.py
deleted file mode 100644
index e2157d0..0000000
--- a/src/neighborly/core/systems.py
+++ /dev/null
@@ -1,443 +0,0 @@
-import logging
-from typing import List, cast, Dict, Optional
-from neighborly.core.builtin.events import (
-    BecomeAdolescentEvent,
-    BecomeAdultEvent,
-    BecomeElderEvent,
-    BecomeYoungAdultEvent,
-    HomePurchaseEvent,
-    MoveResidenceEvent,
-    SocializeEvent,
-)
-
-from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import GameObject, World
-from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.helpers import get_locations, move_character, try_generate_family
-from neighborly.core.life_event import (
-    LifeEventLogger,
-    LifeEventRule,
-    check_gameobject_preconditions,
-    handle_gameobject_effects,
-)
-from neighborly.core.position import Position2D
-from neighborly.core.residence import Residence
-from neighborly.core.routine import Routine
-from neighborly.core.builtin.statuses import (
-    AdolescentStatus,
-    AdultStatus,
-    ChildStatus,
-    ElderStatus,
-    YoungAdultStatus,
-)
-from neighborly.core.time import HOURS_PER_YEAR, SimDateTime
-from neighborly.core.town import Town
-from neighborly.core.utils.utilities import chunk_list
-
-logger = logging.getLogger(__name__)
-
-
-class CharacterSystem:
-    """Updates the age of all alive characters"""
-
-    def __call__(self, world: World, delta_time: int, **kwargs):
-
-        for _, character in world.get_component(GameCharacter):
-            if "deceased" not in character.tags:
-                character.age += float(delta_time) / HOURS_PER_YEAR
-
-
-class RoutineSystem:
-    def __call__(self, world: World, **kwargs) -> None:
-        date = world.get_resource(SimDateTime)
-        engine = world.get_resource(NeighborlyEngine)
-
-        for entity, (character, routine) in world.get_components(
-            GameCharacter, Routine
-        ):
-            character = cast(GameCharacter, character)
-            routine = cast(Routine, routine)
-
-            routine_entries = routine.get_entries(date.weekday_str, date.hour)
-
-            if routine_entries:
-                chosen_entry = engine.get_rng().choice(routine_entries)
-                location_id = (
-                    character.location_aliases[str(chosen_entry.location)]
-                    if isinstance(chosen_entry.location, str)
-                    else chosen_entry.location
-                )
-                move_character(
-                    world,
-                    entity,
-                    location_id,
-                )
-
-            potential_locations = get_locations(world)
-            if potential_locations:
-                loc_id, _ = engine.get_rng().choice(potential_locations)
-                move_character(world, entity, loc_id)
-
-
-class TimeSystem:
-    def __call__(self, world: World, delta_time: int, **kwargs):
-        sim_time = world.get_resource(SimDateTime)
-        sim_time.increment(hours=delta_time)
-
-
-class LifeEventSystem:
-    _event_registry: Dict[str, LifeEventRule] = {}
-
-    def __init__(self, interval: int = 36) -> None:
-        self.interval = interval
-        self.time_until_run = interval
-
-    @classmethod
-    def register_event(cls, *events: LifeEventRule) -> None:
-        """Add a new life event to the registry"""
-        for event in events:
-            cls._event_registry[event.name] = event
-
-    def __call__(self, world: World, delta_time: int, **kwargs) -> None:
-        """Check if life events will fire this round"""
-
-        self.time_until_run -= delta_time
-
-        # Run when timer reaches zero then reset
-        if self.time_until_run > 0:
-            return
-        else:
-            self.time_until_run = self.interval
-
-        rng = world.get_resource(NeighborlyEngine).get_rng()
-
-        for event_rule in self._event_registry.values():
-            for event, participants in event_rule.pattern_fn(world):
-                if rng.random() < event_rule.probability:
-                    preconditions_pass = all(
-                        [check_gameobject_preconditions(g, event) for g in participants]
-                    )
-                    if preconditions_pass:
-                        for g in participants:
-                            handle_gameobject_effects(g, event)
-                    if event_rule.effect_fn:
-                        event_rule.effect_fn(world)
-
-
-class ChildSystem:
-    """Ages children into adolescents"""
-
-    def __call__(self, world: World, **kwargs) -> None:
-
-        date_time = world.get_resource(SimDateTime)
-        event_logger = world.get_resource(LifeEventLogger)
-
-        for _, (character, child_status) in world.get_components(
-            GameCharacter, ChildStatus
-        ):
-            character = cast(GameCharacter, character)
-            if (
-                character.age
-                >= character.character_def.lifecycle.life_stages["adolescent"]
-            ):
-                character.gameobject.remove_component(child_status)
-                character.gameobject.add_component(AdolescentStatus())
-                event = BecomeAdolescentEvent(
-                    timestamp=date_time.to_iso_str(),
-                    character_id=character.gameobject.id,
-                    character_name=str(character.name),
-                )
-                handle_gameobject_effects(character.gameobject, event)
-                event_logger.log_event(event, [character.gameobject.id])
-
-
-class AdolescentSystem:
-    """Ages adolescents into young adults"""
-
-    def __call__(self, world: World, **kwargs) -> None:
-        date_time = world.get_resource(SimDateTime)
-        event_logger = world.get_resource(LifeEventLogger)
-
-        for _, (character, adolescent_status) in world.get_components(
-            GameCharacter, AdolescentStatus
-        ):
-            character = cast(GameCharacter, character)
-            if (
-                character.age
-                >= character.character_def.lifecycle.life_stages["young_adult"]
-            ):
-                character.gameobject.remove_component(adolescent_status)
-                character.gameobject.add_component(YoungAdultStatus())
-                event = BecomeYoungAdultEvent(
-                    timestamp=date_time.to_iso_str(),
-                    character_id=character.gameobject.id,
-                    character_name=str(character.name),
-                )
-                handle_gameobject_effects(character.gameobject, event)
-                event_logger.log_event(event, [character.gameobject.id])
-
-
-class YoungAdultSystem:
-    """Ages young adults into adults"""
-
-    def __call__(self, world: World, **kwargs) -> None:
-        date_time = world.get_resource(SimDateTime)
-        event_logger = world.get_resource(LifeEventLogger)
-
-        for _, (character, young_adult_status) in world.get_components(
-            GameCharacter, YoungAdultStatus
-        ):
-            character = cast(GameCharacter, character)
-            if character.age >= character.character_def.lifecycle.life_stages["adult"]:
-                character.gameobject.remove_component(young_adult_status)
-                character.gameobject.add_component(AdultStatus())
-                event = BecomeAdultEvent(
-                    timestamp=date_time.to_iso_str(),
-                    character_id=character.gameobject.id,
-                    character_name=str(character.name),
-                )
-                handle_gameobject_effects(character.gameobject, event)
-                event_logger.log_event(event, [character.gameobject.id])
-
-
-class AdultSystem:
-    """Ages adults into elders"""
-
-    def __call__(self, world: World, **kwargs) -> None:
-        date_time = world.get_resource(SimDateTime)
-        event_logger = world.get_resource(LifeEventLogger)
-
-        for _, (character, adult_status) in world.get_components(
-            GameCharacter, AdultStatus
-        ):
-            character = cast(GameCharacter, character)
-            if character.age >= character.character_def.lifecycle.life_stages["elder"]:
-                character.gameobject.remove_component(adult_status)
-                character.gameobject.add_component(ElderStatus())
-                event = BecomeElderEvent(
-                    timestamp=date_time.to_iso_str(),
-                    character_id=character.gameobject.id,
-                    character_name=str(character.name),
-                )
-                handle_gameobject_effects(character.gameobject, event)
-                event_logger.log_event(event, [character.gameobject.id])
-
-
-class SocializeSystem:
-    def __init__(self, interval: int = 12) -> None:
-        self.interval = interval
-        self.time_until_run = interval
-
-    def __call__(self, world: World, **kwargs) -> None:
-        for pair in chunk_list(world.get_component(GameCharacter), 2):
-            character_0 = pair[0][1].gameobject
-            character_1 = pair[1][1].gameobject
-
-            # choose an interaction type
-            interaction_type = (
-                world.get_resource(NeighborlyEngine)
-                .get_rng()
-                .choice(
-                    [
-                        "neutral",
-                        "friendly",
-                        "flirty",
-                        "bad",
-                        "boring",
-                        "good",
-                        "exciting",
-                    ]
-                )
-            )
-
-            socialize_event = SocializeEvent(
-                timestamp=world.get_resource(SimDateTime).to_iso_str(),
-                character_names=(
-                    str(character_0.get_component(GameCharacter).name),
-                    str(character_1.get_component(GameCharacter).name),
-                ),
-                character_ids=(
-                    character_0.id,
-                    character_1.id,
-                ),
-                interaction_type=interaction_type,
-            )
-
-            character_0_consent = check_gameobject_preconditions(
-                character_0, socialize_event
-            )
-            character_1_consent = check_gameobject_preconditions(
-                character_1, socialize_event
-            )
-
-            if character_0_consent and character_1_consent:
-                handle_gameobject_effects(character_0, socialize_event)
-                handle_gameobject_effects(character_1, socialize_event)
-
-
-class AddResidentsSystem:
-    """
-    Adds new characters to the simulation
-
-    Attributes
-    ----------
-    character_weights: Optional[Dict[str, int]]
-        Relative frequency overrides for each of the character archetypes
-        registered in the engine instance
-    residence_weights: Optional[Dict[str, int]]
-        Relative frequency overrides for each of the character archetypes
-        registered in the engine instance
-    """
-
-    __slots__ = "interval", "time_until_run", "character_weights", "residence_weights"
-
-    def __init__(
-        self,
-        interval: int = 24,
-        character_weights: Optional[Dict[str, int]] = None,
-        residence_weights: Optional[Dict[str, int]] = None,
-    ) -> None:
-        self.interval = 24
-        self.time_until_run = interval
-        self.character_weights: Dict[str, int] = (
-            character_weights if character_weights else {}
-        )
-        self.residence_weights: Dict[str, int] = (
-            residence_weights if residence_weights else {}
-        )
-
-    def __call__(self, world: World, delta_time: int, **kwargs) -> None:
-        engine = world.get_resource(NeighborlyEngine)
-        date_time = world.get_resource(SimDateTime)
-        event_logger = world.get_resource(LifeEventLogger)
-
-        self.time_until_run -= delta_time
-
-        # Run when timer reaches zero then reset
-        if self.time_until_run > 0:
-            return
-        else:
-            self.time_until_run = self.interval
-
-        # Attempt to build or find a house for this character to move into
-        residence = self.try_build_residence(engine, world)
-        if residence is None:
-            residence = self.try_get_abandoned(engine, world)
-        if residence is None:
-            return
-
-        chosen_archetype = self.select_random_character_archetype(engine)
-        character = engine.create_character(chosen_archetype, age_range="young_adult")
-        world.add_gameobject(character)
-
-        family = try_generate_family(engine, character)
-        spouse: Optional[GameObject] = family.get("spouse")
-        children: List[GameObject] = family.get("children", [])
-
-        if spouse:
-            world.add_gameobject(spouse)
-        for c in children:
-            world.add_gameobject(c)
-
-        # Create a home purchase event
-
-        home_owner_ids: List[int] = [character.id]
-        home_owner_names: List[str] = [str(character.get_component(GameCharacter).name)]
-
-        if spouse:
-            home_owner_ids.append(spouse.id)
-            home_owner_names.append(str(spouse.get_component(GameCharacter).name))
-
-        home_purachase_event = HomePurchaseEvent(
-            timestamp=date_time.to_iso_str(),
-            character_ids=tuple(home_owner_ids),
-            character_names=tuple(home_owner_names),
-            residence_id=residence.id,
-        )
-
-        handle_gameobject_effects(character, home_purachase_event)
-        if spouse:
-            handle_gameobject_effects(spouse, home_purachase_event)
-
-        event_logger.log_event(home_purachase_event, home_owner_ids)
-
-        # Create and handle move event
-
-        resident_ids: List[int] = [character.id]
-        resident_names: List[str] = [str(character.get_component(GameCharacter).name)]
-
-        residence_comp = residence.get_component(Residence)
-        residence_comp.add_tenant(character.id, True)
-
-        if spouse:
-            resident_ids.append(spouse.id)
-            resident_names.append(str(spouse.get_component(GameCharacter).name))
-            residence_comp.add_tenant(spouse.id, True)
-
-        for c in children:
-            resident_ids.append(c.id)
-            resident_names.append(str(c.get_component(GameCharacter).name))
-            residence_comp.add_tenant(c.id)
-
-        residence_move_event = MoveResidenceEvent(
-            timestamp=date_time.to_iso_str(),
-            residence_id=residence.id,
-            character_ids=tuple(resident_ids),
-            character_names=tuple(resident_names),
-        )
-
-        for i in resident_ids:
-            handle_gameobject_effects(world.get_gameobject(i), residence_move_event)
-
-        event_logger.log_event(residence_move_event, resident_ids)
-
-    def select_random_character_archetype(self, engine: NeighborlyEngine) -> str:
-        """Randomly select from the available character archetypes using weighted random"""
-        rng = engine.get_rng()
-
-        archetype_names = [a.get_name() for a in engine.get_character_archetypes()]
-        weights = []
-
-        # override weights
-        for name in archetype_names:
-            weights.append(self.character_weights.get(name, 1))
-
-        return rng.choices(archetype_names, weights=weights, k=1)[0]
-
-    def try_build_residence(
-        self, engine: NeighborlyEngine, world: World
-    ) -> Optional[GameObject]:
-        town = world.get_resource(Town)
-
-        if town.layout.has_vacancy():
-            chosen_archetype = self.select_random_residence_archetype(engine)
-            residence = engine.create_residence(chosen_archetype)
-            space = town.layout.allocate_space(residence.id)
-            residence.get_component(Position2D).x = space[0]
-            residence.get_component(Position2D).y = space[1]
-            world.add_gameobject(residence)
-            return residence
-        return None
-
-    def select_random_residence_archetype(self, engine: NeighborlyEngine) -> str:
-        """Randomly select from the available residence archetypes using weighted random"""
-        rng = engine.get_rng()
-
-        archetype_names = [a.get_name() for a in engine.get_residence_archetypes()]
-        weights = []
-
-        # override weights
-        for name in archetype_names:
-            weights.append(self.residence_weights.get(name, 1))
-
-        return rng.choices(archetype_names, weights=weights, k=1)[0]
-
-    def try_get_abandoned(
-        self, engine: NeighborlyEngine, world: World
-    ) -> Optional[GameObject]:
-        residences = list(
-            filter(lambda res: res[1].is_vacant(), world.get_component(Residence))
-        )
-        if residences:
-            return engine.get_rng().choice(residences)[1].gameobject
-        return None
diff --git a/src/neighborly/core/tags.py b/src/neighborly/core/tags.py
deleted file mode 100644
index 44d608d..0000000
--- a/src/neighborly/core/tags.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from typing import Dict
-from dataclasses import dataclass, field
-from neighborly.core.life_event import ILifeEventCallback
-
-
-@dataclass
-class Tag:
-    """Tags are named objects that apply add event preconditions and effects to a GameObject"""
-
-    name: str
-    description: str
-    event_effects: Dict[str, ILifeEventCallback] = field(default_factory=dict)
-    event_preconditions: Dict[str, ILifeEventCallback] = field(default_factory=dict)
diff --git a/src/neighborly/core/time.py b/src/neighborly/core/time.py
index 1f43beb..d4e5020 100644
--- a/src/neighborly/core/time.py
+++ b/src/neighborly/core/time.py
@@ -170,6 +170,14 @@ def delta_time(self) -> int:
     def weekday_str(self) -> str:
         return _DAYS_OF_WEEK[self._weekday]
 
+    def copy(self) -> SimDateTime:
+        return SimDateTime(
+            hour=self._hour,
+            day=self._day,
+            month=self._month,
+            year=self._year,
+        )
+
     def __repr__(self) -> str:
         return "{}(hour={}, day={}, month={}, year={}, weekday={})".format(
             self.__class__.__name__,
diff --git a/src/neighborly/core/town.py b/src/neighborly/core/town.py
index 21caf03..8e43dcb 100644
--- a/src/neighborly/core/town.py
+++ b/src/neighborly/core/town.py
@@ -1,84 +1,70 @@
+from __future__ import annotations
+
 import itertools
-from abc import abstractmethod
 from dataclasses import dataclass
 from enum import Enum
-from typing import (
-    Any,
-    Callable,
-    Dict,
-    Generic,
-    List,
-    Optional,
-    Protocol,
-    Set,
-    Tuple,
-    TypeVar,
-)
-
-from pydantic import BaseModel
-
-from neighborly.core.ecs import Component
-from neighborly.core.name_generation import get_name
-
-
-class TownConfig(BaseModel):
+from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar
+
+from neighborly.core.ecs import World
+from neighborly.core.engine import NeighborlyEngine
+
+
+class Town:
     """
-    Static configuration parameters for Town instance
+    Simulated town where characters live
+
+    Notes
+    -----
+    The town is stored in the ECS as a global resource
 
     Attributes
     ----------
-    name : str
-        Tracery grammar used to generate a town's name
-    width : int
-        Number of space tiles wide the town will be
-    length : int
-        Number of space tile long the town will be
+    name: str
+        The name of the town
+    population: int
+        The number of active town residents
     """
 
-    name: str = "#town_name#"
-    width: int = 5
-    length: int = 5
-
-
-class Town(Component):
-    """Simulated town where characters live"""
-
-    __slots__ = "name", "layout", "population"
+    __slots__ = "name", "population"
 
-    def __init__(self, name: str, layout: "TownLayout") -> None:
+    def __init__(self, name: str) -> None:
         super().__init__()
         self.name: str = name
         self.population: int = 0
-        self.layout: "TownLayout" = layout
 
     def to_dict(self) -> Dict[str, Any]:
-        return {**super().to_dict(), "name": self.name, "population": self.population}
+        return {"name": self.name, "population": self.population}
+
+    def __str__(self) -> str:
+        return f"{self.name} (Pop. {self.population})"
+
+    def __repr__(self) -> str:
+        return self.to_dict().__repr__()
 
     @classmethod
-    def create(cls, config: TownConfig) -> "Town":
-        """Create a town instance"""
-        town_name = get_name(config.name)
-        layout = TownLayout(config.width, config.length)
-        return cls(name=town_name, layout=layout)
+    def create(cls, world: World, **kwargs) -> Town:
+        """
+        Create a town instance
+
+        Parameters
+        ----------
+        kwargs.name: str
+            The name of the town as a string or Tracery pattern
+        """
+        town_name = kwargs.get("name", "Town")
+        town_name = world.get_resource(NeighborlyEngine).name_generator.get_name(
+            town_name
+        )
+        return cls(name=town_name)
 
 
 @dataclass
 class LayoutGridSpace:
-    # Identifier given to this space
-    # space_id: int
-    # Position of the space in the grid
-    # position: Tuple[int, int]
     # ID of the gameobject for the place occupying this space
-    place_id: Optional[int] = None
-
+    occupant: Optional[int] = None
 
-class ISpaceSelectionStrategy(Protocol):
-    """Uses a heuristic to select a space within the town"""
-
-    @abstractmethod
-    def choose_space(self, spaces: List[LayoutGridSpace]) -> Tuple[int, int]:
-        """Choose a space based on an internal heuristic"""
-        raise NotImplementedError()
+    def reset(self) -> None:
+        self.occupant = None
 
 
 class CompassDirection(Enum):
@@ -169,8 +155,10 @@ def to_dict(self) -> Dict[str, Any]:
         }
 
 
-class TownLayout:
-    """Manages an occupancy grid of what tiles in the town currently have places built on them
+class LandGrid:
+    """
+    Manages an occupancy grid of what tiles in the town
+    currently have places built on them
 
     Attributes
     ----------
@@ -182,7 +170,8 @@ class TownLayout:
 
     __slots__ = "_unoccupied", "_occupied", "_grid"
 
-    def __init__(self, width: int, length: int) -> None:
+    def __init__(self, size: Tuple[int, int]) -> None:
+        width, length = size
         self._grid: Grid[LayoutGridSpace] = Grid(
             width, length, lambda: LayoutGridSpace()
         )
@@ -207,30 +196,23 @@ def has_vacancy(self) -> bool:
         """Returns True if there are empty spaces available in the town"""
         return bool(self._unoccupied)
 
-    def allocate_space(
+    def reserve_space(
         self,
-        place_id: int,
-        selection_strategy: Optional[ISpaceSelectionStrategy] = None,
-    ) -> Tuple[int, int]:
+        position: Tuple[int, int],
+        occupant_id: int,
+    ) -> None:
         """Allocates a space for a location, setting it as occupied"""
-        if self._unoccupied:
-            if selection_strategy:
-                space = selection_strategy.choose_space(
-                    [self._grid[i] for i in self._unoccupied]
-                )
-            else:
-                space = self._unoccupied[0]
-
-            self._grid[space].place_id = place_id
-            self._unoccupied.remove(space)
-            self._occupied.add(space)
 
-            return space
+        if position in self._occupied:
+            raise RuntimeError("Grid space already occupied")
 
-        raise RuntimeError("Attempting to get space from full town layout")
+        space = self._grid[position]
+        space.occupant = occupant_id
+        self._unoccupied.remove(position)
+        self._occupied.add(position)
 
     def free_space(self, space: Tuple[int, int]) -> None:
         """Frees up a space in the town to be used by another location"""
-        self._grid[space].place_id = None
+        self._grid[space].reset()
         self._unoccupied.append(space)
         self._occupied.remove(space)
diff --git a/src/neighborly/core/social_network.py b/src/neighborly/core/utils/graph.py
similarity index 71%
rename from src/neighborly/core/social_network.py
rename to src/neighborly/core/utils/graph.py
index 27b3015..b5a8654 100644
--- a/src/neighborly/core/social_network.py
+++ b/src/neighborly/core/utils/graph.py
@@ -1,20 +1,8 @@
 from collections import defaultdict
-from typing import (
-    Any,
-    DefaultDict,
-    Dict,
-    Generic,
-    List,
-    NamedTuple,
-    Tuple,
-    TypeVar,
-    cast,
-)
+from typing import TypeVar, NamedTuple, Generic, DefaultDict, Dict, Tuple, cast
 
 from ordered_set import OrderedSet
 
-from neighborly.core.relationship import Relationship, RelationshipTag
-
 _T = TypeVar("_T")
 
 
@@ -23,7 +11,7 @@ class BDGraphNode(NamedTuple):
     outgoing: OrderedSet[int]
 
 
-class DirectedSocialGraph(Generic[_T]):
+class DirectedGraph(Generic[_T]):
     """Directed graph that represents information that connects characters"""
 
     __slots__ = "_nodes", "_edges"
@@ -69,7 +57,7 @@ def remove_connection(self, owner: int, target: int) -> None:
         del self._edges[owner, target]
 
 
-class UndirectedSocialGraph(Generic[_T]):
+class UndirectedGraph(Generic[_T]):
     """Manages information about characters shared feeling towards each other
 
     This class is for modeling symmetric social connections
@@ -115,35 +103,3 @@ def remove_connection(self, node_a: int, node_b: int) -> None:
         """Remove a connection from the social graph"""
         nodes = cast(Tuple[int, int], tuple(sorted((node_a, node_b))))
         del self._edges[nodes]
-
-
-class RelationshipNetwork(DirectedSocialGraph[Relationship]):
-    def __init__(self) -> None:
-        super().__init__()
-
-    def get_relationships(self, owner: int) -> List[Relationship]:
-        """Get all the outgoing relationships for this character"""
-        owner_node = self._nodes[owner]
-        return [self._edges[owner, target] for target in owner_node.outgoing]
-
-    def get_all_relationships_with_tags(
-        self, owner: int, tags: RelationshipTag
-    ) -> List[Relationship]:
-        owner_node = self._nodes[owner]
-
-        return list(
-            filter(
-                lambda rel: rel.has_tags(tags),
-                [self._edges[owner, target] for target in owner_node.outgoing],
-            )
-        )
-
-    def to_dict(self) -> Dict[int, Dict[int, Dict[str, Any]]]:
-        network_dict: Dict[int, Dict[int, Dict[str, Any]]] = {}
-
-        for character_id in self._nodes.keys():
-            network_dict[character_id] = {}
-            for relationship in self.get_relationships(character_id):
-                network_dict[character_id][relationship.target] = relationship.to_dict()
-
-        return network_dict
diff --git a/src/neighborly/core/utils/tracery/__init__.py b/src/neighborly/core/utils/tracery/__init__.py
index 2f27123..b98ac98 100644
--- a/src/neighborly/core/utils/tracery/__init__.py
+++ b/src/neighborly/core/utils/tracery/__init__.py
@@ -1,19 +1,14 @@
+import random
 import re
 
-from neighborly.core.rng import DefaultRNG, IRandNumGenerator
-
 basestring = (str, bytes)
 
-__rng: IRandNumGenerator = DefaultRNG()
-
-
-def set_grammar_rng(rng: IRandNumGenerator) -> None:
-    global __rng
-    __rng = rng
+_rng = random.Random()
 
 
-def get_grammar_rng() -> IRandNumGenerator:
-    return __rng
+def set_grammar_rng(rng) -> None:
+    global _rng
+    _rng = rng
 
 
 class Node(object):
@@ -171,7 +166,7 @@ def select_rule(self):
         # in kate's code there's a bunch of stuff for different methods of
         # selecting a rule, none of which seem to be implemented yet! so for
         # now I'm just going to ...
-        return get_grammar_rng().choice(self.default_rules)
+        return _rng.choice(self.default_rules)
 
     def clear_state(self):
         self.default_uses = []
@@ -316,7 +311,7 @@ def create_section(start, end, type_):
                 errors.append(str(start) + ": empty action")
         raw_substring = None
         if last_escaped_char is not None:
-            raw_substring = escaped_substring + "\\" + rule[last_escaped_char + 1 : end]
+            raw_substring = escaped_substring + "\\" + rule[last_escaped_char + 1: end]
         else:
             raw_substring = rule[start:end]
         sections.append({"type": type_, "raw": raw_substring})
diff --git a/src/neighborly/core/utils/utilities.py b/src/neighborly/core/utils/utilities.py
index 12fb931..423deb3 100644
--- a/src/neighborly/core/utils/utilities.py
+++ b/src/neighborly/core/utils/utilities.py
@@ -56,4 +56,12 @@ def parse_number_range(range_str: str) -> Tuple[int, int]:
 def chunk_list(lst: List[_T], n: int) -> Generator[List[_T], None, None]:
     """Yield successive n-sized chunks from lst."""
     for i in range(0, len(lst), n):
-        yield lst[i : i + n]
+        yield lst[i: i + n]
+
+
+_NT = TypeVar("_NT", int, float)
+
+
+def clamp(value: _NT, min_value: _NT, max_value: _NT) -> _NT:
+    """Clamp a numerical value between a min and max range"""
+    return min(max_value, max(min_value, value))
diff --git a/src/neighborly/exporter.py b/src/neighborly/exporter.py
index cd3306c..92bbe93 100644
--- a/src/neighborly/exporter.py
+++ b/src/neighborly/exporter.py
@@ -1,35 +1,38 @@
 import json
-from abc import ABC, abstractmethod
-from typing import Union
+import os
+import pathlib
+from typing import Optional
 
 from neighborly.core.ecs import World
-from neighborly.core.life_event import LifeEventLogger
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.time import SimDateTime
+from neighborly.core.relationship import RelationshipGraph
 from neighborly.core.town import Town
 
 
-class NeighborlyExporter(ABC):
-    """Serializes the simulation into a string or byte format"""
-
-    @abstractmethod
-    def export(self, world: World) -> Union[str, bytes]:
-        raise NotImplementedError()
-
-
-class NeighborlyJsonExporter(NeighborlyExporter):
-    """Serializes the simulation world to a JSON string"""
+class NeighborlyJsonExporter:
+    """Serializes the simulation to a JSON string"""
 
     def export(self, world: World) -> str:
         return json.dumps(
             {
-                "date": world.get_resource(SimDateTime).to_iso_str(),
-                "town": world.get_resource(Town).to_dict(),
                 "gameobjects": {g.id: g.to_dict() for g in world.get_gameobjects()},
-                "relationships": world.get_resource(RelationshipNetwork).to_dict(),
-                "events": [
-                    e.to_dict()
-                    for e in world.get_resource(LifeEventLogger).get_all_events()
-                ],
+                "relationships": world.get_resource(RelationshipGraph).to_dict(),
             }
         )
+
+
+class NeighborlyHTMLExporter:
+    """Generates HTML Pages describing the generated town"""
+
+    def export(
+        self,
+        world: World,
+        output: Optional[str] = None,
+    ) -> None:
+        """Create HTML files for the town"""
+
+        # Step 1: Create a directory with the name of the town
+        output_dir = pathlib.Path(output if output else os.getcwd())
+        output_dir = output_dir / world.get_resource(Town).name
+        output_dir.mkdir(parents=True, exist_ok=True)
+
+        # Step 2: Create a main home page
diff --git a/src/neighborly/inspection_tools.py b/src/neighborly/inspection_tools.py
index 938d791..2062100 100644
--- a/src/neighborly/inspection_tools.py
+++ b/src/neighborly/inspection_tools.py
@@ -1,6 +1,8 @@
+from pprint import pprint
+
 from neighborly.core.character import GameCharacter
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.life_event import LifeEventLogger
+from neighborly.core.location import Location
+from neighborly.core.relationship import RelationshipGraph
 from neighborly.simulation import Simulation
 
 
@@ -16,9 +18,9 @@ def list_relationships(sim: Simulation, character_id: int) -> None:
     gameobject = sim.world.get_gameobject(character_id)
     character = gameobject.get_component(GameCharacter)
 
-    relationship_net = sim.world.get_resource(RelationshipNetwork)
+    rel_graph = sim.world.get_resource(RelationshipGraph)
 
-    relationships = relationship_net.get_relationships(character_id)
+    relationships = rel_graph.get_relationships(character_id)
 
     print(f"Showing {len(relationships)} relationships for: {str(character.name)}")
 
@@ -27,8 +29,20 @@ def list_relationships(sim: Simulation, character_id: int) -> None:
         print(f"{r.target:<30} {r.friendship:<12} {r.romance:<12}")
 
 
-def list_event_history(sim: Simulation, gid: int) -> None:
-    event_logger = sim.world.get_resource(LifeEventLogger)
+def display_gameobject(sim: Simulation, gid: int) -> None:
+    pprint(sim.world.get_gameobject(gid).to_dict())
+
+
+# def list_event_history(sim: Simulation, gid: int) -> None:
+#     event_logger = sim.world.get_resource(LifeEventLogger)
+#
+#     for e in event_logger.get_events_for(gid):
+#         print(str(e))
+
 
-    for e in event_logger.get_events_for(gid):
-        print(str(e))
+def list_locations(sim: Simulation) -> None:
+    print("{:30s} {:25s} {:5s}".format("ID", "Name", "Occupancy"))
+    for gid, location in sim.world.get_component(Location):
+        print(
+            f"{gid:<30} {str(location.name)!r:<25} {int(len(location.characters_present)):<5}"
+        )
diff --git a/src/neighborly/loaders.py b/src/neighborly/loaders.py
index a60e7df..43d39e9 100644
--- a/src/neighborly/loaders.py
+++ b/src/neighborly/loaders.py
@@ -7,13 +7,11 @@
 
 import yaml
 
-from neighborly.core.business import BusinessDefinition
+from neighborly.core.activity import Activity, ActivityLibrary
+from neighborly.core.business import BusinessArchetype
 from neighborly.core.character import CharacterDefinition
-from neighborly.core.engine import (
-    ComponentDefinition,
-    EntityArchetypeDefinition,
-    NeighborlyEngine,
-)
+from neighborly.core.ecs import EntityArchetype
+from neighborly.core.engine import NeighborlyEngine
 from neighborly.core.relationship import RelationshipModifier
 
 logger = logging.getLogger(__name__)
@@ -21,23 +19,6 @@
 AnyPath = Union[str, Path]
 
 
-def load_names(
-    rule_name: str,
-    names: Optional[List[str]] = None,
-    filepath: Optional[AnyPath] = None,
-) -> None:
-    """Load names a list of names from a text file or given list"""
-    from neighborly.core.name_generation import register_rule
-
-    if names:
-        register_rule(rule_name, names)
-    elif filepath:
-        with open(filepath, "r") as f:
-            register_rule(rule_name, f.read().splitlines())
-    else:
-        raise ValueError("Need to supply names list or path to file containing names")
-
-
 class MissingComponentSpecError(Exception):
     """Error raised when an entity archetype is missing an expected component"""
 
@@ -123,20 +104,20 @@ def _load_business_definitions(
 ) -> None:
     """Process data related to defining activities"""
     for business_def in data:
-        BusinessDefinition.register_type(BusinessDefinition(**business_def))
+        BusinessArchetype.register(BusinessArchetype(**business_def))
 
 
 def _load_entity_archetype(
-    engine: NeighborlyEngine, data: Dict[str, Any]
-) -> EntityArchetypeDefinition:
-    archetype = EntityArchetypeDefinition(
+    engine: NeighborlyEngine,
+    data: Dict[str, Any],
+    parent: Optional[EntityArchetype] = None,
+) -> EntityArchetype:
+    archetype = EntityArchetype(
         data["name"],
         is_template=data.get("template", False),
     )
 
-    if data.get("inherits"):
-        parent = engine.get_character_archetype(data["inherits"])
-
+    if parent:
         # Copy component specs from the parent
         for component_spec in parent.get_components().values():
             archetype.add_component(copy.deepcopy(component_spec))
@@ -164,7 +145,11 @@ def _load_entity_archetype(
 def _load_character_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
     """Process data related to defining character archetypes"""
     for character in data:
-        archetype = _load_entity_archetype(engine, character)
+        parent: Optional[EntityArchetype] = None
+        if character.get("inherits"):
+            parent = engine.get_character_archetype(character["inherits"])
+
+        archetype = _load_entity_archetype(engine, character, parent)
 
         if (
             archetype.try_component("GameCharacter") is None
@@ -179,7 +164,10 @@ def _load_character_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -
 def _load_place_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
     """Process information regarding place archetypes"""
     for place in data:
-        archetype = _load_entity_archetype(engine, place)
+        parent: Optional[EntityArchetype] = None
+        if place.get("inherits"):
+            parent = engine.get_place_archetype(place["inherits"])
+        archetype = _load_entity_archetype(engine, place, parent)
         engine.add_place_archetype(archetype)
 
 
@@ -187,7 +175,10 @@ def _load_place_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> No
 def _load_business_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
     """Process information regarding place archetypes"""
     for business in data:
-        archetype = _load_entity_archetype(engine, business)
+        parent: Optional[EntityArchetype] = None
+        if business.get("inherits"):
+            parent = engine.get_business_archetype(business["inherits"])
+        archetype = _load_entity_archetype(engine, business, parent)
 
         if archetype.try_component("Business") is None and not archetype.is_template:
             raise MissingComponentSpecError("Business")
@@ -199,7 +190,10 @@ def _load_business_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) ->
 def _load_residence_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
     """Process information regarding place archetypes"""
     for residence in data:
-        archetype = _load_entity_archetype(engine, residence)
+        parent: Optional[EntityArchetype] = None
+        if residence.get("inherits"):
+            parent = engine.get_residence_archetype(residence["inherits"])
+        archetype = _load_entity_archetype(engine, residence, parent)
         engine.add_residence_archetype(archetype)
 
 
@@ -212,3 +206,12 @@ def _load_relationship_tag_data(
         # Convert the dictionary to an object
         tag = RelationshipModifier(**modifier)
         RelationshipModifier.register_tag(tag)
+
+
+@YamlDataLoader.section_loader("Activities")
+def _load_activity_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
+    """Process data related to defining activities"""
+    for entry in data:
+        ActivityLibrary.register_activity(
+            Activity(entry["name"], trait_names=entry["traits"])
+        )
diff --git a/src/neighborly/plugins/default_plugin/__init__.py b/src/neighborly/plugins/default_plugin/__init__.py
index bee65af..1083a3d 100644
--- a/src/neighborly/plugins/default_plugin/__init__.py
+++ b/src/neighborly/plugins/default_plugin/__init__.py
@@ -1,109 +1,86 @@
+import logging
 import os
 from pathlib import Path
-from typing import Any, Dict, List
 
-from neighborly.core.business import BusinessDefinition, OccupationDefinition
-from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.systems import LifeEventSystem
-from neighborly.core.relationship import RelationshipModifier
-from neighborly.loaders import YamlDataLoader, load_names
-from neighborly.plugins.default_plugin.activity import Activity, register_activity
-from neighborly.plugins.default_plugin.businesses import (
-    bar_type,
-    bartender_type,
-    cashier_type,
-    cook_type,
-    department_store_type,
-    dj_type,
-    manager_type,
-    owner_type,
-    proprietor_type,
-    restaurant_type,
-    sales_associate_type,
-    security_type,
+from neighborly.builtin.events import (
+    become_enemies_event,
+    become_friends_event,
+    dating_break_up_event,
+    depart_due_to_unemployment,
+    divorce_event,
+    hire_employee_event,
+    hiring_business_role,
+    marriage_event,
+    potential_spouse_role,
+    pregnancy_event,
+    retire_event,
+    single_person_role_type,
+    start_dating_event,
+    unemployed_role,
 )
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import EventRoles, LifeEvents
+from neighborly.simulation import Plugin, Simulation
 
-# from neighborly.plugins.default_plugin.events import (
-#     BecameAdolescentEvent,
-#     BecameAdultEvent,
-#     BecameBusinessOwnerEvent,
-#     BecameChildEvent,
-#     BecameSeniorEvent,
-#     BecameYoungAdultEvent,
-#     DatingBreakUpEvent,
-#     DeathEvent,
-#     StartedDatingEvent,
-#     UnemploymentEvent,
-# )
-
-from neighborly.simulation import PluginContext
+logger = logging.getLogger(__name__)
 
 _RESOURCES_DIR = Path(os.path.abspath(__file__)).parent / "data"
 
 
-@YamlDataLoader.section_loader("Activities")
-def _load_activity_data(engine: NeighborlyEngine, data: List[Dict[str, Any]]) -> None:
-    """Process data related to defining activities"""
-    for activity in data:
-        register_activity(Activity(activity["name"], trait_names=activity["traits"]))
-
-
-def setup(ctx: PluginContext, **kwargs) -> None:
-    # RelationshipModifier.register_tag(FriendModifier())
-
-    # LifeEventProcessor.register_event(DeathEvent())
-    # LifeEventProcessor.register_event(BecameChildEvent())
-    # LifeEventProcessor.register_event(BecameAdolescentEvent())
-    # LifeEventProcessor.register_event(BecameYoungAdultEvent())
-    # LifeEventProcessor.register_event(BecameAdultEvent())
-    # LifeEventProcessor.register_event(BecameSeniorEvent())
-    # LifeEventProcessor.register_event(StartedDatingEvent())
-    # LifeEventProcessor.register_event(DatingBreakUpEvent())
-    # LifeEventProcessor.register_event(UnemploymentEvent())
-    # LifeEventProcessor.register_event(BecameBusinessOwnerEvent())
-
-    # ctx.world.add_system(DatingStatus.system_fn, 1)
-    # ctx.world.add_system(MarriedStatus.system_fn, 1)
-    # ctx.world.add_system(UnemployedStatus.system_fn, 1)
-    # ctx.world.add_system(SocializeProcessor(), 2)
-
-    # BusinessDefinition.register_type(restaurant_type)
-    # BusinessDefinition.register_type(bar_type)
-    # BusinessDefinition.register_type(department_store_type)
-
-    # OccupationDefinition.register_type(manager_type)
-    # OccupationDefinition.register_type(sales_associate_type)
-    # OccupationDefinition.register_type(cashier_type)
-    # OccupationDefinition.register_type(dj_type)
-    # OccupationDefinition.register_type(bartender_type)
-    # OccupationDefinition.register_type(security_type)
-    # OccupationDefinition.register_type(cook_type)
-    # OccupationDefinition.register_type(owner_type)
-    # OccupationDefinition.register_type(proprietor_type)
-
-    # Load character name data
-    load_names("last_name", filepath=_RESOURCES_DIR / "names" / "surnames.txt")
-    load_names("first_name", filepath=_RESOURCES_DIR / "names" / "neutral_names.txt")
-    load_names(
-        "feminine_first_name",
-        filepath=_RESOURCES_DIR / "names" / "feminine_names.txt",
-    )
-    load_names(
-        "masculine_first_name",
-        filepath=_RESOURCES_DIR / "names" / "masculine_names.txt",
-    )
-
-    # Load potential names for different structures in the town
-    load_names(
-        "restaurant_name",
-        filepath=_RESOURCES_DIR / "names" / "restaurant_names.txt",
-    )
-    load_names("bar_name", filepath=_RESOURCES_DIR / "names" / "bar_names.txt")
-
-    # Load potential names for the town
-    load_names(
-        "town_name", filepath=_RESOURCES_DIR / "names" / "US_settlement_names.txt"
-    )
-
-    # Load additional data from yaml
-    # YamlDataLoader(filepath=_RESOURCES_DIR / "data.yaml").load(ctx.engine)
+class DefaultNameDataPlugin(Plugin):
+    def setup(self, sim: Simulation, **kwargs) -> None:
+        self.initialize_tracery_name_factory(sim.get_engine())
+
+    def initialize_tracery_name_factory(self, engine: NeighborlyEngine) -> None:
+        # Load character name data
+        engine.name_generator.load_names(
+            rule_name="family_name", filepath=_RESOURCES_DIR / "names" / "surnames.txt"
+        )
+        engine.name_generator.load_names(
+            rule_name="first_name",
+            filepath=_RESOURCES_DIR / "names" / "neutral_names.txt",
+        )
+        engine.name_generator.load_names(
+            rule_name="feminine_first_name",
+            filepath=_RESOURCES_DIR / "names" / "feminine_names.txt",
+        )
+        engine.name_generator.load_names(
+            rule_name="masculine_first_name",
+            filepath=_RESOURCES_DIR / "names" / "masculine_names.txt",
+        )
+
+        # Load potential names for different structures in the town
+        engine.name_generator.load_names(
+            rule_name="restaurant_name",
+            filepath=_RESOURCES_DIR / "names" / "restaurant_names.txt",
+        )
+        engine.name_generator.load_names(
+            rule_name="bar_name", filepath=_RESOURCES_DIR / "names" / "bar_names.txt"
+        )
+
+        # Load potential names for the town
+        engine.name_generator.load_names(
+            rule_name="town_name",
+            filepath=_RESOURCES_DIR / "names" / "US_settlement_names.txt",
+        )
+
+
+class DefaultLifeEventPlugin(Plugin):
+    def setup(self, sim: Simulation, **kwargs) -> None:
+        EventRoles.register(
+            name="PotentialSpouse", event_role_type=potential_spouse_role()
+        )
+        EventRoles.register("SinglePerson", single_person_role_type())
+        EventRoles.register("HiringBusiness", hiring_business_role())
+        EventRoles.register("Unemployed", unemployed_role())
+        LifeEvents.register(hire_employee_event())
+        LifeEvents.register(marriage_event())
+        LifeEvents.register(become_friends_event())
+        LifeEvents.register(become_enemies_event())
+        LifeEvents.register(start_dating_event())
+        LifeEvents.register(dating_break_up_event())
+        LifeEvents.register(divorce_event())
+        LifeEvents.register(marriage_event())
+        LifeEvents.register(pregnancy_event())
+        LifeEvents.register(depart_due_to_unemployment())
+        LifeEvents.register(retire_event())
diff --git a/src/neighborly/plugins/default_plugin/activity.py b/src/neighborly/plugins/default_plugin/activity.py
deleted file mode 100644
index 3a36301..0000000
--- a/src/neighborly/plugins/default_plugin/activity.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from dataclasses import dataclass, field
-from typing import Dict, List, Tuple
-
-from neighborly.core.ecs import Component
-from neighborly.plugins.default_plugin.character_values import CharacterValues
-
-
-@dataclass(frozen=True)
-class Activity:
-    """Activities that a character can do at a location in the town
-
-    Attributes
-    ----------
-    name: str
-        The name of the activity
-    traits_names: Tuple[str, ...]
-        Character values that associated with this activity
-    character_traits: CharacterValues
-        CharacterValues instance that encodes the list of trait_names
-        as a vector of 0's and 1's for non-applicable and applicable
-        character values respectively.
-    """
-
-    name: str
-    trait_names: Tuple[str, ...]
-    character_traits: CharacterValues = field(init=False)
-
-    def __post_init__(self) -> None:
-        object.__setattr__(
-            self,
-            "character_traits",
-            CharacterValues({name: 1 for name in self.trait_names}, default=0),
-        )
-
-
-_activity_registry: Dict[str, Activity] = {}
-_activity_flags: Dict[str, int] = {}
-
-
-def register_activity(activity: Activity) -> None:
-    """Registers an activity instance for use in other places"""
-    next_flag = 1 << len(_activity_registry.keys())
-    _activity_registry[activity.name] = activity
-    _activity_flags[activity.name] = next_flag
-
-
-def get_activity_flags(*activities: str) -> Tuple[int, ...]:
-    """Return flags corresponding to given activities"""
-    return tuple([_activity_flags[activity] for activity in activities])
-
-
-def get_activity(activity: str) -> Activity:
-    """Return Activity instance corresponding to a given string"""
-    return _activity_registry[activity]
-
-
-def get_all_activities() -> List[Activity]:
-    """Return all activity instances in the registry"""
-    return list(_activity_registry.values())
-
-
-class ActivityCenter(Component):
-    def __init__(self, activities: List[str]) -> None:
-        self.activities: List[str] = activities
-        self.activity_flags: int = 0
-
-        for activity in self.activities:
-            self.activity_flags |= get_activity_flags(activity)[0]
-
-    def has_flags(self, *flag_strs: str) -> bool:
-        flags = get_activity_flags(*flag_strs)
-        for flag in flags:
-            if self.activity_flags & flag == 0:
-                return False
-        return True
diff --git a/src/neighborly/plugins/default_plugin/businesses.py b/src/neighborly/plugins/default_plugin/businesses.py
deleted file mode 100644
index 1bb5484..0000000
--- a/src/neighborly/plugins/default_plugin/businesses.py
+++ /dev/null
@@ -1,83 +0,0 @@
-from neighborly.core.business import BusinessDefinition, OccupationDefinition
-
-restaurant_type = BusinessDefinition(
-    name="Restaurant",
-    name_pattern="#restaurant_name#",
-    hours="10:00-21:00",
-    owner_type="Proprietor",
-    employees={
-        "Cook": 1,
-        "Server": 2,
-        "Host": 1,
-    },
-)
-
-bar_type = BusinessDefinition(
-    name="Bar",
-    name_pattern="#bar_name#",
-    hours="RFS 21:00-2:00",
-    owner_type="Proprietor",
-    employees={
-        "Bartender": 2,
-        "DJ": 1,
-        "Security": 1,
-        "Cook": 1
-    }
-)
-
-department_store_type = BusinessDefinition(
-    name="Department Store",
-    name_pattern="Department Store",
-    hours="MTWRF 9:00-17:00, SU 10:00-15:00",
-    owner_type="Owner",
-    employees={
-        "Cashier": 1,
-        "Sales Associate": 2,
-        "Manager": 1,
-    }
-)
-
-cashier_type = OccupationDefinition(
-    name="Cashier",
-    level=1,
-)
-
-sales_associate_type = OccupationDefinition(
-    name="Sales Associate",
-    level=2,
-)
-
-manager_type = OccupationDefinition(
-    name="Manager",
-    level=3
-)
-
-dj_type = OccupationDefinition(
-    name="DJ",
-    level=2
-)
-
-bartender_type = OccupationDefinition(
-    name="Bartender",
-    level=2
-)
-
-security_type = OccupationDefinition(
-    name="Security",
-    level=2
-)
-
-cook_type = OccupationDefinition(
-    name="Cook",
-    level=2
-)
-
-owner_type = OccupationDefinition(
-    name="Owner",
-    level=4,
-)
-
-proprietor_type = OccupationDefinition(
-    name="Proprietor",
-    level=4,
-)
diff --git a/src/neighborly/plugins/default_plugin/data/data.yaml b/src/neighborly/plugins/default_plugin/data/data.yaml
index b17edbd..1f23ed6 100644
--- a/src/neighborly/plugins/default_plugin/data/data.yaml
+++ b/src/neighborly/plugins/default_plugin/data/data.yaml
@@ -2,7 +2,7 @@ CharacterDefinitions:
   - name: BaseCharacter
     generation:
       first_name: "#first_name#"
-      last_name: "#last_name#"
+      last_name: "#family_name#"
       family:
         probability_spouse: 0.5
         probability_children: 0.5
@@ -11,7 +11,7 @@ CharacterDefinitions:
       lifespan: 85
       life_stages:
         child: 0
-        adolescent: 13
+        teen: 13
         young_adult: 18
         adult: 30
         elder: 85
@@ -24,18 +24,18 @@ Characters:
           character_def: BaseCharacter
           events:
             marriage:
-              effect: [on_married_behavior]
+              effect: [ on_married_behavior ]
             child-birth:
-              effect: [on_child_birth_behavior]
+              effect: [ on_child_birth_behavior ]
             move:
-              effect: [on_move_behavior]
+              effect: [ on_move_behavior ]
       - type: Routine
 Residences:
   - name: House
     components:
       - type: Location
         options:
-          activities: [Rest]
+          private: true
       - type: Residence
       - type: Position2D
 Businesses:
@@ -47,7 +47,7 @@ Businesses:
     components:
       - type: Location
         options:
-          activities: [Shopping]
+          activities: [ Shopping ]
           max capacity: 60
       - type: Business
         options:
@@ -63,7 +63,7 @@ Businesses:
     components:
       - type: Location
         options:
-          activities: [Eating, Socializing]
+          activities: [ Eating, Socializing ]
           max capacity: 20
       - type: Business
         options:
@@ -73,24 +73,22 @@ Businesses:
       - type: Location
         options:
           name: "#bar_name#"
-          activities: [Eating, Drinking, Socializing]
+          activities: [ Eating, Drinking, Socializing ]
           max capacity: 35
       - type: Business
         options:
           business_type: Bar
-Places:
   - name: Park
     components:
       - type: Location
         options:
-          activities: [Recreation]
+          private: no
           max capacity: 200
-
   - name: Library
     components:
       - type: Location
         options:
-          activities: [Reading, Studying]
+          private: no
           max capacity: 35
 Activities:
   - name: Rest
diff --git a/src/neighborly/plugins/default_plugin/helpers.py b/src/neighborly/plugins/default_plugin/helpers.py
deleted file mode 100644
index b2416ff..0000000
--- a/src/neighborly/plugins/default_plugin/helpers.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Helper functions used by various systems in this plugin"""
-from typing import List, Tuple
-
-import numpy as np
-
-from neighborly.core.ecs import GameObject, World
-from neighborly.core.life_event import LifeEvent
-from neighborly.core.relationship import Relationship, RelationshipModifier
-
-from .activity import ActivityCenter, get_activity_flags, get_all_activities
-from .character_values import CharacterValues
-
-
-def get_top_activities(
-    character_values: CharacterValues, n: int = 3
-) -> Tuple[str, ...]:
-    """Return the top activities a character would enjoy given their values"""
-
-    scores: List[Tuple[int, str]] = []
-
-    for activity in get_all_activities():
-        score: int = int(
-            np.dot(character_values.traits, activity.character_traits.traits)
-        )
-        scores.append((score, activity.name))
-
-    return tuple(
-        [
-            activity_score[1]
-            for activity_score in sorted(scores, key=lambda s: s[0], reverse=True)
-        ][:n]
-    )
-
-
-def find_places_with_activities(world: World, *activities: str) -> List[int]:
-    """Return a list of entity ID for locations that have the given activities"""
-    locations = world.get_component(ActivityCenter)
-
-    matches: List[int] = []
-
-    for location_id, location in locations:
-        if location.has_flags(*activities):
-            matches.append(location_id)
-
-    return matches
-
-
-def find_places_with_any_activities(world: World, *activities: str) -> List[int]:
-    """Return a list of entity ID for locations that have any of the given activities
-
-    Results are sorted by how many activities they match
-    """
-
-    flags = get_activity_flags(*activities)
-
-    def score_location(loc: ActivityCenter) -> int:
-        location_score: int = 0
-        for flag in flags:
-            if loc.activity_flags & flag > 0:
-                location_score += 1
-        return location_score
-
-    locations = world.get_component(ActivityCenter)
-
-    matches: List[Tuple[int, int]] = []
-
-    for location_id, location in locations:
-        score = score_location(location)
-        if score > 0:
-            matches.append((score, location_id))
-
-    return [match[1] for match in sorted(matches, key=lambda m: m[0], reverse=True)]
-
-
-def try_add_compatibility_modifier(gameobject: GameObject, event: LifeEvent) -> bool:
-    # Add compatibility if both characters have a Character Values component
-    world = gameobject.world
-    owner_has_values = gameobject.has_component(CharacterValues)
-    relationship: Relationship = event.data["relationship"]
-    target = world.get_gameobject(relationship.target)
-    target_has_values = target.has_component(CharacterValues)
-
-    if owner_has_values and target_has_values:
-        compatibility = CharacterValues.calculate_compatibility(
-            gameobject.get_component(CharacterValues),
-            target.get_component(CharacterValues),
-        )
-
-        relationship.add_modifier(
-            RelationshipModifier("Compatibility", friendship_increment=compatibility)
-        )
-
-        return True
-    else:
-        return False
diff --git a/src/neighborly/plugins/default_plugin/relationship_modifiers.py b/src/neighborly/plugins/default_plugin/relationship_modifiers.py
deleted file mode 100644
index db375ad..0000000
--- a/src/neighborly/plugins/default_plugin/relationship_modifiers.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from neighborly.core.relationship import RelationshipModifier
-
-
-def create_friend_modifier() -> RelationshipModifier:
-    return RelationshipModifier(
-        name="Friend",
-        description="These characters are firends",
-        salience_boost=30,
-        friendship_increment=1,
-    )
diff --git a/src/neighborly/plugins/default_plugin/systems.py b/src/neighborly/plugins/default_plugin/systems.py
deleted file mode 100644
index 05a8e79..0000000
--- a/src/neighborly/plugins/default_plugin/systems.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from logging import getLogger
-from typing import Dict, List, Optional
-
-from neighborly.core.business import BusinessDefinition
-from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import GameObject, World
-from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.life_event import LifeEvent
-from neighborly.core.location import Location
-from neighborly.core.position import Position2D
-from neighborly.core.relationship import Relationship, RelationshipModifier
-from neighborly.core.residence import Residence
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.time import SimDateTime
-from neighborly.core.town import Town
-
-logger = getLogger(__name__)
-
-
-class CityPlanner:
-    """Responsible for adding residents to the town"""
-
-    def __init__(self, weights: Optional[Dict[str, int]] = None):
-        self.weights: Dict[str, int] = {}
-        if weights:
-            self.weights.update(weights)
-
-    def __call__(self, world: World, **kwargs):
-        self.try_add_business(world)
-
-    def try_add_business(self, world: World) -> None:
-        town = world.get_resource(Town)
-        engine = world.get_resource(NeighborlyEngine)
-
-        # Get eligible businesses
-        eligible_business_types = self.get_eligible_types(world)
-
-        if not eligible_business_types:
-            return
-
-        business_type_to_build = engine.get_rng().choice(eligible_business_types).name
-
-        if town.layout.has_vacancy():
-            engine.filter_place_archetypes({"includes": []})
-            place = engine.create_business(business_type_to_build)
-            town.layout.allocate_space(place.id)
-            world.add_gameobject(place)
-            logger.debug(f"Added business {place}")
-
-    @classmethod
-    def get_eligible_types(cls, world: World) -> List["BusinessDefinition"]:
-        """
-        Return all business types that may be built
-        given the state of the simulation
-        """
-        population = world.get_resource(Town).population
-        engine = world.get_resource(NeighborlyEngine)
-
-        eligible_types: List["BusinessDefinition"] = []
-
-        for t in BusinessDefinition.get_all_types():
-            if t.instances >= t.max_instances:
-                continue
-
-            if population < t.min_population:
-                continue
-
-            try:
-                engine.get_business_archetype(t.name)
-            except KeyError:
-                continue
-
-            eligible_types.append(t)
-
-        return eligible_types
diff --git a/src/neighborly/plugins/talktown/__init__.py b/src/neighborly/plugins/talktown/__init__.py
index 4259eab..ef532eb 100644
--- a/src/neighborly/plugins/talktown/__init__.py
+++ b/src/neighborly/plugins/talktown/__init__.py
@@ -1,12 +1,18 @@
+import logging
 import os
 import pathlib
-import logging
 
-from neighborly.simulation import PluginContext
-from neighborly.loaders import YamlDataLoader
+import neighborly.plugins.talktown.business_archetypes as tot_businesses
+import neighborly.plugins.talktown.occupation_types as tot_occupations
+from neighborly.core.business import BusinessArchetypeLibrary, OccupationTypeLibrary
+from neighborly.core.character import CharacterArchetype, CharacterArchetypeLibrary
 from neighborly.core.ecs import World
-from neighborly.core.town import Town
-from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.residence import ResidenceArchetype, ResidenceArchetypeLibrary
+from neighborly.core.rng import DefaultRNG
+from neighborly.core.routine import Routine
+from neighborly.core.town import LandGrid
+from neighborly.plugins.talktown.school import SchoolSystem
+from neighborly.simulation import Plugin, Simulation
 
 logger = logging.getLogger(__name__)
 
@@ -31,12 +37,7 @@ def establish_town(world: World, **kwargs) -> None:
     This function is based on the original Simulation.establish_setting
     method in talktown.
     """
-    logger.debug("Establishing town")
-
-    engine = world.get_resource(NeighborlyEngine)
-    town = world.get_resource(Town)
-
-    vacant_lots = town.layout.get_vacancies()
+    vacant_lots = town.get_component(LandGrid).layout.get_vacancies()
     # Each family requires 2 lots (1 for a house, 1 for a business)
     # Save two lots for either a coalmine, quarry, or farm
     n_families_to_add = (len(vacant_lots) // 2) - 1
@@ -44,32 +45,180 @@ def establish_town(world: World, **kwargs) -> None:
     for _ in range(n_families_to_add - 1):
         # create residents
         # create Farm
-        farm = engine.create_business("Farm")
+        farm = world.spawn_archetype(BusinessArchetypeLibrary.get("Farm"))
         # trigger hiring event
         # trigger home move event
 
-    random_num = engine.get_rng().random()
+    random_num = world.get_resource(DefaultRNG).random()
     if random_num < 0.2:
         # Create a Coalmine 20% of the time
-        coalmine = engine.create_business("Coal Mine")
+        coal_mine = world.spawn_archetype(BusinessArchetypeLibrary.get("Coal Mine"))
     elif 0.2 <= random_num < 0.35:
         # Create a Quarry 15% of the time
-        quarry = engine.create_business("Quarry")
+        quarry = world.spawn_archetype(BusinessArchetypeLibrary.get("Quarry"))
     else:
         # Create Farm 65% of the time
-        farm = engine.create_business("Farm")
-
-    world.remove_system(establish_town)
+        farm = world.spawn_archetype(BusinessArchetypeLibrary.get("Farm"))
 
     logger.debug("Town established. 'establish_town' function removed from systems")
 
 
-def setup(ctx: PluginContext, **kwargs) -> None:
-    """Entry point for the plugin"""
-    logger.debug("Loading neighborly_talktown plugin")
-
-    YamlDataLoader(filepath=_RESOURCES_DIR / "data.yaml").load(ctx.engine)
-
-    ctx.world.add_system(establish_town)
-
-    logger.debug("Neighborly_talktown plugin loaded successfully")
+class TalkOfTheTownPlugin(Plugin):
+    def setup(self, sim: Simulation, **kwargs) -> None:
+        sim.add_system(SchoolSystem())
+
+        # Talk of the town only has one residence archetype
+        ResidenceArchetypeLibrary.register(ResidenceArchetype(name="House"))
+
+        # Talk of the town only has one character archetype
+        CharacterArchetypeLibrary.register(
+            CharacterArchetype(
+                name="Person",
+                name_format="#first_name# #family_name#",
+                extra_components={Routine: {}},
+                lifespan=85,
+                life_stages={
+                    "child": 0,
+                    "teen": 13,
+                    "young_adult": 18,
+                    "adult": 30,
+                    "elder": 65,
+                },
+            )
+        )
+
+        # Register OccupationTypes
+        OccupationTypeLibrary.add(tot_occupations.apprentice)
+        OccupationTypeLibrary.add(tot_occupations.architect)
+        OccupationTypeLibrary.add(tot_occupations.baker)
+        OccupationTypeLibrary.add(tot_occupations.banker)
+        OccupationTypeLibrary.add(tot_occupations.bank_teller)
+        OccupationTypeLibrary.add(tot_occupations.barber)
+        OccupationTypeLibrary.add(tot_occupations.barkeeper)
+        OccupationTypeLibrary.add(tot_occupations.bartender)
+        OccupationTypeLibrary.add(tot_occupations.bottler)
+        OccupationTypeLibrary.add(tot_occupations.blacksmith)
+        OccupationTypeLibrary.add(tot_occupations.brewer)
+        OccupationTypeLibrary.add(tot_occupations.bricklayer)
+        OccupationTypeLibrary.add(tot_occupations.builder)
+        OccupationTypeLibrary.add(tot_occupations.busboy)
+        OccupationTypeLibrary.add(tot_occupations.bus_driver)
+        OccupationTypeLibrary.add(tot_occupations.butcher)
+        OccupationTypeLibrary.add(tot_occupations.carpenter)
+        OccupationTypeLibrary.add(tot_occupations.cashier)
+        OccupationTypeLibrary.add(tot_occupations.clothier)
+        OccupationTypeLibrary.add(tot_occupations.concierge)
+        OccupationTypeLibrary.add(tot_occupations.cook)
+        OccupationTypeLibrary.add(tot_occupations.cooper)
+        OccupationTypeLibrary.add(tot_occupations.daycare_provider)
+        OccupationTypeLibrary.add(tot_occupations.dentist)
+        OccupationTypeLibrary.add(tot_occupations.dishwasher)
+        OccupationTypeLibrary.add(tot_occupations.distiller)
+        OccupationTypeLibrary.add(tot_occupations.doctor)
+        OccupationTypeLibrary.add(tot_occupations.dressmaker)
+        OccupationTypeLibrary.add(tot_occupations.druggist)
+        OccupationTypeLibrary.add(tot_occupations.engineer)
+        OccupationTypeLibrary.add(tot_occupations.farmer)
+        OccupationTypeLibrary.add(tot_occupations.farmhand)
+        OccupationTypeLibrary.add(tot_occupations.fire_chief)
+        OccupationTypeLibrary.add(tot_occupations.fire_fighter)
+        OccupationTypeLibrary.add(tot_occupations.grocer)
+        OccupationTypeLibrary.add(tot_occupations.groundskeeper)
+        OccupationTypeLibrary.add(tot_occupations.hotel_maid)
+        OccupationTypeLibrary.add(tot_occupations.inn_keeper)
+        OccupationTypeLibrary.add(tot_occupations.insurance_agent)
+        OccupationTypeLibrary.add(tot_occupations.janitor)
+        OccupationTypeLibrary.add(tot_occupations.jeweler)
+        OccupationTypeLibrary.add(tot_occupations.joiner)
+        OccupationTypeLibrary.add(tot_occupations.laborer)
+        OccupationTypeLibrary.add(tot_occupations.landlord)
+        OccupationTypeLibrary.add(tot_occupations.lawyer)
+        OccupationTypeLibrary.add(tot_occupations.manager)
+        OccupationTypeLibrary.add(tot_occupations.miner)
+        OccupationTypeLibrary.add(tot_occupations.milkman)
+        OccupationTypeLibrary.add(tot_occupations.mayor)
+        OccupationTypeLibrary.add(tot_occupations.molder)
+        OccupationTypeLibrary.add(tot_occupations.mortician)
+        OccupationTypeLibrary.add(tot_occupations.nurse)
+        OccupationTypeLibrary.add(tot_occupations.optometrist)
+        OccupationTypeLibrary.add(tot_occupations.owner)
+        OccupationTypeLibrary.add(tot_occupations.painter)
+        OccupationTypeLibrary.add(tot_occupations.pharmacist)
+        OccupationTypeLibrary.add(tot_occupations.plasterer)
+        OccupationTypeLibrary.add(tot_occupations.plastic_surgeon)
+        OccupationTypeLibrary.add(tot_occupations.plumber)
+        OccupationTypeLibrary.add(tot_occupations.police_chief)
+        OccupationTypeLibrary.add(tot_occupations.principal)
+        OccupationTypeLibrary.add(tot_occupations.professor)
+        OccupationTypeLibrary.add(tot_occupations.proprietor)
+        OccupationTypeLibrary.add(tot_occupations.puddler)
+        OccupationTypeLibrary.add(tot_occupations.quarry_man)
+        OccupationTypeLibrary.add(tot_occupations.realtor)
+        OccupationTypeLibrary.add(tot_occupations.secretary)
+        OccupationTypeLibrary.add(tot_occupations.stocker)
+        OccupationTypeLibrary.add(tot_occupations.shoemaker)
+        OccupationTypeLibrary.add(tot_occupations.stonecutter)
+        OccupationTypeLibrary.add(tot_occupations.tailor)
+        OccupationTypeLibrary.add(tot_occupations.tattoo_artist)
+        OccupationTypeLibrary.add(tot_occupations.taxi_driver)
+        OccupationTypeLibrary.add(tot_occupations.teacher)
+        OccupationTypeLibrary.add(tot_occupations.turner)
+        OccupationTypeLibrary.add(tot_occupations.waiter)
+        OccupationTypeLibrary.add(tot_occupations.whitewasher)
+        OccupationTypeLibrary.add(tot_occupations.woodworker)
+
+        # Register Business Archetypes
+        BusinessArchetypeLibrary.add(tot_businesses.apartment_complex)
+        BusinessArchetypeLibrary.add(tot_businesses.bakery)
+        BusinessArchetypeLibrary.add(tot_businesses.bank)
+        BusinessArchetypeLibrary.add(tot_businesses.bar)
+        BusinessArchetypeLibrary.add(tot_businesses.barbershop)
+        BusinessArchetypeLibrary.add(tot_businesses.bus_depot)
+        BusinessArchetypeLibrary.add(tot_businesses.carpentry_company)
+        BusinessArchetypeLibrary.add(tot_businesses.cemetery)
+        BusinessArchetypeLibrary.add(tot_businesses.city_hall)
+        BusinessArchetypeLibrary.add(tot_businesses.clothing_store)
+        BusinessArchetypeLibrary.add(tot_businesses.coal_mine)
+        BusinessArchetypeLibrary.add(tot_businesses.construction_firm)
+        BusinessArchetypeLibrary.add(tot_businesses.dairy)
+        BusinessArchetypeLibrary.add(tot_businesses.day_care)
+        BusinessArchetypeLibrary.add(tot_businesses.deli)
+        BusinessArchetypeLibrary.add(tot_businesses.dentist_office)
+        BusinessArchetypeLibrary.add(tot_businesses.department_store)
+        BusinessArchetypeLibrary.add(tot_businesses.diner)
+        BusinessArchetypeLibrary.add(tot_businesses.distillery)
+        BusinessArchetypeLibrary.add(tot_businesses.drug_store)
+        BusinessArchetypeLibrary.add(tot_businesses.farm)
+        BusinessArchetypeLibrary.add(tot_businesses.fire_station)
+        BusinessArchetypeLibrary.add(tot_businesses.foundry)
+        BusinessArchetypeLibrary.add(tot_businesses.furniture_store)
+        BusinessArchetypeLibrary.add(tot_businesses.general_store)
+        BusinessArchetypeLibrary.add(tot_businesses.grocery_store)
+        BusinessArchetypeLibrary.add(tot_businesses.hardware_store)
+        BusinessArchetypeLibrary.add(tot_businesses.hospital)
+        BusinessArchetypeLibrary.add(tot_businesses.hotel)
+        BusinessArchetypeLibrary.add(tot_businesses.inn)
+        BusinessArchetypeLibrary.add(tot_businesses.insurance_company)
+        BusinessArchetypeLibrary.add(tot_businesses.jewelry_shop)
+        BusinessArchetypeLibrary.add(tot_businesses.law_firm)
+        BusinessArchetypeLibrary.add(tot_businesses.optometry_clinic)
+        BusinessArchetypeLibrary.add(tot_businesses.painting_company)
+        BusinessArchetypeLibrary.add(tot_businesses.park)
+        BusinessArchetypeLibrary.add(tot_businesses.pharmacy)
+        BusinessArchetypeLibrary.add(tot_businesses.plastic_surgery_clinic)
+        BusinessArchetypeLibrary.add(tot_businesses.plumbing_company)
+        BusinessArchetypeLibrary.add(tot_businesses.police_station)
+        BusinessArchetypeLibrary.add(tot_businesses.quarry)
+        BusinessArchetypeLibrary.add(tot_businesses.realty_firm)
+        BusinessArchetypeLibrary.add(tot_businesses.restaurant)
+        BusinessArchetypeLibrary.add(tot_businesses.school)
+        BusinessArchetypeLibrary.add(tot_businesses.shoemaker_shop)
+        BusinessArchetypeLibrary.add(tot_businesses.supermarket)
+        BusinessArchetypeLibrary.add(tot_businesses.tailor_shop)
+        BusinessArchetypeLibrary.add(tot_businesses.tattoo_parlor)
+        BusinessArchetypeLibrary.add(tot_businesses.tavern)
+        BusinessArchetypeLibrary.add(tot_businesses.taxi_depot)
+
+
+def get_plugin() -> Plugin:
+    return TalkOfTheTownPlugin()
diff --git a/src/neighborly/plugins/talktown/business_archetypes.py b/src/neighborly/plugins/talktown/business_archetypes.py
new file mode 100644
index 0000000..a24d633
--- /dev/null
+++ b/src/neighborly/plugins/talktown/business_archetypes.py
@@ -0,0 +1,404 @@
+from neighborly.core.business import BusinessArchetype
+
+apartment_complex = BusinessArchetype(
+    name="Apartment Complex",
+    hours=["day"],
+    owner_type="Landlord",
+    max_instances=99,
+    min_population=50,
+    services=["housing"],
+    # TODO: Create and add component for ApartmentComplexes
+    extra_components={},
+    employee_types={
+        "Janitor": 1,
+    },
+)
+
+
+bakery = BusinessArchetype(
+    name="Bakery",
+    owner_type="Baker",
+    employee_types={"Apprentice": 1},
+    services=["shopping", "errands"],
+)
+
+bank = BusinessArchetype(
+    name="Bank",
+    hours=["day", "afternoon"],
+    owner_type="Banker",
+    services=["errands"],
+    employee_types={"Bank Teller": 3, "Janitor": 1, "Manager": 1},
+)
+
+bar = BusinessArchetype(
+    name="Bar",
+    hours=["evening", "night"],
+    owner_type="Owner",
+    employee_types={"Bartender": 4, "Manager": 1},
+    services=["drinking", "socializing"],
+)
+
+barbershop = BusinessArchetype(
+    name="Barbershop", hours=["day"], owner_type="Barber", employee_types={"Cashier": 1}
+)
+
+bus_depot = BusinessArchetype(
+    name="Bus Depot",
+    hours=["day", "evening"],
+    employee_types={
+        "Secretary": 1,
+        "Bus Driver": 3,
+        "Manager": 1,
+    },
+)
+
+carpentry_company = BusinessArchetype(
+    name="Carpentry Company",
+    owner_type="Carpenter",
+    employee_types={"Apprentice": 1, "Builder": 1},
+    min_population=70,
+    max_instances=1,
+)
+
+cemetery = BusinessArchetype(
+    name="Cemetery",
+    owner_type="Mortician",
+    employee_types={"Secretary": 1, "Apprentice": 1, "Groundskeeper": 1},
+    max_instances=1,
+)
+
+
+city_hall = BusinessArchetype(
+    name="City Hall",
+    employee_types={
+        "Secretary": 2,
+        "Janitor": 1,
+    },
+    min_population=100,
+    max_instances=1,
+)
+
+clothing_store = BusinessArchetype(
+    name="Clothing Store",
+    owner_type="Clothier",
+    employee_types={"Cashier": 2, "Seamstress": 1, "Dressmaker": 1, "Tailor": 1},
+    max_instances=2,
+    services=["shopping"],
+)
+
+coal_mine = BusinessArchetype(
+    name="Coal Mine",
+    owner_type="Owner",
+    employee_types={"Miner": 2, "Manager": 1},
+    max_instances=1,
+)
+
+construction_firm = BusinessArchetype(
+    name="Construction Firm",
+    max_instances=1,
+    min_population=80,
+    owner_type="Architect",
+    employee_types={"Secretary": 1, "Builder": 3, "Bricklayer": 1},
+)
+
+dairy = BusinessArchetype(
+    name="Dairy",
+    owner_type="Milkman",
+    employee_types={"Apprentice": 1, "Bottler": 1},
+    max_instances=1,
+    min_population=30,
+)
+
+day_care = BusinessArchetype(
+    name="Daycare Service",
+    owner_type="Daycare Provider",
+    employee_types={"Daycare Provider": 2},
+    min_population=200,
+    max_instances=1,
+)
+
+deli = BusinessArchetype(
+    name="Deli",
+    owner_type="Proprietor",
+    employee_types={"Cashier": 2, "Manager": 1},
+    min_population=100,
+    max_instances=2,
+    services=["shopping"],
+)
+
+dentist_office = BusinessArchetype(
+    name="Dentist Office",
+    owner_type="Dentist",
+    employee_types={"Nurse": 2, "Secretary": 1},
+    min_population=75,
+)
+
+department_store = BusinessArchetype(
+    name="Department Store",
+    owner_type="Owner",
+    employee_types={"Cashier": 2, "Manager": 1},
+    min_population=200,
+    max_instances=1,
+    services=["shopping"],
+)
+
+diner = BusinessArchetype(
+    name="Diner",
+    owner_type="Proprietor",
+    employee_types={"Cook": 1, "Waiter": 1, "Busboy": 1, "Manager": 1},
+    year_available=1945,
+    min_population=30,
+    max_instances=3,
+    services=["food", "socializing"],
+)
+
+distillery = BusinessArchetype(
+    name="Distillery",
+    owner_type="Distiller",
+    employee_types={"Bottler": 1, "Cooper": 1},
+    max_instances=1,
+)
+
+drug_store = BusinessArchetype(
+    name="Drug Store",
+    owner_type="Druggist",
+    employee_types={"Cashier": 1},
+    min_population=30,
+    max_instances=1,
+    services=["shopping"],
+)
+
+farm = BusinessArchetype(
+    name="Farm", owner_type="Farmer", employee_types={"Farmhand": 2}, max_instances=99
+)
+
+fire_station = BusinessArchetype(
+    name="Fire Station",
+    employee_types={"Fire Chief": 1, "Fire Fighter": 2},
+    min_population=100,
+    max_instances=1,
+)
+
+foundry = BusinessArchetype(
+    name="Foundry",
+    owner_type="Owner",
+    employee_types={"Molder": 1, "Puddler": 1},
+    year_available=1830,
+    year_obsolete=1950,
+    min_population=20,
+    max_instances=1,
+)
+
+furniture_store = BusinessArchetype(
+    name="Furniture Store",
+    owner_type="Woodworker",
+    employee_types={"Apprentice": 1},
+    min_population=20,
+    max_instances=1,
+    services=["shopping"],
+)
+
+general_store = BusinessArchetype(
+    name="General Store",
+    owner_type="Grocer",
+    employee_types={"Manager": 1, "Stocker": 1, "Cashier": 1},
+    min_population=20,
+    max_instances=2,
+    services=["shopping"],
+)
+
+grocery_store = BusinessArchetype(
+    name="Grocery Store",
+    owner_type="Grocer",
+    employee_types={"Manager": 1, "Stocker": 1, "Cashier": 1},
+    min_population=20,
+    max_instances=2,
+    services=["shopping"],
+)
+
+hardware_store = BusinessArchetype(
+    name="Hardware Store",
+    owner_type="Proprietor",
+    employee_types={"Manager": 1, "Stocker": 1, "Cashier": 1},
+    min_population=40,
+    max_instances=2,
+    services=["shopping"],
+)
+
+hospital = BusinessArchetype(
+    name="Hospital",
+    employee_types={"Doctor": 2, "Nurse": 1, "Secretary": 1},
+    min_population=200,
+)
+
+hotel = BusinessArchetype(
+    name="Hotel",
+    owner_type="Owner",
+    employee_types={"Hotel Maid": 2, "Concierge": 1, "Manager": 1},
+)
+
+inn = BusinessArchetype(
+    name="Inn",
+    owner_type="Innkeeper",
+    employee_types={"Hotel Maid": 2, "Concierge": 1, "Manager": 1},
+)
+
+insurance_company = BusinessArchetype(
+    name="Insurance Company",
+    owner_type="Insurance Agent",
+    employee_types={"Secretary": 1, "Insurance Agent": 2},
+    max_instances=1,
+)
+
+jewelry_shop = BusinessArchetype(
+    name="Jewelry Shop",
+    owner_type="Jeweler",
+    employee_types={"Cashier": 1, "Apprentice": 1},
+    min_population=200,
+    max_instances=1,
+    services=["shopping"],
+)
+
+law_firm = BusinessArchetype(
+    name="Law Firm",
+    owner_type="Lawyer",
+    employee_types={"Lawyer": 1, "Secretary": 1},
+    min_population=150,
+    max_instances=2,
+)
+
+
+optometry_clinic = BusinessArchetype(
+    name="Optometry Clinic",
+    owner_type="Optometrist",
+    employee_types={"Secretary": 1, "Nurse": 1},
+    year_available=1990,
+)
+
+painting_company = BusinessArchetype(
+    name="Painting Company",
+    owner_type="Painter",
+    employee_types={"Painter": 1, "Plasterer": 1},
+)
+
+park = BusinessArchetype(
+    name="Park",
+    employee_types={"Groundskeeper": 1},
+    min_population=100,
+    max_instances=99,
+    services=["shopping", "socializing", "recreation"],
+)
+
+pharmacy = BusinessArchetype(
+    name="Pharmacy",
+    owner_type="Pharmacist",
+    employee_types={"Pharmacist": 1, "Cashier": 1},
+    year_available=1960,
+)
+
+plastic_surgery_clinic = BusinessArchetype(
+    name="Plastic Surgery Clinic",
+    owner_type="Plastic Surgeon",
+    employee_types={"Nurse": 1, "Secretary": 1},
+    min_population=200,
+    max_instances=1,
+    year_available=1970,
+)
+
+plumbing_company = BusinessArchetype(
+    name="Plumbing Company",
+    owner_type="Plumber",
+    employee_types={"Apprentice": 1, "Secretary": 1},
+    min_population=100,
+    max_instances=2,
+)
+
+police_station = BusinessArchetype(
+    name="Police Station",
+    employee_types={"Police Chief": 1, "Police Officer": 2},
+    min_population=100,
+    max_instances=1,
+)
+
+quarry = BusinessArchetype(
+    name="Quarry",
+    owner_type="Owner",
+    employee_types={"Quarryman": 1, "Stone Cutter": 1, "Laborer": 1, "Engineer": 1},
+    max_instances=1,
+)
+
+realty_firm = BusinessArchetype(
+    name="Realty Firm",
+    owner_type="Realtor",
+    employee_types={"Realtor": 1, "Secretary": 1},
+    min_population=80,
+    max_instances=2,
+)
+
+restaurant = BusinessArchetype(
+    name="Restaurant",
+    owner_type="Proprietor",
+    employee_types={"Waiter": 1, "Cook": 1, "Busboy": 1, "Manager": 1},
+    min_population=50,
+    services=["food", "socializing"],
+)
+
+school = BusinessArchetype(
+    name="School",
+    employee_types={"Principal": 1, "Teacher": 2, "Janitor": 1},
+    max_instances=1,
+)
+
+shoemaker_shop = BusinessArchetype(
+    name="Shoemaker Shop",
+    owner_type="Shoemaker",
+    employee_types={"Apprentice": 1},
+    services=["shopping"],
+)
+
+supermarket = BusinessArchetype(
+    name="Supermarket",
+    owner_type="Owner",
+    employee_types={"Cashier": 1, "Stocker": 1, "Manager": 1},
+    min_population=200,
+    max_instances=1,
+    services=["shopping"],
+)
+
+tailor_shop = BusinessArchetype(
+    name="Tailor Shop",
+    owner_type="Tailor",
+    employee_types={"Apprentice": 1},
+    min_population=40,
+    max_instances=1,
+    services=["shopping"],
+)
+
+tattoo_parlor = BusinessArchetype(
+    name="Tattoo Parlor",
+    year_available=1970,
+    min_population=300,
+    max_instances=1,
+    owner_type="Tattoo Artist",
+    employee_types={"Cashier": 1},
+    hours=["afternoon"],
+)
+
+tavern = BusinessArchetype(
+    name="Tavern",
+    hours=["evening", "night"],
+    employee_types={"Cook": 1, "Bartender": 1, "Waiter": 1},
+    min_population=20,
+    services=["drinking", "socializing"],
+)
+
+
+taxi_depot = BusinessArchetype(
+    name="Taxi Depot",
+    owner_type="Proprietor",
+    employee_types={"Taxi Driver": 3},
+    max_instances=1,
+    year_available=1930,
+    year_obsolete=9999,
+    hours=["day", "evening"],
+)
diff --git a/src/neighborly/plugins/talktown/components.py b/src/neighborly/plugins/talktown/components.py
new file mode 100644
index 0000000..212b5cb
--- /dev/null
+++ b/src/neighborly/plugins/talktown/components.py
@@ -0,0 +1,12 @@
+from neighborly.core.ecs import Component
+
+
+class ApartmentComplex(Component):
+    """
+    Apartment complex manages gameobjects with Apartment component instances.
+
+    When the apartment complex is closed, all apartments are deleted, and
+    the residents are displaced
+    """
+
+    pass
diff --git a/src/neighborly/plugins/talktown/data.yaml b/src/neighborly/plugins/talktown/data.yaml
deleted file mode 100644
index 8f32ccc..0000000
--- a/src/neighborly/plugins/talktown/data.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-BusinessDefinitions:
-    - name: 'Bakery'
-      hours: 'MTWRFS 8:00 - 18:00'
-      owner_type: 'Baker'
-      employees:
-          Baker: 1
-    - name: 'Bank'
-      hours: 'MTWRFS 7:00 - 18:00'
-      owner_type: 'Banker'
-      employees:
-          Banker: 1
-          BankTeller: 2
-    - name: 'Bar'
-      hours: 'TWR: 16:00 - 23:00, FSU: 21:00 - 02:00'
-      owner_type: 'Owner'
-      employees:
-          Bartender: 2
-    - name: 'Barbershop'
-      hours: 'MTWRFS: 10:00 - 15:00'
-      owner_type: 'Barber'
-      employees:
-          Barber: 1
-          Apprentice: 1
-    - name: 'Farm'
-      owner_type: 'Farmer'
-      hours: 'everyday 5AM to 5PM'
-      max_instances: 99
-      employees:
-          Farmhand: 2
-    - name: 'Quarry'
-      hours: 'MTWRFS 07:00 - 15:00'
-      owner_type: 'Owner'
-      employees:
-          Quarryman: 1
-          StoneCutter: 1
-          Laborer: 2
-          Engineer: 1
-    - name: 'Coal Mine'
-      hours: 'WTWRFS 07:00 - 15:00'
-      owner_type: 'Owner'
-      employees:
-          Miner: 2
-          Manager: 1
-Businesses:
-    - name: BaseBusiness
-      template: yes
-      components:
-          - type: Location
-    - name: Farm
-      components:
-          - type: Business
-            options:
-                business_type: 'Farm'
-    - name: Coal Mine
-      components:
-          - type: Business
-            options:
-                business_type: 'Coal Mine'
-    - name: Quarry
-      components:
-          - type: Business
-            options:
-                business_type: 'Quarry'
-Residences:
-  - name: House
-    components:
-      - type: Location
-        options:
-          activities: [Rest]
-      - type: Residence
-      - type: Position2D
-CharacterDefinitions:
-  - name: Human
-    generation:
-      first_name: "#first_name#"
-      last_name: "#last_name#"
-      family:
-        probability_spouse: 0.5
-        probability_children: 0.5
-        num_children: "0-2"
-    lifecycle:
-      lifespan: 85
-      life_stages:
-        child: 0
-        adolescent: 13
-        young_adult: 18
-        adult: 30
-        elder: 85
-Characters:
-  - name: Human
-    components:
-      - type: GameCharacter
-        options:
-          character_def: Human
diff --git a/src/neighborly/plugins/talktown/helpers.py b/src/neighborly/plugins/talktown/helpers.py
deleted file mode 100644
index 0841708..0000000
--- a/src/neighborly/plugins/talktown/helpers.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""
-helpers.py
-
-This file contains helper functions that can be used to
-modify the simulation state in response to certain events
-"""
-
-
-def generate_child_name():
-    """Generate the name for a child based"""
-    ...
-
-
-def rename_character(character, name) -> None:
-    """Change the name of the character to the given name"""
-    ...
-
-
-def add_neighbor_relationship_flags(character, residence, world) -> None:
-    """
-    Finds the characters living in adjacent residences and adds
-    a neighbor flag to this characters relationship to them in
-    the relationship network.
-    """
-    ...
-
-
-def remove_neighbor_relationship_flags(character, residence) -> None:
-    """
-    Remove the neighbor flag from the relationship and add a former-neighbor flags
-    for all characters that currently have the neighbor flag
-    """
-    ...
-
-
-def combine_families(family_a, family_b):
-    """Combines two nuclear families into a single nuclear family component"""
-    ...
-
-
-def move_into_residence(character, family):
-    """Moves a character into a home"""
-    ...
-
-
-def add_grieving_status(character):
-    """Adds the grieving status to a character"""
-    ...
-
-
-def remove_grieving_status(character):
-    """Removes grieving status from character"""
-    ...
diff --git a/src/neighborly/plugins/talktown/occupation_types.py b/src/neighborly/plugins/talktown/occupation_types.py
new file mode 100644
index 0000000..1fef1f8
--- /dev/null
+++ b/src/neighborly/plugins/talktown/occupation_types.py
@@ -0,0 +1,528 @@
+"""
+OccupationTypes adapted from Talk of the Town.
+
+Sources:
+https://github.com/ShiJbey/talktown/blob/python3/talktown/occupation.py
+https://github.com/ShiJbey/talktown/blob/python3/talktown/config/businesses_config.py
+"""
+
+from neighborly.builtin.helpers import (
+    after_year,
+    has_any_work_experience,
+    has_experience_as_a,
+    is_college_graduate,
+    is_gender,
+)
+from neighborly.core.business import (
+    OccupationType,
+    join_preconditions,
+    or_preconditions,
+)
+
+apprentice = OccupationType(name="Apprentice", level=1, precondition=is_gender("male"))
+
+architect = OccupationType(
+    name="Architect",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1977)), is_college_graduate()
+    ),
+)
+
+bottler = OccupationType(
+    name="Bottler",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1943)),
+)
+
+bricklayer = OccupationType(
+    name="Bricklayer",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+builder = OccupationType(
+    name="Builder",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+cashier = OccupationType(
+    name="Cashier",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1917)),
+)
+
+cook = OccupationType(
+    name="Cook",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1966)),
+)
+
+dishwasher = OccupationType(
+    name="Dishwasher",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1966)),
+)
+
+groundskeeper = OccupationType(
+    name="Groundskeeper",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+
+hotel_maid = OccupationType(
+    name="Hotel Maid",
+    level=1,
+    precondition=is_gender("female"),
+)
+
+janitor = OccupationType(
+    name="Janitor",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1966)),
+)
+
+laborer = OccupationType(
+    name="Laborer",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+secretary = OccupationType(
+    name="Secretary",
+    level=1,
+    precondition=or_preconditions(is_gender("female")),
+)
+
+waiter = OccupationType(
+    name="Waiter",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1917)),
+)
+
+
+whitewasher = OccupationType(
+    name="Whitewasher",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+
+busboy = OccupationType(
+    name="Busboy",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+stocker = OccupationType(
+    name="Stocker",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1943)),
+)
+
+seamstress = OccupationType(
+    name="Waiter",
+    level=1,
+    precondition=or_preconditions(is_gender("female")),
+)
+
+farmer = OccupationType(
+    name="Farmer",
+    level=2,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+farmhand = OccupationType(
+    name="Farmhand",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+miner = OccupationType(
+    name="Miner",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+painter = OccupationType(
+    name="Painter",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+banker = OccupationType(
+    name="Banker",
+    level=4,
+    precondition=or_preconditions(is_gender("male"), after_year(1968)),
+)
+
+bank_teller = OccupationType(
+    name="Bank Teller",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1950)),
+)
+
+grocer = OccupationType(
+    name="Grocer",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1966)),
+)
+
+bartender = OccupationType(
+    name="Bartender",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1968)),
+)
+
+concierge = OccupationType(
+    name="Concierge",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1968)),
+)
+
+daycare_provider = OccupationType(
+    name="Daycare Provider",
+    level=1,
+    precondition=or_preconditions(is_gender("female"), after_year(1977)),
+)
+
+landlord = OccupationType(
+    name="Landlord",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1925)),
+)
+
+baker = OccupationType(
+    name="Baker",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1935)),
+)
+
+cooper = OccupationType(
+    name="Cooper",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+barkeeper = OccupationType(
+    name="Barkeeper",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+milkman = OccupationType(
+    name="Milkman",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+plasterer = OccupationType(
+    name="Plasterer",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+barber = OccupationType(
+    name="Barber",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+butcher = OccupationType(
+    name="Butcher",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+fire_fighter = OccupationType(
+    name="Fire Fighter",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+carpenter = OccupationType(
+    name="Carpenter",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+taxi_driver = OccupationType(
+    name="Taxi Driver",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+bus_driver = OccupationType(
+    name="Bus Driver",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1972)),
+)
+
+blacksmith = OccupationType(
+    name="Blacksmith",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+woodworker = OccupationType(
+    name="Woodworker",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+stonecutter = OccupationType(
+    name="Stonecutter",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+dressmaker = OccupationType(
+    name="Dressmaker",
+    level=1,
+    precondition=or_preconditions(is_gender("female"), after_year(1972)),
+)
+
+distiller = OccupationType(
+    name="Distiller",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+plumber = OccupationType(
+    name="Plumber",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+joiner = OccupationType(
+    name="Joiner",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+inn_keeper = OccupationType(
+    name="Innkeeper",
+    level=1,
+    precondition=or_preconditions(is_gender("male"), after_year(1928)),
+)
+
+nurse = OccupationType(
+    name="Nurse",
+    level=1,
+    precondition=or_preconditions(is_gender("female"), after_year(1977)),
+)
+
+shoemaker = OccupationType(
+    name="Shoemaker",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1960)),
+)
+
+brewer = OccupationType(
+    name="Brewer",
+    level=2,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+tattoo_artist = OccupationType(
+    name="Tattoo Artist",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1972)),
+)
+
+puddler = OccupationType(
+    name="Puddler",
+    level=1,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+clothier = OccupationType(
+    name="Clothier",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1930)),
+)
+
+teacher = OccupationType(
+    name="Teacher",
+    level=2,
+    precondition=or_preconditions(is_gender("female"), after_year(1955)),
+)
+
+principal = OccupationType(
+    name="Principal",
+    level=3,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1965)),
+        has_experience_as_a("Teacher"),
+    ),
+)
+
+tailor = OccupationType(
+    name="Tailor",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1955)),
+)
+
+molder = OccupationType(
+    name="Molder",
+    level=2,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+turner = OccupationType(
+    name="Turner",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+quarry_man = OccupationType(
+    name="Quarryman",
+    level=2,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+proprietor = OccupationType(
+    name="Proprietor",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1955)),
+)
+
+
+dentist = OccupationType(
+    name="Dentist",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1972)), is_college_graduate()
+    ),
+)
+
+doctor = OccupationType(
+    name="Doctor",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1972)),
+        has_any_work_experience(),
+        is_college_graduate(),
+    ),
+)
+
+druggist = OccupationType(
+    name="Druggist",
+    level=3,
+    precondition=or_preconditions(is_gender("male")),
+)
+
+engineer = OccupationType(
+    name="Engineer",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1977)), is_college_graduate()
+    ),
+)
+
+fire_chief = OccupationType(
+    name="Fire Chief",
+    level=3,
+    precondition=join_preconditions(
+        is_gender("male"), has_experience_as_a("Fire Fighter")
+    ),
+)
+
+insurance_agent = OccupationType(
+    name="Insurance Agent",
+    level=3,
+    precondition=or_preconditions(is_gender("male"), after_year(1972)),
+)
+
+jeweler = OccupationType(
+    name="Jeweler",
+    level=3,
+    precondition=or_preconditions(is_gender("male"), after_year(1972)),
+)
+
+
+lawyer = OccupationType(
+    name="Lawyer",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1977)),
+        has_any_work_experience(),
+        is_college_graduate(),
+    ),
+)
+
+manager = OccupationType(
+    name="Manager",
+    level=2,
+    precondition=or_preconditions(is_gender("male"), after_year(1972)),
+)
+
+mayor = OccupationType(
+    name="Mayor",
+    level=5,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+mortician = OccupationType(
+    name="Mortician",
+    level=3,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+owner = OccupationType(
+    name="Owner",
+    level=5,
+    precondition=or_preconditions(is_gender("male"), after_year(1977)),
+)
+
+professor = OccupationType(
+    name="Professor",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1962)), is_college_graduate()
+    ),
+)
+
+optometrist = OccupationType(
+    name="Optometrist",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1972)), is_college_graduate()
+    ),
+)
+
+
+pharmacist = OccupationType(
+    name="Pharmacist",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1972)), is_college_graduate()
+    ),
+)
+
+plastic_surgeon = OccupationType(
+    name="Plastic Surgeon",
+    level=4,
+    precondition=join_preconditions(
+        or_preconditions(is_gender("male"), after_year(1972)),
+        has_any_work_experience(),
+        is_college_graduate(),
+    ),
+)
+
+police_chief = OccupationType(
+    name="Police Chief",
+    level=3,
+    precondition=join_preconditions(
+        is_gender("male"), has_experience_as_a("Police Officer")
+    ),
+)
+
+
+realtor = OccupationType(
+    name="Realtor",
+    level=3,
+    precondition=or_preconditions(is_gender("male"), after_year(1966)),
+)
diff --git a/src/neighborly/plugins/talktown/personality.py b/src/neighborly/plugins/talktown/personality.py
index 0565612..8ab8b1c 100644
--- a/src/neighborly/plugins/talktown/personality.py
+++ b/src/neighborly/plugins/talktown/personality.py
@@ -3,11 +3,12 @@
 to make character decisions and determine
 compatibility in social interactions
 """
-from typing import Optional
+from __future__ import annotations
 
-from neighborly.core.ecs import Component
-from neighborly.core.engine import AbstractFactory, ComponentDefinition
+from typing import Any, Dict
 
+from neighborly.core.ecs import Component, World
+from neighborly.core.engine import NeighborlyEngine
 
 BIG_FIVE_FLOOR = -1.0
 BIG_FIVE_CAP = 1.0
@@ -39,36 +40,44 @@ def __init__(
         self.agreeableness: float = agreeableness
         self.neuroticism: float = neuroticism
 
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            **super().to_dict(),
+            "openness": self.openness,
+            "conscientiousness": self.conscientiousness,
+            "extroversion": self.extroversion,
+            "agreeableness": self.agreeableness,
+            "neuroticism": self.neuroticism,
+        }
+
+    @classmethod
+    def create(cls, world: World, **kwargs) -> BigFivePersonality:
+        """Create new instance given component spec"""
+        engine: NeighborlyEngine = kwargs["engine"]
 
-class BigFivePersonalityFactory(AbstractFactory):
-    """Creates instances of BigFivePersonality components"""
+        openness: float = (
+                              engine.rng.random() * (BIG_FIVE_CAP - BIG_FIVE_FLOOR)
+                          ) + BIG_FIVE_FLOOR
 
-    def __init__(self) -> None:
-        super().__init__("BigFivePersonality")
+        conscientiousness: float = (
+                                       engine.rng.random() * (BIG_FIVE_CAP - BIG_FIVE_FLOOR)
+                                   ) + BIG_FIVE_FLOOR
 
-    def create(self, spec: ComponentDefinition, **kwargs) -> BigFivePersonality:
-        """Create new instance given component spec"""
-        raise NotImplementedError()
-
-
-def personality_from_parents(
-    parent_a: BigFivePersonality, parent_b: Optional[BigFivePersonality] = None
-) -> BigFivePersonality:
-    """
-    Create a new BigFivePersonality instance using the personality
-    models from parents
-
-    Parameters
-    ----------
-    parent_a : BigFivePersonality
-        Personality model for the first parent
-    parent_b : Optional[BigFivePersonality]
-        Optional personality model for a second parent
-
-    Returns
-    -------
-    BigFivePersonality
-        The final generated personality model
-    """
-
-    raise NotImplementedError()
+        extroversion: float = (
+                                  engine.rng.random() * (BIG_FIVE_CAP - BIG_FIVE_FLOOR)
+                              ) + BIG_FIVE_FLOOR
+
+        agreeableness: float = (
+                                   engine.rng.random() * (BIG_FIVE_CAP - BIG_FIVE_FLOOR)
+                               ) + BIG_FIVE_FLOOR
+
+        neuroticism: float = (
+                                 engine.rng.random() * (BIG_FIVE_CAP - BIG_FIVE_FLOOR)
+                             ) + BIG_FIVE_FLOOR
+
+        return cls(
+            openness, conscientiousness, extroversion, agreeableness, neuroticism
+        )
+
+    def __repr__(self) -> str:
+        return self.to_dict().__repr__()
diff --git a/src/neighborly/plugins/talktown/school.py b/src/neighborly/plugins/talktown/school.py
new file mode 100644
index 0000000..240ef5f
--- /dev/null
+++ b/src/neighborly/plugins/talktown/school.py
@@ -0,0 +1,63 @@
+from ordered_set import OrderedSet
+
+from neighborly.builtin.statuses import Child, Teen, YoungAdult
+from neighborly.core.ecs import Component, ISystem
+from neighborly.core.life_event import EventRole, LifeEvent, LifeEventLog
+from neighborly.core.status import Status
+from neighborly.core.time import SimDateTime
+
+
+class School(Component):
+    """A school is where Characters that are Children through Teen study"""
+
+    __slots__ = "students"
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.students: OrderedSet[int] = OrderedSet()
+
+    def add_student(self, student: int) -> None:
+        """Add student to the school"""
+        self.students.add(student)
+
+    def remove_student(self, student: int) -> None:
+        """Remove student from the school"""
+        self.students.add(student)
+
+
+class Student(Status):
+    def __init__(self):
+        super().__init__("Student", "This character is a student at the local school")
+
+
+class SchoolSystem(ISystem):
+    """Enrolls new students and graduates old students"""
+
+    def process(self, *args, **kwargs) -> None:
+        event_logger = self.world.get_resource(LifeEventLog)
+        date = self.world.get_resource(SimDateTime)
+
+        for _, school in self.world.get_component(School):
+            # Enroll children
+            for gid, child in self.world.get_component(Child):
+                if not child.gameobject.has_component(Student):
+                    school.add_student(gid)
+                    child.gameobject.add_component(Student())
+
+            # Enroll teens
+            for gid, teen in self.world.get_component(Teen):
+                if not teen.gameobject.has_component(Student):
+                    school.add_student(gid)
+                    teen.gameobject.add_component(Student())
+
+            # Graduate young adults
+            for gid, (young_adult, _) in self.world.get_components(YoungAdult, Student):
+                school.remove_student(gid)
+                young_adult.gameobject.remove_component(Student)
+                event_logger.record_event(
+                    LifeEvent(
+                        "GraduatedSchool",
+                        date.to_iso_str(),
+                        [EventRole("Student", gid)],
+                    )
+                )
diff --git a/src/neighborly/plugins/talktown/utils.py b/src/neighborly/plugins/talktown/utils.py
index 1a0d760..c86f89e 100644
--- a/src/neighborly/plugins/talktown/utils.py
+++ b/src/neighborly/plugins/talktown/utils.py
@@ -1,6 +1,6 @@
-from typing import Generic, TypeVar, Union, List, Tuple
-from heapq import heappush
 from dataclasses import dataclass, field
+from heapq import heappush
+from typing import Generic, TypeVar, Union, List, Tuple
 
 _T = TypeVar("_T")
 
@@ -80,8 +80,8 @@ def roman_to_int(roman: str) -> int:
     i = 0
     num = 0
     while i < len(roman):
-        if i + 1 < len(roman) and roman[i : i + 2] in roman_lookup:
-            num += roman_lookup[roman[i : i + 2]]
+        if i + 1 < len(roman) and roman[i: i + 2] in roman_lookup:
+            num += roman_lookup[roman[i: i + 2]]
             i += 2
         else:
             # print(i)
@@ -116,7 +116,7 @@ class PrioritizedItem(Generic[_T]):
 
 class PriorityQueue(Generic[_T]):
     def __init__(self) -> None:
-        self._queue: list[PrioritizedItem[_T]] = []
+        self._queue: List[PrioritizedItem[_T]] = []
 
     def push(self, priority: float, item: _T) -> None:
         heappush(self._queue, PrioritizedItem[_T](priority, item))
diff --git a/src/neighborly/plugins/weather_plugin.py b/src/neighborly/plugins/weather_plugin.py
index 64ea7ed..57f97c6 100644
--- a/src/neighborly/plugins/weather_plugin.py
+++ b/src/neighborly/plugins/weather_plugin.py
@@ -3,8 +3,9 @@
 
 import numpy as np
 
-from neighborly.core.ecs import World
-from neighborly.simulation import PluginContext
+from neighborly.core.ecs import ISystem
+from neighborly.core.time import SimDateTime
+from neighborly.simulation import Plugin, Simulation
 
 
 class Weather(Enum):
@@ -35,7 +36,7 @@ def __init__(self, default: Weather = Weather.SUNNY) -> None:
         self.time_before_change: int = 0
 
 
-class WeatherProcessor:
+class WeatherSystem(ISystem):
     """Updates the current weather state
 
     Attributes
@@ -49,9 +50,9 @@ def __init__(self, avg_change_interval: int = 24) -> None:
         super().__init__()
         self.avg_change_interval: int = avg_change_interval
 
-    def __call__(self, world: World, **kwargs):
-        delta_time: int = kwargs["delta_time"]
-        weather_manager = world.get_resource(WeatherManager)
+    def process(self, *args, **kwargs):
+        delta_time = self.world.get_resource(SimDateTime).delta_time
+        weather_manager = self.world.get_resource(WeatherManager)
 
         if weather_manager.time_before_change <= 0:
             # Select the next weather pattern
@@ -63,6 +64,11 @@ def __call__(self, world: World, **kwargs):
         weather_manager.time_before_change -= delta_time
 
 
-def setup(ctx: PluginContext, **kwargs) -> None:
-    ctx.world.add_system(WeatherProcessor(), 9)
-    ctx.world.add_resource(WeatherManager())
+class WeatherPlugin(Plugin):
+    def setup(self, sim: Simulation, **kwargs) -> None:
+        sim.add_system(WeatherSystem(), 9)
+        sim.add_resource(WeatherManager())
+
+
+def get_plugin() -> Plugin:
+    return WeatherPlugin()
diff --git a/src/neighborly/server/__init__.py b/src/neighborly/server/__init__.py
new file mode 100644
index 0000000..e4d388f
--- /dev/null
+++ b/src/neighborly/server/__init__.py
@@ -0,0 +1,49 @@
+from flask import Flask, render_template
+from flask_socketio import SocketIO, emit
+
+from neighborly.core.character import GameCharacter
+from neighborly.core.location import Location
+from neighborly.simulation import Simulation
+
+
+class NeighborlyServer:
+    def __init__(self, simulation: Simulation) -> None:
+        self.app = Flask(__name__)
+        self.app.config["SECRET"] = "neighborly-server-secret"
+        self.socketio = SocketIO(self.app, cors_allowed_origins="*")
+        self.sim = simulation
+        self._configure_socketio()
+        self._configure_routes()
+
+    def _configure_socketio(self) -> None:
+        @self.socketio.on("step")
+        def step_simulation():
+            self.sim.step()
+            emit("simulation-updated", broadcast=True)
+
+    def _configure_routes(self) -> None:
+        @self.app.route("/characters")
+        def get_characters():
+            return {
+                gid: character.to_dict()
+                for gid, character in self.sim.world.get_component(GameCharacter)
+            }
+
+        @self.app.route("/locations")
+        def get_locations():
+            return {
+                gid: location.to_dict()
+                for gid, location in self.sim.world.get_component(Location)
+            }
+
+        @self.app.route("/gameobject/<gid>")
+        def get_gameobject(gid):
+            gameobject = self.sim.world.get_gameobject(int(gid))
+            return {"components": [c.to_dict() for c in gameobject.components]}
+
+        @self.app.route("/")
+        def index():
+            return render_template("index.html")
+
+    def run(self, host: str = "localhost") -> None:
+        self.socketio.run(self.app, host=host, debug=True)
diff --git a/src/neighborly/server/static/js/index.js b/src/neighborly/server/static/js/index.js
new file mode 100644
index 0000000..161cb0d
--- /dev/null
+++ b/src/neighborly/server/static/js/index.js
@@ -0,0 +1,9 @@
+const ReactAppFromCDN = () => {
+  return (
+    <div className={`alert alert-primary`}>
+      This is a test of React-Bootstrap
+    </div>
+  );
+};
+
+ReactDOM.render(<ReactAppFromCDN />, document.querySelector("#root"));
diff --git a/src/neighborly/server/templates/index.html b/src/neighborly/server/templates/index.html
new file mode 100644
index 0000000..f200584
--- /dev/null
+++ b/src/neighborly/server/templates/index.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
+        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"
+        integrity="sha512-mHO4BJ0ELk7Pb1AzhTi3zvUeRgq3RXVOu9tTRfnA6qOxGK4pG2u57DJYolI4KrEnnLTcH9/J5wNOozRTDaybXg=="
+        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
+    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
+        integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" />
+    <script src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js" crossorigin></script>
+    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
+    <style>
+        .character-info {
+            display: grid;
+            grid-template-columns: 1fr 2fr;
+        }
+
+        #character-names {
+            padding: 8px;
+            border: 2px solid black;
+        }
+
+        #character-info {
+            padding: 8px;
+            border: 2px dotted black;
+        }
+
+    </style>
+</head>
+
+<body>
+    <h1>Neighborly</h1>
+    <button id="step-btn">Step</button>
+
+    <div class="character-info">
+        <div id="character-names">
+
+        </div>
+        <div id="character-info">
+            <h2 id="name"></h2>
+            <p id="gid"></p>
+            <p>Age: <span id="age"></span></p>
+        </div>
+    </div>
+
+    <div id="root"></div>
+
+    <script src="{{ url_for('static', filename='js/index.js') }}" type="text/babel"></script>
+
+    <script>
+        const get_gameobject = (gid) => fetch(`/gameobject/${gid}`).then(res => res.json());
+        const get_characters = () => fetch("/characters").then(res => res.json());
+
+        const populate_character_data = () => {
+            $('#character-names').empty()
+            get_characters()
+                .then(data => {
+                    for (gid of Object.keys(data)) {
+                        const character = data[gid];
+                        $('#character-names')
+                            .append($('<p>')
+                                .text(character.name)
+                                .on('click', () => {
+                                    get_gameobject(gid).then((data) => console.log(data));
+                                    $("#character-info #name").text(character.name)
+                                    $("#character-info #gid").text(gid)
+                                    $("#character-info #age").text(Math.floor(character.age))
+                                }))
+                    }
+                });
+        };
+
+        $(document).ready(() => {
+            const socket = io.connect("http://localhost:5000");
+
+            socket.on('connect', () => {
+                console.log('Client Connected');
+            });
+
+            socket.on('simulation-updated', () => {
+                populate_character_data();
+            });
+
+            $("#step-btn").on('click', () => {
+                socket.emit('step');
+            });
+
+
+        });
+    </script>
+</body>
+
+</html>
diff --git a/src/neighborly/simulation.py b/src/neighborly/simulation.py
index 7f7be96..4d944f3 100644
--- a/src/neighborly/simulation.py
+++ b/src/neighborly/simulation.py
@@ -1,122 +1,49 @@
 from __future__ import annotations
 
-import importlib
-import pathlib
-import os
-import sys
-from dataclasses import dataclass
+import random
+from abc import ABC, abstractmethod
 from logging import getLogger
-from typing import Any, Dict, List, Optional, Union, Generator
-
-from pydantic import BaseModel, Field
-from tqdm import tqdm
-
-import neighborly.core.utils.utilities as utilities
-from neighborly.core.business import BusinessFactory
-from neighborly.core.character import GameCharacterFactory
-from neighborly.core.ecs import World
-from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.life_event import LifeEventLogger
-from neighborly.core.location import LocationFactory
-from neighborly.core.position import Position2DFactory
-from neighborly.core.systems import (
-    AddResidentsSystem,
-    AdolescentSystem,
-    AdultSystem,
-    CharacterSystem,
-    ChildSystem,
-    LifeEventSystem,
+from typing import Any, Literal, Optional, Tuple, Union
+
+from neighborly.builtin.systems import (
+    BuildBusinessSystem,
+    BuildResidenceSystem,
+    BusinessUpdateSystem,
+    CharacterAgingSystem,
+    FindBusinessOwnerSystem,
+    FindEmployeesSystem,
+    LinearTimeSystem,
+    RelationshipStatusSystem,
     RoutineSystem,
-    TimeSystem,
-    YoungAdultSystem,
+    SocializeSystem,
+    SpawnResidentSystem,
+    UnemploymentSystem,
 )
-from neighborly.core.residence import ResidenceFactory
-from neighborly.core.rng import DefaultRNG, IRandNumGenerator
-from neighborly.core.routine import RoutineFactory
-from neighborly.core.social_network import RelationshipNetwork
-from neighborly.core.time import DAYS_PER_YEAR, HOURS_PER_DAY, SimDateTime, TimeDelta
-from neighborly.core.town import Town, TownConfig
+from neighborly.core.ecs import ISystem, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.life_event import LifeEventLog, LifeEventSimulator
+from neighborly.core.relationship import RelationshipGraph
+from neighborly.core.time import SimDateTime, TimeDelta
+from neighborly.core.town import LandGrid, Town
 
 logger = getLogger(__name__)
 
 
-class SimulationConfig(BaseModel):
-    """Configuration parameters for a Neighborly instance
-
-    Attributes
-    ----------
-    seed: int
-        The seed provided to the random number factory
-    hours_per_timestep: int
-        How many in-simulation hours elapse every simulation tic
-    town: TownConfig
-        Configuration settings for town creation
-    """
-
-    seed: int
-    hours_per_timestep: int
-    start_date: str
-    end_date: str
-    days_per_year: int
-    town: TownConfig = Field(default_factory=TownConfig)
-
-
-class PluginConfig(BaseModel):
-    """
-    Settings for loading and constructing a plugin
-
-    Attributes
-    ----------
-    name: str
-        Name of the plugin to load
-    options: Dict[str, Any]
-        Parameters to pass to the plugin when constructing
-        and loading it
-    """
-
-    name: str
-    path: Optional[str] = None
-    options: Dict[str, Any] = Field(default_factory=dict)
-
-
-class NeighborlyConfig(BaseModel):
-    """
-    Static configuration setting for the Neighborly
-
-    Attributes
-    ----------
-    simulation: SimulationConfig
-        Static configuration settings specifically for
-        the simulation instance
-    plugins: List[Union[str, PluginConfig]]
-        Names of plugins to load or names combined with
-        instantiation parameters
-    path: str
-        Path to the config file
-    """
+class PluginSetupError(Exception):
+    def __init__(self, *args: object) -> None:
+        super().__init__(*args)
 
-    quiet: bool = False
-    simulation: SimulationConfig = Field(default_factory=lambda: SimulationConfig())
-    plugins: List[Union[str, PluginConfig]] = Field(default_factory=list)
-    path: str = "."
 
+class Plugin(ABC):
     @classmethod
-    def from_partial(
-        cls, data: Dict[str, Any], defaults: NeighborlyConfig
-    ) -> NeighborlyConfig:
-        """Construct new config from a default config and a partial set of parameters"""
-        return cls(**utilities.merge(data, defaults.dict()))
+    def get_name(cls) -> str:
+        """Return the name of this plugin"""
+        return cls.__name__
 
-
-@dataclass(frozen=True)
-class PluginContext:
-    engine: NeighborlyEngine
-    world: World
-
-
-class PluginSetupError(Exception):
-    def __init__(self, *args: object) -> None:
-        super().__init__(*args)
+    @abstractmethod
+    def setup(self, sim: Simulation, **kwargs) -> None:
+        """Add the plugin data to the simulation"""
+        raise NotImplementedError
 
 
 class Simulation:
@@ -125,195 +52,148 @@ class Simulation:
 
     Attributes
     ----------
-    config: SimulationConfig
-        Configuration settings for how the simulation
     world: World
-        Entity-component system (ECS) that manages entities in the virtual world
+        Entity-component system (ECS) that manages entities and procedures in the virtual world
     """
 
-    __slots__ = "config", "world", "_lod_manager"
-
-    def __init__(self, config: NeighborlyConfig) -> None:
-        self.config: NeighborlyConfig = config
-        self.world: World = World()
-        # Create the default Engine configuration
-        engine = NeighborlyEngine(DefaultRNG(config.simulation.seed))
-        engine.add_component_factory(GameCharacterFactory())
-        engine.add_component_factory(RoutineFactory())
-        engine.add_component_factory(LocationFactory())
-        engine.add_component_factory(ResidenceFactory())
-        engine.add_component_factory(Position2DFactory())
-        engine.add_component_factory(BusinessFactory())
+    __slots__ = (
+        "world",
+        "engine",
+        "seed",
+        "world_gen_start",
+        "world_gen_end",
+        "town_size",
+        "time_system",
+    )
+
+    def __init__(
+        self,
+        seed: int,
+        world: World,
+        engine: NeighborlyEngine,
+        world_gen_start: SimDateTime,
+        world_gen_end: SimDateTime,
+    ) -> None:
+        self.seed: int = seed
+        self.world: World = world
+        self.engine: NeighborlyEngine = engine
         self.world.add_resource(engine)
-        # Add default systems
-        self.world.add_system(TimeSystem(), 10)
-        self.world.add_system(RoutineSystem(), 5)
-        self.world.add_system(CharacterSystem(), 3)
-        self.world.add_system(ChildSystem(), 3)
-        self.world.add_system(AdolescentSystem(), 3)
-        self.world.add_system(YoungAdultSystem(), 3)
-        self.world.add_system(AdultSystem(), 3)
-        self.world.add_system(LifeEventSystem())
-        self.world.add_system(AddResidentsSystem())
-        # Add default resources
-        self.world.add_resource(SimDateTime())
-        self.world.add_resource(RelationshipNetwork())
-        self.world.add_resource(LifeEventLogger())
-        # Load Plugins
-        for plugin in config.plugins:
-            if isinstance(plugin, str):
-                self.load_plugin(plugin)
-            else:
-                plugin_path = os.path.join(
-                    pathlib.Path(config.path).parent, plugin.path if plugin.path else ""
-                )
-                self.load_plugin(plugin.name, plugin_path, **plugin.options)
-        # Create the town
-        town = Town.create(config.simulation.town)
-        logger.debug(f"Created town of {town.name}")
-        self.world.add_resource(town)
-        # Create the LOD manager
-        self._lod_manager: Generator[int, None, None] = create_lod_generator(
-            current_date=self.world.get_resource(SimDateTime),
-            high_lod_step=self.config.simulation.hours_per_timestep,
-            days_per_year=self.config.simulation.days_per_year,
-            rng=self.world.get_resource(NeighborlyEngine).get_rng(),
-            end_date=SimDateTime,
-        )
+        self.world_gen_start: SimDateTime = world_gen_start
+        self.world_gen_end: SimDateTime = world_gen_end
 
-    def load_plugin(
-        self, module_name: str, path: Optional[str] = None, **kwargs
-    ) -> None:
-        """
-        Load a plugin
-
-        Parameters
-        ----------
-        module_name: str
-            Name of module to load
-        """
-        path_prepended = False
-
-        if path:
-            path_prepended = True
-            plugin_abs_path = os.path.abspath(path)
-            sys.path.insert(0, plugin_abs_path)
-            logger.debug(
-                f"Temporarily added plugin path '{plugin_abs_path}' to sys.path"
-            )
+    @classmethod
+    def create(
+        cls,
+        seed: Optional[int] = None,
+        world_gen_start: Union[str, SimDateTime] = "0000-00-00",
+        world_gen_end: Union[str, SimDateTime] = "0100-00-00",
+        time_increment_hours: int = 12,
+        town_name: str = "#town_name#",
+        town_size: Union[
+            Literal["small", "medium", "large"], Tuple[int, int]
+        ] = "medium",
+    ) -> Simulation:
+        """Creates an instance of a Neighborly simulation with an empty world and engine"""
+        seed = seed if seed is not None else random.randint(0, 99999999)
+
+        start_date = (
+            world_gen_start
+            if isinstance(world_gen_start, SimDateTime)
+            else SimDateTime.from_iso_str(world_gen_start)
+        )
 
-        try:
-            plugin_module = importlib.import_module(module_name)
-            plugin_module.__dict__["setup"](
-                PluginContext(engine=self.get_engine(), world=self.world), **kwargs
-            )
-        except KeyError:
-            raise PluginSetupError(
-                f"setup function not found for plugin: {module_name}"
-            )
-        finally:
-            # Remove the given plugin path from the front
-            # of the system path to prevent module resolution bugs
-            if path_prepended:
-                sys.path.pop(0)
-
-    def run(self) -> None:
-        """Run the simulation until it reaches the end date in the config"""
-        start_date: SimDateTime = SimDateTime.from_iso_str(
-            self.config.simulation.start_date
+        end_date = (
+            world_gen_end
+            if isinstance(world_gen_end, SimDateTime)
+            else SimDateTime.from_iso_str(world_gen_end)
         )
-        end_date: SimDateTime = SimDateTime.from_iso_str(
-            self.config.simulation.end_date
+
+        sim = (
+            Simulation(seed, World(), NeighborlyEngine(seed), start_date, end_date)
+            .add_resource(start_date.copy())
+            .add_system(LinearTimeSystem(TimeDelta(hours=time_increment_hours)))
+            .add_system(LifeEventSimulator(interval=TimeDelta(months=1)))
+            .add_resource(LifeEventLog())
+            .add_system(BuildResidenceSystem(interval=TimeDelta(days=5)))
+            .add_system(SpawnResidentSystem(interval=TimeDelta(days=7)))
+            .add_system(BuildBusinessSystem(interval=TimeDelta(days=5)))
+            .add_resource(RelationshipGraph())
+            .add_system(CharacterAgingSystem())
+            .add_system(RoutineSystem(), 5)
+            .add_system(BusinessUpdateSystem())
+            .add_system(FindBusinessOwnerSystem())
+            .add_system(FindEmployeesSystem())
+            .add_system(UnemploymentSystem(days_to_departure=30))
+            .add_system(RelationshipStatusSystem())
+            .add_system(SocializeSystem())
         )
 
-        high_res_days = set(self._generate_sample_days(start_date, end_date, n=10))
+        sim._create_town(town_name, town_size)
+
+        return sim
+
+    def add_system(self, system: ISystem, priority: int = 0) -> Simulation:
+        """Add a new system to the simulation"""
+        self.world.add_system(system, priority)
+        return self
+
+    def add_resource(self, resource: Any) -> Simulation:
+        """Add a new resource to the simulation"""
+        self.world.add_resource(resource)
+        return self
+
+    def add_plugin(self, plugin: Plugin, **kwargs) -> Simulation:
+        """Add plugin to simulation"""
+        plugin.setup(self, **kwargs)
+        logger.debug(f"Successfully loaded plugin: {plugin.get_name()}")
+        return self
+
+    def _create_town(
+        self,
+        name: str,
+        size: Union[Literal["small", "medium", "large"], Tuple[int, int]] = "medium",
+    ) -> Simulation:
+        """Create a new grid of land to build the town on"""
+        if self.world.has_resource(LandGrid) or self.world.has_resource(Town):
+            logger.error("Attempted to overwrite previously created town")
+            return self
+
+        # create town
+        self.add_resource(Town.create(self.world, name=name))
+
+        # Create the land
+        if isinstance(size, tuple):
+            land_size = size
+        else:
+            if size == "small":
+                land_size = (3, 3)
+            elif size == "medium":
+                land_size = (5, 5)
+            else:
+                land_size = (7, 7)
 
-        low_res_days = (end_date - start_date).total_days - len(high_res_days)
+        land_grid = LandGrid(land_size)
 
-        total_timesteps = low_res_days + len(high_res_days) * (
-            HOURS_PER_DAY // self.config.simulation.hours_per_timestep
-        )
+        self.add_resource(land_grid)
 
-        try:
-            for _ in tqdm(range(total_timesteps), disable=self.config.quiet is True):
-                if self.get_time().to_ordinal() in high_res_days:
-                    self.step(hours=self.config.simulation.hours_per_timestep)
-                else:
-                    self.step(hours=24)
+        return self
 
+    def establish_setting(self) -> None:
+        """Run the simulation until it reaches the end date in the config"""
+        try:
+            while self.world_gen_end >= self.get_time():
+                self.step()
         except KeyboardInterrupt:
-            if self.config.quiet is False:
-                print("Stopping Simulation")
-                print(f"Current Date: {self.get_time().to_date_str()}")
+            print("\nStopping Simulation")
 
     def step(self) -> None:
         """Advance the simulation a single timestep"""
-        self.world.process(delta_time=hours, **kwargs)
+        self.world.step()
 
     def get_time(self) -> SimDateTime:
         """Get the simulated DateTime instance used by the simulation"""
         return self.world.get_resource(SimDateTime)
 
-    def get_town(self) -> Town:
-        """Get the Town instance"""
-        return self.world.get_resource(Town)
-
     def get_engine(self) -> NeighborlyEngine:
         """Get the NeighborlyEngine instance"""
-        return self.world.get_resource(NeighborlyEngine)
-
-    def _generate_sample_days(
-        self, start_date: SimDateTime, end_date: SimDateTime, n: int
-    ) -> List[int]:
-        """Samples n days from each year between the start and end dates"""
-        ordinal_start_date: int = start_date.to_ordinal()
-        ordinal_end_date: int = end_date.to_ordinal()
-
-        sampled_ordinal_dates: List[int] = []
-
-        current_date = ordinal_start_date
-
-        while current_date < ordinal_end_date:
-            sampled_dates = (
-                self.get_engine()
-                .get_rng()
-                .sample(range(current_date, current_date + DAYS_PER_YEAR), n)
-            )
-            sampled_ordinal_dates.extend(sorted(sampled_dates))
-            current_date = min(current_date + DAYS_PER_YEAR, ordinal_end_date)
-
-        return sampled_ordinal_dates
-
-
-def create_lod_generator(
-    current_date: SimDateTime,
-    high_lod_step: int,
-    days_per_year: int,
-    rng: IRandNumGenerator,
-    end_date: Optional[SimDateTime] = None,
-) -> Generator[int, None, None]:
-    """
-    Yields time step increments based on the current date,
-    switching between low and high levels-of-detail.
-    """
-    while True:
-        one_year_ahead = current_date.to_ordinal() + DAYS_PER_YEAR
-
-        # Sample the fully simulated days for the next year
-        high_res_days = set(
-            rng.sample(
-                range(current_date.to_ordinal(), one_year_ahead),
-                days_per_year,
-            )
-        )
-
-        while current_date.to_ordinal() < one_year_ahead:
-            if current_date.to_ordinal() in high_res_days:
-                current_date += TimeDelta(hours=high_lod_step)
-                yield high_lod_step
-            else:
-                current_date += TimeDelta(hours=24)
-                yield 24
-
-            if end_date and current_date > end_date:
-                return
+        return self.engine


Test patch:
diff --git a/tests/test_behavior_tree.py b/tests/test_behavior_tree.py
new file mode 100644
index 0000000..a66cb85
--- /dev/null
+++ b/tests/test_behavior_tree.py
@@ -0,0 +1,15 @@
+import neighborly.core.behavior_tree as bt
+from neighborly.core.ecs import World
+from neighborly.core.life_event import LifeEvent, LifeEventType
+
+
+def depart_action_node(self, world: World, event: LifeEvent, **kwargs) -> bool:
+    pass
+
+
+def depart_life_event():
+    pass
+
+
+def test_bt_result():
+    pass
diff --git a/tests/test_business.py b/tests/test_business.py
index 4af73e0..73cb670 100644
--- a/tests/test_business.py
+++ b/tests/test_business.py
@@ -6,11 +6,13 @@
 import pytest
 
 from neighborly.core.business import (
+    Business,
+    BusinessArchetype,
+    BusinessStatus,
     Occupation,
-    OccupationDefinition,
-    BusinessDefinition,
+    OccupationType,
 )
-from neighborly.core.ecs import GameObject
+from neighborly.core.ecs import GameObject, World
 from neighborly.core.status import Status
 
 
@@ -24,7 +26,7 @@ def sample_occupation_types():
     def is_college_graduate(gameobject: GameObject, **kwargs: Any) -> bool:
         return gameobject.has_component(CollegeGraduate)
 
-    ceo_occupation_type = OccupationDefinition(
+    ceo_occupation_type = OccupationType(
         "CEO",
         5,
         [is_college_graduate],
@@ -35,29 +37,26 @@ def is_college_graduate(gameobject: GameObject, **kwargs: Any) -> bool:
 
 @pytest.fixture
 def sample_business_types():
-    restaurant_type = BusinessDefinition(name="Restaurant", hours="MTWRFSU 10:00-21:00")
+    restaurant_type = BusinessArchetype(name="Restaurant", hours="MTWRFSU 10:00-21:00")
 
     return {"restaurant": restaurant_type}
 
 
-def test_register_occupation_type(
-    sample_occupation_types: Dict[str, OccupationDefinition]
-):
-
+def test_register_occupation_type(sample_occupation_types: Dict[str, OccupationType]):
     ceo_occupation_type = sample_occupation_types["ceo"]
 
     assert ceo_occupation_type.name == "CEO"
     assert ceo_occupation_type.level == 5
 
-    OccupationDefinition.register_type(ceo_occupation_type)
+    OccupationType.register_type(ceo_occupation_type)
 
-    assert ceo_occupation_type == OccupationDefinition.get_registered_type("CEO")
+    assert ceo_occupation_type == OccupationTypeLibrary.get("CEO")
 
 
-def test_occupation(sample_occupation_types: Dict[str, OccupationDefinition]):
-    OccupationDefinition.register_type(sample_occupation_types["ceo"])
+def test_occupation(sample_occupation_types: Dict[str, OccupationType]):
+    OccupationType.register_type(sample_occupation_types["ceo"])
 
-    ceo = Occupation(OccupationDefinition.get_registered_type("CEO"), 1)
+    ceo = Occupation(OccupationTypeLibrary.get("CEO"), 1)
 
     assert ceo.get_type().name == "CEO"
     assert ceo.get_business() == 1
@@ -72,9 +71,40 @@ def test_occupation(sample_occupation_types: Dict[str, OccupationDefinition]):
     assert ceo.get_years_held() == 1
 
 
-def test_register_business_type(sample_business_types: Dict[str, BusinessDefinition]):
+def test_register_business_type(sample_business_types: Dict[str, BusinessArchetype]):
     restaurant_type = sample_business_types["restaurant"]
 
-    BusinessDefinition.register_type(restaurant_type)
+    BusinessArchetype.register(restaurant_type)
+
+    BusinessArchetype.get("Restaurant").name = "Restaurant"
+
+
+def test_construct_occupation():
+    """Constructing Occupations from OccupationTypes"""
+    pass
+
+
+def test_construct_business():
+    """Constructing business components using BusinessArchetypes"""
+    restaurant_Archetype = BusinessArchetype(
+        "Restaurant",
+        name_format="#restaurant_name#",
+        hours=["day", "evening"],
+        owner_type="Proprietor",
+        employee_types={
+            "Cook": 1,
+            "Server": 2,
+            "Host": 1,
+        },
+    )
+
+    world = World()
+
+    restaurant = world.spawn_archetype(restaurant_Archetype)
+    restaurant_business = restaurant.get_component(Business)
 
-    BusinessDefinition.get_registered_type("Restaurant").name = "Restaurant"
+    assert restaurant_business.business_type == "Restaurant"
+    assert restaurant_business.owner_type == "Proprietor"
+    assert restaurant_business.status == BusinessStatus.PendingOpening
+    assert restaurant_business.owner is None
+    assert restaurant_business.needs_owner() is True
diff --git a/tests/test_character.py b/tests/test_character.py
index b160024..9b4de95 100644
--- a/tests/test_character.py
+++ b/tests/test_character.py
@@ -14,7 +14,7 @@ def sample_definitions() -> str:
             name: BaseCharacter
             generation:
                 first_name: "#first_name#"
-                last_name: "#last_name#"
+                last_name: "#family_name#"
                 family:
                     probability_spouse: 0.5
                     probability_children: 0.5
diff --git a/tests/test_character_traits.py b/tests/test_character_traits.py
index 4b5d660..e120fc2 100755
--- a/tests/test_character_traits.py
+++ b/tests/test_character_traits.py
@@ -1,12 +1,12 @@
 import pytest
 
-from neighborly.plugins.default_plugin.character_values import CharacterValues
+from neighborly.core.personal_values import PersonalValues
 
 
 def test_constructor():
-    base_values = CharacterValues(default=50)
+    base_values = PersonalValues(default=50)
 
-    overridden_values = CharacterValues({"family": 42, "power": 90, "social": 36})
+    overridden_values = PersonalValues({"family": 42, "power": 90, "social": 36})
 
     assert base_values["family"] == 50
     assert base_values["social"] == 50
@@ -16,7 +16,7 @@ def test_constructor():
 
 
 def test_setter():
-    base_values = CharacterValues()
+    base_values = PersonalValues()
 
     # Set to a value within bounds
     base_values["confidence"] = 75
@@ -35,7 +35,7 @@ def test_setter():
 
 
 def test_getter():
-    base_propensities = CharacterValues(default=0)
+    base_propensities = PersonalValues(default=0)
 
     assert base_propensities["confidence"] == 0
     assert base_propensities["friendship"] == 0
diff --git a/tests/test_ecs.py b/tests/test_ecs.py
new file mode 100644
index 0000000..ff97d1d
--- /dev/null
+++ b/tests/test_ecs.py
@@ -0,0 +1,100 @@
+from dataclasses import dataclass
+
+import pytest
+
+from neighborly.core.ecs import Component, World
+
+
+class SimpleGameCharacter(Component):
+    def __init__(self, name: str) -> None:
+        super().__init__()
+        self.name = name
+
+    def __repr__(self) -> str:
+        return f"GameCharacter(name={self.name})"
+
+
+class SimpleLocation(Component):
+    __slots__ = "capacity"
+
+    def __init__(self, capacity: int = 999) -> None:
+        super().__init__()
+        self.capacity: int = capacity
+
+    def __repr__(self) -> str:
+        return f"Location(capacity={self.capacity})"
+
+
+@dataclass
+class SimpleRoutine(Component):
+    free: bool
+
+
+@dataclass
+class A(Component):
+    value: int = 0
+
+
+@dataclass
+class B(Component):
+    value: int = 0
+
+
+@dataclass
+class FakeResource:
+    config_value: int = 5
+
+
+def test_has_resource():
+    world = World()
+    assert world.has_resource(FakeResource) is False
+    world.add_resource(FakeResource())
+    assert world.has_resource(FakeResource) is True
+
+
+def test_get_resource():
+    world = World()
+    fake_resource = FakeResource()
+    assert world.has_resource(FakeResource) is False
+    # This should throw a key error when not present
+    with pytest.raises(KeyError):
+        assert world.get_resource(FakeResource)
+    world.add_resource(fake_resource)
+    assert world.get_resource(FakeResource) == fake_resource
+
+
+def test_remove_resource():
+    world = World()
+    world.add_resource(FakeResource())
+    assert world.has_resource(FakeResource) is True
+    world.remove_resource(FakeResource)
+    assert world.has_resource(FakeResource) is False
+    # This should throw a key error when not present
+    with pytest.raises(KeyError):
+        world.remove_resource(FakeResource)
+
+
+def test_add_resource():
+    world = World()
+    fake_resource = FakeResource()
+    assert world.has_resource(FakeResource) is False
+    world.add_resource(fake_resource)
+    assert world.has_resource(FakeResource) is True
+
+
+def test_gameobject() -> None:
+    world = World()
+    adrian = world.spawn_gameobject(SimpleRoutine(False), SimpleGameCharacter("Adrian"), name="Adrian")
+    jamie = world.spawn_gameobject(SimpleRoutine(False), SimpleGameCharacter("Jamie"), name="Jamie")
+    park = world.spawn_gameobject(SimpleLocation(), name="Park")
+    office_building = world.spawn_gameobject(SimpleLocation(18), name="Office Building")
+
+    assert jamie.get_component(SimpleGameCharacter).name == "Jamie"
+
+    assert len(world.get_component(SimpleGameCharacter)) == 2
+    assert len(world.get_component(SimpleLocation)) == 2
+
+    assert park.get_component(SimpleLocation).capacity == 999
+    assert office_building.get_component(SimpleLocation).capacity == 18
+
+    assert len(world.get_components(SimpleGameCharacter, SimpleRoutine)) == 2
diff --git a/tests/test_gameobject.py b/tests/test_gameobject.py
deleted file mode 100644
index e78e31f..0000000
--- a/tests/test_gameobject.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from dataclasses import dataclass
-
-from neighborly.core.ecs import World, GameObject, Component
-
-
-class SimpleGameCharacter(Component):
-
-    def __init__(self, name: str) -> None:
-        super().__init__()
-        self.name = name
-
-    def on_add(self) -> None:
-        self.gameobject.set_name(self.name)
-
-    def __repr__(self) -> str:
-        return f"GameCharacter(name={self.name})"
-
-
-class SimpleBuilding(Component):
-    __slots__ = "size", "capacity"
-
-    def __init__(self, size: str) -> None:
-        super().__init__()
-        self.size: str = size
-        if size == 'large':
-            self.capacity = 36
-        elif size == 'medium':
-            self.capacity = 18
-        elif size == 'small':
-            self.capacity = 8
-        else:
-            raise ValueError("Invalid building size")
-
-    def on_start(self) -> None:
-        location = self.gameobject.get_component(SimpleLocation)
-        location.capacity = self.capacity
-
-    def __repr__(self) -> str:
-        return f"Building(size={self.size}, capacity={self.capacity})"
-
-
-class SimpleLocation(Component):
-    __slots__ = "capacity"
-
-    def __init__(self, capacity: int = 999) -> None:
-        super().__init__()
-        self.capacity: int = capacity
-
-    def __repr__(self) -> str:
-        return f"Location(capacity={self.capacity})"
-
-
-@dataclass
-class SimpleRoutine(Component):
-    free: bool
-
-
-def test_gameobject() -> None:
-    world = World()
-
-    adrian = GameObject(
-        components=[SimpleGameCharacter("Adrian"), SimpleRoutine(False)])
-
-    jamie = GameObject(
-        components=[SimpleGameCharacter("Jamie"), SimpleRoutine(False)])
-
-    park = GameObject(name="Park", components=[SimpleLocation()])
-
-    office_building = GameObject(name="Office Building", components=[
-        SimpleLocation(), SimpleBuilding("medium")])
-
-    world.add_gameobject(adrian)
-    world.add_gameobject(jamie)
-    world.add_gameobject(park)
-    world.add_gameobject(office_building)
-
-    assert jamie.name == "Jamie"
-    assert jamie.get_component(SimpleGameCharacter).name == "Jamie"
-
-    assert len(world.get_component(SimpleGameCharacter)) == 2
-    assert len(world.get_component(SimpleLocation)) == 2
-
-    assert park.get_component(SimpleLocation).capacity == 999
-    assert office_building.get_component(SimpleLocation).capacity == 18
-
-    assert len(world.get_components(SimpleGameCharacter, SimpleRoutine)) == 2
diff --git a/tests/test_name_generation.py b/tests/test_name_generation.py
index 09302ce..ffb03b3 100644
--- a/tests/test_name_generation.py
+++ b/tests/test_name_generation.py
@@ -7,4 +7,4 @@ def test_name_generation() -> None:
         "last_name", ["Pizza", "Apple", "Blueberry", "Avocado"]
     )
 
-    assert type(name_generator.get_name("#first_name# #last_name#")) == str
+    assert type(name_generator.get_name("#first_name# #family_name#")) == str
diff --git a/tests/test_routine.py b/tests/test_routine.py
index d484cac..0f50943 100644
--- a/tests/test_routine.py
+++ b/tests/test_routine.py
@@ -13,20 +13,20 @@
 def test_construct_routine_entry():
     """Test that routines entries are created properly"""
 
-    entry_0 = RoutineEntry(8, 12, "home", "liesure", RoutinePriority.HIGH)
+    entry_0 = RoutineEntry(8, 12, "home", "leisure", RoutinePriority.HIGH)
 
     assert entry_0.start == 8
     assert entry_0.end == 12
     assert entry_0.location == "home"
     assert entry_0.priority == RoutinePriority.HIGH
 
-    entry_1 = RoutineEntry(12, 15, "park", "liesure")
+    entry_1 = RoutineEntry(12, 15, "park", "leisure")
 
     assert entry_1.priority == RoutinePriority.LOW
 
     with pytest.raises(ValueError):
         # start greater than the end
-        RoutineEntry(6, 3, "home", "liesure")
+        RoutineEntry(6, 3, "home", "leisure")
         # start less than zero
         RoutineEntry(-1, 12, "work", "work")
         # end greater than 23
@@ -44,7 +44,7 @@ def test_daily_routine():
     assert daily_routine.get_entries(8) == []
 
     # Add a low priority entry
-    go_to_park = RoutineEntry(12, 15, "park", "liesure")
+    go_to_park = RoutineEntry(12, 15, "park", "leisure")
     daily_routine.add_entries(go_to_park)
     assert daily_routine.get_entries(12) == [go_to_park]
 
@@ -88,7 +88,7 @@ def test_routine():
     assert routine.get_entries("tuesday", 12) == [walk_dog]
 
 
-def test_timestr_to_int():
+def test_time_str_to_int():
     """Test converting 24-hour or 12-hour time strings to ints"""
     assert time_str_to_int("01:00") == 1
     assert time_str_to_int("1:00") == 1
diff --git a/tests/test_simulation.py b/tests/test_simulation.py
index 364c2f1..209542c 100644
--- a/tests/test_simulation.py
+++ b/tests/test_simulation.py
@@ -161,4 +161,4 @@ def test_constructing_plugins():
 
     sim = Simulation(config)
 
-    sim.load_plugin("neighborly.plugins.default_plugin")
+    sim._dynamic_load_plugin("neighborly.plugins.default_plugin")
diff --git a/tests/test_social_network.py b/tests/test_social_network.py
index c894387..d93f13f 100644
--- a/tests/test_social_network.py
+++ b/tests/test_social_network.py
@@ -4,9 +4,8 @@
 from neighborly.core.relationship import (
     Relationship,
     RelationshipModifier,
-    RelationshipTag,
+    RelationshipTag, RelationshipGraph,
 )
-from neighborly.core.social_network import RelationshipNetwork
 
 
 @pytest.fixture
@@ -38,7 +37,7 @@ def test_relationship_network():
     maggie = 4
 
     # Construct the social graph
-    social_graph = RelationshipNetwork()
+    social_graph = RelationshipGraph()
 
     homer_to_lisa_rel = Relationship(homer, lisa)
     lisa_to_homer_rel = Relationship(lisa, homer)
diff --git a/tests/test_town.py b/tests/test_town.py
index 5bebaf1..055203f 100644
--- a/tests/test_town.py
+++ b/tests/test_town.py
@@ -1,16 +1,16 @@
-from neighborly.core.town import TownLayout
+from neighborly.core.town import LandGrid
 
 
 def test_town_layout():
-    layout = TownLayout(5, 4)
+    layout = LandGrid((5, 4))
 
     assert layout.grid.shape == (5, 4)
     assert layout.has_vacancy() is True
-    space = layout.allocate_space(0)
+    space = layout.reserve_space(0)
     assert space == (0, 0)
 
     for i in range(19):
-        layout.allocate_space(0)
+        layout.reserve_space(0)
 
     assert layout.has_vacancy() is False