Feature Spec: NPC & Companion System
| Field |
Value |
| Feature |
NPC & Companion System |
| Issue |
#TBD |
| Status |
Draft |
| Author |
The Doctor |
| Date |
2026-04-10 |
Overview
What it does
Adds persistent NPCs with names, roles, personality snippets, and relationships to players. NPCs can become companions that join a party during sessions and appear in combat. The Human DM manages NPCs via the management UI, and Ollama receives NPC context during narrative generation.
Why it's needed
NPCs are the backbone of narrative continuity. Persisted NPCs enable quests, world-building, and companions that evolve across sessions.
Out of scope
- Full merchant economy or shop UI
- Romance/affinity branching narratives beyond a simple relationship score
- AI-generated NPC creation (future spec)
Architecture Notes (The Doctor)
Projects / layers touched
New interfaces in DungeonMaster.Core
INpcService — CRUD for NPCs
ICompanionService — add/remove companions
INpcRelationshipService — relationship tracking per player
Integration points with existing systems
Domain Model
Entities
Npc
Name (string)
Description (string, markdown)
FactionId (Guid?, optional)
Role (NpcRole enum)
PersonalityPrompt (string)
NpcRelationship
NpcId (Guid FK)
PlayerCharacterId (Guid FK)
Disposition (Disposition enum)
AffinityScore (int, 0–100)
Companion
NpcId (Guid FK)
CampaignId (Guid FK)
JoinedAt (DateTimeOffset)
LeftAt (DateTimeOffset?)
Enums
NpcRole
Shopkeeper
QuestGiver
Companion
Villain
Citizen
Disposition
Service Interfaces (CQRS)
public interface INpcService
{
Task<IReadOnlyList<Npc>> GetAllAsync(CancellationToken ct = default);
Task<Npc?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<Npc> CreateAsync(Npc npc, CancellationToken ct = default);
Task<Npc> UpdateAsync(Npc npc, CancellationToken ct = default);
Task DeleteAsync(Guid id, CancellationToken ct = default);
}
public interface ICompanionService
{
Task AddCompanionAsync(Guid campaignId, Guid npcId, CancellationToken ct = default);
Task RemoveCompanionAsync(Guid campaignId, Guid npcId, CancellationToken ct = default);
}
API Contract (Rory)
Endpoints
GET /api/npcs
POST /api/npcs — DM-only
PUT /api/npcs/{id} — DM-only
DELETE /api/npcs/{id} — DM-only
GET /api/campaigns/{id}/companions
POST /api/campaigns/{id}/companions — DM-only
DELETE /api/campaigns/{id}/companions/{npcId} — DM-only
UI Specification (Clara)
Page / component breakdown
- NpcRegistry (
/npcs) — list, search, and edit NPCs
- CompanionRoster (
/campaigns/{id}/companions) — active companions
Blazor component list
| Component |
File |
Purpose |
NpcRegistry |
Pages/Npcs/NpcRegistry.razor / .razor.cs |
NPC list and CRUD |
NpcDetail |
Pages/Npcs/NpcDetail.razor / .razor.cs |
NPC detail/edit view |
CompanionRoster |
Components/Npcs/CompanionRoster.razor / .razor.cs |
Displays active companions |
RelationshipTable |
Components/Npcs/RelationshipTable.razor / .razor.cs |
Player disposition list |
Theme / styling requirements
- Use
var(--color-surface) for cards
- Use
var(--color-primary) for friendly disposition badges
- Use
var(--color-danger) for hostile disposition badges
Discord Commands (Rory — Bot Layer)
/npc talk {name} — initiates NPC dialogue via narrative engine
/npc recruit {name} — DM-only; adds companion to campaign
/npc dismiss {name} — DM-only; removes companion
NLP / AI Behaviour (Missy)
Trigger
NPC dialogue requests from /npc talk or narrative contexts that include NPCs.
Context sent to Ollama
- NPC name, role, and personality prompt
- Relationship disposition and affinity score
- Recent conversation history (last N turns)
Expected behaviour
- NPC stays in character based on personality prompt
- Must not contradict the Human DM’s overrides
- Max length: 1–3 paragraphs
Test Scenarios (Danny) ⚠️ COMPLETE BEFORE IMPLEMENTATION
Happy path
-
Given a DM creates an NPC
When GET /api/npcs is called
Then the NPC appears in the list with correct fields
-
Given a DM recruits a companion
When POST /companions runs
Then the NPC appears in the companion roster
Edge cases
-
Given affinity drops below 0
When relationship updates
Then disposition becomes Hostile
-
Given an NPC is marked as Companion role
When added to combat
Then CombatantType is Companion
Error / failure cases
-
Given a non-DM attempts NPC creation
When POST /npcs is called
Then the API returns 403 Forbidden
-
Given a missing NPC ID
When /npc talk executes
Then the bot returns an error message
Acceptance Criteria
Functional
Non-functional
Dependencies
Agent Work Breakdown
| Agent |
Task |
Depends On |
| The Doctor |
Approve spec |
— |
| Danny |
Write failing tests from Test Scenarios |
Spec approved |
| Rory |
Implement NPC entities + API endpoints |
Danny's tests |
| Clara |
Implement NPC UI |
Rory's API |
| Danny |
Confirm all tests pass |
All implementation |
Definition of Done
Feature Spec: NPC & Companion System
Overview
What it does
Adds persistent NPCs with names, roles, personality snippets, and relationships to players. NPCs can become companions that join a party during sessions and appear in combat. The Human DM manages NPCs via the management UI, and Ollama receives NPC context during narrative generation.
Why it's needed
NPCs are the backbone of narrative continuity. Persisted NPCs enable quests, world-building, and companions that evolve across sessions.
Out of scope
Architecture Notes (The Doctor)
Projects / layers touched
DungeonMaster.Core— NPC entities + servicesDungeonMaster.Infrastructure— EF Core mappings + migrationsDungeonMaster.Api— NPC endpointsDungeonMaster.Web— NPC management UIDungeonMaster.Bot— NPC interaction commandsDungeonMaster.Shared— NPC DTOsNew interfaces in
DungeonMaster.CoreINpcService— CRUD for NPCsICompanionService— add/remove companionsINpcRelationshipService— relationship tracking per playerIntegration points with existing systems
NpcContextDomain Model
Entities
NpcName(string)Description(string, markdown)FactionId(Guid?, optional)Role(NpcRole enum)PersonalityPrompt(string)NpcRelationshipNpcId(Guid FK)PlayerCharacterId(Guid FK)Disposition(Disposition enum)AffinityScore(int, 0–100)CompanionNpcId(Guid FK)CampaignId(Guid FK)JoinedAt(DateTimeOffset)LeftAt(DateTimeOffset?)Enums
NpcRoleShopkeeperQuestGiverCompanionVillainCitizenDispositionFriendlyNeutralHostileService Interfaces (CQRS)
API Contract (Rory)
Endpoints
GET /api/npcsPOST /api/npcs— DM-onlyPUT /api/npcs/{id}— DM-onlyDELETE /api/npcs/{id}— DM-onlyGET /api/campaigns/{id}/companionsPOST /api/campaigns/{id}/companions— DM-onlyDELETE /api/campaigns/{id}/companions/{npcId}— DM-onlyUI Specification (Clara)
Page / component breakdown
/npcs) — list, search, and edit NPCs/campaigns/{id}/companions) — active companionsBlazor component list
NpcRegistryPages/Npcs/NpcRegistry.razor/.razor.csNpcDetailPages/Npcs/NpcDetail.razor/.razor.csCompanionRosterComponents/Npcs/CompanionRoster.razor/.razor.csRelationshipTableComponents/Npcs/RelationshipTable.razor/.razor.csTheme / styling requirements
var(--color-surface)for cardsvar(--color-primary)for friendly disposition badgesvar(--color-danger)for hostile disposition badgesDiscord Commands (Rory — Bot Layer)
/npc talk {name}— initiates NPC dialogue via narrative engine/npc recruit {name}— DM-only; adds companion to campaign/npc dismiss {name}— DM-only; removes companionNLP / AI Behaviour (Missy)
Trigger
NPC dialogue requests from
/npc talkor narrative contexts that include NPCs.Context sent to Ollama
Expected behaviour
Test Scenarios (Danny)⚠️ COMPLETE BEFORE IMPLEMENTATION
Happy path
Given a DM creates an NPC
When GET
/api/npcsis calledThen the NPC appears in the list with correct fields
Given a DM recruits a companion
When POST
/companionsrunsThen the NPC appears in the companion roster
Edge cases
Given affinity drops below 0
When relationship updates
Then disposition becomes Hostile
Given an NPC is marked as Companion role
When added to combat
Then CombatantType is Companion
Error / failure cases
Given a non-DM attempts NPC creation
When POST
/npcsis calledThen the API returns
403 ForbiddenGiven a missing NPC ID
When
/npc talkexecutesThen the bot returns an error message
Acceptance Criteria
Functional
Non-functional
Dependencies
Agent Work Breakdown
Definition of Done