# Jovian Encounter System Data-driven encounter authoring for Unity. One composable `Encounter` type carrying a polymorphic `IEncounterKind` payload, dialog options with designer-authored events, cross-table quest chaining, and gated quest progression that serialises cleanly. ## Package Structure ``` Packages/com.jovian.encounter-system/ ├── Runtime/ │ ├── IEncounter.cs ← interface + Encounter + definition/visuals/properties │ ├── IEncounterKind.cs ← marker interface + Combat/Quest/Social/Puzzle/... │ ├── EncounterTable.cs ← ScriptableObject: list of encounters + roll helpers │ ├── EncountersCollection.cs ← ScriptableObject: group of tables │ ├── EncounterDialogOptionSet.cs ← ScriptableObject: shared option list (auto-renames to id) │ ├── DialogLineLibrary.cs ← ScriptableObject: id → text registry for reusable lines │ ├── DialogLineRef.cs ← struct: library+id reference with inline fallback │ ├── EncounterLink.cs ← cross-table reference (table + internalId) │ ├── EncounterReference.cs ← MonoBehaviour scaffold wiring common UI fields │ ├── EncounterRegistry.cs ← id → encounter lookup cache │ ├── SubclassSelectorAttribute.cs← attribute powering the concrete-type dropdown │ ├── IEncounterEvent.cs ← event interface + ChainTo/StartCombat/Log/GiveReward starters │ ├── Reward.cs ← ScriptableObject: reward asset (id + displayName + icon + kind) │ ├── IRewardKind.cs ← marker interface + Currency/Item/Experience/Unlockable/Other │ ├── EncounterContext.cs ← per-resolution data bag │ ├── EncounterResolver.cs ← Register/Resolve dispatch by concrete event type │ ├── QuestProgress.cs ← gated progression + QuestStarted/Advanced/Completed events │ └── QuestLog.cs ← chronological serialisable record of quest events └── Editor/ ├── SubclassSelectorDrawer.cs ← dropdown + inline children for [SerializeReference] fields ├── EncounterLinkDrawer.cs ← two-row table + encounter picker ├── DialogLineRefDrawer.cs ← library+id picker with inline fallback and live preview ├── EncounterDrawer.cs ← list element label shows encounter id ├── EncounterValidator.cs ← project-wide scan + "Validate All" menu + browser badges └── EncounterBrowserWindow.cs ← Jovian → Encounters → Encounter Browser ``` ## Quick Start 1. Add the package to your project (local package in `Packages/`). 2. **Create a table**: `Assets → Create → Jovian → Encounter System → Encounter Table`. 3. **Create a collection**: `Assets → Create → Jovian → Encounter System → Encounters Collection`, drag tables into its array. 4. In the table inspector, add encounters to the list. For each: pick the **kind** in the dropdown, fill in shared fields (id/name/description), and populate the kind-specific payload. 5. For a quest chain, set `QuestKind.nextEncounter` on each step — pick the target table, then pick the encounter inside it. 6. **Browse everything**: `Jovian → Encounters → Encounter Browser` for a searchable, filterable list view. ## Key Concepts ### `Encounter` is one class; `IEncounterKind` is where types live A single concrete `Encounter` carries shared fields (`EncounterDefinition`, `EncounterProperties`, `EncounterVisuals`, `EncounterDialogOptionSet`). Its `Kind` property is a `[SerializeReference, SubclassSelector]` polymorphic slot. Adding a new kind is one small class: ```csharp [Serializable] public class MerchantKind : IEncounterKind { public string shopInventoryId; public float priceMultiplier = 1f; } ``` The editor dropdown picks it up automatically via reflection. No base class to extend, no subclass of `Encounter` to write. ### Dialog option events are designer-authored, code-dispatched Each `EncounterDialogOption` holds an ordered `List` (also `[SerializeReference, SubclassSelector]`). Events are data only — what happens when the option is chosen is determined by handlers registered on `EncounterResolver`: ```csharp var resolver = new EncounterResolver(); resolver.Register((evt, ctx) => combatSystem.Start(evt.combatEncounterId)); resolver.Register((evt, ctx) => encounterFlow.GoTo(evt.nextEncounterId)); resolver.Register((evt, _) => Debug.Log(evt.message)); // When the player picks an option: resolver.Resolve(chosenOption.events, new EncounterContext(currentEncounter)); ``` ### Rewards are reusable assets, referenced by events `Reward` is a ScriptableObject — one file per reusable reward (`10_Gold.asset`, `Rusty_Sword.asset`, `Unlock_Riverfort.asset`). Each asset has shared metadata (id, display name, icon) plus a polymorphic `IRewardKind` payload: ```csharp [Serializable] public class PotionOfHealingRewardKind : IRewardKind { public int healAmount = 50; } ``` To grant rewards, drop `GiveRewardEvent`s onto a dialog option and assign a `Reward` asset to each. Multiple rewards = multiple events — the resolver iterates them in order. Game-side wiring applies the reward: ```csharp resolver.Register((evt, ctx) => rewardApplier.Apply(evt.reward, ctx)); ``` `rewardApplier` lives in game code — the package ships only the data types. ### Reusable dialog lines via `DialogLineLibrary` + `DialogLineRef` A single `DialogLineLibrary` asset (or a handful split by topic) holds `{ id → text }` entries. Every `EncounterDialogOption.text` is a `DialogLineRef` struct that resolves in this order: 1. If the library reference + id resolves, use that. 2. Otherwise fall back to `inlineText`. The drawer shows all three inputs plus a live preview of the final text — WYSIWYG survives. Designers prototype inline and promote common lines to the library later without changing the field's type. ```csharp resolver.Register((evt, ctx) => { var text = option.text.Resolve(); // library lookup, inline fallback, null if both empty }); ``` ### Cross-table references via `EncounterLink` `EncounterLink` stores a table asset + stable `internalId` GUID. Rename-safe (the GUID doesn't change) and diffable. The custom drawer renders two dropdowns: first pick a table, then pick an encounter inside it. ### Gated quest progression `QuestProgress` walks an `EncountersCollection` at construction and builds a map of "predecessor quest encounter" for every target of a `QuestKind.nextEncounter`. An encounter is gated iff its predecessor hasn't been resolved: ```csharp var questProgress = new QuestProgress(encountersCollection); // When rolling from a table, exclude gated entries: var next = table.GetRandomEncounter(e => !questProgress.IsGated(e)); // After showing any encounter to the party: questProgress.OnEncounterTriggered(shown); ``` Progression fires `QuestStarted` / `QuestAdvanced(from, to)` / `QuestCompleted` events. Single-step quests (root with no `nextEncounter`) fire both `QuestStarted` and `QuestCompleted`. ### Quest log (serialisable record) `QuestLog` subscribes to the three `QuestProgress` events and appends a `QuestLogEntry` for each. `CreateSnapshot()` returns the save payload; `Restore(saved)` rehydrates. Pair it with `QuestProgress.LoadResolvedIds(questLog.ResolvedEncounterIds())` on load to restore the gating set. ## Runtime API Cheatsheet ```csharp // Encounter rolling with gating IEncounter next = table.GetRandomEncounter(e => !questProgress.IsGated(e)); // Resolving a picked dialog option resolver.Resolve(chosenOption.events, new EncounterContext(currentEncounter)); // After any encounter fires questProgress.OnEncounterTriggered(shown); // Save/load quest state save.questLogEntries = questLog.CreateSnapshot(); questLog.Restore(save.questLogEntries); questProgress.LoadResolvedIds(questLog.ResolvedEncounterIds()); // Fast id → encounter lookup encounterRegistry.GetEncounters().TryGetValue(id, out var encounter); ``` ## Menu Items | Menu Path | Description | |-----------|-------------| | Assets → Create → Jovian → Encounter System → Encounter Table | New table asset | | Assets → Create → Jovian → Encounter System → Encounters Collection | New collection asset | | Assets → Create → Jovian → Encounter System → Dialog Option Set | New dialog option set (auto-renames to id) | | Assets → Create → Jovian → Encounter System → Dialog Line Library | New shared dialog line library | | Assets → Create → Jovian → Encounter System → Encounter Registry | New registry asset | | Jovian → Encounters → Encounter Browser | Searchable designer browser | | Jovian → Encounters → Validate All | Scan all tables/rewards for issues, print click-through report | ## Composition Pattern Per the project's composition-over-inheritance stance, extending behaviour means: - **New encounter type?** Add a new `IEncounterKind` implementation. - **New dialog side effect?** Add a new `IEncounterEvent` + `resolver.Register`. - **Never** subclass `Encounter` or stack `IEncounter` inheritance chains. Inheritance is only used where Unity effectively requires it — ScriptableObject assets (`EncounterTable`, `EncountersCollection`, `EncounterDialogOptionSet`, `EncounterRegistry`) and the `EncounterReference` MonoBehaviour scaffold. ## Known Fragilities - `[SerializeReference]` stores the concrete type's full name + assembly in the YAML. Renaming or moving an `IEncounterKind` or `IEncounterEvent` class drops existing instances silently. Use `[MovedFrom]` when refactoring: ```csharp [MovedFrom("Jovian.EncounterSystem", "Jovian.EncounterSystem", "StartCombatEvent")] public class BeginCombatEvent : IEncounterEvent { ... } ``` - `EncounterDefinition.internalId` is a GUID created at class construction. If you duplicate an encounter by copying YAML, dedupe the id before saving — otherwise `EncounterLink` lookups become ambiguous.