Heroes Required

Project Inspiration

The idea started with a simple observation: in every party-based fantasy game ever made, somebody organized the party. Somebody booked the inn, negotiated the mission contract, scouted to figure out what the dragon actually wanted before they knocked on the cave door. That person was never the hero. That person was never the player.

The deeper version of that observation came from Baldur's Gate. Not the story — the pattern underneath it. After enough playthroughs, the encounters stopped being encounters. They became known quantities. You could plan your party composition around a fight that hadn't happened yet, because you already knew exactly how it would unfold. The dragon would open from the left. The ambush would trigger at the doorway. The persuasion check would fail on a Tuesday just as reliably as it failed last time. The world remembered nothing. Only the player did. That asymmetry is what turned a great game into a checklist. Mastery collapsed into memorization the moment you had seen it twice.

That problem pointed toward a design question: what would it take to make the world accumulate knowledge too — to make it respond to the player's patterns the way a real opponent would? The answer the game arrived at was to lock the spine and flex the branches. The main narrative plot is fixed. The ending exists. The shape of the arc does not change. But every sub-plot in between — every contract, every faction grudge, every NPC relationship — is free to drift, compound, and resolve differently depending on what the player did last session and what they ignored. Those sub-plots don't run parallel to the main story; they weave back into it at fixed junction points, carrying whatever weight they've accumulated. The result is a game where repeat playthroughs are not easier — they are differently hard. You know where the story goes. You do not know what it will cost to get there this time.

Stardew Valley proved that warmth and systems are not opposites. The Adventure Zone proved that absurdist sincerity lands harder than irony. Dungeons & Dragons Online proved that hired NPCs become characters the moment they have a personality. Those three things together suggested the setting: guild management, not dungeon crawling. The business behind the adventure. The people the hero never mentions in the bard's song.

The design locked in when we asked what happens if the NPC the player spent three sessions befriending changes their mind — silently, without announcement, on a schedule only the world knows. Suddenly the game had a retention engine that was not a grind. It was curiosity. Why did that stop working? That question is the whole game.

Built With

Share this project:

Updates

posted an update —

Heroes Required: Building the Prototype With the Machine That Knows the Design

From design documents to a working UI in a single session — then porting it to Horizon · devlog gamedev unity ai mcp


Post 1 covered the rules engine. Post 2 covered the content pipeline. This post is about the part we didn't expect to talk about: using an AI coding agent connected directly to Unity to build the entire game interface from the design documents, without touching the Unity Editor by hand.

This is the part that changed how we think about prototyping.


The Constraint That Made This Work

We had a complete design. The GDD defined the session loop, the data schema, the bridge contract between backend and client, and the visual hierarchy of every screen. What we didn't have was time to translate all of that into Unity scenes manually.

The Unity MCP server connects Claude directly to a live Unity Editor instance. Instead of telling the editor what to do through menus and inspectors, you describe what you want to exist and the agent writes C# code that creates it.

The decision we made early: don't build scenes interactively. Build builders — Editor scripts that construct scenes programmatically from data, so the UI can be rebuilt from scratch any time the data changes.


Code That Builds Code

The entire UI lives in five Editor scripts under Assets/Editor/. Each one is a static class with a [MenuItem] method that creates a complete Unity scene when triggered:

Tools ▸ Heroes Required ▸ Scenes ▸ Build Notice Board
Tools ▸ Heroes Required ▸ Scenes ▸ Build Encounter
Tools ▸ Heroes Required ▸ Scenes ▸ Build Party
Tools ▸ Heroes Required ▸ Scenes ▸ Build Guild Hub
Tools ▸ Heroes Required ▸ Scenes ▸ Build Bootstrap (runtime test)

Every primitive — image panels, text labels, buttons, accent strips — is created through a shared HrUiKit library of factory methods. HrPalette defines the brand colours. Nothing is placed by hand.

// From HrUiKit — the entire UI vocabulary is these four calls
public static Image  Image(string name, Transform parent, Color c, Sprite sprite = null)
public static Image  Icon(string name, Transform parent, Sprite sprite)
public static TextMeshProUGUI Text(string name, Transform parent, string text, ...)
public static Button Button(string name, Transform parent, string label, Color c, ...)

