buff
npx skills add https://github.com/simhacker/moollm --skill buff
Agent 安装分布
Skill 文档
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.
Characters Only
Buffs only target characters. This is a design constraint, not a limitation.
- Single closure signature:
(world, subject, verb, object) subjectis always a character â no type checking needed- Rooms that need buffs get a “room spirit” character
# Room needs to be "haunted"? Create its spirit.
character:
id: dark-cave-spirit
name: "Spirit of the Dark Cave"
location: room/dark-cave
buffs:
- ref: buff/haunted
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 Spirit Stats
Room spirits are characters whose stats affect the room they haunt:
character:
id: forge-spirit
name: "Spirit of the Forge"
location: room/blacksmith-forge
# These stats affect everyone in the room
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: master-craftsman-blessing
source: "Pleased the forge spirit"
effect: { production_speed: +30, error_rate: -5 }
duration: "until you leave"
| Spirit 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 |
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 |
is_finished_js |
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"
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:
- Focused attention â LLM only thinks about one buff
- Predictable structure â Same input/output format each time
- Debuggable â Can trace exactly which buff caused what
- Parallelizable â Independent buffs can run in parallel
- Interruptible â Can pause/resume between iterations
- Cacheable â Compiled
_jsbuffs skip LLM entirely
Speed-of-Light Compatible:
This fits the 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/ for full natural duration examples.
Decay
When LLM judges turn(s) have passed:
- Decrement duration on timed buffs
- Remove buffs that hit 0
- 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 | effective_in_darkness: 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 | has_visited_room_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 |