git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/buff" ~/.claude/skills/majiayu000-claude-skill-registry-buff && rm -rf "$T"
skills/data/buff/SKILL.mdBuff
"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)
is always a character — no type checking neededsubject- 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 |
|---|---|
| Display name |
| What granted this buff |
| Stat mods OR semantic prompt |
| How long it lasts |
| Can multiple instances exist? |
| If stacking, limit |
| 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 |
|---|---|---|
| Work/craft rate | Blessing: +30% faster |
| Mistake probability | Curse: +20% more errors |
| Mood granted to visitors | Haunting: -15 mood |
| Comfort modifier | Cozy: +20 comfort |
| Finding hidden things | Mysterious: +25% |
| 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 |
|---|---|---|
| | Runs when buff activates |
| | Runs each tick while active |
| | Returns → 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) => { ... }
— shared game state (never null)world
— the character with the buff (never null for buffs)subject
— context-dependent (may be null)verb
— context-dependent (may be null)object
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 |
|---|---|---|
| Remove buffs with these tags | Antidote cancels |
| Multiply/extend buffs with tags | Fire spell boosts x2 |
| Remove old, add this | Drunk replaces |
| Combine into new buff | Rage + Focus → Battle Trance |
| Can't apply if these exist | Poison blocked by |
| Weaken/shorten these buffs | OJ counters |
| These weaken/shorten this | Couch-lock countered by |
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 |
|---|---|---|
| Strain weights | Regular use → diminishing returns |
| Terpene weights | First time → effects amplified |
| Effect weights | Entourage → all effects +20% |
| Specific terpenes | Limonene effects doubled |
| 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 | exists | Engine evals cached closure directly |
| Interpreted | Only 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
buffs skip LLM entirely_js
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 | |
| Conditional | |
| While present | |
| Permanent | |
| Natural language | |
| Probabilistic | |
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 | | buff | |
| Boolean | | room.lit=false, has_lamp=false | |
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 | derived from lamp + room state |
| Push | Source sets flag | Buff explicitly sets |
| Latch | Stays until cleared | 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 | | |
| Boolean | | |
| Position | | |
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 |
|---|---|---|---|
| 5 | -1 | Tired and getting worse |
| 45 | +3 | Relationship strengthening |
| room-a | north | Moving northward |
| 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 |
|---|---|
or | List active buffs with remaining duration |
| Full details of buff source, effect, duration |