Position is declared as anchor ratios, not pixel coordinates — the canvas scales to mobile portrait (1080×1920, match height) so the same numbers work at any resolution. A full screen layout is about 20 lines of anchor calls:

var bg     = HrUiKit.Image("Board_BG", canvas.transform, HrPalette.WoodFrame, frame);
HrUiKit.Fill(bg.rectTransform);

var header = HrUiKit.Text("Header", canvas.transform, "NOTICE  BOARD", 90, HrPalette.Gold);
HrUiKit.Anchor(header.rectTransform, 0.05f, 0.9f, 0.95f, 0.985f);

The MCP agent wrote all of this from a brief: "build a portrait-mode fantasy UI with parchment cards, a wood-frame border, and a corkboard background. Use the HONETi asset pack textures at these paths." It read the asset paths from the project, matched them to the design, and produced the builder scripts in one pass.


The Integration That Made the UI Real

The Notice Board builder does something the others don't: it reads the actual content engine output.

// NoticeBoardBuilder.LoadFromExport() — THE integration point
var path = Path.Combine(Application.streamingAssetsPath, "world_export_sample.json");
var export = JsonUtility.FromJson<WorldExportPayload>(File.ReadAllText(path));
foreach (var e in export.encounters)
{
    list.Add(new CDef
    {
        Id = e.id, Title = e.title, Reward = e.reward,
        Power = e.powerLevel, MinPlayers = e.minPlayers,
        Mode  = e.minPlayers > 1 ? $"GROUP {e.minPlayers}" : "SOLO",
        Accent = e.powerLevel < 30 ? HrPalette.Safe
               : e.powerLevel < 100 ? HrPalette.Risky : HrPalette.Mystery,
    });
}

The workflow is:

  1. Backend generates a new world state
  2. Tools ▸ Backend ▸ Pull Live Export calls GET /api/world/export and writes the result to StreamingAssets/world_export_sample.json
  3. Tools ▸ Scenes ▸ Build Notice Board rebuilds the scene from that file
  4. The board now shows the real quest cards with the real titles, rewards, and power ratings that the content engine produced

Change what the generator produces, rebuild, the board changes. No manual data entry. No placeholder text that someone has to remember to update.

Power level drives card colour automatically — green accent for safe contracts, amber for risky ones, crimson for mystery-tier. The NPC mood from dialogueMood drives the encounter screen's colour and approach logic. The data schema we designed for the backend became the visual language of the UI without a translation step.


Art References, Not Art

The bridge contract has a deliberate decision about images: the backend never ships binary data. It ships prompts.

public sealed class ArtRef
{
    public int Revision { get; init; }          // bumped when narrative state warrants new art
    public string Prompt { get; init; }         // derived from current mood, plot beats, accumulated change
    public List<string> Tags { get; init; }
    public bool UsePreviousAsReference { get; init; } = true; // img2img continuity hint
}

Each NPC and each encounter contract has an ArtRef. The art key is deterministic: Assets/HeroArt/npc_{NpcId}.png for characters, Assets/HeroArt/contract_{Id}.png for mission cards.

// EncounterBuilder.cs — portrait slot
var pArt = HrUiKit.HeroArt("npc_" + NpcId);  // → Assets/HeroArt/npc_2001.png
if (pArt != null) HrUiKit.Anchor(HrUiKit.Icon("Art", portrait.transform, pArt).rectTransform, ...);
else HrUiKit.Fill(HrUiKit.Text("ph", portrait.transform, "ART", 40, HrPalette.Gold).rectTransform);

If the file exists at that path, it slots in. If it doesn't, the UI shows a scroll icon placeholder. The scene builder doesn't know or care whether the image has been generated yet — it just checks the path.

The Horizon developer skill took the same prompt data and used it to describe character and location art for generation: not the technical format (PNG at a path), but the same underlying thing — the backend's current narrative state expressed as a visual description. An NPC who's been marked wary after three weeks of failed expeditions has a different Prompt than they had at season launch. The art reflects the story.


The Session Log

AgentHistory.txt in the project root captures what MCP actually did to build this. The pattern is consistent throughout:

Read the existing shape. Write the next piece. Run the tests. Report the count. Ask what's next.

The backend went from domain entities to a running API in one session: domain model → simulation engine → export pipeline → REST controllers → DI wiring → Hangfire scheduling. 144 tests, 0 failures, all written by the agent against the design documents.

