forked from Shardstone/trail-into-darkness
added encounter system
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
[Serializable]
|
||||
public class DialogLine {
|
||||
public string id;
|
||||
[TextArea(2, 6)] public string text;
|
||||
}
|
||||
|
||||
/// <summary>Flat registry of reusable dialog lines. Referenced via <see cref="DialogLineRef"/>.</summary>
|
||||
[CreateAssetMenu(fileName = "DialogLineLibrary", menuName = "Jovian/Encounter System/Dialog Line Library", order = 4)]
|
||||
public class DialogLineLibrary : ScriptableObject {
|
||||
public List<DialogLine> lines = new();
|
||||
|
||||
private Dictionary<string, string> cache;
|
||||
|
||||
public string Resolve(string id) {
|
||||
if(string.IsNullOrEmpty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EnsureCache();
|
||||
return cache.TryGetValue(id, out var text) ? text : null;
|
||||
}
|
||||
|
||||
public void InvalidateCache() {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
private void EnsureCache() {
|
||||
if(cache != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache = new Dictionary<string, string>();
|
||||
if(lines == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var line in lines) {
|
||||
if(line != null && !string.IsNullOrEmpty(line.id)) {
|
||||
cache[line.id] = line.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() {
|
||||
InvalidateCache();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 142d6e5b0f6a6cb41beddeae92b56fee
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Looks up <see cref="id"/> in the library passed to <see cref="Resolve"/>; falls back to <see cref="inlineText"/>.</summary>
|
||||
[Serializable]
|
||||
public struct DialogLineRef {
|
||||
public string id;
|
||||
[TextArea(2, 6)] public string inlineText;
|
||||
|
||||
public string Resolve(DialogLineLibrary library) {
|
||||
if(library != null && !string.IsNullOrEmpty(id)) {
|
||||
var text = library.Resolve(id);
|
||||
if(!string.IsNullOrEmpty(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return inlineText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9a6f9e062e474e4b9e7172f4230ca95
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Per-resolution scratch object passed to every event handler. Extend with fields as handlers need them.</summary>
|
||||
public class EncounterContext {
|
||||
public IEncounter CurrentEncounter { get; }
|
||||
|
||||
public EncounterContext(IEncounter currentEncounter) {
|
||||
CurrentEncounter = currentEncounter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d643c80eeffa65f4d804a795ba13bdf9
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Reusable dialog option list. Asset file auto-renames to match <see cref="id"/>.</summary>
|
||||
[CreateAssetMenu(fileName = "EncounterDialogOptionSet", menuName = "Jovian/Encounter System/Dialog Option Set", order = 2)]
|
||||
public class EncounterDialogOptionSet : ScriptableObject {
|
||||
public string id;
|
||||
|
||||
/// <summary>Shared library for every option's <see cref="EncounterDialogOption.text"/> lookup.</summary>
|
||||
public DialogLineLibrary library;
|
||||
|
||||
public List<EncounterDialogOption> options;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() {
|
||||
if(string.IsNullOrEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// AssetDatabase calls are unsafe from OnValidate — defer.
|
||||
UnityEditor.EditorApplication.delayCall += RenameToMatchId;
|
||||
}
|
||||
|
||||
private void RenameToMatchId() {
|
||||
if(this == null || string.IsNullOrEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path = UnityEditor.AssetDatabase.GetAssetPath(this);
|
||||
if(string.IsNullOrEmpty(path) || name == id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var error = UnityEditor.AssetDatabase.RenameAsset(path, id);
|
||||
if(!string.IsNullOrEmpty(error)) {
|
||||
Debug.LogWarning($"[EncounterDialogOptionSet] Could not rename '{path}' to '{id}': {error}", this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c47caaa92bb94eeca3e47dd86fd010cf
|
||||
timeCreated: 1776587040
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Cross-table reference. Rename-safe — the stored key is a GUID.</summary>
|
||||
[Serializable]
|
||||
public struct EncounterLink {
|
||||
public EncounterTable table;
|
||||
public string internalId;
|
||||
|
||||
public IEncounter Resolve() {
|
||||
if(table == null || table.encounters == null || string.IsNullOrEmpty(internalId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach(var encounter in table.encounters) {
|
||||
if(encounter?.EncounterDefinition?.internalId == internalId) {
|
||||
return encounter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60455ea141b903c4390dbcdc29b46f99
|
||||
@@ -0,0 +1,14 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Shared view scaffold — game code binds an <see cref="IEncounter"/> to these widgets.</summary>
|
||||
public class EncounterReference : MonoBehaviour {
|
||||
public TextMeshProUGUI encounterName;
|
||||
public TextMeshProUGUI encounterDescription;
|
||||
public Image encounterArt;
|
||||
public Transform encounterOptionsContainer;
|
||||
public Button submitButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0173ea7fbf1e45b1932694938ecd3058
|
||||
timeCreated: 1776508767
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>id → encounter cache. Editor auto-repopulates on asset changes; runtime must call <see cref="PopulateEncounters"/>.</summary>
|
||||
[CreateAssetMenu(fileName = "EncounterRegistry", menuName = "Jovian/Encounter System/Encounter Registry")]
|
||||
public class EncounterRegistry : ScriptableObject {
|
||||
public EncountersCollection[] encounterCollections = Array.Empty<EncountersCollection>();
|
||||
|
||||
private readonly Dictionary<string, IEncounter> encounters = new();
|
||||
|
||||
public Dictionary<string, IEncounter> GetEncounters() => encounters;
|
||||
|
||||
public void RegisterEncounter(IEncounter encounter) {
|
||||
encounters?.TryAdd(encounter?.EncounterDefinition?.internalId, encounter);
|
||||
}
|
||||
|
||||
public void UnregisterEncounter(IEncounter encounter) {
|
||||
encounters.Remove(encounter.EncounterDefinition.internalId);
|
||||
}
|
||||
|
||||
public void ClearEncounters() {
|
||||
encounters.Clear();
|
||||
}
|
||||
|
||||
public void PopulateEncounters() {
|
||||
foreach(var collection in encounterCollections) {
|
||||
foreach(var encounter in collection.encounterTables) {
|
||||
foreach(var encounterInstance in encounter.encounters) {
|
||||
RegisterEncounter(encounterInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Rebuilds the registry (Addressables key "EncounterRegistry") on any asset import/move/delete.</summary>
|
||||
public class EncounterRegister : UnityEditor.AssetPostprocessor {
|
||||
private static EncounterRegistry registryCache;
|
||||
|
||||
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
|
||||
registryCache ??= Addressables.LoadAssetAsync<EncounterRegistry>("EncounterRegistry").WaitForCompletion();
|
||||
registryCache.ClearEncounters();
|
||||
registryCache.PopulateEncounters();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dfee180882d49c9a3d4474f389d4905
|
||||
timeCreated: 1776584974
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Dispatches <see cref="IEncounterEvent"/> instances to per-type handlers. Unknown types are skipped.</summary>
|
||||
public class EncounterResolver {
|
||||
private readonly Dictionary<Type, Action<IEncounterEvent, EncounterContext>> handlers = new();
|
||||
|
||||
public void Register<T>(Action<T, EncounterContext> handler) where T : IEncounterEvent {
|
||||
// Wrap the typed delegate so the dictionary can hold handlers for any event type uniformly.
|
||||
// Cast is safe because the wrapper is only invoked via lookup under typeof(T).
|
||||
handlers[typeof(T)] = (evt, ctx) => handler((T)evt, ctx);
|
||||
}
|
||||
|
||||
public void Unregister<T>() where T : IEncounterEvent {
|
||||
handlers.Remove(typeof(T));
|
||||
}
|
||||
|
||||
public void Resolve(IEnumerable<IEncounterEvent> events, EncounterContext context) {
|
||||
if(events == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var evt in events) {
|
||||
if(evt == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(handlers.TryGetValue(evt.GetType(), out var handler)) {
|
||||
handler(evt, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d3f07fa2d5f9804d8c75e7026566757
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
[CreateAssetMenu(fileName = "EncounterTable", menuName = "Jovian/Encounter System/Encounter Table", order = 1)]
|
||||
public class EncounterTable : ScriptableObject {
|
||||
public string id;
|
||||
public List<Encounter> encounters;
|
||||
|
||||
public IEncounter GetRandomEncounter() {
|
||||
if(encounters == null || encounters.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return encounters[UnityEngine.Random.Range(0, encounters.Count)];
|
||||
}
|
||||
|
||||
public IEncounter GetRandomEncounter(Type type) {
|
||||
if(encounters == null || encounters.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var encountersOfType = encounters.FindAll(encounter => encounter.GetType() == type);
|
||||
if(encountersOfType.Count > 0) {
|
||||
return encountersOfType[UnityEngine.Random.Range(0, encountersOfType.Count)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Random pick limited by a predicate. Used with <see cref="QuestProgress.IsGated"/> to exclude gated encounters.</summary>
|
||||
public IEncounter GetRandomEncounter(Predicate<IEncounter> filter) {
|
||||
if(encounters == null || encounters.Count == 0 || filter == null) {
|
||||
return GetRandomEncounter();
|
||||
}
|
||||
|
||||
var pool = encounters.FindAll(filter);
|
||||
if(pool.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return pool[UnityEngine.Random.Range(0, pool.Count)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e480a30007b949679b8ca1e0e6088675
|
||||
timeCreated: 1776507230
|
||||
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
[CreateAssetMenu(fileName = "EncountersCollection", menuName = "Jovian/Encounter System/Encounters Collection", order = 0)]
|
||||
public class EncountersCollection : ScriptableObject {
|
||||
public EncounterTable[] encounterTables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96ab08e2592347f68b8ad2e6e8d45187
|
||||
timeCreated: 1776506926
|
||||
56
Packages/com.jovian.encounter-system/Runtime/IEncounter.cs
Normal file
56
Packages/com.jovian.encounter-system/Runtime/IEncounter.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
public interface IEncounter {
|
||||
EncounterDefinition EncounterDefinition { get; set; }
|
||||
EncounterProperties EncounterProperties { get; set; }
|
||||
EncounterVisuals EncounterVisuals { get; set; }
|
||||
EncounterDialogOptionSet EncounterDialogOptionSet { get; set; }
|
||||
IEncounterKind Kind { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Default concrete encounter. Extend via a new <see cref="IEncounterKind"/>, not by subclassing.</summary>
|
||||
[Serializable]
|
||||
public class Encounter : IEncounter {
|
||||
[field: SerializeField] public EncounterDefinition EncounterDefinition { get; set; }
|
||||
[field: SerializeField] public EncounterProperties EncounterProperties { get; set; }
|
||||
[field: SerializeField] public EncounterVisuals EncounterVisuals { get; set; }
|
||||
[field: SerializeField] public EncounterDialogOptionSet EncounterDialogOptionSet { get; set; }
|
||||
|
||||
[field: SerializeReference, SubclassSelector]
|
||||
public IEncounterKind Kind { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EncounterDefinition {
|
||||
/// <summary>Stable GUID assigned at creation. Never edit manually.</summary>
|
||||
[HideInInspector]
|
||||
public string internalId = Guid.NewGuid().ToString();
|
||||
|
||||
public string id;
|
||||
public string name;
|
||||
public string description;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EncounterDialogOption {
|
||||
public DialogLineRef text;
|
||||
|
||||
[SerializeReference, SubclassSelector]
|
||||
public List<IEncounterEvent> events;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EncounterVisuals {
|
||||
public Sprite icon;
|
||||
public Color encounterColor;
|
||||
public Sprite encounterArt;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EncounterProperties {
|
||||
public int difficulty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d9c61fd5089459d8ef34cbbde0666b5
|
||||
timeCreated: 1776506880
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Data-only dialog option side effect. Handlers are registered on <see cref="EncounterResolver"/>.</summary>
|
||||
public interface IEncounterEvent {
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ChainToEncounterEvent : IEncounterEvent {
|
||||
public string nextEncounterId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class StartCombatEvent : IEncounterEvent {
|
||||
public string combatEncounterId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LogEvent : IEncounterEvent {
|
||||
public string message;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GiveRewardEvent : IEncounterEvent {
|
||||
public Reward reward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e42b3f5f74428d944a68a320a821f0c9
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Polymorphic payload on an <see cref="IEncounter"/>. Add a new kind by implementing
|
||||
/// this interface; the SubclassSelector drawer surfaces it automatically.</summary>
|
||||
public interface IEncounterKind {
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CombatKind : IEncounterKind {
|
||||
public string enemyGroupId;
|
||||
public string rewardTableId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuestKind : IEncounterKind {
|
||||
public EncounterLink nextEncounter;
|
||||
public string questTitle;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SocialKind : IEncounterKind {
|
||||
public string npcId;
|
||||
public string factionId;
|
||||
public int reputationDelta;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class PuzzleKind : IEncounterKind {
|
||||
public string puzzleId;
|
||||
public int difficultyClass;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ExplorationKind : IEncounterKind {
|
||||
public int perceptionDC;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TutorialKind : IEncounterKind {
|
||||
public string tutorialId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class HazardKind : IEncounterKind {
|
||||
public int damageAmount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OtherKind : IEncounterKind {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28cc80b36b63e6e44b7f1cfb6c57bf62
|
||||
33
Packages/com.jovian.encounter-system/Runtime/IRewardKind.cs
Normal file
33
Packages/com.jovian.encounter-system/Runtime/IRewardKind.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Polymorphic payload on a <see cref="Reward"/>. Add a new kind by implementing this interface.</summary>
|
||||
public interface IRewardKind {
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CurrencyRewardKind : IRewardKind {
|
||||
public string currencyId;
|
||||
public int amount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemRewardKind : IRewardKind {
|
||||
public string itemId;
|
||||
public int quantity;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ExperienceRewardKind : IRewardKind {
|
||||
public int amount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UnlockableRewardKind : IRewardKind {
|
||||
public string unlockableId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OtherRewardKind : IRewardKind {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2793c02adec79d4088031ab399c16e1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Jovian.EncounterSystem",
|
||||
"rootNamespace": "Jovian.EncounterSystem",
|
||||
"references": [
|
||||
"UnityEngine.UI",
|
||||
"Unity.TextMeshPro",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0452817d1bdb1084da85c56a64179c01
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Packages/com.jovian.encounter-system/Runtime/QuestLog.cs
Normal file
63
Packages/com.jovian.encounter-system/Runtime/QuestLog.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
public enum QuestLogEventType {
|
||||
Started,
|
||||
Advanced,
|
||||
Completed
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuestLogEntry {
|
||||
public QuestLogEventType type;
|
||||
public string encounterInternalId;
|
||||
public string encounterName;
|
||||
public string fromEncounterName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chronological, serialisable record of quest events. Subscribes at construction —
|
||||
/// build before any encounter fires or early entries will be missed.
|
||||
/// </summary>
|
||||
public class QuestLog {
|
||||
private readonly List<QuestLogEntry> entries = new();
|
||||
|
||||
public IReadOnlyList<QuestLogEntry> Entries => entries;
|
||||
|
||||
public QuestLog(QuestProgress progress) {
|
||||
progress.QuestStarted += quest => Record(QuestLogEventType.Started, null, quest);
|
||||
progress.QuestAdvanced += (from, to) => Record(QuestLogEventType.Advanced, from, to);
|
||||
progress.QuestCompleted += quest => Record(QuestLogEventType.Completed, null, quest);
|
||||
}
|
||||
|
||||
public List<QuestLogEntry> CreateSnapshot() {
|
||||
return new List<QuestLogEntry>(entries);
|
||||
}
|
||||
|
||||
public void Restore(IEnumerable<QuestLogEntry> saved) {
|
||||
entries.Clear();
|
||||
if(saved == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.AddRange(saved);
|
||||
}
|
||||
|
||||
public IEnumerable<string> ResolvedEncounterIds() {
|
||||
return entries
|
||||
.Select(entry => entry.encounterInternalId)
|
||||
.Where(id => !string.IsNullOrEmpty(id));
|
||||
}
|
||||
|
||||
private void Record(QuestLogEventType type, IEncounter from, IEncounter to) {
|
||||
entries.Add(new QuestLogEntry {
|
||||
type = type,
|
||||
encounterInternalId = to?.EncounterDefinition?.internalId,
|
||||
encounterName = to?.EncounterDefinition?.name,
|
||||
fromEncounterName = from?.EncounterDefinition?.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74f83e6449bec1847bcb25cb5398a682
|
||||
103
Packages/com.jovian.encounter-system/Runtime/QuestProgress.cs
Normal file
103
Packages/com.jovian.encounter-system/Runtime/QuestProgress.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>
|
||||
/// Gated quest progression. Encounter E is gated iff some QuestKind encounter P has
|
||||
/// <c>P.nextEncounter == E</c> and P hasn't been resolved. Predecessor map is built once at
|
||||
/// construction; rolling and advancement are O(1).
|
||||
/// </summary>
|
||||
public class QuestProgress {
|
||||
private readonly HashSet<string> resolvedIds = new();
|
||||
private readonly Dictionary<string, IEncounter> predecessorOf = new();
|
||||
|
||||
public event Action<IEncounter> QuestStarted;
|
||||
public event Action<IEncounter, IEncounter> QuestAdvanced;
|
||||
public event Action<IEncounter> QuestCompleted;
|
||||
|
||||
public IReadOnlyCollection<string> ResolvedIds => resolvedIds;
|
||||
|
||||
public QuestProgress(EncountersCollection encountersCollection) {
|
||||
IndexQuests(encountersCollection);
|
||||
}
|
||||
|
||||
public bool IsGated(IEncounter encounter) {
|
||||
var id = encounter?.EncounterDefinition?.internalId;
|
||||
if(string.IsNullOrEmpty(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!predecessorOf.TryGetValue(id, out var predecessor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !resolvedIds.Contains(predecessor.EncounterDefinition.internalId);
|
||||
}
|
||||
|
||||
public void OnEncounterTriggered(IEncounter encounter) {
|
||||
if(encounter?.Kind is not QuestKind questKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
var id = encounter.EncounterDefinition?.internalId;
|
||||
if(string.IsNullOrEmpty(id) || !resolvedIds.Add(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var next = questKind.nextEncounter.Resolve();
|
||||
var hasPredecessor = predecessorOf.TryGetValue(id, out var predecessor);
|
||||
|
||||
if(!hasPredecessor) {
|
||||
QuestStarted?.Invoke(encounter);
|
||||
}
|
||||
else {
|
||||
QuestAdvanced?.Invoke(predecessor, encounter);
|
||||
}
|
||||
|
||||
if(next == null) {
|
||||
QuestCompleted?.Invoke(encounter);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadResolvedIds(IEnumerable<string> ids) {
|
||||
resolvedIds.Clear();
|
||||
if(ids == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var id in ids) {
|
||||
if(!string.IsNullOrEmpty(id)) {
|
||||
resolvedIds.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void IndexQuests(EncountersCollection collection) {
|
||||
if(collection?.encounterTables == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var table in collection.encounterTables) {
|
||||
if(table?.encounters == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach(var encounter in table.encounters) {
|
||||
if(encounter?.Kind is not QuestKind questKind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = questKind.nextEncounter.Resolve();
|
||||
if(next == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextId = next.EncounterDefinition?.internalId;
|
||||
if(!string.IsNullOrEmpty(nextId)) {
|
||||
predecessorOf[nextId] = encounter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2eacdbd0e462b9b46b7f3cce64d26765
|
||||
13
Packages/com.jovian.encounter-system/Runtime/Reward.cs
Normal file
13
Packages/com.jovian.encounter-system/Runtime/Reward.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Reusable reward asset referenced by <see cref="GiveRewardEvent"/>.</summary>
|
||||
[CreateAssetMenu(fileName = "Reward", menuName = "Jovian/Encounter System/Reward", order = 3)]
|
||||
public class Reward : ScriptableObject {
|
||||
public string id;
|
||||
public string displayName;
|
||||
|
||||
[SerializeReference, SubclassSelector]
|
||||
public IRewardKind kind;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce47d4bfb319877429589295ac214255
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem {
|
||||
/// <summary>Renders a concrete-type picker dropdown for a <c>[SerializeReference]</c> field.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class SubclassSelectorAttribute : PropertyAttribute {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef3578112ccfa3447b74712009145c75
|
||||
Reference in New Issue
Block a user