forked from Shardstone/trail-into-darkness
added encounter system
This commit is contained in:
@@ -115,6 +115,11 @@ MonoBehaviour:
|
|||||||
m_ReadOnly: 0
|
m_ReadOnly: 0
|
||||||
m_SerializedLabels: []
|
m_SerializedLabels: []
|
||||||
FlaggedDuringContentUpdateRestriction: 0
|
FlaggedDuringContentUpdateRestriction: 0
|
||||||
|
- m_GUID: 7a17d50d1abe3764695c6cd9598487ca
|
||||||
|
m_Address: EncounterRegistry
|
||||||
|
m_ReadOnly: 0
|
||||||
|
m_SerializedLabels: []
|
||||||
|
FlaggedDuringContentUpdateRestriction: 0
|
||||||
- m_GUID: 7b5e9961dadecea4bba3be6de61909f3
|
- m_GUID: 7b5e9961dadecea4bba3be6de61909f3
|
||||||
m_Address: CalendarSettings
|
m_Address: CalendarSettings
|
||||||
m_ReadOnly: 0
|
m_ReadOnly: 0
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ namespace Nox.Game {
|
|||||||
private readonly PlayModeSettings bootstrapSettings;
|
private readonly PlayModeSettings bootstrapSettings;
|
||||||
private readonly GameDataState gameDataState;
|
private readonly GameDataState gameDataState;
|
||||||
private readonly ISaveSystem saveSystem;
|
private readonly ISaveSystem saveSystem;
|
||||||
private PartyDefinition partyDefinition;
|
private readonly PartyDefinition partyDefinition;
|
||||||
|
private readonly AdventureSettings adventureSettings;
|
||||||
private AdventureData adventureData;
|
private AdventureData adventureData;
|
||||||
private AdventureModePrefabs scenePrefabs;
|
private AdventureModePrefabs scenePrefabs;
|
||||||
private ICameraController cameraController;
|
private ICameraController cameraController;
|
||||||
@@ -38,7 +39,6 @@ namespace Nox.Game {
|
|||||||
private AdventureView adventureView;
|
private AdventureView adventureView;
|
||||||
private ZoneSystem zoneSystem;
|
private ZoneSystem zoneSystem;
|
||||||
private GuiReferences guiReferences;
|
private GuiReferences guiReferences;
|
||||||
private AdventureSettings adventureSettings;
|
|
||||||
private TimeHandler timeHandler;
|
private TimeHandler timeHandler;
|
||||||
private PartyInventoryHandler partyInventoryHandler;
|
private PartyInventoryHandler partyInventoryHandler;
|
||||||
private PartyGuiView partyGuiView;
|
private PartyGuiView partyGuiView;
|
||||||
|
|||||||
3
Assets/Code/GameState/PlayModes/Encounters.meta
Normal file
3
Assets/Code/GameState/PlayModes/Encounters.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d6216fd41db9494aaa6c127d9d790b93
|
||||||
|
timeCreated: 1776506857
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nox.Game {
|
||||||
|
|
||||||
|
public class EncounterHandler {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 523274f9158f453dbfac02601a77c3f7
|
||||||
|
timeCreated: 1776506833
|
||||||
19
Assets/Code/GameState/PlayModes/Encounters/EncounterView.cs
Normal file
19
Assets/Code/GameState/PlayModes/Encounters/EncounterView.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Nox.Game.UI;
|
||||||
|
|
||||||
|
namespace Nox.Game {
|
||||||
|
public class EncounterView : IMenuView{
|
||||||
|
|
||||||
|
public void Initialize() {
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Show() {
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Hide() {
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Tick() {
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b69490c9f1d8471a84b4594d1b1be117
|
||||||
|
timeCreated: 1776590016
|
||||||
8
Assets/Database/Encounters.meta
Normal file
8
Assets/Database/Encounters.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 83509b822db37ec4e863ff7a2b4c01ae
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Assets/Database/Encounters/EncounterRegistry.asset
Normal file
16
Assets/Database/Encounters/EncounterRegistry.asset
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 0dfee180882d49c9a3d4474f389d4905, type: 3}
|
||||||
|
m_Name: EncounterRegistry
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.EncounterRegistry
|
||||||
|
encounterCollections:
|
||||||
|
- {fileID: 11400000, guid: b3c3371ae34b4e34ea57a013b5125022, type: 2}
|
||||||
8
Assets/Database/Encounters/EncounterRegistry.asset.meta
Normal file
8
Assets/Database/Encounters/EncounterRegistry.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7a17d50d1abe3764695c6cd9598487ca
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Database/Encounters/EncounterTables.meta
Normal file
8
Assets/Database/Encounters/EncounterTables.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 453cbe825e21cba41b61175034c2b5d1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Database/Encounters/EncounterTables/Dialogs.meta
Normal file
8
Assets/Database/Encounters/EncounterTables/Dialogs.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65c1422c9083d1b4d9721cf275cfe7f3
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 142d6e5b0f6a6cb41beddeae92b56fee, type: 3}
|
||||||
|
m_Name: DialogLineLibrary
|
||||||
|
m_EditorClassIdentifier: Jovian.EncounterSystem::Jovian.EncounterSystem.DialogLineLibrary
|
||||||
|
lines:
|
||||||
|
- id: test_you_die
|
||||||
|
text: Click me and see what happens. MIght all your dreams com true?
|
||||||
|
- id: click_to_continue
|
||||||
|
text: This is a test like that should you click it, it will continue and record
|
||||||
|
quest progress
|
||||||
|
- id: right_choice
|
||||||
|
text: You have chose right, padwan, here is 4 RON!
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11b94daa76442834198b68996afe0013
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: c47caaa92bb94eeca3e47dd86fd010cf, type: 3}
|
||||||
|
m_Name: Dialog_Set_0
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.EncounterDialogOptionSet
|
||||||
|
id: Dialog_Set_1
|
||||||
|
library: {fileID: 11400000, guid: 11b94daa76442834198b68996afe0013, type: 2}
|
||||||
|
options:
|
||||||
|
- text:
|
||||||
|
id: click_to_continue
|
||||||
|
inlineText:
|
||||||
|
events:
|
||||||
|
- rid: 1352971465325281416
|
||||||
|
references:
|
||||||
|
version: 2
|
||||||
|
RefIds:
|
||||||
|
- rid: 1352971465325281416
|
||||||
|
type: {class: LogEvent, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||||
|
data:
|
||||||
|
message: An thus, you continue...
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7bc554ae0760bbb4796a1b3acef22cb3
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: c47caaa92bb94eeca3e47dd86fd010cf, type: 3}
|
||||||
|
m_Name: Dialog_Set_1
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.EncounterDialogOptionSet
|
||||||
|
id: Dialog_Set_1
|
||||||
|
library: {fileID: 11400000, guid: 11b94daa76442834198b68996afe0013, type: 2}
|
||||||
|
options:
|
||||||
|
- text:
|
||||||
|
id: test_you_die
|
||||||
|
inlineText:
|
||||||
|
events:
|
||||||
|
- rid: 1352971465325281414
|
||||||
|
- text:
|
||||||
|
id: right_choice
|
||||||
|
inlineText:
|
||||||
|
events:
|
||||||
|
- rid: 1352971465325281412
|
||||||
|
references:
|
||||||
|
version: 2
|
||||||
|
RefIds:
|
||||||
|
- rid: 1352971465325281412
|
||||||
|
type: {class: GiveRewardEvent, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||||
|
data:
|
||||||
|
reward: {fileID: 11400000, guid: 2c02212ce09c5a246a8fe11a5253bfd4, type: 2}
|
||||||
|
- rid: 1352971465325281414
|
||||||
|
type: {class: LogEvent, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||||
|
data:
|
||||||
|
message: You died! HAhahAhaha!
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9496570aa3d05624a9b8bbbf6009c453
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e480a30007b949679b8ca1e0e6088675, type: 3}
|
||||||
|
m_Name: TestEncounterTable
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.EncounterTable
|
||||||
|
id: TestEncounterTable
|
||||||
|
encounters:
|
||||||
|
- <EncounterDefinition>k__BackingField:
|
||||||
|
internalId: adce0a09-6402-4c2e-b24a-db7c9c67e3e5
|
||||||
|
id: test_quest_1
|
||||||
|
name: Test Quest Stage I
|
||||||
|
description: An encounter like no other which leads to another encounter like
|
||||||
|
no other
|
||||||
|
<EncounterProperties>k__BackingField:
|
||||||
|
difficulty: 0
|
||||||
|
<EncounterVisuals>k__BackingField:
|
||||||
|
icon: {fileID: 21300000, guid: ea02ea44fa86ee445be0f7ca82098b75, type: 3}
|
||||||
|
encounterColor: {r: 0, g: 0, b: 0, a: 0}
|
||||||
|
encounterArt: {fileID: 21300000, guid: a9c4c7681315e25419b9381d28aa9d80, type: 3}
|
||||||
|
<EncounterDialogOptionSet>k__BackingField: {fileID: 11400000, guid: 9496570aa3d05624a9b8bbbf6009c453, type: 2}
|
||||||
|
<Kind>k__BackingField:
|
||||||
|
rid: 1352971465325281411
|
||||||
|
- <EncounterDefinition>k__BackingField:
|
||||||
|
internalId: adce0a09-6402-4c2e-b24a-db7c9c67e3e5
|
||||||
|
id: test_quest_2
|
||||||
|
name: Test Quest Stage II
|
||||||
|
description: An encounter like no other which should be now completed
|
||||||
|
<EncounterProperties>k__BackingField:
|
||||||
|
difficulty: 0
|
||||||
|
<EncounterVisuals>k__BackingField:
|
||||||
|
icon: {fileID: 21300000, guid: ea02ea44fa86ee445be0f7ca82098b75, type: 3}
|
||||||
|
encounterColor: {r: 0, g: 0, b: 0, a: 0}
|
||||||
|
encounterArt: {fileID: 21300000, guid: 819d7a244820ad84585a1de7566bf9d0, type: 3}
|
||||||
|
<EncounterDialogOptionSet>k__BackingField: {fileID: 11400000, guid: 9496570aa3d05624a9b8bbbf6009c453, type: 2}
|
||||||
|
<Kind>k__BackingField:
|
||||||
|
rid: -2
|
||||||
|
references:
|
||||||
|
version: 2
|
||||||
|
RefIds:
|
||||||
|
- rid: -2
|
||||||
|
type: {class: , ns: , asm: }
|
||||||
|
- rid: 1352971465325281411
|
||||||
|
type: {class: QuestKind, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||||
|
data:
|
||||||
|
nextEncounter:
|
||||||
|
table: {fileID: 11400000}
|
||||||
|
internalId: adce0a09-6402-4c2e-b24a-db7c9c67e3e5
|
||||||
|
questTitle:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 396a5409178bf0d4b938094eefe22cca
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Database/Encounters/Rewards.meta
Normal file
8
Assets/Database/Encounters/Rewards.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 57820b3b9a698e24393172741670d8fe
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
26
Assets/Database/Encounters/Rewards/Reward.asset
Normal file
26
Assets/Database/Encounters/Rewards/Reward.asset
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: ce47d4bfb319877429589295ac214255, type: 3}
|
||||||
|
m_Name: Reward
|
||||||
|
m_EditorClassIdentifier: Jovian.EncounterSystem::Jovian.EncounterSystem.Reward
|
||||||
|
id: gold
|
||||||
|
displayName: All Ze Money
|
||||||
|
kind:
|
||||||
|
rid: 1352971465325281413
|
||||||
|
references:
|
||||||
|
version: 2
|
||||||
|
RefIds:
|
||||||
|
- rid: 1352971465325281413
|
||||||
|
type: {class: CurrencyRewardKind, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||||
|
data:
|
||||||
|
currencyId: gold
|
||||||
|
amount: 100
|
||||||
8
Assets/Database/Encounters/Rewards/Reward.asset.meta
Normal file
8
Assets/Database/Encounters/Rewards/Reward.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2c02212ce09c5a246a8fe11a5253bfd4
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Assets/Database/Encounters/TestEncountersCollection.asset
Normal file
16
Assets/Database/Encounters/TestEncountersCollection.asset
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 96ab08e2592347f68b8ad2e6e8d45187, type: 3}
|
||||||
|
m_Name: TestEncountersCollection
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.EncountersCollection
|
||||||
|
encounterTables:
|
||||||
|
- {fileID: 11400000, guid: 396a5409178bf0d4b938094eefe22cca, type: 2}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3c3371ae34b4e34ea57a013b5125022
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
17
CLAUDE.md
17
CLAUDE.md
@@ -109,3 +109,20 @@ Assets/Code/
|
|||||||
- Factory pattern for character/party creation
|
- Factory pattern for character/party creation
|
||||||
- Action delegates for UI event communication (e.g., `menuGameStateData.startGameRequests += handler`)
|
- Action delegates for UI event communication (e.g., `menuGameStateData.startGameRequests += handler`)
|
||||||
- Mixed async patterns: both `async Task` and `IEnumerator` coroutines for async operations
|
- Mixed async patterns: both `async Task` and `IEnumerator` coroutines for async operations
|
||||||
|
|
||||||
|
## Planning Docs Convention
|
||||||
|
|
||||||
|
Non-trivial features are planned in `docs/plans/` as paired markdown files: a design doc and an implementation doc, both prefixed with the date. Examples: `2026-04-05-ingame-logging-design.md` + `2026-04-05-ingame-logging-implementation.md`, `2026-04-06-popup-system-design.md` + `2026-04-06-popup-system-implementation.md`. When starting a new feature, look here first for existing context, and follow the same pair-of-docs pattern for new work.
|
||||||
|
|
||||||
|
## In-House Jovian Packages
|
||||||
|
|
||||||
|
Several packages under `Packages/` are first-party code maintained alongside the game, not third-party dependencies. Before adding new capabilities, check whether one of these already provides what you need:
|
||||||
|
|
||||||
|
- `Jovian.SaveSystem` — JSON persistence, used by `GamePersistenceController`
|
||||||
|
- `Jovian.Calendar` — custom in-game calendar system
|
||||||
|
- `Jovian.InGameLogging` — runtime logging surface
|
||||||
|
- `Jovian.EncounterSystem` — data-driven encounter authoring, `IEncounterKind` polymorphic payloads, `EncounterLink` cross-table refs, `QuestProgress` gated progression + `QuestLog`. Designer browser at `Jovian → Encounters → Encounter Browser`.
|
||||||
|
- `Jovian.PopupSystem` — popup/dialog UI
|
||||||
|
- `Jovian.ZoneSystem` — zone/region management
|
||||||
|
- `Jovian.Logger` — developer logging
|
||||||
|
- `Jovian.InspectorTools` / `JovianUtilities` — editor and general utilities
|
||||||
|
|||||||
8
Packages/com.jovian.encounter-system/Editor.meta
Normal file
8
Packages/com.jovian.encounter-system/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7d89f89053ce0384c9f7b48a5b491bca
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Id dropdown (library inherited from the owning asset's <c>library</c> field) + inline fallback + resolved preview.</summary>
|
||||||
|
[CustomPropertyDrawer(typeof(DialogLineRef))]
|
||||||
|
public class DialogLineRefDrawer : PropertyDrawer {
|
||||||
|
private const string NonePlaceholder = "<none>";
|
||||||
|
private const string EmptyLibraryPlaceholder = "<set library on the parent asset>";
|
||||||
|
private const float PreviewHeight = 32f;
|
||||||
|
|
||||||
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||||
|
var idProp = property.FindPropertyRelative("id");
|
||||||
|
var inlineProp = property.FindPropertyRelative("inlineText");
|
||||||
|
var library = ResolveLibrary(property);
|
||||||
|
|
||||||
|
EditorGUI.BeginProperty(position, label, property);
|
||||||
|
|
||||||
|
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||||
|
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
|
||||||
|
var idRect = new Rect(position.x, position.y, position.width, lineHeight);
|
||||||
|
DrawIdPicker(idRect, library, idProp, label);
|
||||||
|
|
||||||
|
var inlineRect = new Rect(
|
||||||
|
position.x,
|
||||||
|
position.y + lineHeight + spacing,
|
||||||
|
position.width,
|
||||||
|
lineHeight * 2);
|
||||||
|
EditorGUI.PropertyField(inlineRect, inlineProp, new GUIContent("Inline"));
|
||||||
|
|
||||||
|
var previewRect = new Rect(
|
||||||
|
position.x,
|
||||||
|
position.y + lineHeight + spacing + lineHeight * 2 + spacing,
|
||||||
|
position.width,
|
||||||
|
PreviewHeight);
|
||||||
|
DrawPreview(previewRect, library, idProp, inlineProp);
|
||||||
|
|
||||||
|
EditorGUI.EndProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||||
|
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||||
|
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
// id + (inline 2 lines) + preview
|
||||||
|
return lineHeight + spacing + lineHeight * 2 + spacing + PreviewHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DialogLineLibrary ResolveLibrary(SerializedProperty property) {
|
||||||
|
var libraryProp = property.serializedObject.FindProperty("library");
|
||||||
|
return libraryProp?.objectReferenceValue as DialogLineLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawIdPicker(Rect rect, DialogLineLibrary library, SerializedProperty idProp, GUIContent label) {
|
||||||
|
if(library == null || library.lines == null || library.lines.Count == 0) {
|
||||||
|
using(new EditorGUI.DisabledScope(true)) {
|
||||||
|
EditorGUI.Popup(rect, label.text, 0, new[] { EmptyLibraryPlaceholder });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = library.lines.Count;
|
||||||
|
var ids = new string[count + 1];
|
||||||
|
var names = new string[count + 1];
|
||||||
|
ids[0] = string.Empty;
|
||||||
|
names[0] = NonePlaceholder;
|
||||||
|
|
||||||
|
var currentIndex = 0;
|
||||||
|
for(int i = 0; i < count; i++) {
|
||||||
|
var line = library.lines[i];
|
||||||
|
var id = line?.id ?? string.Empty;
|
||||||
|
ids[i + 1] = id;
|
||||||
|
names[i + 1] = string.IsNullOrEmpty(id) ? $"<unnamed {i}>" : id;
|
||||||
|
if(id == idProp.stringValue && !string.IsNullOrEmpty(id)) {
|
||||||
|
currentIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = EditorGUI.Popup(rect, label.text, currentIndex, names);
|
||||||
|
if(newIndex != currentIndex) {
|
||||||
|
idProp.stringValue = ids[newIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawPreview(Rect rect, DialogLineLibrary library, SerializedProperty idProp, SerializedProperty inlineProp) {
|
||||||
|
var id = idProp.stringValue;
|
||||||
|
var inline = inlineProp.stringValue;
|
||||||
|
|
||||||
|
string resolved = null;
|
||||||
|
if(library != null && !string.IsNullOrEmpty(id)) {
|
||||||
|
resolved = library.Resolve(id);
|
||||||
|
}
|
||||||
|
if(string.IsNullOrEmpty(resolved)) {
|
||||||
|
resolved = inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = new GUIStyle(EditorStyles.helpBox) {
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
wordWrap = true
|
||||||
|
};
|
||||||
|
var label = string.IsNullOrEmpty(resolved) ? "<no text will display>" : resolved;
|
||||||
|
EditorGUI.LabelField(rect, new GUIContent("Preview: " + label), style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a55b866cc153f5749a71928930faeb62
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.UIElements;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Browser for every encounter across all tables. Search + kind filter + detail pane. Quest chains render as a tree rooted at the first step.</summary>
|
||||||
|
public class EncounterBrowserWindow : EditorWindow {
|
||||||
|
private const string AllKinds = "All";
|
||||||
|
|
||||||
|
private class Record {
|
||||||
|
public EncounterTable table;
|
||||||
|
public int index;
|
||||||
|
public IEncounter encounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<Record> allRecords = new();
|
||||||
|
private string searchText = string.Empty;
|
||||||
|
private string kindFilter = AllKinds;
|
||||||
|
|
||||||
|
private readonly Dictionary<IEncounter, List<ValidationIssue>> issuesByEncounter = new();
|
||||||
|
|
||||||
|
private TreeView treeView;
|
||||||
|
private VisualElement detailPane;
|
||||||
|
private ToolbarMenu kindDropdown;
|
||||||
|
|
||||||
|
[MenuItem("Jovian/Encounters/Encounter Browser")]
|
||||||
|
public static void Open() {
|
||||||
|
var window = GetWindow<EncounterBrowserWindow>("Encounters");
|
||||||
|
window.minSize = new Vector2(640, 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateGUI() {
|
||||||
|
BuildToolbar();
|
||||||
|
BuildSplit();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildToolbar() {
|
||||||
|
var toolbar = new Toolbar();
|
||||||
|
|
||||||
|
var search = new ToolbarSearchField();
|
||||||
|
search.style.flexGrow = 1f;
|
||||||
|
search.RegisterValueChangedCallback(evt => {
|
||||||
|
searchText = evt.newValue ?? string.Empty;
|
||||||
|
ApplyFilter();
|
||||||
|
});
|
||||||
|
toolbar.Add(search);
|
||||||
|
|
||||||
|
kindDropdown = new ToolbarMenu { text = $"Kind: {AllKinds}" };
|
||||||
|
foreach(var choice in GetKindChoices()) {
|
||||||
|
var captured = choice;
|
||||||
|
kindDropdown.menu.AppendAction(captured, _ => {
|
||||||
|
kindFilter = captured;
|
||||||
|
kindDropdown.text = $"Kind: {captured}";
|
||||||
|
ApplyFilter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toolbar.Add(kindDropdown);
|
||||||
|
|
||||||
|
var refreshButton = new ToolbarButton(Refresh) { text = "Refresh" };
|
||||||
|
toolbar.Add(refreshButton);
|
||||||
|
|
||||||
|
rootVisualElement.Add(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSplit() {
|
||||||
|
var split = new TwoPaneSplitView(0, 280, TwoPaneSplitViewOrientation.Horizontal);
|
||||||
|
split.style.flexGrow = 1f;
|
||||||
|
rootVisualElement.Add(split);
|
||||||
|
|
||||||
|
treeView = new TreeView {
|
||||||
|
makeItem = MakeRow,
|
||||||
|
bindItem = BindRow,
|
||||||
|
fixedItemHeight = 22,
|
||||||
|
selectionType = SelectionType.Single
|
||||||
|
};
|
||||||
|
treeView.selectionChanged += OnSelectionChanged;
|
||||||
|
treeView.style.flexGrow = 1f;
|
||||||
|
split.Add(treeView);
|
||||||
|
|
||||||
|
detailPane = new ScrollView(ScrollViewMode.Vertical) {
|
||||||
|
style = { paddingLeft = 8, paddingTop = 8, paddingRight = 8, flexGrow = 1f }
|
||||||
|
};
|
||||||
|
ShowEmptyDetail();
|
||||||
|
split.Add(detailPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisualElement MakeRow() {
|
||||||
|
var row = new VisualElement {
|
||||||
|
style = {
|
||||||
|
flexDirection = FlexDirection.Row,
|
||||||
|
alignItems = Align.Center,
|
||||||
|
paddingLeft = 6,
|
||||||
|
paddingRight = 6,
|
||||||
|
height = 22
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var badge = new VisualElement {
|
||||||
|
name = "issue-badge",
|
||||||
|
style = {
|
||||||
|
width = 8,
|
||||||
|
height = 8,
|
||||||
|
marginRight = 6,
|
||||||
|
borderTopLeftRadius = 4,
|
||||||
|
borderTopRightRadius = 4,
|
||||||
|
borderBottomLeftRadius = 4,
|
||||||
|
borderBottomRightRadius = 4,
|
||||||
|
visibility = Visibility.Hidden
|
||||||
|
}
|
||||||
|
};
|
||||||
|
row.Add(badge);
|
||||||
|
|
||||||
|
var label = new Label {
|
||||||
|
name = "row-label",
|
||||||
|
style = {
|
||||||
|
flexGrow = 1f,
|
||||||
|
unityTextAlign = TextAnchor.MiddleLeft
|
||||||
|
}
|
||||||
|
};
|
||||||
|
row.Add(label);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindRow(VisualElement element, int index) {
|
||||||
|
var record = treeView.GetItemDataForIndex<Record>(index);
|
||||||
|
var label = element.Q<Label>("row-label");
|
||||||
|
var badge = element.Q<VisualElement>("issue-badge");
|
||||||
|
|
||||||
|
var name = record.encounter?.EncounterDefinition?.name;
|
||||||
|
var kind = record.encounter?.Kind?.GetType().Name ?? "—";
|
||||||
|
label.text = string.IsNullOrEmpty(name)
|
||||||
|
? $"<unnamed> [{kind}]"
|
||||||
|
: $"{name} [{kind}]";
|
||||||
|
|
||||||
|
if(record.encounter != null && issuesByEncounter.TryGetValue(record.encounter, out var issues) && issues.Count > 0) {
|
||||||
|
var hasError = issues.Exists(i => i.severity == ValidationSeverity.Error);
|
||||||
|
badge.style.backgroundColor = new StyleColor(hasError ? new Color(0.85f, 0.25f, 0.25f) : new Color(0.95f, 0.75f, 0.1f));
|
||||||
|
badge.style.visibility = Visibility.Visible;
|
||||||
|
element.tooltip = BuildTooltip(issues);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
badge.style.visibility = Visibility.Hidden;
|
||||||
|
element.tooltip = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildTooltip(List<ValidationIssue> issues) {
|
||||||
|
var lines = new List<string>(issues.Count);
|
||||||
|
foreach(var issue in issues) {
|
||||||
|
var prefix = issue.severity == ValidationSeverity.Error ? "ERROR" : "WARN";
|
||||||
|
lines.Add($"[{prefix}] {issue.path} — {issue.message}");
|
||||||
|
}
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetKindChoices() {
|
||||||
|
yield return AllKinds;
|
||||||
|
|
||||||
|
var kindTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(assembly => {
|
||||||
|
try { return assembly.GetTypes(); }
|
||||||
|
catch { return Array.Empty<Type>(); }
|
||||||
|
})
|
||||||
|
.Where(type => typeof(IEncounterKind).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface)
|
||||||
|
.Select(type => type.Name)
|
||||||
|
.OrderBy(name => name);
|
||||||
|
|
||||||
|
foreach(var name in kindTypes) {
|
||||||
|
yield return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh() {
|
||||||
|
allRecords.Clear();
|
||||||
|
|
||||||
|
var guids = AssetDatabase.FindAssets("t:" + nameof(EncounterTable));
|
||||||
|
foreach(var guid in guids) {
|
||||||
|
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
var table = AssetDatabase.LoadAssetAtPath<EncounterTable>(path);
|
||||||
|
if(table?.encounters == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < table.encounters.Count; i++) {
|
||||||
|
allRecords.Add(new Record {
|
||||||
|
table = table,
|
||||||
|
index = i,
|
||||||
|
encounter = table.encounters[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RebuildIssueIndex();
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildIssueIndex() {
|
||||||
|
issuesByEncounter.Clear();
|
||||||
|
var issues = EncounterValidator.ValidateProject();
|
||||||
|
foreach(var issue in issues) {
|
||||||
|
if(issue.encounter == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!issuesByEncounter.TryGetValue(issue.encounter, out var list)) {
|
||||||
|
list = new List<ValidationIssue>();
|
||||||
|
issuesByEncounter[issue.encounter] = list;
|
||||||
|
}
|
||||||
|
list.Add(issue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter() {
|
||||||
|
var filtered = allRecords.Where(Matches).ToList();
|
||||||
|
var items = BuildTreeItems(filtered);
|
||||||
|
if(treeView != null) {
|
||||||
|
treeView.SetRootItems(items);
|
||||||
|
treeView.Rebuild();
|
||||||
|
treeView.ClearSelection();
|
||||||
|
treeView.ExpandAll();
|
||||||
|
}
|
||||||
|
ShowEmptyDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TreeViewItemData<Record>> BuildTreeItems(List<Record> records) {
|
||||||
|
var byEncounter = new Dictionary<IEncounter, Record>();
|
||||||
|
foreach(var r in records) {
|
||||||
|
if(r.encounter != null) {
|
||||||
|
byEncounter[r.encounter] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any encounter that is a `nextEncounter` target of another record in the filtered set
|
||||||
|
// becomes a non-root (rendered as a child), not a top-level item.
|
||||||
|
var nonRoot = new HashSet<IEncounter>();
|
||||||
|
foreach(var r in records) {
|
||||||
|
if(r.encounter?.Kind is not QuestKind quest) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var next = quest.nextEncounter.Resolve();
|
||||||
|
if(next != null && byEncounter.ContainsKey(next)) {
|
||||||
|
nonRoot.Add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<TreeViewItemData<Record>>();
|
||||||
|
var uid = 0;
|
||||||
|
foreach(var r in records) {
|
||||||
|
if(r.encounter != null && nonRoot.Contains(r.encounter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var children = BuildChainChildren(r, byEncounter, ref uid);
|
||||||
|
result.Add(new TreeViewItemData<Record>(uid++, r, children));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TreeViewItemData<Record>> BuildChainChildren(Record root, Dictionary<IEncounter, Record> byEncounter, ref int uid) {
|
||||||
|
var children = new List<TreeViewItemData<Record>>();
|
||||||
|
if(root.encounter?.Kind is not QuestKind) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
var current = root.encounter;
|
||||||
|
var visited = new HashSet<IEncounter> { current };
|
||||||
|
while(current?.Kind is QuestKind quest) {
|
||||||
|
var next = quest.nextEncounter.Resolve();
|
||||||
|
if(next == null || !visited.Add(next)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!byEncounter.TryGetValue(next, out var nextRecord)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
children.Add(new TreeViewItemData<Record>(uid++, nextRecord));
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Matches(Record record) {
|
||||||
|
var kindName = record.encounter?.Kind?.GetType().Name ?? string.Empty;
|
||||||
|
if(kindFilter != AllKinds && kindName != kindFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(searchText)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needle = searchText.ToLowerInvariant();
|
||||||
|
var definition = record.encounter?.EncounterDefinition;
|
||||||
|
return Contains(definition?.id, needle)
|
||||||
|
|| Contains(definition?.name, needle)
|
||||||
|
|| Contains(definition?.description, needle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Contains(string source, string needle) {
|
||||||
|
return !string.IsNullOrEmpty(source) && source.ToLowerInvariant().Contains(needle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChanged(IEnumerable<object> selection) {
|
||||||
|
var record = selection.OfType<Record>().FirstOrDefault();
|
||||||
|
if(record == null) {
|
||||||
|
ShowEmptyDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailPane.Clear();
|
||||||
|
|
||||||
|
var serializedObject = new SerializedObject(record.table);
|
||||||
|
var encountersProp = serializedObject.FindProperty(nameof(EncounterTable.encounters));
|
||||||
|
var elementProp = encountersProp.GetArrayElementAtIndex(record.index);
|
||||||
|
|
||||||
|
var header = new Label($"{record.table.name} → [{record.index}]") {
|
||||||
|
style = {
|
||||||
|
unityFontStyleAndWeight = FontStyle.Bold,
|
||||||
|
marginBottom = 6,
|
||||||
|
color = new StyleColor(new Color(0.75f, 0.75f, 0.75f))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
detailPane.Add(header);
|
||||||
|
|
||||||
|
elementProp.isExpanded = true;
|
||||||
|
var field = new PropertyField(elementProp);
|
||||||
|
field.Bind(serializedObject);
|
||||||
|
detailPane.Add(field);
|
||||||
|
|
||||||
|
AddChainPreviewIfQuest(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddChainPreviewIfQuest(Record record) {
|
||||||
|
if(record.encounter?.Kind is not QuestKind questKind) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var predecessor = FindPredecessor(record.encounter.EncounterDefinition?.internalId);
|
||||||
|
var next = questKind.nextEncounter.Resolve();
|
||||||
|
|
||||||
|
var predName = predecessor?.encounter.EncounterDefinition?.name ?? "—";
|
||||||
|
var currName = record.encounter.EncounterDefinition?.name ?? "<unnamed>";
|
||||||
|
var nextName = next?.EncounterDefinition?.name ?? "—";
|
||||||
|
|
||||||
|
var chainLabel = new Label($"Chain: {predName} ← {currName} → {nextName}") {
|
||||||
|
style = {
|
||||||
|
marginTop = 12,
|
||||||
|
paddingTop = 6,
|
||||||
|
paddingBottom = 6,
|
||||||
|
paddingLeft = 6,
|
||||||
|
paddingRight = 6,
|
||||||
|
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)),
|
||||||
|
unityFontStyleAndWeight = FontStyle.Bold
|
||||||
|
}
|
||||||
|
};
|
||||||
|
detailPane.Add(chainLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Record FindPredecessor(string targetId) {
|
||||||
|
if(string.IsNullOrEmpty(targetId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var record in allRecords) {
|
||||||
|
if(record.encounter?.Kind is not QuestKind kind) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = kind.nextEncounter.Resolve();
|
||||||
|
if(next?.EncounterDefinition?.internalId == targetId) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowEmptyDetail() {
|
||||||
|
if(detailPane == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailPane.Clear();
|
||||||
|
var empty = new Label("Select an encounter to edit.") {
|
||||||
|
style = { color = new StyleColor(Color.gray), marginTop = 8 }
|
||||||
|
};
|
||||||
|
detailPane.Add(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0312d015c183e2f4582358d17867585c
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Label list elements with the option's text id (fallback: inline text preview, then default).</summary>
|
||||||
|
[CustomPropertyDrawer(typeof(EncounterDialogOption))]
|
||||||
|
public class EncounterDialogOptionDrawer : PropertyDrawer {
|
||||||
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||||
|
var displayLabel = ResolveLabel(property, label);
|
||||||
|
|
||||||
|
var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||||
|
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, displayLabel, true);
|
||||||
|
|
||||||
|
if(!property.isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||||
|
var r = new Rect(position.x, y, position.width, h);
|
||||||
|
EditorGUI.PropertyField(r, iterator, true);
|
||||||
|
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||||
|
var height = EditorGUIUtility.singleLineHeight;
|
||||||
|
if(!property.isExpanded) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIContent ResolveLabel(SerializedProperty property, GUIContent fallback) {
|
||||||
|
var textProp = property.FindPropertyRelative("text");
|
||||||
|
if(textProp == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = textProp.FindPropertyRelative("id")?.stringValue;
|
||||||
|
if(!string.IsNullOrEmpty(id)) {
|
||||||
|
return new GUIContent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inline = textProp.FindPropertyRelative("inlineText")?.stringValue;
|
||||||
|
if(!string.IsNullOrEmpty(inline)) {
|
||||||
|
var preview = inline.Length > 40 ? inline.Substring(0, 40) + "…" : inline;
|
||||||
|
return new GUIContent(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5636dc9f15ce02c4ca189c3d5f46eb34
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Label list elements with the encounter id (fallback: name, then default).</summary>
|
||||||
|
[CustomPropertyDrawer(typeof(Encounter))]
|
||||||
|
public class EncounterDrawer : PropertyDrawer {
|
||||||
|
private const string DefinitionBackingField = "<EncounterDefinition>k__BackingField";
|
||||||
|
|
||||||
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||||
|
var displayLabel = ResolveLabel(property, label);
|
||||||
|
|
||||||
|
var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||||
|
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, displayLabel, true);
|
||||||
|
|
||||||
|
if(!property.isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||||
|
var r = new Rect(position.x, y, position.width, h);
|
||||||
|
EditorGUI.PropertyField(r, iterator, true);
|
||||||
|
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||||
|
var height = EditorGUIUtility.singleLineHeight;
|
||||||
|
if(!property.isExpanded) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIContent ResolveLabel(SerializedProperty property, GUIContent fallback) {
|
||||||
|
var definition = property.FindPropertyRelative(DefinitionBackingField);
|
||||||
|
if(definition == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = definition.FindPropertyRelative("id")?.stringValue;
|
||||||
|
if(!string.IsNullOrEmpty(id)) {
|
||||||
|
return new GUIContent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = definition.FindPropertyRelative("name")?.stringValue;
|
||||||
|
if(!string.IsNullOrEmpty(name)) {
|
||||||
|
return new GUIContent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0ee5ffe2ea6903547ac75470249af31f
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Table object-field + encounter dropdown picker. Changing tables clears the id.</summary>
|
||||||
|
[CustomPropertyDrawer(typeof(EncounterLink))]
|
||||||
|
public class EncounterLinkDrawer : PropertyDrawer {
|
||||||
|
private const string NonePlaceholder = "<none>";
|
||||||
|
private const string EmptyTablePlaceholder = "<select a table first>";
|
||||||
|
|
||||||
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||||
|
var tableProp = property.FindPropertyRelative("table");
|
||||||
|
var idProp = property.FindPropertyRelative("internalId");
|
||||||
|
|
||||||
|
EditorGUI.BeginProperty(position, label, property);
|
||||||
|
|
||||||
|
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||||
|
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
|
||||||
|
var tableRect = new Rect(position.x, position.y, position.width, lineHeight);
|
||||||
|
var encounterRect = new Rect(position.x, position.y + lineHeight + spacing, position.width, lineHeight);
|
||||||
|
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
EditorGUI.PropertyField(tableRect, tableProp, label);
|
||||||
|
var tableChanged = EditorGUI.EndChangeCheck();
|
||||||
|
|
||||||
|
using(new EditorGUI.IndentLevelScope()) {
|
||||||
|
var table = tableProp.objectReferenceValue as EncounterTable;
|
||||||
|
if(table == null || table.encounters == null || table.encounters.Count == 0) {
|
||||||
|
using(new EditorGUI.DisabledScope(true)) {
|
||||||
|
EditorGUI.Popup(encounterRect, "Encounter", 0, new[] { EmptyTablePlaceholder });
|
||||||
|
}
|
||||||
|
if(tableChanged) {
|
||||||
|
idProp.stringValue = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DrawEncounterPicker(encounterRect, table, idProp, tableChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.EndProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||||
|
return EditorGUIUtility.singleLineHeight * 2 + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawEncounterPicker(Rect rect, EncounterTable table, SerializedProperty idProp, bool tableChanged) {
|
||||||
|
var count = table.encounters.Count;
|
||||||
|
var ids = new string[count + 1];
|
||||||
|
var names = new string[count + 1];
|
||||||
|
ids[0] = string.Empty;
|
||||||
|
names[0] = NonePlaceholder;
|
||||||
|
|
||||||
|
var currentIndex = 0;
|
||||||
|
for(int i = 0; i < count; i++) {
|
||||||
|
var encounter = table.encounters[i];
|
||||||
|
var id = encounter?.EncounterDefinition?.internalId ?? string.Empty;
|
||||||
|
var name = encounter?.EncounterDefinition?.name;
|
||||||
|
ids[i + 1] = id;
|
||||||
|
names[i + 1] = string.IsNullOrEmpty(name)
|
||||||
|
? $"<unnamed> ({encounter?.GetType().Name ?? "null"})"
|
||||||
|
: name;
|
||||||
|
if(!tableChanged && id == idProp.stringValue && !string.IsNullOrEmpty(id)) {
|
||||||
|
currentIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tableChanged) {
|
||||||
|
idProp.stringValue = string.Empty;
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = EditorGUI.Popup(rect, "Encounter", currentIndex, names);
|
||||||
|
if(newIndex != currentIndex) {
|
||||||
|
idProp.stringValue = ids[newIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 385fe4b7b2663e54aa3f520a809a33bc
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
public enum ValidationSeverity {
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidationIssue {
|
||||||
|
public Object asset;
|
||||||
|
public Encounter encounter;
|
||||||
|
public string path;
|
||||||
|
public ValidationSeverity severity;
|
||||||
|
public string message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Project-wide scan of encounter tables and rewards. Runs on demand, no caching.</summary>
|
||||||
|
public static class EncounterValidator {
|
||||||
|
public static List<ValidationIssue> ValidateProject() {
|
||||||
|
var issues = new List<ValidationIssue>();
|
||||||
|
|
||||||
|
var tables = FindAssetsOfType<EncounterTable>();
|
||||||
|
var idIndex = new Dictionary<string, (Encounter encounter, EncounterTable table)>();
|
||||||
|
|
||||||
|
foreach(var table in tables) {
|
||||||
|
ValidateTable(table, issues, idIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var reward in FindAssetsOfType<Reward>()) {
|
||||||
|
ValidateReward(reward, issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ValidationIssue> ValidateEncounter(EncounterTable table, int index) {
|
||||||
|
var issues = new List<ValidationIssue>();
|
||||||
|
if(table?.encounters == null || index < 0 || index >= table.encounters.Count) {
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encounter = table.encounters[index];
|
||||||
|
ValidateEncounterEntry(table, index, encounter, issues, duplicateCheck: null);
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTable(EncounterTable table, List<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> idIndex) {
|
||||||
|
if(table?.encounters == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < table.encounters.Count; i++) {
|
||||||
|
ValidateEncounterEntry(table, i, table.encounters[i], issues, idIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateEncounterEntry(EncounterTable table, int index, Encounter encounter, List<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> duplicateCheck) {
|
||||||
|
var pathPrefix = $"encounters[{index}]";
|
||||||
|
|
||||||
|
if(encounter == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = table,
|
||||||
|
path = pathPrefix,
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = "Null encounter entry."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var definition = encounter.EncounterDefinition;
|
||||||
|
if(definition == null || string.IsNullOrEmpty(definition.internalId)) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = table,
|
||||||
|
encounter = encounter,
|
||||||
|
path = $"{pathPrefix}.EncounterDefinition.internalId",
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = "Missing internalId (cannot be referenced by EncounterLink)."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(duplicateCheck != null) {
|
||||||
|
if(duplicateCheck.TryGetValue(definition.internalId, out var prior)) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = table,
|
||||||
|
encounter = encounter,
|
||||||
|
path = $"{pathPrefix}.EncounterDefinition.internalId",
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = $"Duplicate internalId '{definition.internalId}' (also in table '{prior.Item2.name}')."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
duplicateCheck[definition.internalId] = (encounter, table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(encounter.Kind == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = table,
|
||||||
|
encounter = encounter,
|
||||||
|
path = $"{pathPrefix}.Kind",
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "Encounter.Kind is null — pick a kind in the inspector."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(encounter.Kind is QuestKind questKind) {
|
||||||
|
ValidateEncounterLink(table, pathPrefix + ".Kind.nextEncounter", encounter, questKind.nextEncounter, issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateDialogEvents(table, index, encounter, issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateEncounterLink(EncounterTable owningTable, string path, Encounter encounter, EncounterLink link, List<ValidationIssue> issues) {
|
||||||
|
if(link.table == null && string.IsNullOrEmpty(link.internalId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(link.table == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = owningTable,
|
||||||
|
encounter = encounter,
|
||||||
|
path = path,
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = "EncounterLink has internalId set but no table assigned."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(link.Resolve() == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = owningTable,
|
||||||
|
encounter = encounter,
|
||||||
|
path = path,
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = $"Broken EncounterLink — table '{link.table.name}' has no encounter with internalId '{link.internalId}'."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateDialogEvents(EncounterTable table, int index, Encounter encounter, List<ValidationIssue> issues) {
|
||||||
|
var optionSet = encounter.EncounterDialogOptionSet;
|
||||||
|
if(optionSet?.options == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int o = 0; o < optionSet.options.Count; o++) {
|
||||||
|
var option = optionSet.options[o];
|
||||||
|
if(option == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateDialogLineRef(optionSet, encounter, $"options[{o}].text", option.text, issues);
|
||||||
|
|
||||||
|
if(option.events == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int e = 0; e < option.events.Count; e++) {
|
||||||
|
var evt = option.events[e];
|
||||||
|
if(evt == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = optionSet,
|
||||||
|
encounter = encounter,
|
||||||
|
path = $"options[{o}].events[{e}]",
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "Null event entry (type may have been renamed/deleted)."
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(evt is GiveRewardEvent give && give.reward == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = optionSet,
|
||||||
|
encounter = encounter,
|
||||||
|
path = $"options[{o}].events[{e}].reward",
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "GiveRewardEvent has no Reward asset assigned."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateDialogLineRef(EncounterDialogOptionSet optionSet, Encounter encounter, string path, DialogLineRef lineRef, List<ValidationIssue> issues) {
|
||||||
|
var library = optionSet.library;
|
||||||
|
var hasId = !string.IsNullOrEmpty(lineRef.id);
|
||||||
|
var hasInline = !string.IsNullOrEmpty(lineRef.inlineText);
|
||||||
|
|
||||||
|
if(!hasId && !hasInline) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = optionSet,
|
||||||
|
encounter = encounter,
|
||||||
|
path = path,
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "Dialog line is empty (no id and no inline text)."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasId && library == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = optionSet,
|
||||||
|
encounter = encounter,
|
||||||
|
path = path,
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = $"DialogLineRef references id '{lineRef.id}' but the option set has no library assigned."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasId && library != null && string.IsNullOrEmpty(library.Resolve(lineRef.id))) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = optionSet,
|
||||||
|
encounter = encounter,
|
||||||
|
path = path,
|
||||||
|
severity = ValidationSeverity.Error,
|
||||||
|
message = $"DialogLineRef id '{lineRef.id}' not found in library '{library.name}'."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateReward(Reward reward, List<ValidationIssue> issues) {
|
||||||
|
if(reward == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(reward.kind == null) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = reward,
|
||||||
|
path = "kind",
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "Reward.kind is null — pick a reward kind in the inspector."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(reward.id)) {
|
||||||
|
issues.Add(new ValidationIssue {
|
||||||
|
asset = reward,
|
||||||
|
path = "id",
|
||||||
|
severity = ValidationSeverity.Warning,
|
||||||
|
message = "Reward has no id."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<T> FindAssetsOfType<T>() where T : Object {
|
||||||
|
var result = new List<T>();
|
||||||
|
var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
|
||||||
|
foreach(var guid in guids) {
|
||||||
|
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||||
|
if(asset != null) {
|
||||||
|
result.Add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EncounterValidatorMenu {
|
||||||
|
[MenuItem("Jovian/Encounters/Validate All")]
|
||||||
|
public static void ValidateAll() {
|
||||||
|
var issues = EncounterValidator.ValidateProject();
|
||||||
|
if(issues.Count == 0) {
|
||||||
|
Debug.Log("[EncounterValidator] No issues found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int errors = 0;
|
||||||
|
int warnings = 0;
|
||||||
|
foreach(var issue in issues) {
|
||||||
|
if(issue.severity == ValidationSeverity.Error) {
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[EncounterValidator] {issues.Count} issue(s) found — {errors} error(s), {warnings} warning(s). Click any log row to ping the offending asset.");
|
||||||
|
|
||||||
|
foreach(var issue in issues) {
|
||||||
|
var assetName = issue.asset != null ? issue.asset.name : "<null>";
|
||||||
|
var message = $"[EncounterValidator] {assetName} · {issue.path} — {issue.message}";
|
||||||
|
if(issue.severity == ValidationSeverity.Error) {
|
||||||
|
Debug.LogError(message, issue.asset);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Debug.LogWarning(message, issue.asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bab1779bc53230a4291cd3ff35774558
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "Jovian.EncounterSystem.Editor",
|
||||||
|
"rootNamespace": "Jovian.EncounterSystem.Editor",
|
||||||
|
"references": [
|
||||||
|
"Jovian.EncounterSystem"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fa750d6ecf6f41540ab10d47c136fc33
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>Concrete-type dropdown for <c>[SerializeReference]</c> fields, arrays, and list elements.</summary>
|
||||||
|
[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
|
||||||
|
public class SubclassSelectorDrawer : PropertyDrawer {
|
||||||
|
private static readonly Dictionary<Type, Type[]> TypeCache = new();
|
||||||
|
|
||||||
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||||
|
if(property.propertyType != SerializedPropertyType.ManagedReference) {
|
||||||
|
EditorGUI.LabelField(position, label.text, "[SubclassSelector] requires [SerializeReference]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseType = ResolveBaseType(fieldInfo.FieldType);
|
||||||
|
var concreteTypes = GetConcreteTypes(baseType);
|
||||||
|
var currentType = GetCurrentType(property);
|
||||||
|
var currentIndex = Array.IndexOf(concreteTypes, currentType);
|
||||||
|
|
||||||
|
var names = new string[concreteTypes.Length + 1];
|
||||||
|
names[0] = "<None>";
|
||||||
|
for(int i = 0; i < concreteTypes.Length; i++) {
|
||||||
|
names[i + 1] = concreteTypes[i].Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayLabel = ResolveDisplayLabel(property, label);
|
||||||
|
var headerRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||||
|
var labelRect = new Rect(headerRect.x, headerRect.y, EditorGUIUtility.labelWidth, headerRect.height);
|
||||||
|
var popupRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth, headerRect.y, headerRect.width - EditorGUIUtility.labelWidth, headerRect.height);
|
||||||
|
|
||||||
|
EditorGUI.LabelField(labelRect, displayLabel);
|
||||||
|
var newIndex = EditorGUI.Popup(popupRect, currentIndex + 1, names);
|
||||||
|
if(newIndex != currentIndex + 1) {
|
||||||
|
property.managedReferenceValue = newIndex == 0 ? null : Activator.CreateInstance(concreteTypes[newIndex - 1]);
|
||||||
|
property.serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(property.managedReferenceValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||||
|
var r = new Rect(position.x, y, position.width, h);
|
||||||
|
EditorGUI.PropertyField(r, iterator, true);
|
||||||
|
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||||
|
var height = EditorGUIUtility.singleLineHeight;
|
||||||
|
if(property.propertyType != SerializedPropertyType.ManagedReference || property.managedReferenceValue == null) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterator = property.Copy();
|
||||||
|
var end = iterator.GetEndProperty();
|
||||||
|
if(iterator.NextVisible(true)) {
|
||||||
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||||
|
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||||
|
if(!iterator.NextVisible(false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type ResolveBaseType(Type fieldType) {
|
||||||
|
if(fieldType.IsArray) {
|
||||||
|
return fieldType.GetElementType();
|
||||||
|
}
|
||||||
|
if(fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) {
|
||||||
|
return fieldType.GetGenericArguments()[0];
|
||||||
|
}
|
||||||
|
return fieldType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type[] GetConcreteTypes(Type baseType) {
|
||||||
|
if(TypeCache.TryGetValue(baseType, out var cached)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(assembly => {
|
||||||
|
try {
|
||||||
|
return assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return Array.Empty<Type>();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Where(type => baseType.IsAssignableFrom(type)
|
||||||
|
&& !type.IsAbstract
|
||||||
|
&& !type.IsInterface
|
||||||
|
&& !typeof(UnityEngine.Object).IsAssignableFrom(type)
|
||||||
|
&& type.GetConstructor(Type.EmptyTypes) != null)
|
||||||
|
.OrderBy(type => type.Name)
|
||||||
|
.ToArray();
|
||||||
|
TypeCache[baseType] = types;
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIContent ResolveDisplayLabel(SerializedProperty property, GUIContent fallback) {
|
||||||
|
var value = property.managedReferenceValue;
|
||||||
|
if(value is not IEncounter) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
var definitionProp = value.GetType().GetProperty("EncounterDefinition");
|
||||||
|
var definition = definitionProp?.GetValue(value);
|
||||||
|
var nameField = definition?.GetType().GetField("name");
|
||||||
|
var name = nameField?.GetValue(definition) as string;
|
||||||
|
return string.IsNullOrEmpty(name) ? fallback : new GUIContent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetCurrentType(SerializedProperty property) {
|
||||||
|
var full = property.managedReferenceFullTypename;
|
||||||
|
if(string.IsNullOrEmpty(full)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var space = full.IndexOf(' ');
|
||||||
|
if(space < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var assembly = full.Substring(0, space);
|
||||||
|
var typeName = full.Substring(space + 1);
|
||||||
|
return Type.GetType($"{typeName}, {assembly}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e00ec3e6bdfd024aa73689289578192
|
||||||
21
Packages/com.jovian.encounter-system/LICENSE
Normal file
21
Packages/com.jovian.encounter-system/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Sebastian Bularca
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
7
Packages/com.jovian.encounter-system/LICENSE.meta
Normal file
7
Packages/com.jovian.encounter-system/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 181516b3331a8824abeaa656991d77ee
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
186
Packages/com.jovian.encounter-system/README.md
Normal file
186
Packages/com.jovian.encounter-system/README.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 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<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
|
||||||
|
├── 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<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.
|
||||||
|
|
||||||
|
### 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<SomeEvent>((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<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.
|
||||||
7
Packages/com.jovian.encounter-system/README.md.meta
Normal file
7
Packages/com.jovian.encounter-system/README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 158607149986285429176c786851ed82
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/com.jovian.encounter-system/Runtime.meta
Normal file
8
Packages/com.jovian.encounter-system/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a3909887107f1f24aad1cd7db8a0bb28
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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
|
||||||
19
Packages/com.jovian.encounter-system/package.json
Normal file
19
Packages/com.jovian.encounter-system/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "com.jovian.encounter-system",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"displayName": "Jovian Encounter System",
|
||||||
|
"description": "Data-driven encounter authoring with polymorphic encounter kinds, dialog options with designer-authored events, cross-table quest chaining, and gated quest progression.",
|
||||||
|
"unity": "2022.3",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.addressables": "1.21.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"encounter",
|
||||||
|
"quest",
|
||||||
|
"dialog",
|
||||||
|
"rpg"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Jovian"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Packages/com.jovian.encounter-system/package.json.meta
Normal file
7
Packages/com.jovian.encounter-system/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 75b61f91daea15646839c76fcba9a1d7
|
||||||
|
PackageManifestImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -52,14 +52,14 @@ namespace Jovian.ZoneSystem.Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("Window/Zone System/Settings")]
|
[MenuItem("Jovian/Zone System/Settings")]
|
||||||
private static void SelectOrCreateSettings() {
|
private static void SelectOrCreateSettings() {
|
||||||
ZoneEditorSettings settings = FindOrCreateSettings();
|
ZoneEditorSettings settings = FindOrCreateSettings();
|
||||||
Selection.activeObject = settings;
|
Selection.activeObject = settings;
|
||||||
EditorGUIUtility.PingObject(settings);
|
EditorGUIUtility.PingObject(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("Window/Zone System/Documentation")]
|
[MenuItem("Jovian/Zone System/Documentation")]
|
||||||
private static void OpenDocumentation() {
|
private static void OpenDocumentation() {
|
||||||
// Find the Documentation~ folder relative to this package
|
// Find the Documentation~ folder relative to this package
|
||||||
string[] guids = AssetDatabase.FindAssets("t:Script ZoneEditorSettings");
|
string[] guids = AssetDatabase.FindAssets("t:Script ZoneEditorSettings");
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ namespace Jovian.ZoneSystem.Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("Window/Zone System/Zone Editor")]
|
[MenuItem("Jovian/Zone System/Zone Editor")]
|
||||||
public static void Open() {
|
public static void Open() {
|
||||||
GetWindow<ZoneEditorWindow>("Zone Editor");
|
GetWindow<ZoneEditorWindow>("Zone Editor");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Packages/com.jovian.zonesystem/
|
|||||||
│ ├── ShapeFactory.cs ← Default shape generation (square, circle, polygon)
|
│ ├── ShapeFactory.cs ← Default shape generation (square, circle, polygon)
|
||||||
│ └── ZoneExporter.cs ← Serialization to JSON
|
│ └── ZoneExporter.cs ← Serialization to JSON
|
||||||
├── Editor/
|
├── Editor/
|
||||||
│ ├── ZoneEditorWindow.cs ← Main editor window (Window → Zone System → Zone Editor)
|
│ ├── ZoneEditorWindow.cs ← Main editor window (Jovian → Zone System → Zone Editor)
|
||||||
│ ├── ZoneEditorSettings.cs ← Configurable settings: folder path, role colors
|
│ ├── ZoneEditorSettings.cs ← Configurable settings: folder path, role colors
|
||||||
│ ├── ZoneInstanceEditor.cs ← Custom inspector + scene handles for shape editing
|
│ ├── ZoneInstanceEditor.cs ← Custom inspector + scene handles for shape editing
|
||||||
│ └── ZoneDataEditor.cs ← Role-aware ZoneData inspector
|
│ └── ZoneDataEditor.cs ← Role-aware ZoneData inspector
|
||||||
@@ -30,7 +30,7 @@ Packages/com.jovian.zonesystem/
|
|||||||
|
|
||||||
1. Add the package to your project (local package in `Packages/`).
|
1. Add the package to your project (local package in `Packages/`).
|
||||||
2. Create a **ZonesObjectHolder** GameObject and set **Map Plane** to match your map (e.g. `XZ`).
|
2. Create a **ZonesObjectHolder** GameObject and set **Map Plane** to match your map (e.g. `XZ`).
|
||||||
3. Open **Window → Zone System → Zone Editor**.
|
3. Open **Jovian → Zone System → Zone Editor**.
|
||||||
4. Click **Create New Zone**, set a name and shape, then click **Create & Edit**.
|
4. Click **Create New Zone**, set a name and shape, then click **Create & Edit**.
|
||||||
5. Edit all zone data fields in the editor, then click **Save Zone**.
|
5. Edit all zone data fields in the editor, then click **Save Zone**.
|
||||||
6. Use scene handles to adjust the polygon shape.
|
6. Use scene handles to adjust the polygon shape.
|
||||||
@@ -51,9 +51,9 @@ Packages/com.jovian.zonesystem/
|
|||||||
|
|
||||||
| Menu Path | Description |
|
| Menu Path | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| Window → Zone System → Zone Editor | Main editor window |
|
| Jovian → Zone System → Zone Editor | Main editor window |
|
||||||
| Window → Zone System → Settings | Select or create ZoneEditorSettings asset |
|
| Jovian → Zone System → Settings | Select or create ZoneEditorSettings asset |
|
||||||
| Window → Zone System → Documentation | Open HTML documentation |
|
| Jovian → Zone System → Documentation | Open HTML documentation |
|
||||||
|
|
||||||
## Runtime API
|
## Runtime API
|
||||||
|
|
||||||
@@ -75,4 +75,4 @@ List<ZoneData> zones = api.GetOverlappingZones(partyWorldPosition);
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Full documentation is available at `Documentation~/index.html`. Open it via **Window → Zone System → Documentation**.
|
Full documentation is available at `Documentation~/index.html`. Open it via **Jovian → Zone System → Documentation**.
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
"defineConstraints": [],
|
"defineConstraints": [],
|
||||||
"versionDefines": [],
|
"versionDefines": [],
|
||||||
"noEngineReferences": false
|
"noEngineReferences": false
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,14 @@
|
|||||||
"source": "embedded",
|
"source": "embedded",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"com.jovian.encounter-system": {
|
||||||
|
"version": "file:com.jovian.encounter-system",
|
||||||
|
"depth": 0,
|
||||||
|
"source": "embedded",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.addressables": "1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"com.jovian.ingame-logging": {
|
"com.jovian.ingame-logging": {
|
||||||
"version": "file:com.jovian.ingame-logging",
|
"version": "file:com.jovian.ingame-logging",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"displayName":"Save System","packagePath":"Packages/com.jovian.savesystem","repoPath":"D:\\repos\\unity-save-system","selected":false},{"displayName":"Zone System","packagePath":"Packages/com.jovian.zonesystem","repoPath":"D:\\repos\\unity-zone-system","selected":false},{"displayName":"Unity Inspector","packagePath":"Packages/com.jovian.inspector-tools","repoPath":"d:\\repos\\unity-inspector\\","selected":false},{"displayName":"Unity Logger","packagePath":"Packages/com.jovian.logger","repoPath":"d:\\repos\\unity-logger\\","selected":false},{"displayName":"Unity Recent Assets","packagePath":"Packages/com.jovian.assets-history","repoPath":"d:\\repos\\unity-recentassets\\","selected":false},{"displayName":"Unity Utilities","packagePath":"Packages/com.jovian.utilities","repoPath":"d:\\repos\\unity-utilities\\","selected":false},{"displayName":"Unity Popup System","packagePath":"Packages/com.jovian.popup-system","repoPath":"d:\\repos\\unity-popup-system\\","selected":false},{"displayName":"Unity Calendar System","packagePath":"Packages/com.jovian.calendar","repoPath":"d:\\repos\\unity-calendar-system\\","selected":false}]
|
[{"displayName":"Save System","packagePath":"Packages/com.jovian.savesystem","repoPath":"D:\\repos\\unity-save-system","selected":false},{"displayName":"Zone System","packagePath":"Packages/com.jovian.zonesystem","repoPath":"D:\\repos\\unity-zone-system","selected":false},{"displayName":"Unity Inspector","packagePath":"Packages/com.jovian.inspector-tools","repoPath":"d:\\repos\\unity-inspector\\","selected":false},{"displayName":"Unity Logger","packagePath":"Packages/com.jovian.logger","repoPath":"d:\\repos\\unity-logger\\","selected":false},{"displayName":"Unity Recent Assets","packagePath":"Packages/com.jovian.assets-history","repoPath":"d:\\repos\\unity-recentassets\\","selected":false},{"displayName":"Unity Utilities","packagePath":"Packages/com.jovian.utilities","repoPath":"d:\\repos\\unity-utilities\\","selected":false},{"displayName":"Unity Popup System","packagePath":"Packages/com.jovian.popup-system","repoPath":"d:\\repos\\unity-popup-system\\","selected":false},{"displayName":"Unity Calendar System","packagePath":"Packages/com.jovian.calendar","repoPath":"d:\\repos\\unity-calendar-system\\","selected":false},{"displayName":"Unity Encounter System","packagePath":"Packages/com.jovian.encounter-system","repoPath":"d:\\repos\\encounter-system\\","selected":false}]
|
||||||
Reference in New Issue
Block a user