Then the Unity side: the agent read that the unity/ folder was empty ("greenfield"), scaffolded the script structure, wrote WorldSyncService, WorldManager, NpcBehaviorApplicator, and the [Serializable] DTOs that mirror the backend JSON — all in one pass, all matching the bridge contract we'd defined.

The editor builders followed: the agent read the HONETi asset paths from the project, matched them to the palette from the GDD, and produced five scene builders that construct the full session loop from scratch on demand.

The only manual work was approving what it produced.


The Session Loop That Came Out of It

Five scenes, one loop:

04_GuildHub       ← session start; gold, renown, week, vassal roster
      ↓ "To the Notice Board"
01_NoticeBoard    ← live encounter cards from the content engine
      ↓ ACCEPT
03_Party          ← vassal selection, ≤3, combined power vs requirement
      ↓ DEPLOY
02_Encounter      ← NPC portrait, mood, three approach cards
      ↓ CONTINUE
04_GuildHub       ← back with updated gold/renown/consequences

The Encounter screen shows an NPC portrait (if generated), their mood in the HrPalette.Risky amber or HrPalette.Champion crimson, and three approach cards colour-coded by identity — Champion, Diplomat, Shadow. The outcome text is driven by the NPC's mood combined with the chosen approach. A WARY NPC responds differently to the Champion approach than a WELCOMING one.

All of this was running in Play mode, reading from the live backend, before we touched the Horizon tooling.


Where We Are Now

The Horizon developer skill took over once the Unity prototype had a working loop. The asset pipeline is the same: the backend generates ArtRef prompts, images are produced and placed at the deterministic paths, and the UI slots them in. The data schema didn't change — the same WorldExportPayload, the same encounter cards, the same NPC mood values — just a different runtime consuming them.

The gameplay scripts are being wired against the live content: the world the generator has been running for several weeks of testing, with real NPC mutation history, real relationship scores, and real vassal drama accumulated across simulated expeditions.

What we're testing now is the thing that was always the real question: when an NPC has been through five weeks of weekly mutation, has relationships with specific vassal types, and has an ArtRef Prompt that reflects all of that accumulated state — does the encounter actually feel different? Does the quest feel like it was written for this NPC in this week, or does it feel like a procedurally generated filler contract?

That's the question the Unity prototype couldn't answer. It required the live world.

Next post: what it looks like when the content engine's week-12 output runs against real players, and what the mutation logs tell us about whether narrative pressure is doing its job.

Log in or sign up for Devpost to join the conversation.

posted an update —

Heroes Required: The Machine That Writes the World

How the authoring tool generates seasons, and how the weekly engine keeps them alive · devlog gamedev csharp ai


The previous post covered how the rules engine works — deterministic outcomes, the canon firewall, the 52-week test. This post is about what happens before any of that: how content gets into the game, and how it keeps changing after it's there.

There are two separate problems here, and they needed two separate systems.

Problem one: Creating a new story season is a lot of work. 40 named NPCs, each with personality profiles, cultural alignments, and narrative roles. 60 hireable vassals spread across five specialist classes. 10 story chapters. A zone map with locations that connect meaningfully. Writing all of this by hand is a wall we'd never get over.

Problem two: Once content is in the game, it can't just sit there. An NPC who says the same thing in week 1 as week 12 isn't a character — they're furniture. The world has to actually move.

The same AI that creates the content updates it. But the two pipelines are completely different, because the risks are completely different.


World Forge: Authoring Tool for New Seasons

World Forge is the developer-facing content pipeline. It's not a visual editor — it's a set of API endpoints we call when we want to build something new. The workflow is four steps.

Step 1 — Import the Lore

Before generating anything, we feed the engine the source material: backstory documents, cultural profiles, historical events, world rules. Anything that should inform what gets created.

POST /api/worldforge/lore/import
{ "name": "The Dragon Accord", "rawContent": "..." }

The import service splits the document into chunks, runs each through OpenAI's text-embedding-3-small, and persists the vectors to the database. That's the RAG library — when we generate a season, the prompt assembler searches this library and pulls the most relevant chunks into context. The AI writes NPCs that are actually aware of the world's history, because it has the history in front of it.

