smithery.ai

buff

Temporary effects system — curses are just shitty buffs

First seen Mar 23, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsread_file, write_file

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 48,050 B
  • docs SUMMARY.md 69 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Buff

"All effects are buffs. Some are just shitty."

Buffs modify stats, abilities, or behavior. They have durations, can stack, and come from various sources. Curses are just negative buffs — no separate system.

Hosts — a buff attaches to whatever the effect belongs to

A Sim gets caffeinated. A tavern gets haunted. A lantern burns down. Silver, as a material, harms anyone who wields it. Bob is smitten with Alice, whatever Alice thinks. In Fluxx, the rules themselves get a new one. All of those are buffs, and each belongs to a different kind of thing:

# a character
buff:
  name: "Caffeinated"
  host: character/mrs-crumplebottom
  effect: { energy: +2, focus: +1 }
  expires: { after: 5, unit: turns }

# a room — and note it reaches the people inside it, not just the room
buff:
  name: "Poisoned Air"
  host: room/lower-mine
  effect: { breathable: false }
  radiates: { to: occupants, effect: { health: -1 }, while: present }
  expires: { when: "ventilation restored" }

# an object, carrying its own countdown
buff:
  name: "Burning Down"
  host: object/brass-lantern
  effect: { light_radius: -1 }
  tick: { every: 1, effect: { fuel: -1 } }
  expires: { when: "fuel <= 0" }

# a prototype — every instance inherits it, including ones made later
buff:
  name: "Cursed Silver"
  host: prototype/material/silver
  effect: { harms_wielder: true }

# a relationship — the edge, with a direction
buff:
  name: "Smitten"
  host: relationship/{ from: bob, to: alice }
  effect: { romance: +80 }
  expires: { after: 40, unit: turns }

# the ruleset
buff:
  name: "Everyone Draws Two"
  host: ruleset/current-game
  effect: { draw_per_turn: 2 }
  expires: { when: "superseded by another Draw rule" }

Nothing in the buff's structure changes with the host. Same fields, same clock, same conflict resolution.

Rooms and objects can simply be characters

MOOLLM has no rigid entity classes to work around. Everything is a prototype, and anything can delegate to [character](../character/) — so a room that needs to want things, take turns, and narrate becomes animate by saying so:

room:
  id: the-forge
  delegates_to: [room, character]     # it is a place AND someone
  wants: { be_tended: 8, be_admired: 3 }
  buffs:
    - { name: "Well Banked", effect: { production_speed: +30 }, expires: { after: 12, unit: turns } }

Rooms already [ADVERTISE](../room/) in the same auction characters bid into, so an animate room is a small step rather than a special case. A forge that resents neglect is a character who is a place, and it holds its buffs itself.

This is also why the host list above is not a type system. It is a list of things that turn out to want modifiers, and host: accepts any of them — plus whatever else a world invents.

Restricting a buff to certain hosts

Most buffs are host-agnostic and should say nothing. Where a buff genuinely only makes sense somewhere, it says so:

buff:
  name: "Haunted"
  hosts: [room, object]              # a place or a thing, not a mood someone has

buff:
  name: "Waterlogged"
  # nothing declared — anything can get wet
  effect: { weight: +1, flammable: false }

hosts: omitted means any; be liberal in what you accept ([postel](../postel/)). A world may also narrow it for a whole game, which is where a ruleset's own restrictions belong:

world:
  buff_policy:
    allowed_hosts: [character]        # this game keeps effects on people
    on_violation: "refuse and explain"

Three things a host has to answer

The structure is the same everywhere, but every host must have an answer to who narrates, when it ends, and how far it reaches:

Host Who narrates its tick Ends when
character the character death, scene end, dispel
room the room if it is animate; otherwise nobody, and it ticks silently an explicit condition — see below
object the object if animate, else its holder destruction, or leaving play
prototype nothing — a prototype does not tick; its instances do the buff is removed from the prototype
relationship either party, or the narrator either party leaves play, or dispel
group any member, or the narrator per onmemberleave: — dissolve, shrink, or persist
ruleset the narrator repeal, or supersession by a later rule

Places and materials outlive everything. A timer keyed to the host's own death never fires on a room, a material, or a ruleset. Those hosts need a real end condition — a turn count, an event, or a predicate:

expires: { after: 12, unit: turns }         # good
expires: { when: "ventilation restored" }   # good
expires: { with: host }                     # never fires on a room

radiates: is how a buff reaches beyond its host. Buffing a room and buffing everyone in it are different claims, and a buff that means both says so:

buff:
  name: "Sweltering"
  host: room/the-forge
  effect: { comfort: -10 }                  # the room's own property
  radiates:
    to: occupants
    effect: { energy: -1, mood: -2 }        # everyone inside
    while: present                          # leaving ends it

The same field carries auras from a character, contagion from an object, and region effects from a place. to: accepts occupants, holder, wearer, nearby: <radius>, or a query.

Relationship hosts — the matrix cell is the host

Some effects belong to neither party. Cupid, in Don's SimProv playset for The Sims 1, makes Bob love Alice — and whether Alice loves Bob back is a separate value the player chooses to set or not. The effect is not on Bob and not on Alice. It is on the edge, and the edge has a direction.

