Added the system from trail
This commit is contained in:
167
README.md
167
README.md
@@ -1,3 +1,166 @@
|
||||
# encounter-system
|
||||
# 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.
|
||||
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
|
||||
│ ├── 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<T>/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
|
||||
├── 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<IEncounterEvent>` (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<StartCombatEvent>((evt, ctx) => combatSystem.Start(evt.combatEncounterId));
|
||||
resolver.Register<ChainToEncounterEvent>((evt, ctx) => encounterFlow.GoTo(evt.nextEncounterId));
|
||||
resolver.Register<LogEvent>((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<GiveRewardEvent>((evt, ctx) => rewardApplier.Apply(evt.reward, ctx));
|
||||
```
|
||||
|
||||
`rewardApplier` lives in game code — the package ships only the data types.
|
||||
|
||||
### 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 |
|
||||
| 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<T>`.
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user