Step 2 — Generate the Season

One request, a few minutes, a lot of output:

POST /api/worldforge/season/generate
{
  "theme": "The Dragon Accord Reconsidered",
  "toneGuide": "Hitchhiker's Guide absurdity. Zork precision. Dirk Gently causality. Absurd is allowed; random is FORBIDDEN.",
  "requiredTruths": ["TRUTH_GREAT_FORGETTING", "TRUTH_TIAMAT_SCROLL"],
  "loreSourceNames": ["The Dragon Accord", "Magic Rules"]
}

SeasonGenerator runs two passes. First, it generates the season skeleton: 40 NPCs, 10 story chapters, world truths consistent with the required canon. Then, with those truths available as context, it runs five parallel passes to generate 12 vassals per class — Sages, Scouts, Craftsmen, Defenders, Traders — each class knowing about the world the skeleton just defined.

The result is a draft. Nothing is live yet.

Step 3 — Generate the Zone Map

Locations are a separate pass that runs after the NPC roster exists, because placement depends on knowing who needs to be placed:

POST /api/worldforge/season/{id}/locations

The AI designs 8 locations (City, Dungeon, Camp, DragonLair, etc.) and places the NPC roster into them. It also seeds initial dialogue — 5 opening lines per NPC, generated at launch so the world isn't silent on day one. Up to 5 concurrent dialogue requests run in parallel; individual failures don't abort the batch.

Step 4 — Human Review

Everything generated goes into Draft state and gets an ApprovalRecord. We read through the output, make notes, and choose:

PUT /api/worldforge/approvals/{id}/approve   { "notes": "NPC names approved, tone consistent" }
PUT /api/worldforge/approvals/{id}/revise    { "notes": "Brunna's dialogue is too cheerful, revise" }
PUT /api/worldforge/approvals/{id}/reject

The revision path is where name changes and tone corrections happen. We flag the specific issue, the draft goes back into the queue with the notes attached as context, and the generator tries again. Nothing reaches SeasonState.Live until a human has explicitly approved it. This is enforced in code, not in discipline:

// Only the approved season becomes Live — the exporter reads nothing else
foreach (var s in current) s.CurrentState = SeasonState.Archived;
var season = await db.Set<Season>().FindAsync([approval.ContentId], ct);
if (season is not null) season.CurrentState = SeasonState.Live;

Drafts are invisible to the game. Approval is the gate.


The Weekly Content Job: Keeping Things Moving

Once a season is live, the content engine takes over. Every week, a background job runs the full mutation cycle in six steps.

Step 1 — Freeze World State

A snapshot of every NPC, relationship, subplot, and usage count is taken at the start of the cycle. The mutation job works against this frozen state so mid-cycle changes don't interfere.

Step 2 — Select Mutation Targets

This is the part that replaces randomness with intent. MutationSelector scores every NPC using a composite formula:

var score = (tension - usage + 1)    // social heat minus player attention
          * (pressure.Total + 1)     // narrative pressure from context
          * storyRelevance           // how tied this NPC is to active plot nodes
          * instability;             // Rewrite-flagged NPCs mutate more easily

Mutation rates by type:

Target Weekly rate Logic
NPCs 10–15% Weighted by narrative pressure score
Minor subplots 20–40% Prioritises stale, Major/Rewrite-flagged nodes
Relationships Tension-driven Selected when Rivalry + Resentment − Trust > 50
Dialogue & rumours 50%+ Highest rotation — the thing players read most

An NPC who's been interacted with heavily gets a usage penalty — the engine backs off and lets other characters breathe. An NPC who's been dormant accumulates pressure and is more likely to be selected. This produces natural narrative rhythm rather than random churn.

Step 3 — Generate AI Mutations

Each selected NPC gets a mutation request: current state JSON, allowed mutation level (Minor, Major, or Rewrite), and the last few interaction history notes. The AI returns an updated goal, behavior profile, and new dialogue lines.

Every result goes through IContentValidator before it's accepted:

var validation = await contentValidator.ValidateAsync(result, ct);
if (!validation.Passed)
{
    logger.LogWarning("NPC mutation for {Name} failed validation: {Count} violation(s).",
        req.TargetName, validation.Violations.Count);
    continue;  // mutation is silently dropped, not applied
}