The relationship matrix already is that host. A character's [relationships:](../character/SKILL.md) map is keyed by target and holds an open mapping per target, so a pair-hosted buff needs no new storage — it goes in the cell:

# in bob.yml
relationships:
  alice:
    feeling: "Can't stop thinking about her. This is new."
    daily: 45
    lifetime: 30
    buffs:
      - name: "Smitten"
        source: "object/cupid"
        effect: { romance: +80 }
        expires: { after: 40, unit: turns }
        tick: "he finds a reason to walk past her"

host: relationship/{ from: bob, to: alice } names that cell. The direction is not a field on the buff — it is where the buff is stored, which is why the asymmetry maintains itself. Bob's file says he is smitten; Alice's file is untouched, and stays untouched unless someone sets it.

Why the cell, rather than two coordinated buffs on two characters:

  • Nothing keeps two halves consistent. A buff on Bob referencing Alice plus another on Alice referencing Bob gives two objects that expire, dispel, and get edited independently, with no invariant tying them.
  • Dispel wants to name the edge. "End whatever is between those two" is one operation on one host, not a search for buffs whose payload mentions the other party.
  • Asymmetry is the normal case. Unrequited feelings are the interesting ones, which is why the storage is per-direction in the first place.

The matrix is YAML Jazz, so the dimensions are open

The Sims 1 matrix holds a fixed numeric tuple per directed pair. MOOLLM's holds whatever the world needs, in whatever notation fits, with comments carrying meaning the way they do everywhere else. So a relationship buff is not confined to the dimensions someone declared in advance:

relationships:
  marieke:
    owes_her: "three coffees and an apology"     # invented for this world, needs no schema
    buffs:
      - name: "Awkward Since the Thing"
        effect:
          eye_contact: -3
          # not a stat anybody registered — the guard reads the prose
        guard: "while neither of them has mentioned it"
        expires: { when: "one of them mentions it" }

Two consequences worth naming. The daily/lifetime pair inherited from the Sims chunk format is one convention among many, not the schema — it stays for [beaming to and from The Sims](../character/CARD.yml), and a world that does not care about it simply omits it. And an effect can be purely semantic, with no numeric footprint at all, because the cell is prose-capable and the guard is compiled from English ([BUFF-IN-TIME-COMPILER.md](BUFF-IN-TIME-COMPILER.md)).

Pairs of anything, including yourself

The relationships map already points at characters, objects, and places — so pair-hosted buffs come along for free:

# a person and an object
relationships:
  object/grandfathers-watch:
    buffs:
      - { name: "Can't Bear to Sell It", effect: { will_trade: false } }

# a person and a place
  room/the-old-library:
    buffs:
      - { name: "Homesick For It", effect: { pull: +4 }, expires: { when: "she visits" } }

# a person and themselves — `self:` is a relationship like any other
  self:
    buffs:
      - name: "Ashamed"
        effect: { confidence: -20 }
        expires: { when: "she tells someone what happened" }

That last one is the case a character-hosted buff models badly and a relationship-hosted one models exactly: shame is not a property of a person, it is a stance toward oneself, and it ends when the stance changes.

The same shape covers grudges, trust, debts, jealousy, and disguises — a disguise's effect lives in the observer-wearer pair and belongs to neither ([buffopedia/systems/equipment-inventory/](buffopedia/systems/equipment-inventory/SYSTEM.yml)). In Korz terms this is a slot whose coordinates include both parties, which is why [SELF-KORZ.md](SELF-KORZ.md) wants the receiver dropped.

⚠ Reciprocity is about existence, not values. If A has a cell for B, B should have a cell for A — but the contents must be free to disagree, or Cupid is unimplementable. See the note in [../coherence-engine/](../coherence-engine/GLANCE.yml).

Bulk operations over the matrix

Cupid edits one directed cell. Super Cupid — make everyone love everyone, or hate, or go neutral — operates on a whole region of the matrix at once, and it needs three fields:

operation:
  scope: { among: everyone }        # or: a lot, a family, a named set
  topology: complete_symmetric      # who ends up pointing at whom — see below
  write: { romance: 100 }

Neutral is a different kind of operation from love and hate, and the difference is the base/effective split ([EFFECTIVE-VALUES.md](EFFECTIVE-VALUES.md)):

Layer written Behavior What "neutral" means there
base permanent; prior values are gone social amnesia — history destroyed, nobody remembers the feud
modifier (a buff per cell) temporary; originals return on expiry a truce — everyone is civil for an hour, then remembers

That is one object with two wildly different characters, and the buff version is only possible because base values are preserved underneath. A party where everyone adores each other for an hour and then the grudges come back is the whole reason the split exists.

buff:
  name: "Universal Adoration"
  host: relationship/{ among: everyone }   # applied per cell in scope
  effect: { romance: +100 }
  expires: { after: 60, unit: minutes }    # base values return, feuds intact

Topology is the interesting parameter, because it turns one object into several:

Topology Shape What it makes
complete_symmetric everyone ↔ everyone a commune
complete_asymmetric random per direction unrequited chaos
star_inbound everyone → one a cult — adoration flows one way
star_outbound one → everyone the doting leader
perfect_matching disjoint pairs mass pairing, the guru assigning couples
clique_partition love inside, hate across factions — a schism generator

