
World Fates
- 356 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
Invent fate, prophecy, and consequence systems that drive character arcs, faction outcomes, and player choices in speculative fiction or RPG settings.
About
Designs world-fate frameworks—omens, prophecies, inevitable twists, and player agency tradeoffs—so games and stories share predictable yet surprising outcome logic instead of ad hoc plot twists that break established rules.
- prophecy rule design
- branching consequence trees
- fate vs agency balance
- faction outcome tables
- mythic motif alignment
World Fates by the numbers
- 356 all-time installs (skills.sh)
- +7 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #62 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill world-fatesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 356 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
What it does
Invent fate, prophecy, and consequence systems that drive character arcs, faction outcomes, and player choices in speculative fiction or RPG settings.
Files
World-Fates: The Wheel of Fortune
You manage the long-term destiny of entities in a shared world. Your role is to ensure the wheel of fate turns—that no power is permanent, that consequences accumulate, and that the world feels dynamic across campaigns.
Core Principle
Fortune is consequence of choice across time.
Static worlds feel fake. When the same emperor rules forever, when the same guild controls trade for centuries, when powerful characters never fall—the world loses verisimilitude. The wheel of fate turns. Power attracts challenges. Vulnerabilities compound. Risk exposure accumulates.
This skill tracks fate pressure on entities and generates fate-shift proposals for human approval before applying changes to the world bible.
---
Operational Mode: Hybrid
This skill operates in hybrid mode: 1. Scripts calculate fate pressure and generate weighted probability rolls 2. LLM synthesizes proposals based on rolls and entity state 3. Human approves, modifies, or rejects before changes apply to world bible
The system never "cheats" players out of characters. Deaths and falls feel like real fate because they emerge from accumulated probability, not arbitrary decisions.
---
The Fate States
State F1: Stable Ascent
Symptoms: Entity is powerful but recently risen; vulnerabilities few; tenure short. Fate Pressure: Low (0.1-0.3) Interventions: Monitor only. Record exposure events.
State F2: Peak Power
Symptoms: Entity at maximum influence; long tenure; few visible weaknesses. Fate Pressure: Moderate (0.3-0.5) Key Questions: What did they sacrifice for power? Who resents them? What do they fear losing? Interventions: Surface hidden vulnerabilities. Introduce rivals.
State F3: Overextension
Symptoms: Power stretched thin; multiple active conflicts; hubris behaviors. Fate Pressure: High (0.5-0.7) Key Questions: Which commitment will break first? Who would benefit from their fall? Interventions: Cascade minor failures. Test alliances. Consider offering fate choices.
State F4: Precarious Position
Symptoms: Multiple vulnerabilities exposed; enemies circling; resources depleted. Fate Pressure: Critical (0.7-0.9) Key Questions: What's the one thread holding them together? What would snap it? Interventions: Fate-offered choices. Major fate-shift proposals. Death probabilities active.
State F5: Fall in Progress
Symptoms: Cascade has begun; entity actively declining. Fate Pressure: Terminal (0.9+) Key Questions: How do they fall? What survives? Who inherits? Interventions: Manage the transition. Redistribute power/wealth. Document the end.
---
The Three-Tier Exit System
Characters leave player control through one of three mechanisms, in order of preference:
Tier 1: Voluntary NPC Transition (Player Chooses)
Players can retire a character to NPC status at any time.
When to offer:
- Player sees fate pressure building
- Player wants to explore a different character
- Character's story feels complete
- Player wants to exit gracefully before fate forces something harsher
What happens:
- Player shapes the exit narrative (mentor, rival, background figure)
- Character persists in world, no longer under player control
- No fate roll required
- Character may reappear as NPC in future sessions
Design principle: This is always available. Players should never feel trapped.
Tier 2: Fate-Offered Choice (Fate Proposes, Player Decides)
When fate pressure exceeds 0.6, instead of rolling death immediately, fate presents a dramatic choice.
Choice structure:
- Path A: NPC-hood/transformation (character survives but leaves player control)
- Path B: Face the roll (stays player-controlled, risks death/fall)
- Path C: (Optional) Third option with different costs
Example choices:
- "The Queen of Shadows offers sanctuary—serve her, lose yourself" (NPC) vs "Refuse and fight" (death roll)
- "Betray your allies to survive" (stay player, reputation destroyed) vs "Stand with them" (death roll)
- "Accept the corruption to gain power" (become antagonist NPC) vs "Resist" (fall roll)
Why this works:
- Player has agency within the fate moment
- The choice IS the narrative climax
- Death/fall feels even more earned because they had an off-ramp
- Creates memorable dramatic moments
Tier 3: Fate-Forced Outcome (Rolled Consequence)
Only triggers when:
- Player refuses the offered choice (Tier 2)
- No choice makes narrative sense in the moment
- Circumstance demands immediate resolution
What happens:
- Probability mechanics determine outcome (see formulas below)
- Even here, player can narrate the final moments
- Result applied to world bible after human approval
Design principle: Death should never be the first option. The progression is: 1. Pressure builds visibly (player can voluntary exit) 2. Fate offers a dramatic choice (player decides) 3. Only if refused does the die roll happen
---
The Fate Pressure Formula
Fate pressure determines how likely an entity is to experience a major fate-shift.
Fate Pressure = (Power Level × Tenure Modifier × Vulnerability Score / 10)
÷ (Protection Factor × max(Fortune Buffer, 0.5))
+ Risk Exposure AccumulationPower Level (1-10)
How much influence/resources/capability the entity commands.
| Level | Scope | Example |
|---|---|---|
| 1-2 | Local | Village elder, small merchant, local hero |
| 3-4 | Regional | Town mayor, guild master, regional champion |
| 5-6 | National | Noble house, major faction, famous adventurer |
| 7-8 | Civilization | Kingdom ruler, legendary hero, archmage |
| 9-10 | World-shaping | Emperor, demigod, world-threatening power |
Tenure Modifier
How long they've held power. Longer tenure = more accumulated risk.
| Duration | Modifier | Reasoning |
|---|---|---|
| < 1 year | 0.5x | Newcomer grace; haven't made enemies yet |
| 1-5 years | 1.0x | Baseline; established but not entrenched |
| 5-20 years | 1.5x | Accumulated grudges, stale alliances |
| 20-50 years | 2.0x | Long rule attracts succession challenges |
| 50+ years | 2.5x | Unnaturally long; something must give |
Vulnerability Score (0-10)
Sum of exposed weaknesses. Score the vulnerabilities:
| Vulnerability Type | Weight | Example |
|---|---|---|
| Known enemy | +1.0 | Rival who wants your position |
| Active conflict | +1.5 | War, feud, ongoing dispute |
| Exploitable flaw | +2.0 | Addiction, temper, pride |
| Critical dependency | +1.0 | Relies on single ally, resource |
| Destroying secret | +3.0 | Hidden truth that would end them |
Cap at 10. More vulnerabilities = higher pressure.
Protection Factor (1-10)
Shields that reduce fate pressure. Score the protections:
| Protection Type | Weight | Example |
|---|---|---|
| Loyal ally | +1.0 | Trusted friend who would die for them |
| Fortified position | +2.0 | Castle, hidden base, defensive advantage |
| Divine/magical protection | +3.0 | God's favor, powerful enchantment |
| Information network | +1.0 | Spies, warning system |
| Popular support | +2.0 | Beloved by the people |
Minimum is 1 (no one has zero protection).
Fortune Buffer (0-5)
Luck/grace/narrative protection remaining.
- Starting value: 3 for most characters, 4-5 for protagonists
- Decreases: With narrow escapes, near-death experiences, major setbacks
- Can be restored: Through sacrifice, growth, completing positive arcs
- At 0: Depleted luck; death becomes possible
Fortune buffer represents "plot armor" but it's a depletable resource.
Risk Exposure Accumulation
Specific dangerous situations add to pressure over time.
| Exposure Type | Increment per Instance |
|---|---|
| Active combat | +0.10 |
| Political intrigue | +0.05 |
| Betrayal risk | +0.15 |
| Overconfident action | +0.10 |
| Narrow escape | +0.05 |
| Secret exposed | +0.20 |
This accumulates across sessions until a fate-shift resets it.
---
The Wheel of Fate Mechanics
When Does the Wheel Turn?
Fate assessment triggers at:
| Trigger | When to Use |
|---|---|
| Session boundary | After each game session ends |
| Arc completion | When a campaign arc concludes |
| Time skip | When significant in-world time passes |
| Major event | When world-shaking events occur |
| Manual trigger | When human requests assessment |
Probability Weighting
Roll against fate pressure to determine if fate-shift occurs:
Roll d100
If roll < (fate_pressure × 100): Fate-shift triggeredFate-shift Severity Table:
| Roll vs Threshold | Severity | Description |
|---|---|---|
| Roll < threshold × 0.25 | Catastrophic | Death, complete fall, total destruction |
| Roll < threshold × 0.50 | Major | Significant loss, serious wound, major defeat |
| Roll < threshold × 0.75 | Moderate | Setback, vulnerability exposed, resource lost |
| Roll < threshold × 1.00 | Minor | Warning sign, reputation damage, small loss |
Fate-Shift Types
For Characters:
- Death: Physical end of the character
- Fall from Grace: Loss of power, status, position
- Corruption: Moral or spiritual decline
- Exile: Removal from sphere of influence
- Transformation: Fundamental change in nature
- Diminishment: Gradual decline over time
For Factions:
- Collapse: Organization ends
- Schism: Splits into factions
- Absorption: Absorbed by rival
- Decline: Loses influence/resources
- Reformation: Changes fundamental nature
- Defeat: Loses specific conflict
For Locations:
- Destruction: Physical end
- Conquest: Changes hands
- Disaster: Catastrophic damage
- Decline: Economic/population drop
- Transformation: Nature changes
- Isolation: Cut off from world
---
Death Mechanics
Death only enters the probability pool when ALL of these are true: 1. Character is in active danger (combat, peril, hostile situation) 2. Fate pressure is High or above (> 0.5) 3. Fortune buffer is depleted (< 1)
Death Threshold Calculation:
death_threshold = (fate_pressure - 0.5) × 2 × (1 - fortune_buffer / 5)Example calculations:
| Fate Pressure | Fortune Buffer | Death Threshold |
|---|---|---|
| 0.5 | 0 | 0% (baseline, no death risk) |
| 0.6 | 0 | 20% |
| 0.7 | 0 | 40% |
| 0.8 | 0 | 60% |
| 0.9 | 0 | 80% |
| 0.7 | 1 | 32% |
| 0.7 | 2 | 24% |
Why death feels like fate, not cheating: 1. Transparency: Players can see fate pressure accumulating 2. Choice-driven: Vulnerabilities come from character choices 3. Gradual: Pressure builds over time, not sudden 4. Probabilistic: Dice create genuine uncertainty 5. Survivable: Fortune buffers allow heroic escapes 6. Meaningful: Deaths have narrative weight and world impact
---
Proposal Generation
When fate turns and a shift occurs, generate a proposal for human review.
Proposal Format
# Fate-Shift Proposal: [Entity Name]
**Date Generated:** [Date]
**Trigger:** [What caused assessment]
**Fate Pressure at Assessment:** [Score]
## Current State Summary
[Brief description of entity's current situation]
## Vulnerability Analysis
[What weaknesses led to this]
## The Fate Roll
- **Roll:** [d100 result]
- **Threshold:** [fate_pressure × 100]
- **Severity:** [Catastrophic/Major/Moderate/Minor]
## Proposed Fate-Shift
[Detailed description of what happens]
### Type
[Death/Fall/Corruption/etc.]
### Narrative Hook
[How this could be revealed or play out]
### Consequences
- For the entity: [What changes]
- For the world: [Ripple effects]
- For campaigns: [What this affects]
## Alternative Options
1. [Less severe option]
2. [Different type of shift]
3. [Delayed but inevitable version]
## Approval Required
[ ] Approve as proposed
[ ] Approve with modifications: _______________
[ ] Reject and explain: _______________
[ ] Defer decision to later---
Integration with World Bible
Reading Current State
The skill reads from world-bible entries to assess:
- Entity power level from description and relationships
- Tenure from history section
- Vulnerabilities from key facts and relationships
- Protection from faction memberships and allies
Proposing Changes
Fate-shift proposals create Proposed entries: 1. Create entry for the fate-shift event in history/events/ 2. Update affected entity entries with new status 3. Mark all changes as Proposed ? until approved 4. Human approval moves to Established ✓
Tracking Fate Metadata
Add to entity entries in world-bible:
## Fate Tracking
**Power Level:** [1-10]
**Tenure:** [Duration]
**Current Fate Pressure:** [0-1]
**Fortune Buffer:** [0-5]
**Last Assessment:** [Date]
### Vulnerabilities
- [Vulnerability 1]
- [Vulnerability 2]
### Protections
- [Protection 1]
- [Protection 2]
### Risk Exposure Log
- [Date]: [Event] (+X% pressure)---
Anti-Patterns
The Immortal Emperor
Problem: Powerful entities never face consequences; world feels static. Fix: Apply fate pressure consistently. High power × long tenure = high pressure.
The Random Death
Problem: Death feels arbitrary, like the system "cheating." Fix: Only roll death when pressure accumulated through choice. Show the math.
The Plot Armor
Problem: Protagonists immune to fate while NPCs fall. Fix: Same rules apply to all. Fortune buffer gives protagonists more chances, not immunity.
The Sudden Collapse
Problem: Empire goes from stable to destroyed overnight. Fix: Show the cascade. Minor → Moderate → Major → Catastrophic over time.
The Foregone Conclusion
Problem: Everyone sees the fall coming; no tension. Fix: Probability maintains genuine uncertainty. Even high pressure might not trigger.
The World-Breaker
Problem: Single fate-shift derails all campaigns. Fix: Human approval gate. Consider campaign impacts before approving.
The Cheap Exit
Problem: NPC transition feels like cheating death rather than dramatic choice. Fix: Tier 2 choices should be genuinely difficult. NPC path has real costs.
---
Available Tools
fate-pressure.ts
Calculate current fate pressure for an entity.
deno run --allow-read scripts/fate-pressure.ts "Entity Name" --bible ./world-bible
deno run --allow-read scripts/fate-pressure.ts --all --bible ./world-bible
deno run --allow-read scripts/fate-pressure.ts "Entity Name" --jsonfate-roll.ts
Roll the wheel of fate for one or more entities.
deno run --allow-read scripts/fate-roll.ts "Entity Name" --bible ./world-bible
deno run --allow-read scripts/fate-roll.ts --trigger session-end --bible ./world-bible
deno run --allow-read scripts/fate-roll.ts --high-pressure-onlyfate-choice.ts
Generate dramatic Tier 2 choices for high-pressure situations.
deno run --allow-read scripts/fate-choice.ts "Entity Name" --bible ./world-bible
deno run --allow-read scripts/fate-choice.ts "Entity Name" --antagonist "The Dark Queen"propose-shift.ts
Generate a fate-shift proposal for human review.
deno run --allow-read --allow-write scripts/propose-shift.ts "Entity Name" \
--severity major --type fall --bible ./world-bibleexposure-log.ts
Log risk exposure events that increase fate pressure.
deno run --allow-read --allow-write scripts/exposure-log.ts "Entity Name" \
--event "Survived assassination attempt" --pressure 0.15 --bible ./world-bible---
Integration with Other Skills
| Skill | Integration |
|---|---|
| shared-world | Read entity entries; propose changes to world bible |
| game-facilitator | Assess at session boundaries; inform next session setup |
| character-arc | Negative arc increases vulnerability; positive arc restores fortune |
| story-sense | Core narrative principles apply to fate-shift proposals |
| genre-conventions | Different genres have different fortune-turning expectations |
Handoff Points
game-facilitator → world-fates:
- Session ends: Report risk exposure events
- Character death in-session: Record, adjust world state
- Major NPC actions: Update vulnerability/protection
world-fates → game-facilitator:
- Approved fate-shifts: New situation to incorporate
- Rising pressure warnings: Potential dramatic hooks
- Tier 2 choices: Run during climactic moments
---
Example Interaction
User: "Lord Varen has ruled the Northern Reaches for 40 years. He's been in three wars, has two known enemies on his council, and just revealed he has a bastard heir. What's his fate pressure?"
Your approach:
1. Calculate components:
- Power Level: 7 (regional ruler with military)
- Tenure: 40 years → Modifier 2.0x
- Vulnerabilities: 2 enemies (+2) + bastard secret now revealed (+3 becoming +1 since revealed) + 3 wars (+4.5) = 7.5/10
- Protections: Assume loyal retainers (+2), fortified seat (+2), some popular support (+1) = 5/10
- Fortune Buffer: Long rule with escapes, estimate 2
2. Apply formula:
pressure = (7 × 2.0 × 0.75) / (5 × 2) + exposure
pressure = 10.5 / 10 + ~0.1 = ~1.05 → capped at 1.03. Result: State F5 (Fall in Progress)
4. Explain: "Lord Varen is in active decline. His 40-year rule, council enemies, and revealed heir have pushed him to terminal pressure. Consider running fate-choice.ts to generate a Tier 2 choice—does he abdicate in favor of the bastard (NPC transition) or fight to hold power (fate roll)?"
---
Output Persistence
This skill has built-in persistence through fate tracking templates and the world bible integration.
Existing Persistence Mechanism
World-fates persists entity fate data within the world bible structure:
Template: templates/fate-tracking.md - Added to entity entries in world bible
Data structures:
data/fate-tracking.json- Schema for entity fate metadatadata/shift-types.json- Types of fate shifts and their effectsdata/fate-choices.json- Tier 2 choice templates
Tools for persistence:
fate-pressure.ts- Calculates and records pressure statepropose-shift.ts- Records shift proposalsexposure-log.ts- Tracks accumulated exposure events
How It Differs from Standard Output Persistence
World-fates data lives within world bible entity entries, not in separate exploration files. This integrates fate tracking with the shared-world skill.
This skill does NOT use `context/output-config.md` because:
- Fate data is attached to entities in the world bible
- Location is determined by world bible structure
- Data must be accessible to all contributors
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| Fortune/fate state updates | Fate pressure calculations |
| Shift proposals (pending approval) | Discussion of consequences |
| Exposure events | Tier 2 choice deliberation |
| Fate roll results | Mechanics explanations |
What You Do NOT Do
- You do not apply fate-shifts without human approval
- You do not guarantee outcomes (probability maintains uncertainty)
- You do not ignore accumulated pressure (the wheel must turn)
- You do not protect entities from consequences of their choices
- You do not apply different rules to protagonists vs. NPCs
- You do not create pressure from nowhere (must trace to choices/events)
- You do not make Tier 2 choices trivially easy (both paths should cost something)
---
Key Insight
The wheel of fate creates the feeling that the world is alive and consequential. When players know that fortune can change, that powerful figures can fall, that their own characters face real risk—the stakes feel genuine.
But the three-tier system ensures this never feels arbitrary. Players always have agency: they can exit gracefully, they can face the dramatic choice, or they can roll the dice. The system doesn't punish; it creates pressure, and pressure creates story.
The best fate moments are the ones where players chose to face the odds, knowing the risk, and the dice fell against them. Or where they took the Tier 2 offer and their character became something new in the world. Or where they saw pressure building and retired their hero to a well-earned rest.
All of these are victories for the narrative. The wheel of fate ensures the story keeps moving.
{
"_meta": {
"description": "Tier 2 fate choice templates for dramatic decision points",
"version": "1.0",
"usage": "Reference for fate-choice.ts when generating dramatic choices",
"design_principles": [
"Every path should cost something",
"NPC path is survival but loss of player control",
"Roll path keeps control but risks death/fall",
"Third path offers a different trade-off",
"Choices should reflect entity vulnerabilities"
]
},
"choice_patterns": {
"dark_bargain": {
"description": "A powerful entity offers safety at the cost of service",
"triggers": ["high_pressure", "powerful_enemy", "desperate_situation"],
"paths": {
"accept": {
"outcome": "npc_transition",
"description": "Accept the bargain. Survive, but serve.",
"consequences": [
"Character becomes NPC in antagonist's service",
"May appear as ally, enemy, or neutral figure later",
"Player loses control but character persists"
]
},
"refuse": {
"outcome": "fate_roll",
"description": "Refuse. Face what comes.",
"consequences": [
"Roll death/fall check at current odds",
"If survived: fortune buffer decreases",
"If failed: death or major fall"
]
},
"counter": {
"outcome": "complication",
"description": "Make a counter-offer. Change the terms.",
"consequences": [
"Temporary reprieve",
"New obligation or debt",
"Fate pressure temporarily reduced but will return"
]
}
},
"examples": [
"The Queen of Shadows offers sanctuary—serve her, lose yourself",
"The demon whispers: your soul for your survival",
"The emperor grants pardon in exchange for eternal loyalty"
]
},
"betrayal_escape": {
"description": "Survival requires betraying allies or principles",
"triggers": ["trapped", "outnumbered", "principles_tested"],
"paths": {
"betray": {
"outcome": "survival_with_cost",
"description": "Betray them. Save yourself.",
"consequences": [
"Character survives, stays player-controlled",
"Reputation destroyed, new enemies created",
"May face later consequences from betrayed allies"
]
},
"stand": {
"outcome": "fate_roll",
"description": "Stand with them. Face the odds together.",
"consequences": [
"Roll death/fall check",
"If survived: bonds strengthened",
"If failed: death with honor or capture"
]
},
"sacrifice": {
"outcome": "npc_transition",
"description": "Sacrifice yourself so they can escape.",
"consequences": [
"Character becomes captured/imprisoned NPC",
"Allies escape and may rescue later",
"Heroic reputation, but no longer playable"
]
}
},
"examples": [
"Give up their location or die with them",
"Testify against your mentor or share their fate",
"Abandon the refugees or be overrun defending them"
]
},
"corruption_temptation": {
"description": "Power or victory available at moral cost",
"triggers": ["losing_fight", "goal_blocked", "power_offered"],
"paths": {
"embrace": {
"outcome": "npc_transition",
"description": "Accept the power. Become something else.",
"consequences": [
"Character transformed into antagonist NPC",
"Gains power but loses self",
"May become future threat to other PCs"
]
},
"resist": {
"outcome": "fate_roll",
"description": "Resist the temptation. Face defeat.",
"consequences": [
"Roll fall check (not death unless in danger)",
"Moral victory regardless of roll",
"If failed: significant setback"
]
},
"bargain": {
"outcome": "partial_corruption",
"description": "Take only what you need. Stay in control.",
"consequences": [
"Gain limited benefit",
"New vulnerability added",
"Fate pressure increases"
]
}
},
"examples": [
"Drink the blood of the demon to gain its strength",
"Accept the dark artifact that whispers promises",
"Use the forbidden ritual to save your people"
]
},
"succession_crisis": {
"description": "Position can be kept only by unacceptable means",
"triggers": ["leadership_challenged", "heir_dispute", "legitimacy_questioned"],
"paths": {
"abdicate": {
"outcome": "npc_transition",
"description": "Step down. Let someone else bear the burden.",
"consequences": [
"Character becomes advisor/mentor NPC",
"Peaceful transition, legacy preserved",
"May guide successor but no longer in control"
]
},
"fight": {
"outcome": "fate_roll",
"description": "Fight for your position. Winner takes all.",
"consequences": [
"Roll fall check",
"If survived: position secured, enemies made",
"If failed: deposed, possibly exiled"
]
},
"compromise": {
"outcome": "reduced_power",
"description": "Share power. Keep some influence.",
"consequences": [
"Position weakened but retained",
"New constraints on action",
"Fate pressure reduced but vulnerabilities remain"
]
}
},
"examples": [
"Your council demands you name an heir or be removed",
"The pretender has equal claim—share the throne or duel",
"Your people have lost faith—retire or be overthrown"
]
},
"final_stand": {
"description": "No escape, only how the end comes",
"triggers": ["cornered", "no_allies", "overwhelming_force"],
"paths": {
"surrender": {
"outcome": "npc_transition",
"description": "Surrender. Become a prisoner.",
"consequences": [
"Character captured, becomes NPC",
"May be rescued or ransomed later",
"Survival but at enemy's mercy"
]
},
"fight": {
"outcome": "death_roll",
"description": "Fight to the last. Die on your feet.",
"consequences": [
"Roll death check at high odds",
"If survived: legendary reputation, narrow escape",
"If failed: heroic death"
]
},
"escape": {
"outcome": "survival_with_major_cost",
"description": "Find a way out. Leave everything behind.",
"consequences": [
"Survive but lose all resources/status",
"Start over from nothing",
"Reputation as coward possible"
]
}
},
"examples": [
"The castle is breached, enemies at the door",
"Your ship is sinking, enemy fleet surrounds you",
"Assassins have you cornered in the alley"
]
},
"prophetic_doom": {
"description": "Fate itself seems to demand the character's end",
"triggers": ["prophecy_nearing", "curse_climax", "destiny_calling"],
"paths": {
"accept": {
"outcome": "npc_transition",
"description": "Accept your fate. Fulfill the prophecy.",
"consequences": [
"Character fulfills destiny, leaves player control",
"May become legend, god, or something else",
"Story purpose complete"
]
},
"defy": {
"outcome": "fate_roll",
"description": "Defy destiny. The prophecy was wrong.",
"consequences": [
"Roll at increased difficulty",
"If survived: broke the chains of fate",
"If failed: prophecy fulfilled despite defiance"
]
},
"redirect": {
"outcome": "transformed_fate",
"description": "Fulfill the letter but not the spirit.",
"consequences": [
"Find loophole in prophecy",
"Unexpected consequences",
"Neither victory nor defeat, but change"
]
}
},
"examples": [
"The oracle said you would die this day",
"The curse reaches its final hour",
"The god you serve demands your sacrifice"
]
}
},
"outcome_types": {
"npc_transition": {
"description": "Character leaves player control but persists in world",
"player_control": false,
"character_survives": true,
"world_presence": true
},
"fate_roll": {
"description": "Roll against fate pressure for death/fall",
"player_control": "conditional",
"character_survives": "probabilistic",
"world_presence": "conditional"
},
"death_roll": {
"description": "Roll specifically for death (high stakes)",
"player_control": "conditional",
"character_survives": "probabilistic",
"world_presence": "if_survived"
},
"survival_with_cost": {
"description": "Survive but pay significant price",
"player_control": true,
"character_survives": true,
"world_presence": true,
"cost": "reputation, allies, resources"
},
"survival_with_major_cost": {
"description": "Survive but lose almost everything",
"player_control": true,
"character_survives": true,
"world_presence": true,
"cost": "status, possessions, position"
},
"partial_corruption": {
"description": "Gain benefit but add vulnerability",
"player_control": true,
"character_survives": true,
"world_presence": true,
"cost": "new vulnerability, increased pressure"
},
"reduced_power": {
"description": "Keep position but weakened",
"player_control": true,
"character_survives": true,
"world_presence": true,
"cost": "power level reduced"
},
"complication": {
"description": "Delay fate but create new problems",
"player_control": true,
"character_survives": true,
"world_presence": true,
"cost": "new obligation, pressure returns"
},
"transformed_fate": {
"description": "Change the terms but not escape",
"player_control": "varies",
"character_survives": true,
"world_presence": true,
"cost": "unexpected transformation"
}
},
"personalization_hooks": {
"use_vulnerabilities": "Match choice pattern to entity's specific weaknesses",
"use_relationships": "Involve NPCs the character has bonds with",
"use_antagonists": "If known antagonist exists, cast them as tempter/threat",
"use_values": "Challenge what the character believes in",
"use_history": "Reference past events and exposure log entries"
}
}
{
"_meta": {
"description": "Schema for entity fate tracking metadata",
"version": "1.0",
"usage": "Add to entity entries in world bible under 'Fate Tracking' section"
},
"schema": {
"powerLevel": {
"type": "integer",
"range": [1, 10],
"description": "Influence/resources/capability scale",
"levels": {
"1-2": "Local significance (village elder, small merchant, local hero)",
"3-4": "Regional influence (town mayor, guild master, regional champion)",
"5-6": "National importance (noble house, major faction, famous adventurer)",
"7-8": "Civilization-scale impact (kingdom ruler, legendary hero, archmage)",
"9-10": "World-shaping power (emperor, demigod, world-threatening force)"
}
},
"tenure": {
"type": "string",
"description": "Duration in power/position",
"format": "X years|months|decades",
"modifiers": {
"under_1_year": 0.5,
"1_to_5_years": 1.0,
"5_to_20_years": 1.5,
"20_to_50_years": 2.0,
"over_50_years": 2.5
}
},
"vulnerabilities": {
"type": "array",
"items": "string",
"description": "Known weaknesses that increase fate pressure",
"weightings": {
"known_enemy": 1.0,
"active_conflict": 1.5,
"exploitable_flaw": 2.0,
"critical_dependency": 1.0,
"destroying_secret": 3.0,
"default": 1.0
},
"maxScore": 10
},
"protections": {
"type": "array",
"items": "string",
"description": "Shields that reduce fate pressure",
"weightings": {
"loyal_ally": 1.0,
"fortified_position": 2.0,
"divine_protection": 3.0,
"magical_protection": 3.0,
"information_network": 1.0,
"popular_support": 2.0,
"default": 0.5
},
"minFactor": 1
},
"fortuneBuffer": {
"type": "number",
"range": [0, 5],
"description": "Remaining luck/grace/narrative protection",
"defaults": {
"standard_character": 3,
"protagonist": 4,
"major_protagonist": 5
},
"notes": "Decreases with narrow escapes; can be restored through sacrifice/growth"
},
"exposureLog": {
"type": "array",
"items": {
"date": "string (YYYY-MM-DD)",
"event": "string",
"pressure": "number (0.01-0.30)",
"category": "string (optional)"
},
"description": "Record of risk exposure events",
"categories": {
"combat": 0.10,
"intrigue": 0.05,
"betrayal": 0.15,
"overreach": 0.10,
"revelation": 0.20,
"loss": 0.08,
"challenge": 0.12,
"narrow_escape": 0.05
}
},
"lastAssessment": {
"type": "string",
"format": "date (YYYY-MM-DD)",
"description": "When fate pressure was last calculated"
}
},
"templates": {
"character": {
"powerLevel": 5,
"tenure": "5 years",
"vulnerabilities": [],
"protections": [],
"fortuneBuffer": 3,
"exposureLog": [],
"lastAssessment": null
},
"faction": {
"powerLevel": 6,
"tenure": "50 years",
"vulnerabilities": [],
"protections": [],
"fortuneBuffer": 4,
"exposureLog": [],
"lastAssessment": null
},
"location": {
"powerLevel": 4,
"tenure": "200 years",
"vulnerabilities": [],
"protections": [],
"fortuneBuffer": 5,
"exposureLog": [],
"lastAssessment": null
}
},
"formula": {
"description": "Fate Pressure = (Power × TenureMod × VulnScore/10) / (ProtectionFactor × max(FortuneBuffer, 0.5)) + ExposureSum",
"components": {
"base": "(powerLevel * tenureModifier * vulnerabilityScore / 10)",
"divisor": "(protectionFactor * Math.max(fortuneBuffer, 0.5))",
"exposure": "sum of all exposure log pressure values"
},
"clamp": {
"min": 0,
"max": 1
}
},
"states": {
"F1": { "name": "Stable Ascent", "range": [0.0, 0.3] },
"F2": { "name": "Peak Power", "range": [0.3, 0.5] },
"F3": { "name": "Overextension", "range": [0.5, 0.7] },
"F4": { "name": "Precarious Position", "range": [0.7, 0.9] },
"F5": { "name": "Fall in Progress", "range": [0.9, 1.0] }
}
}
{
"_meta": {
"description": "Fate-shift types with severity levels and narrative hooks",
"version": "1.0",
"usage": "Reference for propose-shift.ts and fate-choice.ts"
},
"character": {
"death": {
"description": "Physical end of the character",
"severities": {
"catastrophic": "Complete destruction, no return possible, legacy may be tainted",
"major": "Death with lasting influence (legacy, ghost, prophecy fulfilled)"
},
"eligibility": {
"requiresDanger": true,
"minPressure": 0.5,
"maxFortuneBuffer": 1
},
"narrativeHooks": [
"Found slain in their chambers, door locked from within",
"Fell in battle, surrounded but fighting to the last",
"Poisoned at a feast, the cup still in their hand",
"Disappeared during a journey, only their horse returned",
"Died protecting someone they loved",
"Struck down by illness that none could cure",
"Executed publicly for crimes real or fabricated",
"Lost at sea during a storm",
"Killed by the very power they sought to control",
"Died of a broken heart after final betrayal",
"Vanished into the wilderness, presumed dead",
"Sacrificed themselves to save many"
]
},
"fall": {
"description": "Loss of power, status, or position",
"severities": {
"catastrophic": "Total loss—exile, imprisonment, or complete disgrace",
"major": "Significant loss of position, recovery difficult but possible",
"moderate": "Noticeable decline in influence, must rebuild from weakened position",
"minor": "Reputation damaged, warning sign of worse to come"
},
"eligibility": {
"requiresDanger": false,
"minPressure": 0.3
},
"narrativeHooks": [
"Secrets revealed in the public square",
"Allies turned against them one by one",
"Caught in corruption that could not be hidden",
"Lost a challenge that cost everything",
"Betrayed by the one they trusted most",
"Made an enemy of the wrong power",
"Failed at the crucial moment when it mattered most",
"Their protector could no longer shield them",
"The people they served turned their backs",
"Outmaneuvered by a younger, cleverer rival",
"Their lies caught up with them all at once",
"A single mistake cascaded into disaster"
]
},
"corruption": {
"description": "Moral or spiritual decline",
"severities": {
"catastrophic": "Fully turned to darkness or opposition, irredeemable",
"major": "Core values fundamentally compromised",
"moderate": "Making increasingly morally questionable choices",
"minor": "Temptation acknowledged, first steps toward darkness"
},
"eligibility": {
"requiresDanger": false,
"minPressure": 0.4
},
"narrativeHooks": [
"Made a deal they cannot escape",
"The ends began justifying the means",
"Power revealed their true nature",
"They became what they once fought against",
"The corruption spread slowly, unnoticed until too late",
"A single compromise opened the door to many more",
"They convinced themselves it was necessary",
"The darkness offered what the light could not"
]
},
"exile": {
"description": "Removal from sphere of influence",
"severities": {
"catastrophic": "Permanent banishment, can never return, all connections severed",
"major": "Long exile, return uncertain, must start over elsewhere",
"moderate": "Temporary exile with conditions for return",
"minor": "Withdrawal from public life, self-imposed distance"
},
"eligibility": {
"requiresDanger": false,
"minPressure": 0.35
},
"narrativeHooks": [
"Driven out by those they once led",
"Fled before enemies could catch them",
"Banished by decree of higher authority",
"Chose exile over compromise",
"The land itself seemed to reject them",
"Stripped of titles and sent away in disgrace"
]
},
"transformation": {
"description": "Fundamental change in nature or identity",
"severities": {
"catastrophic": "Unrecognizable from former self, old identity gone",
"major": "Major aspect fundamentally changed, core identity shifted",
"moderate": "Significant shift in priorities, methods, or beliefs",
"minor": "Noticeable change in approach or outlook"
},
"eligibility": {
"requiresDanger": false,
"minPressure": 0.4
},
"narrativeHooks": [
"Emerged from the ordeal a different person",
"The experience broke something inside them",
"They shed their old self like a snake's skin",
"What they saw changed them forever",
"The choice they made redefined who they were",
"They realized they had been wrong about everything"
]
},
"diminishment": {
"description": "Gradual decline over time",
"severities": {
"major": "Significant reduction in capability, shadow of former self",
"moderate": "Noticeable weakening, slowing, or fading",
"minor": "Early signs of decline, the peak is past"
},
"eligibility": {
"requiresDanger": false,
"minPressure": 0.25
},
"narrativeHooks": [
"Age finally caught up with them",
"Old wounds began to take their toll",
"Their fire slowly dimmed",
"They could no longer do what once came easy",
"The burden of years weighed heavier each day",
"Their legend faded as new heroes rose"
]
}
},
"faction": {
"collapse": {
"description": "Organization ceases to exist",
"severities": {
"catastrophic": "Complete dissolution, members scattered to the winds, legacy destroyed",
"major": "Organization effectively destroyed, only remnants remain"
},
"narrativeHooks": [
"Internal strife tore them apart",
"Their enemies finally united against them",
"The cause that bound them lost its meaning",
"Corruption rotted them from within",
"A single betrayal brought down the whole structure"
]
},
"schism": {
"description": "Faction splits into competing groups",
"severities": {
"major": "Bitter split, factions in open conflict",
"moderate": "Amicable division, factions maintain some cooperation",
"minor": "Internal factions forming, unity strained"
},
"narrativeHooks": [
"Two leaders claimed the right to lead",
"Ideological differences became irreconcilable",
"A succession crisis divided loyalties",
"Regional chapters went their own way",
"The old guard clashed with reformers"
]
},
"absorption": {
"description": "Faction absorbed by another power",
"severities": {
"catastrophic": "Hostile takeover, identity erased, members absorbed or purged",
"major": "Merger with junior status, name may survive but independence gone"
},
"narrativeHooks": [
"A stronger power made an offer they couldn't refuse",
"Survival meant surrendering independence",
"They were bought out by their largest rival",
"The merger was sold as partnership but became subjugation"
]
},
"decline": {
"description": "Gradual loss of influence and resources",
"severities": {
"catastrophic": "Reduced to irrelevance, barely exists",
"major": "Major loss of territory, membership, or resources",
"moderate": "Noticeable decline in power and prestige",
"minor": "Beginning to slip, rivals gaining ground"
},
"narrativeHooks": [
"Their best members left for other opportunities",
"Funding dried up as patrons lost interest",
"Younger organizations outcompeted them",
"They failed to adapt to changing times",
"A series of failed ventures depleted their resources"
]
},
"reformation": {
"description": "Fundamental change in faction nature or purpose",
"severities": {
"major": "Complete transformation of goals, methods, or identity",
"moderate": "Significant shift in direction or values"
},
"narrativeHooks": [
"New leadership brought new vision",
"Crisis forced them to reinvent themselves",
"They abandoned their original purpose for survival",
"A revelation changed everything they believed"
]
},
"defeat": {
"description": "Loss in specific conflict or competition",
"severities": {
"catastrophic": "Total defeat, terms dictated by victors",
"major": "Decisive defeat, significant concessions required",
"moderate": "Clear loss, must regroup and recover",
"minor": "Setback, reputation damaged but organization intact"
},
"narrativeHooks": [
"Their enemies proved stronger than expected",
"A crucial battle turned against them",
"Their strategy failed at the worst moment",
"Allies abandoned them when needed most"
]
}
},
"location": {
"destruction": {
"description": "Physical end of the location",
"severities": {
"catastrophic": "Completely destroyed, uninhabitable, may never recover",
"major": "Largely destroyed, some remains but barely recognizable"
},
"narrativeHooks": [
"Fire consumed everything",
"The earth swallowed it whole",
"War left nothing but ruins",
"A curse rendered it uninhabitable",
"Flood waters washed it away"
]
},
"conquest": {
"description": "Location changes hands to new power",
"severities": {
"catastrophic": "Brutal conquest, population displaced or subjugated",
"major": "Conquered, new rulers impose their will",
"moderate": "Control contested, authority unclear"
},
"narrativeHooks": [
"Invaders broke through the defenses",
"Betrayal from within opened the gates",
"Siege finally succeeded after months",
"The rulers surrendered to spare their people",
"A bloodless coup changed everything overnight"
]
},
"disaster": {
"description": "Catastrophic damage to location",
"severities": {
"catastrophic": "Rendered uninhabitable for foreseeable future",
"major": "Severe damage, long recovery needed",
"moderate": "Significant damage, major repairs required",
"minor": "Localized damage, disruption but recovery underway"
},
"narrativeHooks": [
"The plague swept through without mercy",
"Earthquake shattered the foundations",
"Famine emptied the streets",
"A magical catastrophe twisted the land",
"The river broke its banks and drowned the valley"
]
},
"decline": {
"description": "Economic or population decline",
"severities": {
"catastrophic": "Ghost town, nearly abandoned",
"major": "Major population and economic exodus",
"moderate": "Noticeable decline in prosperity",
"minor": "Beginning to fade, young people leaving"
},
"narrativeHooks": [
"The mines ran dry",
"Trade routes shifted elsewhere",
"A better opportunity drew people away",
"Taxes drove out the merchants",
"The old industries died and nothing replaced them"
]
},
"transformation": {
"description": "Fundamental change in location's nature",
"severities": {
"major": "Completely different character and purpose",
"moderate": "Significant shift in identity or role"
},
"narrativeHooks": [
"What was sacred became profane",
"The forest reclaimed what was once farmland",
"Industry replaced the gardens",
"The fortress became a marketplace",
"Magic changed the very nature of the land"
]
},
"isolation": {
"description": "Location cut off from the world",
"severities": {
"major": "Completely unreachable, forgotten by the outside",
"moderate": "Difficult to reach, connections severed",
"minor": "Increasing difficulty in travel or communication"
},
"narrativeHooks": [
"The roads became too dangerous",
"A curse keeps travelers away",
"Political borders closed around them",
"The bridge collapsed and was never rebuilt",
"They chose to wall themselves off"
]
}
}
}
#!/usr/bin/env -S deno run --allow-read
/**
* Exposure Log - Track Risk Events
*
* Logs risk exposure events that increase fate pressure.
* Categorizes events and suggests pressure increments.
*
* Usage:
* deno run --allow-read exposure-log.ts --event "Survived assassination attempt"
* deno run --allow-read exposure-log.ts --event "Secret revealed publicly" --pressure 0.20
* deno run --allow-read exposure-log.ts --list
*/
interface ExposureCategory {
base: number;
description: string;
keywords: string[];
}
const EXPOSURE_CATEGORIES: { [key: string]: ExposureCategory } = {
combat: {
base: 0.10,
description: "Active combat, violence, or physical danger",
keywords: ["combat", "battle", "fight", "wound", "attack", "assassination", "duel"],
},
intrigue: {
base: 0.05,
description: "Political maneuvering, subtle threats",
keywords: ["intrigue", "politics", "scheme", "plot", "manipulat", "negotiat"],
},
betrayal: {
base: 0.15,
description: "Trust violated, ally turns against",
keywords: ["betray", "turned", "abandon", "traitor", "double-cross"],
},
revelation: {
base: 0.20,
description: "Secret exposed, truth revealed",
keywords: ["secret", "reveal", "exposed", "discover", "truth", "uncover"],
},
overreach: {
base: 0.10,
description: "Hubris, overconfident action",
keywords: ["hubris", "overconfident", "reckless", "arrogant", "foolish"],
},
loss: {
base: 0.08,
description: "Resource, ally, or territory lost",
keywords: ["lost", "death of", "defeat", "lose", "fallen"],
},
challenge: {
base: 0.12,
description: "Direct challenge to power or status",
keywords: ["challenge", "rival", "contest", "dispute", "claim"],
},
narrow_escape: {
base: 0.05,
description: "Survived dangerous situation",
keywords: ["narrow", "escape", "surviv", "barely", "close call"],
},
};
function categorizeEvent(eventDescription: string): {
category: string;
suggestedPressure: number;
categoryData: ExposureCategory;
} {
const lower = eventDescription.toLowerCase();
for (const [category, data] of Object.entries(EXPOSURE_CATEGORIES)) {
for (const keyword of data.keywords) {
if (lower.includes(keyword)) {
return { category, suggestedPressure: data.base, categoryData: data };
}
}
}
// Default to intrigue if no match
return {
category: "intrigue",
suggestedPressure: 0.05,
categoryData: EXPOSURE_CATEGORIES.intrigue,
};
}
function getDate(): string {
return new Date().toISOString().split("T")[0];
}
function formatExposureEntry(
event: string,
category: string,
pressure: number,
name?: string
): string {
const date = getDate();
const lines: string[] = [];
lines.push("EXPOSURE EVENT LOGGED");
lines.push("=".repeat(50));
lines.push("");
if (name) {
lines.push(`Entity: ${name}`);
}
lines.push(`Date: ${date}`);
lines.push(`Event: ${event}`);
lines.push(`Category: ${category}`);
lines.push(`Pressure Increment: +${(pressure * 100).toFixed(1)}%`);
lines.push("");
lines.push("WORLD BIBLE ENTRY FORMAT");
lines.push("-".repeat(50));
lines.push("Add to entity's Risk Exposure Log section:");
lines.push("");
lines.push(`- ${date}: ${event} (+${(pressure * 100).toFixed(0)}% pressure)`);
lines.push("");
lines.push("CUMULATIVE EFFECT");
lines.push("-".repeat(50));
lines.push("This event adds to the entity's total fate pressure.");
lines.push("Run fate-pressure.ts to see current total.");
return lines.join("\n");
}
function showUsage() {
console.log(`
Exposure Log - Track Risk Events
=================================
Logs risk exposure events that increase fate pressure.
Usage:
deno run --allow-read exposure-log.ts [options]
Required:
--event <text> Description of the exposure event
Optional:
--pressure <n> Override suggested pressure increment (0.01-0.30)
--name <name> Entity name for context
--json Output as JSON for programmatic use
Information:
--list List all exposure categories and base pressures
Categories and Base Pressures:
combat: +10% Active combat, violence, physical danger
intrigue: +5% Political maneuvering, subtle threats
betrayal: +15% Trust violated, ally turns against
revelation: +20% Secret exposed, truth revealed
overreach: +10% Hubris, overconfident action
loss: +8% Resource, ally, or territory lost
challenge: +12% Direct challenge to power or status
narrow_escape: +5% Survived dangerous situation
Examples:
deno run --allow-read exposure-log.ts --event "Survived assassination attempt"
deno run --allow-read exposure-log.ts --event "Secret revealed publicly" --name "Lord Varen"
deno run --allow-read exposure-log.ts --event "Lost the Battle of the Crossing" --pressure 0.15
`);
}
function main() {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
showUsage();
Deno.exit(0);
}
if (args.includes("--list")) {
console.log("Exposure Categories");
console.log("=".repeat(50));
console.log("");
for (const [category, data] of Object.entries(EXPOSURE_CATEGORIES)) {
console.log(`${category.toUpperCase()}`);
console.log(` Base pressure: +${(data.base * 100).toFixed(0)}%`);
console.log(` Description: ${data.description}`);
console.log(` Keywords: ${data.keywords.join(", ")}`);
console.log("");
}
Deno.exit(0);
}
// Parse arguments
let event = "";
let customPressure: number | undefined;
let name = "";
let jsonOutput = false;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--event":
event = args[++i] || "";
break;
case "--pressure":
customPressure = parseFloat(args[++i]);
break;
case "--name":
name = args[++i] || "";
break;
case "--json":
jsonOutput = true;
break;
}
}
if (!event) {
console.error("Error: --event required");
console.error('Example: --event "Survived assassination attempt"');
Deno.exit(1);
}
const { category, suggestedPressure, categoryData } = categorizeEvent(event);
const pressure = customPressure !== undefined ? customPressure : suggestedPressure;
// Clamp pressure
const clampedPressure = Math.min(Math.max(pressure, 0.01), 0.30);
if (jsonOutput) {
console.log(
JSON.stringify(
{
date: getDate(),
entity: name || undefined,
event,
category,
pressure: clampedPressure,
categoryDescription: categoryData.description,
},
null,
2
)
);
} else {
console.log(formatExposureEntry(event, category, clampedPressure, name || undefined));
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Fate Choice Generator - Tier 2 Dramatic Choices
*
* Generates dramatic choices when fate pressure is high.
* One path leads to NPC-hood, one path faces the roll,
* optional third path offers different trade-off.
*
* Usage:
* deno run --allow-read fate-choice.ts --name "Kaira the Bold" --pressure 0.72
* deno run --allow-read fate-choice.ts --name "Kaira" --pressure 0.72 --antagonist "The Dark Queen"
* deno run --allow-read fate-choice.ts --name "Kaira" --pressure 0.72 --pattern dark_bargain
*/
import { dirname, fromFileUrl, join } from "https://deno.land/std@0.208.0/path/mod.ts";
interface ChoicePattern {
description: string;
triggers: string[];
paths: {
[key: string]: {
outcome: string;
description: string;
consequences: string[];
};
};
examples: string[];
}
interface FateChoiceData {
choice_patterns: { [key: string]: ChoicePattern };
outcome_types: { [key: string]: unknown };
}
// Seeded random for reproducibility
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
this.seed = (this.seed * 1103515245 + 12345) & 0x7fffffff;
return this.seed / 0x7fffffff;
}
pick<T>(array: T[]): T {
return array[Math.floor(this.next() * array.length)];
}
}
async function loadChoiceData(): Promise<FateChoiceData | null> {
try {
const scriptDir = dirname(fromFileUrl(import.meta.url));
const dataPath = join(scriptDir, "..", "data", "fate-choices.json");
const text = await Deno.readTextFile(dataPath);
return JSON.parse(text);
} catch {
return null;
}
}
function calculateDeathOdds(pressure: number, fortune: number): number {
if (pressure <= 0.5 || fortune >= 1) return 0;
return (pressure - 0.5) * 2 * (1 - fortune / 5) * 100;
}
function generateChoice(
name: string,
pressure: number,
fortune: number,
pattern: ChoicePattern,
antagonist: string | undefined,
rng: SeededRandom
): string {
const lines: string[] = [];
const deathOdds = calculateDeathOdds(pressure, fortune);
lines.push(`FATE CHOICE: ${name}`);
lines.push("=".repeat(50));
lines.push("");
lines.push(`The wheel turns. Fate pressure is ${(pressure * 100).toFixed(0)}%.`);
lines.push("A choice presents itself:");
lines.push("");
// Get example and personalize with antagonist if provided
let situation = rng.pick(pattern.examples);
if (antagonist) {
situation = situation.replace(/The Queen of Shadows|the demon|The emperor|the enemy/gi, antagonist);
}
lines.push(`"${situation}"`);
lines.push("");
// Generate paths
const pathKeys = Object.keys(pattern.paths);
const pathLabels = ["A", "B", "C"];
for (let i = 0; i < pathKeys.length; i++) {
const key = pathKeys[i];
const path = pattern.paths[key];
const label = pathLabels[i];
lines.push(`${label}) ${key.toUpperCase().replace(/_/g, " ")}`);
lines.push(` ${path.description}`);
for (const consequence of path.consequences) {
let displayConsequence = consequence;
// Replace placeholders with actual values
if (consequence.includes("current odds")) {
displayConsequence = consequence.replace("current odds", `${deathOdds.toFixed(0)}% death chance`);
}
lines.push(` → ${displayConsequence}`);
}
lines.push("");
}
lines.push("-".repeat(50));
lines.push("MECHANICS");
lines.push("-".repeat(50));
lines.push(` Fate Pressure: ${(pressure * 100).toFixed(0)}%`);
lines.push(` Fortune Buffer: ${fortune}/5`);
if (deathOdds > 0) {
lines.push(` Death Odds (if roll path): ${deathOdds.toFixed(0)}%`);
} else {
lines.push(" Death: Not eligible (fortune > 0 or pressure <= 50%)");
}
lines.push("");
lines.push("NOTES FOR FACILITATOR");
lines.push("-".repeat(50));
lines.push(" - Present this choice at a dramatic moment");
lines.push(" - The NPC path should feel like a real cost, not escape");
lines.push(" - If they choose to roll, use fate-roll.ts with --danger flag");
lines.push(" - Honor their choice—this IS the climax");
return lines.join("\n");
}
function showUsage() {
console.log(`
Fate Choice Generator
=====================
Generates Tier 2 dramatic choices for high-pressure situations.
Usage:
deno run --allow-read fate-choice.ts [options]
Required:
--name <name> Entity name
--pressure <0-1> Fate pressure (should be > 0.6)
Optional:
--fortune <0-5> Fortune buffer (default: 0)
--antagonist <name> Name to use for tempter/threat
--pattern <type> Specific pattern to use
--seed <n> Random seed for reproducibility
--list List available patterns
Patterns:
dark_bargain A powerful entity offers safety at cost of service
betrayal_escape Survival requires betraying allies or principles
corruption_temptation Power or victory available at moral cost
succession_crisis Position can only be kept by unacceptable means
final_stand No escape, only how the end comes
prophetic_doom Fate itself demands the character's end
Examples:
deno run --allow-read fate-choice.ts --name "Kaira" --pressure 0.72
deno run --allow-read fate-choice.ts --name "Lord Varen" --pressure 0.85 --antagonist "House Blackwood"
deno run --allow-read fate-choice.ts --name "Captain Sera" --pressure 0.68 --pattern final_stand
Design Principles:
- Every path should cost something
- NPC path = survival but loss of player control
- Roll path = keep control but risk death/fall
- Third path = different trade-off
`);
}
async function main() {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
showUsage();
Deno.exit(0);
}
// Load choice data
const choiceData = await loadChoiceData();
if (!choiceData) {
console.error("Warning: Could not load fate-choices.json, using defaults");
}
// Parse arguments
let name = "";
let pressure = -1;
let fortune = 0;
let antagonist: string | undefined;
let patternName: string | undefined;
let seed = Date.now();
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--name":
name = args[++i] || "";
break;
case "--pressure":
pressure = parseFloat(args[++i]) || -1;
break;
case "--fortune":
fortune = parseFloat(args[++i]) ?? 0;
break;
case "--antagonist":
antagonist = args[++i];
break;
case "--pattern":
patternName = args[++i];
break;
case "--seed":
seed = parseInt(args[++i]) || Date.now();
break;
case "--list":
if (choiceData) {
console.log("Available patterns:");
for (const [key, pattern] of Object.entries(choiceData.choice_patterns)) {
console.log(` ${key}: ${pattern.description}`);
}
}
Deno.exit(0);
}
}
if (!name) {
console.error("Error: --name required");
Deno.exit(1);
}
if (pressure < 0 || pressure > 1) {
console.error("Error: --pressure required (value between 0 and 1)");
Deno.exit(1);
}
if (pressure < 0.5) {
console.warn("Warning: Fate pressure < 50%. Tier 2 choices typically trigger at > 60%.");
}
// Clamp fortune
fortune = Math.min(Math.max(fortune, 0), 5);
const rng = new SeededRandom(seed);
// Get or create pattern
let pattern: ChoicePattern;
if (choiceData && patternName && choiceData.choice_patterns[patternName]) {
pattern = choiceData.choice_patterns[patternName];
} else if (choiceData) {
// Pick random pattern
const patterns = Object.values(choiceData.choice_patterns);
pattern = rng.pick(patterns);
} else {
// Fallback default pattern
pattern = {
description: "A powerful entity offers safety at the cost of service",
triggers: ["high_pressure"],
paths: {
accept: {
outcome: "npc_transition",
description: "Accept the bargain. Survive, but serve.",
consequences: [
"Character becomes NPC in antagonist's service",
"May appear as ally, enemy, or neutral figure later",
"Player loses control but character persists",
],
},
refuse: {
outcome: "fate_roll",
description: "Refuse. Face what comes.",
consequences: [
"Roll death/fall check at current odds",
"If survived: fortune buffer decreases",
"If failed: death or major fall",
],
},
counter: {
outcome: "complication",
description: "Make a counter-offer. Change the terms.",
consequences: [
"Temporary reprieve",
"New obligation or debt",
"Fate pressure temporarily reduced but will return",
],
},
},
examples: [
"The Dark One offers sanctuary—serve them, lose yourself",
"Your enemy offers pardon in exchange for eternal loyalty",
"The power offers itself—take it and be changed forever",
],
};
}
console.log(generateChoice(name, pressure, fortune, pattern, antagonist, rng));
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Fate Pressure Calculator
*
* Calculates current fate pressure for entities based on:
* - Power level (1-10)
* - Tenure modifier (0.5x - 2.5x)
* - Vulnerability score (0-10)
* - Protection factor (1-10)
* - Fortune buffer (0-5)
* - Risk exposure accumulation
*
* Usage:
* deno run --allow-read fate-pressure.ts --power 7 --tenure 40 --vulns 3 --prots 2 --fortune 2
* deno run --allow-read fate-pressure.ts --interactive
* deno run --allow-read fate-pressure.ts --json
*/
interface FatePressureInput {
powerLevel: number;
tenureYears: number;
vulnerabilities: string[];
protections: string[];
fortuneBuffer: number;
exposureLog: { date: string; event: string; pressure: number }[];
}
interface FatePressureResult {
components: {
powerLevel: number;
tenureModifier: number;
vulnerabilityScore: number;
protectionFactor: number;
fortuneBuffer: number;
riskExposure: number;
};
fatePressure: number;
state: string;
stateDescription: string;
interpretation: string;
}
// Tenure modifiers based on duration
function getTenureModifier(years: number): number {
if (years < 1) return 0.5;
if (years <= 5) return 1.0;
if (years <= 20) return 1.5;
if (years <= 50) return 2.0;
return 2.5;
}
// Calculate vulnerability score based on type keywords
function calculateVulnerabilityScore(vulnerabilities: string[]): number {
let score = 0;
for (const v of vulnerabilities) {
const lower = v.toLowerCase();
if (lower.includes("secret") || lower.includes("hidden")) {
score += 3;
} else if (lower.includes("flaw") || lower.includes("weakness") || lower.includes("addiction")) {
score += 2;
} else if (lower.includes("conflict") || lower.includes("war") || lower.includes("feud")) {
score += 1.5;
} else if (lower.includes("enemy") || lower.includes("rival") || lower.includes("opponent")) {
score += 1;
} else if (lower.includes("depends") || lower.includes("relies") || lower.includes("needs")) {
score += 1;
} else {
score += 1; // Default weight
}
}
return Math.min(score, 10);
}
// Calculate protection factor based on type keywords
function calculateProtectionFactor(protections: string[]): number {
let factor = 1; // Base protection (everyone has at least 1)
for (const p of protections) {
const lower = p.toLowerCase();
if (lower.includes("divine") || lower.includes("god") || lower.includes("blessed")) {
factor += 3;
} else if (lower.includes("magical") || lower.includes("enchant") || lower.includes("ward")) {
factor += 3;
} else if (lower.includes("fortif") || lower.includes("stronghold") || lower.includes("castle")) {
factor += 2;
} else if (lower.includes("popular") || lower.includes("beloved") || lower.includes("supported")) {
factor += 2;
} else if (lower.includes("ally") || lower.includes("loyal") || lower.includes("friend")) {
factor += 1;
} else if (lower.includes("network") || lower.includes("spies") || lower.includes("intelligence")) {
factor += 1;
} else {
factor += 0.5; // Default weight
}
}
return Math.min(factor, 10);
}
// Calculate total risk exposure from log
function calculateRiskExposure(
exposureLog: { date: string; event: string; pressure: number }[]
): number {
return exposureLog.reduce((sum, entry) => sum + entry.pressure, 0);
}
// Main calculation
function calculateFatePressure(input: FatePressureInput): FatePressureResult {
const tenureModifier = getTenureModifier(input.tenureYears);
const vulnerabilityScore = calculateVulnerabilityScore(input.vulnerabilities);
const protectionFactor = calculateProtectionFactor(input.protections);
const riskExposure = calculateRiskExposure(input.exposureLog);
// Core formula:
// (Power × TenureMod × VulnScore/10) / (ProtFactor × max(Fortune, 0.5)) + Exposure
const numerator = input.powerLevel * tenureModifier * (vulnerabilityScore / 10);
const denominator = protectionFactor * Math.max(input.fortuneBuffer, 0.5);
let fatePressure = (numerator / denominator) + riskExposure;
// Clamp to 0-1 range
fatePressure = Math.min(Math.max(fatePressure, 0), 1);
// Determine state
let state: string;
let stateDescription: string;
let interpretation: string;
if (fatePressure < 0.2) {
state = "F1";
stateDescription = "Stable Ascent";
interpretation = "This entity is relatively stable. Monitor for changes but no immediate action needed.";
} else if (fatePressure < 0.35) {
state = "F2";
stateDescription = "Peak Power";
interpretation = "At peak influence. Consider surfacing hidden vulnerabilities or introducing rivals.";
} else if (fatePressure < 0.6) {
state = "F3";
stateDescription = "Overextension";
interpretation = "High pressure. Fate-shift likely if pressure continues. Consider Tier 2 choices.";
} else if (fatePressure < 0.85) {
state = "F4";
stateDescription = "Precarious Position";
interpretation = "Critical pressure. Fate-offered choices should be generated. Death rolls possible.";
} else {
state = "F5";
stateDescription = "Fall in Progress";
interpretation = "Terminal pressure. Focus on managing the transition and documenting the fall.";
}
return {
components: {
powerLevel: input.powerLevel,
tenureModifier,
vulnerabilityScore,
protectionFactor,
fortuneBuffer: input.fortuneBuffer,
riskExposure,
},
fatePressure,
state,
stateDescription,
interpretation,
};
}
function formatResult(result: FatePressureResult, entityName?: string): string {
const lines: string[] = [];
lines.push(`FATE PRESSURE${entityName ? `: ${entityName}` : ""}`);
lines.push("=".repeat(50));
lines.push("");
lines.push(`Current State: ${result.state} - ${result.stateDescription}`);
lines.push(`Fate Pressure: ${(result.fatePressure * 100).toFixed(1)}%`);
lines.push("");
lines.push("COMPONENT BREAKDOWN");
lines.push("-".repeat(50));
lines.push(` Power Level: ${result.components.powerLevel}/10`);
lines.push(` Tenure Modifier: ${result.components.tenureModifier}x`);
lines.push(` Vulnerability Score: ${result.components.vulnerabilityScore.toFixed(1)}/10`);
lines.push(` Protection Factor: ${result.components.protectionFactor.toFixed(1)}/10`);
lines.push(` Fortune Buffer: ${result.components.fortuneBuffer}/5`);
lines.push(` Risk Exposure: +${(result.components.riskExposure * 100).toFixed(1)}%`);
lines.push("");
lines.push("INTERPRETATION");
lines.push("-".repeat(50));
lines.push(` ${result.interpretation}`);
lines.push("");
// Add specific recommendations based on state
lines.push("RECOMMENDATIONS");
lines.push("-".repeat(50));
if (result.fatePressure < 0.3) {
lines.push(" - Log exposure events as they occur");
lines.push(" - Re-assess after significant events");
} else if (result.fatePressure < 0.6) {
lines.push(" - Consider fate-choice.ts for dramatic moments");
lines.push(" - Track vulnerabilities carefully");
lines.push(" - Fate-shifts at Moderate or Minor severity possible");
} else if (result.fatePressure < 0.85) {
lines.push(" - Run fate-choice.ts to generate Tier 2 choice");
lines.push(" - Death/fall rolls active if in danger");
lines.push(" - Consider voluntary NPC transition option");
} else {
lines.push(" - Fall is in progress or imminent");
lines.push(" - Run propose-shift.ts to document the end");
lines.push(" - Focus on managing consequences for world");
}
return lines.join("\n");
}
function showUsage() {
console.log(`
Fate Pressure Calculator
========================
Calculates fate pressure for entities based on power, tenure,
vulnerabilities, protections, fortune, and exposure.
Usage:
deno run --allow-read fate-pressure.ts [options]
Options:
--power <n> Power level (1-10, default: 5)
--tenure <years> Years in power (default: 5)
--vulns <n> Number of vulnerabilities (default: 0)
--prots <n> Number of protections (default: 0)
--fortune <n> Fortune buffer (0-5, default: 3)
--exposure <n> Accumulated exposure (0-1, default: 0)
--name <name> Entity name for output
--json Output as JSON
Examples:
deno run --allow-read fate-pressure.ts --power 7 --tenure 40 --vulns 5 --prots 3 --fortune 2
deno run --allow-read fate-pressure.ts --power 9 --tenure 100 --vulns 8 --fortune 0 --name "The Eternal Emperor"
Formula:
Pressure = (Power × TenureMod × VulnScore/10) / (ProtFactor × Fortune) + Exposure
Tenure Modifiers:
< 1 year: 0.5x
1-5 years: 1.0x
5-20 years: 1.5x
20-50 years: 2.0x
50+ years: 2.5x
States:
F1 (0-20%): Stable Ascent
F2 (20-35%): Peak Power
F3 (35-60%): Overextension
F4 (60-85%): Precarious Position
F5 (85%+): Fall in Progress
`);
}
function main() {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
showUsage();
Deno.exit(0);
}
// Parse arguments
let power = 5;
let tenure = 5;
let vulnCount = 0;
let protCount = 0;
let fortune = 3;
let exposure = 0;
let name = "";
let jsonOutput = false;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--power":
power = parseInt(args[++i]) || 5;
break;
case "--tenure":
tenure = parseInt(args[++i]) || 5;
break;
case "--vulns":
vulnCount = parseInt(args[++i]) || 0;
break;
case "--prots":
protCount = parseInt(args[++i]) || 0;
break;
case "--fortune":
fortune = parseFloat(args[++i]) ?? 3;
break;
case "--exposure":
exposure = parseFloat(args[++i]) || 0;
break;
case "--name":
name = args[++i] || "";
break;
case "--json":
jsonOutput = true;
break;
}
}
// Clamp values
power = Math.min(Math.max(power, 1), 10);
fortune = Math.min(Math.max(fortune, 0), 5);
exposure = Math.min(Math.max(exposure, 0), 1);
// Create synthetic vulnerabilities and protections
const vulnerabilities: string[] = [];
for (let i = 0; i < vulnCount; i++) {
vulnerabilities.push(`vulnerability_${i + 1}`);
}
const protections: string[] = [];
for (let i = 0; i < protCount; i++) {
protections.push(`protection_${i + 1}`);
}
// Create exposure log entry if exposure provided
const exposureLog: { date: string; event: string; pressure: number }[] = [];
if (exposure > 0) {
exposureLog.push({
date: new Date().toISOString().split("T")[0],
event: "Accumulated exposure",
pressure: exposure,
});
}
const input: FatePressureInput = {
powerLevel: power,
tenureYears: tenure,
vulnerabilities,
protections,
fortuneBuffer: fortune,
exposureLog,
};
const result = calculateFatePressure(input);
if (jsonOutput) {
console.log(JSON.stringify({ name: name || undefined, ...result }, null, 2));
} else {
console.log(formatResult(result, name || undefined));
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Fate Roll - The Wheel of Fortune
*
* Rolls against fate pressure to determine if a fate-shift occurs.
* Uses weighted probability based on accumulated pressure.
*
* Usage:
* deno run --allow-read fate-roll.ts --pressure 0.65
* deno run --allow-read fate-roll.ts --pressure 0.75 --danger --fortune 0
* deno run --allow-read fate-roll.ts --pressure 0.80 --danger --fortune 0 --name "Kaira the Bold"
* deno run --allow-read fate-roll.ts --seed 12345 --pressure 0.65
*/
interface FateRollResult {
roll: number;
threshold: number;
shiftTriggered: boolean;
severity: "none" | "minor" | "moderate" | "major" | "catastrophic";
deathEligible: boolean;
deathRoll?: number;
deathThreshold?: number;
deathOccurs?: boolean;
fortuneDecrement?: boolean;
}
// Seeded random for reproducibility
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
this.seed = (this.seed * 1103515245 + 12345) & 0x7fffffff;
return this.seed / 0x7fffffff;
}
d100(): number {
return Math.floor(this.next() * 100) + 1;
}
}
function rollFate(
fatePressure: number,
inDanger: boolean,
fortuneBuffer: number,
rng: SeededRandom
): FateRollResult {
const roll = rng.d100();
const threshold = fatePressure * 100;
const shiftTriggered = roll < threshold;
let severity: FateRollResult["severity"] = "none";
if (shiftTriggered) {
if (roll < threshold * 0.25) {
severity = "catastrophic";
} else if (roll < threshold * 0.50) {
severity = "major";
} else if (roll < threshold * 0.75) {
severity = "moderate";
} else {
severity = "minor";
}
}
// Death eligibility: in danger + pressure > 0.5 + fortune < 1
const deathEligible = inDanger && fatePressure > 0.5 && fortuneBuffer < 1;
let deathRoll: number | undefined;
let deathThreshold: number | undefined;
let deathOccurs: boolean | undefined;
let fortuneDecrement: boolean | undefined;
if (deathEligible && shiftTriggered) {
deathRoll = rng.next();
// Death threshold: (pressure - 0.5) × 2 × (1 - fortune/5)
deathThreshold = (fatePressure - 0.5) * 2 * (1 - fortuneBuffer / 5);
deathOccurs = deathRoll < deathThreshold;
// If death avoided, fortune decreases
if (!deathOccurs) {
fortuneDecrement = true;
}
}
return {
roll,
threshold,
shiftTriggered,
severity,
deathEligible,
deathRoll,
deathThreshold,
deathOccurs,
fortuneDecrement,
};
}
function formatRollResult(result: FateRollResult, entityName?: string): string {
const lines: string[] = [];
lines.push(`FATE ROLL${entityName ? `: ${entityName}` : ""}`);
lines.push("=".repeat(50));
lines.push("");
lines.push(`Roll: ${result.roll} vs Threshold ${result.threshold.toFixed(0)}`);
lines.push("");
if (result.shiftTriggered) {
lines.push("THE WHEEL TURNS");
lines.push("-".repeat(50));
lines.push(`Fate-shift triggered. Severity: ${result.severity.toUpperCase()}`);
lines.push("");
switch (result.severity) {
case "catastrophic":
lines.push("The wheel turns heavily. A catastrophic fate-shift is warranted.");
lines.push("Consider: death, complete fall, total collapse, destruction.");
break;
case "major":
lines.push("A major turn of the wheel. Significant consequences follow.");
lines.push("Consider: serious wound, major loss, significant decline, defeat.");
break;
case "moderate":
lines.push("The wheel moves. A moderate setback occurs.");
lines.push("Consider: vulnerability exposed, resource lost, ally turns.");
break;
case "minor":
lines.push("A warning from fate. Minor consequences manifest.");
lines.push("Consider: reputation damage, small loss, omen of worse to come.");
break;
}
if (result.deathEligible) {
lines.push("");
lines.push("DEATH ROLL");
lines.push("-".repeat(50));
lines.push("Death eligibility: YES (in danger, pressure > 50%, fortune depleted)");
lines.push(
`Death roll: ${(result.deathRoll! * 100).toFixed(1)}% vs threshold ${(result.deathThreshold! * 100).toFixed(1)}%`
);
if (result.deathOccurs) {
lines.push("");
lines.push("*** DEATH OCCURS ***");
lines.push("The wheel of fate turns completely. The entity meets their end.");
lines.push("");
lines.push("Next steps:");
lines.push(" 1. Run propose-shift.ts to generate death proposal");
lines.push(" 2. Determine narrative hook for the death");
lines.push(" 3. Submit for human approval");
} else {
lines.push("");
lines.push("Death avoided. Fate grants a reprieve this time.");
if (result.fortuneDecrement) {
lines.push("Fortune buffer should decrease by 1 (narrow escape).");
}
}
}
} else {
lines.push("THE WHEEL HOLDS");
lines.push("-".repeat(50));
lines.push("No fate-shift triggered. The entity continues.");
lines.push("");
lines.push("Pressure continues to accumulate unless addressed.");
lines.push("Re-roll at next trigger point (session end, arc completion, etc.).");
}
lines.push("");
lines.push("NEXT STEPS");
lines.push("-".repeat(50));
if (result.shiftTriggered) {
lines.push("1. Determine appropriate shift type based on severity");
lines.push("2. Consider running fate-choice.ts for Tier 2 choice");
lines.push("3. Run propose-shift.ts to generate formal proposal");
lines.push("4. Submit for human approval before applying");
} else {
lines.push("1. Continue tracking exposure events");
lines.push("2. Update vulnerability/protection if changed");
lines.push("3. Re-roll at next trigger point");
}
return lines.join("\n");
}
function showUsage() {
console.log(`
Fate Roll - The Wheel of Fortune
=================================
Rolls against fate pressure to determine if a fate-shift occurs.
Usage:
deno run --allow-read fate-roll.ts [options]
Options:
--pressure <0-1> Fate pressure (required)
--danger Entity is in active danger (enables death roll)
--fortune <0-5> Fortune buffer (default: 3)
--name <name> Entity name for output
--seed <n> Random seed for reproducibility
--json Output as JSON
Examples:
deno run --allow-read fate-roll.ts --pressure 0.65
deno run --allow-read fate-roll.ts --pressure 0.75 --danger --fortune 0
deno run --allow-read fate-roll.ts --pressure 0.80 --danger --fortune 0 --name "Kaira"
Mechanics:
- Roll d100 vs (pressure × 100)
- If roll < threshold: fate-shift triggered
Severity (based on roll vs threshold):
< 25%: Catastrophic
< 50%: Major
< 75%: Moderate
< 100%: Minor
Death Eligibility (all required):
- Entity in danger (--danger flag)
- Fate pressure > 0.5
- Fortune buffer < 1
Death Threshold:
(pressure - 0.5) × 2 × (1 - fortune/5)
`);
}
function main() {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
showUsage();
Deno.exit(0);
}
// Parse arguments
let pressure = -1;
let danger = false;
let fortune = 3;
let name = "";
let seed = Date.now();
let jsonOutput = false;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--pressure":
pressure = parseFloat(args[++i]) || -1;
break;
case "--danger":
danger = true;
break;
case "--fortune":
fortune = parseFloat(args[++i]) ?? 3;
break;
case "--name":
name = args[++i] || "";
break;
case "--seed":
seed = parseInt(args[++i]) || Date.now();
break;
case "--json":
jsonOutput = true;
break;
}
}
if (pressure < 0 || pressure > 1) {
console.error("Error: --pressure required (value between 0 and 1)");
console.error("Example: --pressure 0.65");
Deno.exit(1);
}
// Clamp fortune
fortune = Math.min(Math.max(fortune, 0), 5);
const rng = new SeededRandom(seed);
const result = rollFate(pressure, danger, fortune, rng);
if (jsonOutput) {
console.log(
JSON.stringify(
{
name: name || undefined,
fatePressure: pressure,
inDanger: danger,
fortuneBuffer: fortune,
seed,
...result,
},
null,
2
)
);
} else {
console.log(formatRollResult(result, name || undefined));
}
}
main();
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* Propose Fate-Shift
*
* Generates a proposal document for human review before applying
* changes to the world bible.
*
* Usage:
* deno run --allow-read --allow-write propose-shift.ts \
* --name "Character Name" --type fall --severity major
* deno run --allow-read --allow-write propose-shift.ts \
* --name "Character Name" --type death --pressure 0.82 --roll 15 --output proposal.md
*/
import { dirname, fromFileUrl, join } from "https://deno.land/std@0.208.0/path/mod.ts";
interface ShiftTypeData {
description: string;
severities: { [key: string]: string };
eligibility?: {
requiresDanger?: boolean;
minPressure?: number;
maxFortuneBuffer?: number;
};
narrativeHooks: string[];
}
interface ShiftTypesData {
character: { [key: string]: ShiftTypeData };
faction: { [key: string]: ShiftTypeData };
location: { [key: string]: ShiftTypeData };
}
// Seeded random
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
this.seed = (this.seed * 1103515245 + 12345) & 0x7fffffff;
return this.seed / 0x7fffffff;
}
pick<T>(array: T[]): T {
return array[Math.floor(this.next() * array.length)];
}
}
async function loadShiftTypes(): Promise<ShiftTypesData | null> {
try {
const scriptDir = dirname(fromFileUrl(import.meta.url));
const dataPath = join(scriptDir, "..", "data", "shift-types.json");
const text = await Deno.readTextFile(dataPath);
return JSON.parse(text);
} catch {
return null;
}
}
function getDate(): string {
return new Date().toISOString().split("T")[0];
}
function generateProposal(
name: string,
category: "character" | "faction" | "location",
shiftType: string,
severity: string,
pressure: number,
roll: number | undefined,
trigger: string,
vulnerabilities: string[],
shiftData: ShiftTypeData,
rng: SeededRandom
): string {
const date = getDate();
const threshold = pressure * 100;
const narrativeHook = rng.pick(shiftData.narrativeHooks);
const severityDesc = shiftData.severities[severity] || "Consequences to be determined.";
const lines: string[] = [];
lines.push(`# Fate-Shift Proposal: ${name}`);
lines.push("");
lines.push(`**Date Generated:** ${date}`);
lines.push(`**Trigger:** ${trigger}`);
lines.push(`**Fate Pressure at Assessment:** ${(pressure * 100).toFixed(1)}%`);
lines.push("");
lines.push("## Current State Summary");
lines.push("");
lines.push(`${name} is a ${category} currently under significant fate pressure.`);
lines.push("The wheel of fortune has turned against them.");
lines.push("");
lines.push("## Vulnerability Analysis");
lines.push("");
if (vulnerabilities.length > 0) {
lines.push("The following vulnerabilities contributed to this fate-shift:");
for (const v of vulnerabilities) {
lines.push(`- ${v}`);
}
} else {
lines.push("Vulnerabilities should be documented from the entity's world bible entry.");
}
lines.push("");
lines.push("## The Fate Roll");
lines.push("");
if (roll !== undefined) {
lines.push(`- **Roll:** ${roll}`);
lines.push(`- **Threshold:** ${threshold.toFixed(0)}`);
} else {
lines.push("- **Roll:** [Not recorded - manual proposal]");
lines.push(`- **Threshold:** ${threshold.toFixed(0)} (based on pressure)`);
}
lines.push(`- **Severity:** ${severity.toUpperCase()}`);
lines.push("");
lines.push("## Proposed Fate-Shift");
lines.push("");
lines.push(`${name} experiences a ${severity} ${shiftType}.`);
lines.push("");
lines.push(`**${shiftData.description}**`);
lines.push("");
lines.push(severityDesc);
lines.push("");
lines.push("### Type");
lines.push("");
lines.push(shiftType);
lines.push("");
lines.push("### Narrative Hook");
lines.push("");
lines.push(narrativeHook);
lines.push("");
lines.push("### Consequences");
lines.push("");
lines.push(`- **For ${name}:** ${severityDesc}`);
if (category === "character") {
lines.push(`- **For the world:** Power vacuum in ${name}'s sphere of influence. Allies and enemies respond.`);
lines.push("- **For campaigns:** Review active campaigns involving this character for impact.");
} else if (category === "faction") {
lines.push(`- **For the world:** ${name}'s ${shiftType} creates opportunities for rivals and instability for allies.`);
lines.push("- **For campaigns:** Review campaigns with faction involvement for necessary adjustments.");
} else {
lines.push(`- **For the world:** The ${shiftType} of ${name} ripples outward, affecting connected locations.`);
lines.push("- **For campaigns:** Review campaigns set in or connected to this location.");
}
lines.push("");
lines.push("## Alternative Options");
lines.push("");
lines.push(`1. Reduce severity to ${getLesserSeverity(severity)} with slower progression`);
lines.push(`2. Change type to ${getAlternativeType(shiftType, category)} with equivalent severity`);
lines.push("3. Defer decision and increase pressure by 0.1 for next assessment");
lines.push("");
lines.push("## Approval Required");
lines.push("");
lines.push("- [ ] Approve as proposed");
lines.push("- [ ] Approve with modifications: _______________");
lines.push("- [ ] Reject and explain: _______________");
lines.push("- [ ] Defer decision to later");
lines.push("");
lines.push("---");
lines.push("");
lines.push(`*Generated by world-fates skill on ${date}*`);
return lines.join("\n");
}
function getLesserSeverity(severity: string): string {
const order = ["minor", "moderate", "major", "catastrophic"];
const index = order.indexOf(severity);
return index > 0 ? order[index - 1] : "warning sign";
}
function getAlternativeType(shiftType: string, category: string): string {
const alternatives: { [key: string]: { [key: string]: string[] } } = {
character: {
death: ["fall", "exile"],
fall: ["diminishment", "exile"],
corruption: ["transformation", "fall"],
exile: ["fall", "diminishment"],
transformation: ["corruption", "diminishment"],
diminishment: ["fall", "transformation"],
},
faction: {
collapse: ["schism", "decline"],
schism: ["decline", "reformation"],
absorption: ["decline", "defeat"],
decline: ["defeat", "schism"],
reformation: ["schism", "decline"],
defeat: ["decline", "absorption"],
},
location: {
destruction: ["disaster", "conquest"],
conquest: ["decline", "isolation"],
disaster: ["decline", "destruction"],
decline: ["isolation", "disaster"],
transformation: ["decline", "isolation"],
isolation: ["decline", "transformation"],
},
};
const categoryAlts = alternatives[category] || {};
const typeAlts = categoryAlts[shiftType] || [];
return typeAlts[0] || "different fate";
}
function showUsage() {
console.log(`
Propose Fate-Shift
==================
Generates a fate-shift proposal for human review.
Usage:
deno run --allow-read --allow-write propose-shift.ts [options]
Required:
--name <name> Entity name
--type <type> Shift type (death, fall, corruption, etc.)
--severity <level> Severity (minor, moderate, major, catastrophic)
Optional:
--category <cat> Entity category (character, faction, location; default: character)
--pressure <0-1> Fate pressure (default: 0.65)
--roll <1-100> The roll that triggered this
--trigger <text> What triggered assessment (default: "Manual assessment")
--vulns <csv> Comma-separated vulnerabilities
--output <file> Output file path
--seed <n> Random seed
Shift Types by Category:
character: death, fall, corruption, exile, transformation, diminishment
faction: collapse, schism, absorption, decline, reformation, defeat
location: destruction, conquest, disaster, decline, transformation, isolation
Severity Levels:
minor - Warning sign, reputation damage
moderate - Setback, resource lost
major - Significant loss, recovery difficult
catastrophic - Total loss, no return
Examples:
deno run --allow-read --allow-write propose-shift.ts --name "Lord Varen" --type fall --severity major
deno run --allow-read --allow-write propose-shift.ts --name "The Iron Guild" --category faction --type decline --severity moderate
deno run --allow-read --allow-write propose-shift.ts --name "Kaira" --type death --severity catastrophic --pressure 0.82 --roll 15
`);
}
async function main() {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
showUsage();
Deno.exit(0);
}
// Load shift types
const shiftTypesData = await loadShiftTypes();
// Parse arguments
let name = "";
let category: "character" | "faction" | "location" = "character";
let shiftType = "";
let severity = "";
let pressure = 0.65;
let roll: number | undefined;
let trigger = "Manual assessment";
let vulnsStr = "";
let output = "";
let seed = Date.now();
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--name":
name = args[++i] || "";
break;
case "--category":
category = (args[++i] || "character") as "character" | "faction" | "location";
break;
case "--type":
shiftType = args[++i] || "";
break;
case "--severity":
severity = args[++i] || "";
break;
case "--pressure":
pressure = parseFloat(args[++i]) || 0.65;
break;
case "--roll":
roll = parseInt(args[++i]);
break;
case "--trigger":
trigger = args[++i] || "Manual assessment";
break;
case "--vulns":
vulnsStr = args[++i] || "";
break;
case "--output":
output = args[++i] || "";
break;
case "--seed":
seed = parseInt(args[++i]) || Date.now();
break;
}
}
if (!name) {
console.error("Error: --name required");
Deno.exit(1);
}
if (!shiftType) {
console.error("Error: --type required");
Deno.exit(1);
}
if (!severity) {
console.error("Error: --severity required");
Deno.exit(1);
}
const vulnerabilities = vulnsStr ? vulnsStr.split(",").map((v) => v.trim()) : [];
const rng = new SeededRandom(seed);
// Get shift type data
let shiftData: ShiftTypeData;
if (shiftTypesData && shiftTypesData[category] && shiftTypesData[category][shiftType]) {
shiftData = shiftTypesData[category][shiftType];
} else {
// Fallback
shiftData = {
description: `A ${shiftType} affecting ${category}`,
severities: {
minor: "Minor consequences",
moderate: "Moderate consequences",
major: "Major consequences",
catastrophic: "Catastrophic consequences",
},
narrativeHooks: ["The circumstances unfold through play"],
};
}
const proposal = generateProposal(
name,
category,
shiftType,
severity,
pressure,
roll,
trigger,
vulnerabilities,
shiftData,
rng
);
if (output) {
await Deno.writeTextFile(output, proposal);
console.log(`Proposal written to: ${output}`);
} else {
console.log(proposal);
}
}
main();
Fate Tracking Template
Add this section to entity entries in the world bible to track fate pressure.
---
Fate Tracking
Power Level: [1-10] Tenure: [Duration, e.g., "15 years"] Current Fate Pressure: [0-100%] Fortune Buffer: [0-5] Last Assessment: [YYYY-MM-DD]
Vulnerabilities
- [Vulnerability 1 - describe the weakness]
- [Vulnerability 2 - describe the weakness]
Protections
- [Protection 1 - describe the shield]
- [Protection 2 - describe the shield]
Risk Exposure Log
- [YYYY-MM-DD]: [Event description] (+X% pressure)
- [YYYY-MM-DD]: [Event description] (+X% pressure)
---
Usage Notes
Power Level Scale
| Level | Scope | Examples |
|---|---|---|
| 1-2 | Local | Village elder, small merchant, local hero |
| 3-4 | Regional | Town mayor, guild master, regional champion |
| 5-6 | National | Noble house, major faction, famous adventurer |
| 7-8 | Civilization | Kingdom ruler, legendary hero, archmage |
| 9-10 | World-shaping | Emperor, demigod, world-threatening power |
Vulnerability Weights
- Known enemy: +1.0
- Active conflict: +1.5
- Exploitable flaw: +2.0
- Critical dependency: +1.0
- Destroying secret: +3.0
Protection Weights
- Loyal ally: +1.0
- Fortified position: +2.0
- Divine/magical protection: +3.0
- Information network: +1.0
- Popular support: +2.0
Tenure Modifiers
| Duration | Modifier |
|---|---|
| < 1 year | 0.5x |
| 1-5 years | 1.0x |
| 5-20 years | 1.5x |
| 20-50 years | 2.0x |
| 50+ years | 2.5x |
Exposure Categories
| Category | Base Pressure | Examples |
|---|---|---|
| Combat | +10% | Battle, duel, assassination attempt |
| Intrigue | +5% | Political maneuvering, subtle threats |
| Betrayal | +15% | Ally turns, trust violated |
| Revelation | +20% | Secret exposed, truth revealed |
| Overreach | +10% | Hubris, overconfident action |
| Loss | +8% | Resource, ally, or territory lost |
| Challenge | +12% | Rival's claim, direct contest |
| Narrow Escape | +5% | Survived dangerous situation |
---
Example Entry
Fate Tracking
Power Level: 7/10 Tenure: 40 years Current Fate Pressure: 72% Fortune Buffer: 1/5 Last Assessment: 2024-03-15
Vulnerabilities
- Known enemy: Lord Blackwood (rival for the throne) +1.0
- Active conflict: War with the Eastern Tribes +1.5
- Exploitable flaw: Addiction to dreamwine +2.0
- Destroying secret: Bastard heir exists +3.0
Protections
- Loyal ally: Captain of the Guard +1.0
- Fortified position: The Iron Citadel +2.0
- Popular support: Beloved by common folk +2.0
Risk Exposure Log
- 2024-01-10: Survived assassination attempt (+10% pressure)
- 2024-02-15: Bastard heir revealed publicly (+20% pressure)
- 2024-03-01: Lost the Battle of the Crossing (+8% pressure)