Failed validations are logged and discarded. The world gets the NPCs whose mutations passed; the ones that didn't are simply unchanged that week. No broken state, no partial updates.

Step 4 — Compile Behavior Trees

Approved NPC mutations include behavior tree definitions. BehaviorCompiler converts these into the JSON format Unity's BehaviorRunner ticks against — selectors, sequences, conditions, actions — with fresh content for this week's state.

Step 5 — Generate Transition Narrative

Before publishing, the job generates a "what changed this week" summary:

var summaryPoints = allNpcResults
    .Select(r => $"{r.NpcName}'s goal changed to '{r.NewGoal}'.")
    .ToList();

var transitionNarrative = await aiContent.GenerateTransitionNarrativeAsync(
    new TransitionNarrativeRequest { Season = season, Week = week, SummaryPoints = summaryPoints }, ct);

This is available to us as a quality check and could eventually drive in-game news boards or loading screen copy.

Step 6 — Publish the Package

Everything bundles into five JSON files written to the filesystem:

content-packages/
└── Season01_Week03/
    ├── manifest.json      ← version, season, week, required client version
    ├── npcs.json          ← updated goals, moods, quest keys per NPC
    ├── behaviors.json     ← compiled behavior tree JSON per NPC
    ├── dialogue.json      ← new dialogue lines per NPC
    └── relationships.json ← relationship score mutations

Unity polls GET /api/content/version at session start. If the version number has changed, it downloads whichever files are new. If the backend is unreachable, it falls back to the local cache from the previous session. If there's no cache, it falls back to a bundled sample baked into the build.

Three fallback levels, because players shouldn't see a blank Notice Board just because the backend is having a slow day.


What This Buys Us

The two pipelines solve different problems the same way: the AI does the heavy lifting, humans control what reaches the game.

For season creation: a theme, a tone guide, and some lore imports is enough to generate a playable season skeleton in a few minutes. We review it, fix anything that's off, and approve. What would have taken weeks of manual writing takes an afternoon of review.

For weekly updates: the mutation engine decides what needs to change based on narrative pressure and usage patterns, the AI decides how it changes, the validator decides if the change is allowed, and the package lands in Unity without anyone touching a file manually.

The important property isn't that it's fast. It's that every layer can reject what the layer above it generated. The validator rejects bad AI output. The approval workflow rejects bad season drafts. The canon firewall rejects mutations that touch immutable truths regardless of what anyone approved.

Every strange event requires forgotten logic. That's the tone guide we give the AI. It's also the design principle for the whole engine.

Next post: the Unity side — how BehaviorRunner ticks those behavior trees, how WorldSyncService keeps the export fresh, and what the player actually sees when all of this resolves into an NPC on screen.

Log in or sign up for Devpost to join the conversation.

posted an update —


title: "Heroes Required: How We're Building a World That Runs Without Us" date: 2026-06-25

tags: [devlog, gamedev, csharp, procedural-generation]

Heroes Required: How We're Building a World That Runs Without Us

A dev blog — the game where your guild matters more than your hero


The Problem We're Solving

Most RPGs have a content problem: the world is pre-scripted. Every NPC says the same thing on Tuesday as they did on Monday. Quests don't change. Relationships don't decay. The game world is a stage set, not a living place.

Heroes Required is built around a different premise: the world keeps changing whether the player is looking at it or not, and the content engine — not a game designer — drives that change every week.

This post explains how the engine works under the hood, without touching any story details.


What We've Actually Built

The core system is a C# .NET 9 backend: Domain → Application → Infrastructure → API. Clean architecture, nothing exotic. The interesting decisions live in the design, not the stack.

Vassals Own Everything

The foundational mechanic: knowledge, abilities, and relationships never belong to the hero. They belong to hired specialists called vassals.

A Scout named Mossbridge knows where the dragon cave is. Not you. Mossbridge.

If Mossbridge leaves your guild — unhappy, underpaid, overshadowed by a more famous colleague — that knowledge walks out with her. The "Find the Dragon" quest that was available last week is now locked. Not because a designer flipped a switch, but because your team no longer has the expertise to pursue it.

guild.RemoveVassal("Mossbridge");
Assert.False(guild.Knows("DragonLocation")); // the most important unit test