starinbound and cliquepartition are worth calling out: one is how a cult's affection graph actually looks, and the other manufactures rival factions in a single gesture, which is otherwise hours of hand-editing.

Group hosts — one buff over a set, not a mesh of pairs

Some bonds are held by three or more parties at once: a marriage of three, a band, a crew, a conspiracy, a cult. The host is the set:

buff:
  name: "Married"
  host: group/{ members: [bob, alice, chandra] }
  effect: { household: shared, inheritance: equal }

buff:
  name: "In On It"
  host: group/{ members: [marieke, henk] }        # two is just the common case
  effect: { may_discuss: the-thing }
  guard: "only when no one else is present"

A group host is a hyperedge, and it is not reducible to the pairwise cells in the relationship matrix:

  • The bond is one thing, not its pairs. Three people in one marriage is a

single commitment among three, not three couples. Storing it as pairs asserts something false about its structure.

  • Dissolution is one operation. Ending a group bond means removing one buff

from one host. Pairwise, it is n(n−1)/2 removals that can partly fail, leaving a marriage that half exists.

  • Membership can change without the bond ending. Members join and leave a crew

that persists. A pair bond, by contrast, dies when either party leaves — which is correct for pairs and wrong for groups.

So use a relationship host when the asymmetry matters (Bob's feelings for Alice, which Alice need not return) and a group host when the togetherness is the thing being modified. They coexist: three people can share one marriage while each holding a different private opinion of the others.

Two questions a group-hosted buff must answer, because the pair case answered them implicitly:

Question Options
A member leaves dissolve · shrink · block the departure
Membership is edited new buff · amend in place (a Fluxx New Rule played onto the host)
buff:
  name: "The Crew"
  host: group/{ members: [a, b, c] }
  on_member_leave: shrink        # dissolve | shrink | block
  min_members: 2                 # dissolve if it would drop below

Worked example — an officiant that summons a participant list and marries it, whoever and however many they are: [life-events-playset.md](../../../WillWrightShowForFood/designs/orchestrator-playsets/life-events-playset.md).

Prior art, in Don's own code, is written up at [buffopedia/systems/simprov/](buffopedia/systems/simprov/SYSTEM.yml). What The Sims itself eventually did about relationship-hosted effects is in [buffopedia/systems/sims-4/](buffopedia/systems/sims-4/SYSTEM.yml).

Prototype hosts inherit for free

Attaching a buff to a prototype means everything delegating to it is affected, because that is what delegation does:

buff:
  name: "Cursed Silver"
  hosts: [prototype]
  attached_to: material/silver
  effect: { harms_wielder: true }
# every object made of silver now harms its wielder, including ones made later

Most engines cannot do this at all; Dwarf Fortress is the notable exception, and MOOLLM gets it from the object model rather than as a feature. See [buffopedia/registry.yml](buffopedia/registry.yml) under the syndrome family.

Structure

buff:
  name: "Caffeinated"
  source: "Espresso"
  effect: { energy: +2, focus: +1 }
  duration: 5  # simulation turns
  stacks: false
Field Purpose
name Display name
source What granted this buff
effect Stat mods OR semantic prompt
duration How long it lasts
stacks Can multiple instances exist?
max_stacks If stacking, limit
decay How it ends (time, action, condition)

Buff Types

Numeric

Traditional stat modifiers:

buff:
  name: "Caffeinated"
  effect: { energy: +2, focus: +1 }
  duration: 5

Semantic

Arbitrary effect prompts interpreted by the LLM — not predefined stats, just vibes:

  • "feeling lucky"
  • "cats seem to like you today"
  • "slightly cursed"
  • "radiating calm energy"
  • "shadows feel watchful"

How it works:

Buff: "cats seem to like you today"
Action: PAT TERPIE
LLM: Gives bonus, narrates extra warmth

Mixed

Combine numeric and semantic:

buff:
  name: "Terpie's Blessing"
  effect:
    calm: +2
    vibe: "cats trust you more"
  duration: "a while"

Standard Properties Buffs Affect

Player/NPC Stats (Sims-Style Needs)

# Numeric needs — decay over time, restored by actions
needs:
  hunger: 80      # 0=starving, 100=full
  energy: 65      # 0=exhausted, 100=rested
  social: 45      # 0=lonely, 100=connected
  hygiene: 90     # 0=filthy, 100=clean
  bladder: 30     # 0=desperate, 100=empty
  fun: 55         # 0=bored, 100=entertained
  comfort: 70     # 0=miserable, 100=cozy

Mind-Mirror Stats (Cognitive/Emotional)

# Mental state — affects decision-making and narration
mind:
  focus: 75       # Concentration (0-100)
  mood: 20        # Emotional valence (-100 to +100)
  stress: 35      # Anxiety level (0-100)
  creativity: 60  # Creative capacity (0-100)
  confidence: 50  # Self-assurance (0-100)
  patience: 40    # Frustration tolerance (0-100)
  curiosity: 80   # Exploration drive (0-100)

Room Stats

A room carries its own stats and its own buffs, directly. No spirit required:

room:
  id: blacksmith-forge
  name: "The Forge"

  # The room's own stats
  production_speed: 120   # +20% crafting speed
  error_rate: 8           # 8% chance of mistakes
  mood_influence: +5      # Slight pride boost
  comfort_bonus: -10      # Hot and uncomfortable
  discovery_chance: 15    # Sometimes find rare materials
  danger_level: 25        # Burns, sparks, accidents

  buffs:
    - id: well-tended
      source: "Someone banked the fire properly"
      effect: { production_speed: +30, error_rate: -5 }
      expires: { after: 12, unit: turns }
      radiates:
        to: occupants
        effect: { mood: +5 }
        while: present
Room Stat What It Does Example Buff Effect
production_speed Work/craft rate Blessing: +30% faster
error_rate Mistake probability Curse: +20% more errors
mood_influence Mood granted to visitors Haunting: -15 mood
comfort_bonus Comfort modifier Cozy: +20 comfort
discovery_chance Finding hidden things Mysterious: +25%
danger_level Hazard intensity Cursed: traps more deadly

Note expires: rather than a duration tied to the host's lifetime — a room does not die, so a room buff that waits for its host to expire waits forever.

A forge that remembers who banked the fire, wants to be tended, and can be pleased or offended is a room that also delegates to character — see above. It holds its own buffs either way; delegating to character is what lets it want things and act on them.

Sources

Source Example
Interactions Petting a cat grants joy
Consumables Coffee grants energy
Locations Being in pub grants comfort
Items Lit lamp grants grue immunity
Relationships High friendship grants trust
Personas Wearing persona grants themed buffs

Lifecycle Hooks

Three hooks control buff behavior, written as natural language and compiled to JS:

Hook → Compiles To Purpose
start start_js Runs when buff activates
simulate simulate_js Runs each tick while active
is_finished isfinishedjs Returns true → buff ends

Example: Poison Buff

buff:
  id: poison
  name: "Poisoned"
  tags: [curse, damage-over-time, dispellable]
  
  # Natural language prompts (author writes these)
  start: "Mark character as poisoned, turn them slightly green"
  simulate: "Reduce HP by 1, chance of groaning sound"
  is_finished: "Return true after 5 ticks OR if HP drops below 10"
  
  # Compiled by buff compiler (generated)
  start_js: |
    subject.poisoned = true;
    subject.tint = 'green';
  simulate_js: |
    subject.hp -= 1;
    if (Math.random() < 0.3) world.emit('*groan*');
  is_finished_js: |
    return subject.poisonTicks >= 5 || subject.hp < 10;

Closure Signature

All compiled hooks use the same signature:

(world, subject, verb, object) => { ... }
  • world — shared game state (never null)
  • subject — the character with the buff (never null for buffs)
  • verb — context-dependent (may be null)
  • object — context-dependent (may be null)

Body-only in YAML: Write just the code body, engine wraps it.

Buff Interactions

Buffs can look up and modify other buffs by tag:

Interaction Effect Example
cancels Remove buffs with these tags Antidote cancels [poison]
boosts Multiply/extend buffs with tags Fire spell boosts [fire] x2
replaces Remove old, add this Drunk replaces [tipsy]
merges_with Combine into new buff Rage + Focus → Battle Trance
blocked_by Can't apply if these exist Poison blocked by [immunity-poison]
counters Weaken/shorten these buffs OJ counters [hangover]
countered_by These weaken/shorten this Couch-lock countered by [citrus]

Cancel Example

buff:
  id: cleanse
  name: "Cleanse"
  tags: [holy, dispel]
  cancels: [curse, poison, disease]  # Remove all matching
  start: "Holy light purges dark afflictions"

Pending — issued, but not yet in force

A buff can exist without being in effect. The model has active and absent, and the missing third state is issued and awaiting ratification:

buff:
  name: "Married"
  host: group/{ members: [bob, alice] }
  status: pending                       # exists, contributes nothing yet
  pending_expires: { after: 90, unit: days }   # the licence lapses if unused
  ratified_by: officiant                # who can flip it to active

Two clocks, not one. The pending window is how long the offer stands; the buff's own expires: does not start until ratification. A lapsed pending buff was never in force at all, which is a different outcome from one that expired.

Why it earns its place:

  • Prior art is everywhere. A marriage licence is issued, then solemnized, then

recorded. A statute is enacted, then commences on a later date. A contract is signed, then takes effect. Enactment and force are separate events in every system that takes records seriously.

  • It makes batch ratification possible. Pending buffs are a work queue, so one

event can ratify all of them — a mass wedding is a scheduler over every unratified licence present.

  • The gap between issue and ratification is where the stories are. Someone

assigned and never showing up is only representable if the assignment can exist unfulfilled.

status: omitted means active on application, which is the ordinary case.

Removal policy — who may end this, at what cost, leaving what

The table above matches on tags: what cancels what. It says nothing about who is permitted to remove a buff, what removal costs, or what it leaves behind — and those three questions are where most of the interesting cases live. A cursed item you cannot drop, a marriage you can end cheaply or properly, a contract that needs a specialist: all three are the same missing field.

buff:
  name: "Married"
  host: object/marriage-certificate     # the artifact is the host
  removal:
    - by: host_owner                    # throw the certificate out
      cost: 0
      unwinds: false                    # record gone, entanglement remains
      residue:
        relationship: { bitter: +40 }
        room: "a lighter rectangle on the wallpaper"
    - by: service/divorce-attorney      # hire someone to do it properly
      cost: { simoleons: 3000 }
      unwinds: true                     # pairwise state, property, paperwork
      residue: {}

Three fields, each answering one question:

Field Question Prior art
by: who is permitted to remove it D&D's remove-curse-requires-a-caster; cursed items you cannot unequip
cost: what removal takes hiring a specialist as an economy rather than a spell list
unwinds: whether the state is cleaned up or just the record deleted the orchestrator as unwind handler ([SELF-KORZ.md § phase extent](SELF-KORZ.md))

A cheap removal destroys the record. An expensive one unwinds the state. The difference is residue:, and residue is where the stories are — the ex who is still owed money, the rectangle on the wall where the certificate hung.

A buff with no removal: block behaves as it always has: it expires on its own terms and cancels: can clear it.

Second-order: a buff that edits another buff's removal

Because removal: is data on the buff, another buff can modify it. A prenuptial agreement does not change what being married does — it changes what divorce costs and what it leaves:

buff:
  name: "Prenuptial Agreement"
  host: object/marriage-certificate     # rides on the same artifact
  modifies_removal_of: buff/married
  set:
    - { by: service/divorce-attorney, cost: { simoleons: 500 }, unwinds: true, residue: {} }

That is a modifier whose target is another modifier's dispel policy, which the tag-matching table cannot express at all. Worked design, with the object family it belongs to: [life-events-playset.md](../../../WillWrightShowForFood/designs/orchestrator-playsets/life-events-playset.md).

Boost Example

buff:
  id: fire-attunement
  name: "Fire Attunement"
  boosts:
    tags: [fire]
    multiplier: 2.0
    extend_duration: 5
  start: "Fire spells burn twice as hot"

Merge Example

buff:
  id: rage
  name: "Rage"
  tags: [combat, aggression]
  merges_with:
    tags: [focus, discipline]
    result: battle-trance  # Creates new combined buff
    
buff:
  id: battle-trance
  name: "Battle Trance"
  tags: [combat, legendary]
  effect: { damage: +50%, focus: +30, pain_immunity: true }
  start: "Fury and focus unite — you become a weapon"

Blocked By Example

buff:
  id: poison
  name: "Poisoned"
  tags: [poison, damage-over-time]
  blocked_by: [immunity-poison, divine-protection]
  # Won't apply if target has these tags

Weight Trees (ML-Style Mixtures)

Buffs can form hierarchical weighted mixtures, like neural network layers:

Blend → Strains → Terpenes → Effects
  ↓        ↓          ↓          ↓
weights  weights    weights    final
  ↑        ↑          ↑          ↑
 BUFFS   BUFFS      BUFFS     BUFFS   ← Each stage can be modified by buffs!

Meta-buffs can modify the weight tree itself:

Buff Affects Example
tolerance Strain weights Regular use → diminishing returns
sensitivity Terpene weights First time → effects amplified
synergy-boost Effect weights Entourage → all effects +20%
citrus-clarity Specific terpenes Limonene effects doubled
indica-affinity Strain category Indica strains hit harder

Tolerance Relationships

Tolerances use the character relationship map — same system as NPC friendships:

character:
  id: player
  name: "Don"
  
  # Relationships include people AND substances
  # Terpenes are unidirectional — they don't have feelings back
  relationships:
    # NPCs (bidirectional)
    bob: { trust: 45, friendship: 60 }
    alice: { trust: 80, friendship: 75 }
    
    # Terpene tolerances (unidirectional — no reciprocal)
    terpene/myrcene: { tolerance: 45 }      # Couch-lock less effective
    terpene/limonene: { tolerance: 12 }     # Citrus hits hard
    terpene/pinene: { tolerance: 30 }
    terpene/linalool: { tolerance: 5 }      # Lavender knocks you out
    terpene/caryophyllene: { tolerance: 60 } # Need more for pain relief
    terpene/humulene: { tolerance: 20 }
    terpene/terpinolene: { tolerance: 8 }   # Full creative boost
    terpene/ocimene: { tolerance: 3 }       # Maximum effect

Key difference from NPC relationships:

Aspect NPC Relationship Terpene Relationship
Direction Bidirectional Unidirectional
Reciprocal Bob likes you back Myrcene has no feelings
Tracked on Both characters Player only
Decay Neglect hurts both Time heals tolerance

Tolerance mechanics:

Tolerance Multiplier Experience
0 (virgin) 1.5x "Whoa, this is intense"
25 (light) 1.2x "Nice, I feel it"
50 (moderate) 1.0x "Standard effect"
75 (heavy) 0.7x "Need more than usual"
100 (maxed) 0.4x "Barely feel anything"

Tolerance changes:

# Each use increases tolerance
on_use:
  tolerance_gain: 2-5 points per use
  
# Tolerance decays over time (T-break!)
on_rest:
  tolerance_decay: 1 point per day of abstinence
  
# Full reset after extended break
t_break:
  duration: 2 weeks
  effect: "Reset to 50% of current tolerance"

Effective weight calculation:

def get_effective_terpene_weight(character, terpene, base_weight):
    tolerance = character.tolerances.get(terpene, 0)
    
    # Convert tolerance to multiplier
    if tolerance < 25:
        multiplier = 1.5 - (tolerance / 50)  # 1.5x → 1.0x
    elif tolerance < 75:
        multiplier = 1.0 - ((tolerance - 50) / 100)  # 1.0x → 0.75x
    else:
        multiplier = 0.75 - ((tolerance - 75) / 100)  # 0.75x → 0.5x
    
    return base_weight * multiplier

Layer 1: Terpenes → Effects

Each terpene has weighted effects:

myrcene-blessing:
  effects_weighted:
    relaxation: { value: +30, weight: 1.0 }   # Full effect
    pain_relief: { value: +20, weight: 0.8 }  # 80%
    sedation: { value: +25, weight: 0.9 }     # 90%

Layer 2: Strains → Terpenes

Each strain is a weighted mixture of terpenes:

strain-og-kush:
  terpene_profile:
    myrcene: 0.35      # 35% of profile
    limonene: 0.25     # 25%
    caryophyllene: 0.20
    linalool: 0.10
    humulene: 0.10

Layer 3: Blends → Strains

Blends mix multiple strains:

blend-wake-and-bake:
  strain_mixture:
    sour-diesel: 0.50     # Half the blend
    jack-herer: 0.30      # 30%
    pineapple-express: 0.20

Computing Final Effects

# Blend → Strain → Terpene → Effect propagation
def compute_blend_effects(blend):
    final_terpenes = {}
    
    # Layer 3→2: Blend weights × Strain terpene profiles
    for strain_id, strain_weight in blend.strain_mixture.items():
        strain = get_strain(strain_id)
        for terpene, terpene_weight in strain.terpene_profile.items():
            final_terpenes[terpene] += strain_weight * terpene_weight
    
    # Layer 2→1: Terpene amounts × Effect weights
    final_effects = {}
    for terpene, amount in final_terpenes.items():
        terpene_buff = get_terpene_buff(terpene)
        for effect, config in terpene_buff.effects_weighted.items():
            final_effects[effect] += amount * config.weight * config.value
    
    return final_effects

Example Calculation

Wake & Bake Blend:
├── Sour Diesel (50%)
│   ├── limonene: 0.30 × 0.50 = 0.15
│   └── pinene: 0.15 × 0.50 = 0.075
├── Jack Herer (30%)
│   ├── limonene: 0.20 × 0.30 = 0.06
│   └── pinene: 0.25 × 0.30 = 0.075
└── Pineapple Express (20%)
    ├── limonene: 0.30 × 0.20 = 0.06
    └── pinene: 0.25 × 0.20 = 0.05

Final limonene: 0.15 + 0.06 + 0.06 = 0.27
Final pinene: 0.075 + 0.075 + 0.05 = 0.20

Then: limonene × mood_boost, pinene × focus → final character effects

This is essentially a mini neural network where:

  • Weights are terpene profiles and strain mixtures
  • Activations are effect values
  • Forward pass computes final buff effects

Buff Orchestration (Simulation Loop)

The orchestrator runs buff rounds during simulation ticks:

1. Scan Phase

Orchestrator collects all active buffs across all characters:

# Orchestrator builds active-buff manifest
active_buffs:
  - character: player
    buff_ref: "skills/buff/buffs/INDEX.yml#caffeinated"
    remaining: 6
    stacks: 2
    
  - character: player
    buff_ref: "skills/buff/buffs/INDEX.yml#high"
    remaining: 8
    stacks: 1
    
  - character: bob-npc
    buff_ref: "skills/buff/buffs/INDEX.yml#drunk"
    remaining: 4
    stacks: 1

2. Event Generation

Create buff-tick events with pointers:

buff_round:
  tick: 42
  events:
    - type: buff-simulate
      character: player
      buff: caffeinated
      simulate_js: "subject.energy_effective += 20; subject.focus_effective += 15;"
      
    - type: buff-simulate  
      character: player
      buff: high
      simulate: "Deep thoughts about random topics, food cravings"
      simulate_js: "if (Math.random() < 0.3) world.emit('*ponders existence*');"
      
    - type: buff-simulate
      character: bob-npc
      buff: drunk
      simulate: "Occasional slurred speech, may say embarrassing things"

3. LLM Simulation Prompt

Orchestrator instructs LLM to enumerate and simulate:

prompt: |
  BUFF ROUND — Tick 42
  
  Enumerate and simulate each active buff:
  
  1. PLAYER — Caffeinated (6 ticks remaining, 2 stacks)
     Effect: +20 energy, +15 focus per stack
     Simulate: Apply effects, note jitteriness if 2+ stacks
     
  2. PLAYER — High (8 ticks remaining)
     Effect: -25 stress, +20 creativity, +30 hunger
     Simulate: "Deep thoughts about random topics, food cravings"
     → Narrate any random musings or munchie urges
     
  3. BOB — Drunk (4 ticks remaining)
     Effect: +30 confidence, -25 focus, -30 judgement
     Simulate: "Occasional slurred speech, may say embarrassing things"
     → Decide if Bob says something regrettable this tick
  
  For each buff:
  - Apply stat modifications to _effective values
  - Run simulate behavior (chance-based events)
  - Check is_finished conditions
  - Decrement remaining duration
  - Remove expired buffs, trigger spawns_after
  
  Return updated character states and any narration.

4. Buff Lifecycle Per Tick

┌─────────────────────────────────────────────────────────────────┐
│                        BUFF TICK                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  For each character:                                             │
│    For each active buff:                                         │
│                                                                  │
│      1. APPLY EFFECTS                                            │
│         stat_effective += buff.effect × buff.stacks             │
│                                                                  │
│      2. RUN SIMULATE                                             │
│         Execute simulate_js OR let LLM interpret simulate       │
│         (chance-based events, narration, random behaviors)      │
│                                                                  │
│      3. CHECK IS_FINISHED                                        │
│         If is_finished_js returns true → mark for removal       │
│         If remaining <= 0 → mark for removal                    │
│                                                                  │
│      4. DECREMENT DURATION                                       │
│         remaining -= 1                                           │
│                                                                  │
│      5. HANDLE EXPIRATION                                        │
│         If marked for removal:                                   │
│           - Remove buff from character                           │
│           - Trigger spawns_after buffs (with delay/chance)      │
│           - Emit buff-expired event                              │
│                                                                  │
│      6. HANDLE INTERACTIONS                                      │
│         Check for cancels, boosts, replaces, merges             │
│         Apply buff-on-buff effects                               │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5. Compiled vs Interpreted

Mode When How
Compiled simulate_js exists Engine evals cached closure directly
Interpreted Only simulate text LLM reads prompt, narrates behavior
Hybrid Both exist JS runs effects, LLM narrates flavor
buff:
  id: drunk
  # LLM interprets this for narration
  simulate: "Occasional slurred speech, may say embarrassing things"
  # Engine runs this for mechanics
  simulate_js: |
    if (Math.random() < 0.2) {
      world.emit(subject.name + " slurs something incomprehensible");
    }

6. Attention Concentration (Time-Slicing)

The event-based design concentrates LLM attention on specific tasks:

┌──────────────────────────────────────────────────────────────────┐
│              LLM ATTENTION TIME-SLICING                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Instead of: "Simulate everything at once" (diffuse attention)   │
│                                                                   │
│  We do: Series of focused micro-tasks                            │
│                                                                   │
│  ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐          │
│  │ Buff 1  │ → │ Buff 2  │ → │ Buff 3  │ → │ Buff 4  │          │
│  │ PLAYER  │   │ PLAYER  │   │ BOB     │   │ ROOM    │          │
│  │ caffein │   │ high    │   │ drunk   │   │ haunted │          │
│  └─────────┘   └─────────┘   └─────────┘   └─────────┘          │
│       ↓             ↓             ↓             ↓                │
│   [focused]    [focused]    [focused]    [focused]               │
│   attention    attention    attention    attention               │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Why this works:

Problem Solution
LLM loses track with many buffs One buff at a time, clear context
Effects get confused/merged Each buff isolated in its own slice
Hard to debug Each event is traceable, logged
Inconsistent simulation Same prompt structure every time

Iteration Pattern:

# Orchestrator feeds LLM one task at a time
iteration_1:
  focus: "PLAYER's Caffeinated buff"
  context: [player_state, buff_definition, tick_number]
  task: "Apply effects, check finish condition, narrate if needed"
  output: [updated_state, narration, events]
  
iteration_2:
  focus: "PLAYER's High buff"
  context: [player_state, buff_definition, tick_number]  
  task: "Apply effects, chance of munchies event, narrate thoughts"
  output: [updated_state, narration, events]
  
# ... and so on

Benefits:

  1. Focused attention — LLM only thinks about one buff
  2. Predictable structure — Same input/output format each time
  3. Debuggable — Can trace exactly which buff caused what
  4. Parallelizable — Independent buffs can run in parallel
  5. Interruptible — Can pause/resume between iterations
  6. Cacheable — Compiled _js buffs skip LLM entirely

Speed-of-Light Compatible:

This fits the [speed-of-light](../speed-of-light/) pattern — many focused micro-operations in a single LLM call, or batched across calls:

# Single call, multiple focused tasks
prompt: |
  Process these buff events in sequence:
  
  [1/4] PLAYER — Caffeinated
  → Apply: energy +40, focus +30 (2 stacks)
  → Check: is_finished? No (6 remaining)
  → Output: state changes only
  
  [2/4] PLAYER — High  
  → Apply: stress -25, creativity +20, hunger +30
  → Simulate: "Deep thoughts" — roll for musing
  → Output: state + optional narration
  
  [3/4] BOB — Drunk
  → Apply: confidence +30, focus -25
  → Simulate: "Slurred speech" — roll for embarrassment
  → Output: state + optional narration
  
  [4/4] LIBRARY-SPIRIT — Haunted
  → Apply: error_rate +15, mood_influence -20
  → Simulate: "Poltergeist activity" — roll for book fall
  → Output: room effects + optional event

Stacking

  • Same source: Doesn't stack — refresh duration instead
  • Different sources: Stack additively up to category limit

Category Limits

terpene_effects: 3
charm_effects: 5
consumable_effects: 4
negative_effects: 3  # 3+ same negative = LEGENDARY

Synergies

Some buffs COMBINE into stronger effects:

  • Myr + Lily = "Sedation Stack"
  • Lemon + Pine = "Focus Boost"
  • All 8 kittens = "ENTOURAGE EFFECT" (legendary)

Negative Buffs (Curses)

Curses are just shitty buffs. Same structure, negative effects.

buff:
  name: "Scratched"
  source: "Failed BELLY RUB"
  effect: { hp: -1, visible_marks: true }
  duration: "Until healed"

Persistent Curses

Long-term negative buffs with lift conditions:

buff:
  name: "Curse of Darkness"
  effect: { lamp_efficiency: -25% }
  duration: conditional
  lift_condition: "Light 3 dark places"
  reward_on_lift: "LIGHT-BEARER title"

Duration Types

Type Example
Turns duration: 4
Conditional duration: until you eat
While present duration: while in pub
Permanent duration: forever
Natural language duration: a few minutes
Probabilistic duration: 25% fade chance per turn

Natural Language Durations

We're not tracking real time — the LLM interprets and makes its best guess:

  • "forever"
  • "5 minutes"
  • "a day"
  • "until sunset"
  • "randomly 50%"
  • "a while"
  • "briefly"
  • "until you forget"

See [time/](../time/) for full natural duration examples.

Decay

When LLM judges turn(s) have passed:

  1. Decrement duration on timed buffs
  2. Remove buffs that hit 0
  3. Apply new buffs from current turn

Effective Derived Values: Flags Edition

This is the effective derived values protocol for booleans.

Type Base Modifiers Effective
Numeric energy: 5 buff +2 effective_energy: 7
Boolean in_darkness: false room.lit=false, has_lamp=false effectiveindarkness: true

Same pattern:

  • Numeric: base + sum(modifiers) = effective
  • Boolean: base OR any(conditions) = effective flag

Push / Pull / Latch

The LLM can handle any combination:

Mode Pattern Example
Pull Compute on demand in_darkness derived from lamp + room state
Push Source sets flag Buff explicitly sets urgent_situation: true
Latch Stays until cleared hasvisitedroom_a: true persists
# PULL — derived on demand, not stored
in_darkness: (room.lit == false) AND (has_lamp == false)

# PUSH — buff explicitly sets
buff:
  sets_flags: [urgent_situation]

# LATCH — persists in state until cleared
player:
  visited_rooms: [room-a, room-b]  # grows, never shrinks

Traditional reactive systems pick one mode. The LLM does all three simultaneously — it sees the whole context and figures out which pattern applies.

Tweening and Animation

Values don't have to snap — they can interpolate over time:

Type Instant Tweened
Numeric energy: 5 → 7 energy: 5 → 7 over 3 turns
Boolean lit: false → true lit: fading in over 2 turns
Position room-a → room-b walking through hallway
buff:
  name: "Warming Up"
  effect: { warmth: +3 }
  tween: ease-in    # Gradual increase
  duration: 5

animation:
  entering_room:
    from: hallway
    to: pub
    frames: [approaching, at_door, stepping_in, arrived]

The LLM narrates intermediate states. "You feel yourself warming up..." not just "You are warm now."

Velocity

Any reactive variable can have a rate of change:

energy:
  value: 5
  velocity: -1      # Draining 1 per turn
  
trust:
  value: 45
  velocity: +3      # Building rapport
  
mood:
  value: "content"
  velocity: "improving"  # Semantic velocity works too
Variable Value Velocity Meaning
energy 5 -1 Tired and getting worse
trust 45 +3 Relationship strengthening
position room-a north Moving northward
mood anxious calming Settling down

The LLM reads velocity to predict and narrate: "You're running low on energy and fading fast..." vs "Low energy but recovering."

Physics Simulation

Extend to full 2D/3D cartoon physics:

thrown_ball:
  position: [5, 3]
  velocity: [2, 4]       # Moving up-right
  acceleration: [0, -1]  # Gravity pulling down
  
bouncing:
  elasticity: 0.8        # Loses 20% on bounce
  
cartoon_physics:
  hang_time: true        # Pause at apex
  squash_stretch: true   # Deform on impact
  delayed_fall: true     # Look down first, then fall

The LLM narrates physics with cartoon timing:

*The ball arcs gracefully upward... hangs for a moment at the peak...
then plummets, SQUASHING flat against the floor before bouncing back
slightly less enthusiastically.*

Works for:

  • Thrown objects (ball, inventory items)
  • Character movement (jumping, falling, knockback)
  • Environmental effects (swinging doors, rolling boulders)
  • Looney Tunes logic (run off cliff, pause, look down, THEN fall)
  • Temperature cooling or warming (ice cream melting, water freezing)

Commands

Command Effect
BUFFS or STATUS List active buffs with remaining duration
EXAMINE [buff] Full details of buff source, effect, duration