The same principle applies to combat approaches, diplomatic paths, trade routes, and ancient languages. Your guild's composition at expedition time is the thing that determines what options exist — not a skill tree, not a level cap.

The Anti-Metagaming Engine

The same in-world obstacle gives different players completely different options depending on what vassals they brought.

  • Guild A (Sage + Trader): unlocks diplomatic paths via historical knowledge and faction contacts
  • Guild B (Defenders only): unlocks combat paths

Neither is correct. Neither is playing wrong. The system breaks the ability for online guides to publish "the answer" — because the answer depends on who you hired.

Assert.NotEqual(
    guildA.AvailableActions.Count(a => a.IsAvailable),
    guildB.AvailableActions.Count(a => a.IsAvailable)
);

Cultural Intelligence — Every Conflict Has Two Correct Sides

One rule governs all generated conflict: the world is not ridiculous to the people living in it.

When an NPC does something that seems strange, it's because their culture has a completely valid internal logic that outsiders misread. We've built cultural profiles for eight species — each one has a core belief, rules for how they interpret behavior, and documented historical misunderstandings with every other group.

None of this backstory is exposition-dumped on the player. It's the engine that generates the reasons NPCs give for conflict. A generated dispute between a dwarf and elf vassal reads differently from a dispute between an orc and a goblin vassal, because the rules engine draws from different cultural templates.

The profiles load from JSON at startup:

"coreIdentity": {
  "primaryBelief": "Anything worth doing should last longer than the person who created it."
}

That single line drives how every dwarf NPC behaves in conflict scenarios — patient, literal, and genuinely baffled by anyone who signs a contract without waiting two years.

The Weekly Mutation Engine

Every week, 10–15% of the world's mutable content changes. Not randomly — the mutation selector uses narrative pressure: NPCs who have been overused get a rest, NPCs who've been dormant get pushed forward, story tension accumulates and discharges.

public WorldState RunWeek(WorldState state)
{
    state.Week++;
    var targets = _selector.SelectTargets(state); // 10-15% of mutable NPCs
    foreach (var npc in targets)
        ApplyMutation(npc, state.Week);    // AI generates the content here in Phase 2
    _drama.Generate(guild, relationships); // jealousy, rivalry, recognition
    return state;
}

Right now the mutation content is a placeholder — the AI slot is intentionally empty. The philosophy: don't let the model touch anything until the rules engine can reject bad ideas. The validator that sits in front of the AI is the thing that keeps the world coherent.

The Canon Firewall

Some things about this world can never change, no matter what the AI generates. A layer called WorldTruthValidator sits in front of every mutation. It maintains a set of immutable facts. Any proposed change that targets one of those facts is rejected before it reaches the content generator.

var result = validator.Validate(
    new Mutation { Target = "GreatForgetting", Change = "Spell worked correctly" },
    canon);

Assert.False(result.Accepted); // the engine enforces this, not discipline

The backstory of this world has specific historical events that must remain true regardless of what the content engine generates week to week. The AI is allowed to be weird — to produce NPCs with unexpected motivations, alliances that make no surface sense, disputes that seem absurd. It's not allowed to break the foundation.

The 52-Week Test

The validation suite includes a full-year simulation. It runs 52 weekly ticks, applies all mutation, drama, and relationship logic, then checks:

  • The world's core truths are still intact
  • NPCs have actually changed (the engine ran)
  • No locked NPC was ever touched
  • Every guild still has at least one viable expedition path — no dominant strategy locked everything out

This is what "game design survives automation" looks like as a test suite. 124 passing tests, 0 failures.


What's Next

Phase 2: plug in the language model. The WeeklySimulationRunner has an empty slot where IContentGenerator will go — a FakeContentGenerator for tests, an OpenAIContentGenerator for production. Once the deterministic engine has sufficient test coverage, we'll trust it to validate AI output rather than just running it unchecked.

Phase 3: Unity integration. The backend will export a JSON payload per NPC that Unity consumes to drive dialogue mood, quest availability, and faction reputation modifiers. The engine tells Unity what an NPC is feeling and why. Unity just renders it.

The goal is a game that genuinely earns the phrase "living world" — not as marketing copy, but as something verifiable in a test run.


Next post: wiring the language model and building the validation layer that keeps it honest.

Log in or sign up for Devpost to join the conversation.