forked from Shardstone/trail-into-darkness
Compare commits
9 Commits
09fe607aab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a49a21c0cb | ||
|
|
7be3499f14 | ||
|
|
3d13dac256 | ||
|
|
a239e6286b | ||
|
|
b9985eaa71 | ||
|
|
e5771b113a | ||
|
|
50ec5d44a8 | ||
|
|
1e4aeb0e5b | ||
|
|
e7f7da985b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -98,4 +98,3 @@ InitTestScene*.unity*
|
||||
# Auto-generated scenes by play mode tests
|
||||
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
|
||||
.idea/
|
||||
Packages/com.jovian.tag-system
|
||||
|
||||
81
AGENTS.md
Normal file
81
AGENTS.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
Nox — Unity 6 party-based RPG. Code lives under `Assets/Code/` with namespaces `Nox.Core`, `Nox.Game`, `Nox.Input`, `Nox.Platform`, `Nox.Util`. In-house packages under `Packages/com.jovian.*` are first-party code, not third-party.
|
||||
|
||||
## Unity & Tools
|
||||
|
||||
- **Unity**: 6000.3.7f1 (C# 9.0, .NET Framework 4.7.1)
|
||||
- **URP**: 17.3.0
|
||||
- **Input System**: 1.18.0 (com.unity.inputsystem)
|
||||
- **Editor only** — no CI/CD, no CLI build command. Build via Unity Editor → Windows target `Nox.exe`.
|
||||
- **Solution**: `trail-into-darkness.sln` at root. Rider is primary IDE (`com.unity.ide.rider`).
|
||||
|
||||
## Boot Sequence
|
||||
|
||||
1. `Boot.cs` `[RuntimeInitializeOnLoadMethod]` loads `"Initializer"` addressable prefab (or `"Startup"` scene in FullBoot mode)
|
||||
2. `EntryPoint.Start()` coroutine initializes platform, creates game states, adds `GameStateRunner` MonoBehaviour
|
||||
3. `GameStateRunner` ticks current state: `SplashGameState` → `MainMenuGameState` → `GameModeGameState`
|
||||
|
||||
Editor-only: `BootMode.cs` (`Nox.EditorCode`) controls boot mode (Full Boot, Scene Boot, Unity Default).
|
||||
|
||||
## State Architecture
|
||||
|
||||
Dual-layer state:
|
||||
- **Game States** (`IGameState`): Application flow. Lifecycle: `EnterGameState()` → `Tick()`/`LateTick()` → `ExitGameState()` → `Dispose()`
|
||||
- **Play Modes** (`IPlayMode`): GameModes inside `GameModeGameState`. Same tick lifecycle.
|
||||
|
||||
Central store: `GameDataState` holds `ActiveGameState`, `ActivePlayMode`, `ActiveParty`, `platformSelector`, `activeSessionId`.
|
||||
|
||||
Scene authoring: `SceneReference` MonoBehaviour per scene sets initial `GameState`. Enables standalone scene playback in editor.
|
||||
|
||||
## Addressable Keys
|
||||
|
||||
Assets loaded by string key. Do not change keys without checking all callers: `"Initializer"`, `"AdventureMapPrefabs"`, `"PauseMenuPrefabs"`, `"AdventureSettings"`, `"CharacterBaseSettings"`, `"PerkRegistry"`, `"CharacterRegistry"`, `"BootstrapReferences"`.
|
||||
|
||||
## Character System
|
||||
|
||||
Factory chain initialized from ScriptableObjects: `CharacterSystemsFactory.Create()` builds `PerkFactory` → `CharacterAttributesFactory` → `CharacterStatsFactory` → `CharacterFactory` → `PartyFactory`. Config loaded via Addressables.
|
||||
|
||||
## Persistence
|
||||
|
||||
`Jovian.SaveSystem` package handles JSON serialization to `Application.persistentDataPath`. Nox wrapper: `NoxSaveData.RestoreSavedData()` loads `NoxSavedDataSet` (playMode, party, position, adventure data, game log). Save slots managed by `SaveSlotManager`.
|
||||
|
||||
## Platform Abstraction
|
||||
|
||||
`IPlatform` interface with `DesktopPlatform` and `UnityEditorPlatform` implementations. Each initializes platform-specific `IInput`. Switched via `PlatformSelector`.
|
||||
|
||||
## In-House Jovian Packages
|
||||
|
||||
- `com.jovian.savesystem` — JSON persistence
|
||||
- `com.jovian.encounter-system` — data-driven encounters, `IEncounterKind` payloads, `EncounterLink` refs, `QuestProgress` gating. Designer tool: `Jovian → Encounters → Encounter Browser`
|
||||
- `com.jovian.calendar` — in-game calendar
|
||||
- `com.jovian.ingame-logging` — runtime game log
|
||||
- `com.jovian.popup-system` — popup/dialog UI
|
||||
- `com.jovian.zonesystem` — zone/region management
|
||||
- `com.jovian.logger` — dev logging
|
||||
- `com.jovian.utilities` / `com.jovian.inspector-tools` — editor utilities
|
||||
|
||||
## C# Style (from .editorconfig)
|
||||
|
||||
- 4 spaces, LF endings
|
||||
- `var` preferred when type is apparent
|
||||
- Target-typed `new()` when type is evident
|
||||
- No space after keywords in control flow: `if(` not `if (`
|
||||
- Braces on same line; `else`/`catch`/`finally` on new line
|
||||
- Block-scoped namespaces, usings outside namespace
|
||||
- PascalCase types/methods/properties, camelCase all fields (public and private), `I` prefix interfaces
|
||||
- Expression-bodied: yes for properties/accessors/lambdas; no for constructors/methods/operators/local functions
|
||||
- No `this.` qualifier; language keywords (`int` not `Int32`)
|
||||
|
||||
## Planning Docs
|
||||
|
||||
Non-trivial features planned in `docs/plans/` as paired files: `{date}-{name}-design.md` + `{date}-{name}-implementation.md`. Check here before starting new feature work.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- No test suite exists despite `com.unity.test-framework` in manifest
|
||||
- No runtime build/lint/typecheck commands — all validation happens in Unity Editor
|
||||
- Generated `.csproj` files are Unity output, not source of truth for assembly structure
|
||||
- `Assets/Code/` is the only code location to modify; never edit `Packages/com.jovian.*` core unless working on that package
|
||||
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: d7f58d36cc4da874fa45d38c0070c2c2
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 41c3275372ee23ae1595642e0bc286bf
|
||||
Hash: a9de470e3ae86c2ab2d0ba2a6812fa41
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_CatalogRequestsTimeout: 0
|
||||
|
||||
@@ -8,9 +8,9 @@ using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using Nox.Game.UI;
|
||||
using Nox.UI;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using ZLinq;
|
||||
using PlayMode = Nox.Core.PlayMode;
|
||||
|
||||
namespace Nox.Game {
|
||||
@@ -84,27 +84,10 @@ namespace Nox.Game {
|
||||
inputActions.UI.PauseMenu.Enable();
|
||||
Debug.Log("Entering Adventure Play Mode");
|
||||
if(partyDefinition == null) {
|
||||
var sessions = saveSystem.GetAllSessions().AsValueEnumerable().OrderByDescending(s => s.lastSaveDateUtc).ToList();
|
||||
if(sessions.Count == 0) {
|
||||
return;
|
||||
var restoreResult = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData);
|
||||
if(restoreResult == null) {
|
||||
Debug.LogError("AdventurePlayMode: no save data found for auto-recovery. Party will be null.");
|
||||
}
|
||||
|
||||
var latestSession = sessions[0];
|
||||
var slots = saveSystem.GetSlots(latestSession.sessionId).AsValueEnumerable().OrderByDescending(s => s.timestampUtc).ToList();
|
||||
if(slots.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var latestSlot = slots[0];
|
||||
var saveData = saveSystem.Load<NoxSaveData>(latestSlot);
|
||||
Debug.Log($"Loaded save {latestSlot.DisplayLabel}");
|
||||
if(saveData == null) {
|
||||
Debug.LogError("Failed to load save data");
|
||||
return;
|
||||
}
|
||||
|
||||
NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData);
|
||||
Debug.LogWarning("AdventurePlayMode started from the Adventure Scene. Loading the last Autosave");
|
||||
}
|
||||
|
||||
encounterRegistry ??= Addressables.LoadAssetAsync<EncounterRegistry>("EncounterRegistry").WaitForCompletion();
|
||||
@@ -171,7 +154,7 @@ namespace Nox.Game {
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if(!IsGameModeInitialized) {
|
||||
if(!IsGameModeInitialized || gameDataState.ActiveParty == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +180,8 @@ namespace Nox.Game {
|
||||
return new NoxSavedDataSet {
|
||||
activePlayMode = PlayMode.Adventure,
|
||||
activeParty = partyDefinition,
|
||||
partyPosition = partyRef ? SerializableVector3.FromVector3(partyRef.transform.position) : SerializableVector3.Zero
|
||||
partyPosition = partyRef ? SerializableVector3.FromVector3(partyRef.transform.position) : SerializableVector3.Zero,
|
||||
adventureData = this.adventureData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,10 +189,10 @@ namespace Nox.Game {
|
||||
inputActions.Player.Disable();
|
||||
inputActions.UI.PauseMenu.Disable();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
partyGuiView?.Dispose();
|
||||
popupSystem?.Dispose();
|
||||
|
||||
cameraController?.Dispose();
|
||||
partyMovementHandler?.Dispose();
|
||||
encounterHandler?.Dispose();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class AnswerPrefab : MonoBehaviour {
|
||||
public class AnswerReference : MonoBehaviour {
|
||||
public TextMeshProUGUI number;
|
||||
public TextMeshProUGUI dialogText;
|
||||
public Button button;
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ namespace Nox.Game {
|
||||
private readonly EncounterRegistry encounterRegistry;
|
||||
private readonly EncounterView encounterView;
|
||||
private string previousZoneId;
|
||||
private IEncounter activeEncounter;
|
||||
|
||||
public EncounterHandler(ZoneSystem zoneSystem, EncounterRegistry encounterRegistry, EncounterPrefabs encounterPrefabs) {
|
||||
this.zoneSystem = zoneSystem;
|
||||
this.encounterRegistry = encounterRegistry;
|
||||
encounterView = new EncounterView(encounterPrefabs);
|
||||
encounterView.OptionSelected += OnOptionSelected;
|
||||
}
|
||||
|
||||
public bool AskForRandomEncounter(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter) {
|
||||
@@ -37,15 +39,16 @@ namespace Nox.Game {
|
||||
var shouldTrigger = randomChance <= zoneContext.finalEncounterChance;
|
||||
|
||||
if(!shouldTrigger) {
|
||||
Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> none");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(encounterKind == null) {
|
||||
encounter = encounterRegistry.GetRandomEncounter(encounterTableId);
|
||||
return encounter != null;
|
||||
}
|
||||
encounter = encounterKind == null
|
||||
? encounterRegistry.GetRandomEncounter(encounterTableId)
|
||||
: encounterRegistry.GetRandomEncounter(encounterTableId, encounterKind);
|
||||
|
||||
encounter = encounterRegistry.GetRandomEncounter(encounterTableId, encounterKind);
|
||||
var resultName = encounter?.EncounterDefinition?.name ?? "none";
|
||||
Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> {resultName}");
|
||||
return encounter != null;
|
||||
}
|
||||
|
||||
@@ -76,12 +79,56 @@ namespace Nox.Game {
|
||||
case CombatKind:
|
||||
return;
|
||||
default:
|
||||
activeEncounter = encounter;
|
||||
encounterView?.SetCurrentEncounter(encounter);
|
||||
encounterView?.Show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOptionSelected(int optionIndex) {
|
||||
if(activeEncounter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var options = activeEncounter.EncounterDialogOptionSet?.options;
|
||||
if(options == null || optionIndex < 0 || optionIndex >= options.Count) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveOption(activeEncounter, options[optionIndex]);
|
||||
encounterView?.Hide();
|
||||
activeEncounter = null;
|
||||
}
|
||||
|
||||
private void ResolveOption(IEncounter encounter, EncounterDialogOption option) {
|
||||
if(option?.events == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < option.events.Count; i++) {
|
||||
var encounterEvent = option.events[i];
|
||||
if(encounterEvent == null) {
|
||||
continue;
|
||||
}
|
||||
switch(encounterEvent) {
|
||||
case ChainToEncounterEvent chain:
|
||||
if(AskForEncounter(chain.nextEncounterId, out var next)) {
|
||||
TriggerEncounter(next);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case LogEvent log:
|
||||
Debug.Log($"[Encounter '{encounter.EncounterDefinition.id}'] {log.message}");
|
||||
break;
|
||||
case StartCombatEvent _:
|
||||
case GiveRewardEvent _:
|
||||
Debug.Log($"[Encounter] unhandled event {encounterEvent.GetType().Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckForEncounters(Vector3 position) {
|
||||
VerifyZones(position);
|
||||
}
|
||||
@@ -89,7 +136,11 @@ namespace Nox.Game {
|
||||
public void Tick() { }
|
||||
|
||||
public void Dispose() {
|
||||
// nothing here
|
||||
if(encounterView != null) {
|
||||
encounterView.OptionSelected -= OnOptionSelected;
|
||||
encounterView.Dispose();
|
||||
}
|
||||
activeEncounter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "EncounterPrefabs", menuName = "Nox/EncounterPrefabs")]
|
||||
public class EncounterPrefabs : ScriptableObject {
|
||||
public EncounterSet[] encounterSets;
|
||||
public AnswerPrefab answerPrefab;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -14,5 +13,6 @@ namespace Nox.Game {
|
||||
[field: SerializeReference, SubclassSelector]
|
||||
public IEncounterKind encounterKind;
|
||||
public EncounterReference encounterReference;
|
||||
public AnswerReference answerReference;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using Nox.Game.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class EncounterView : IMenuView{
|
||||
private readonly EncounterPrefabs encounterPrefabs;
|
||||
private readonly IEncounterKind encounterKind;
|
||||
private IEncounter currentEncounter;
|
||||
public class EncounterView : IMenuView {
|
||||
private const int MaxAnswers = 4;
|
||||
|
||||
private Dictionary<IEncounterKind, EncounterReference> encounterKindToPrefab = new ();
|
||||
private IEncounterKind currentActiveKind;
|
||||
private readonly EncounterPrefabs encounterPrefabs;
|
||||
private readonly Dictionary<Type, EncounterReference> kindToReference = new();
|
||||
private readonly Dictionary<Type, List<AnswerReference>> kindToAnswerPool = new();
|
||||
|
||||
private IEncounter currentEncounter;
|
||||
private EncounterReference currentReference;
|
||||
private List<AnswerReference> currentAnswerPool;
|
||||
|
||||
public event Action<int> OptionSelected;
|
||||
|
||||
public EncounterView(EncounterPrefabs encounterPrefabs) {
|
||||
this.encounterPrefabs = encounterPrefabs;
|
||||
@@ -19,25 +25,143 @@ namespace Nox.Game {
|
||||
|
||||
public void SetCurrentEncounter(IEncounter encounter) {
|
||||
currentEncounter = encounter;
|
||||
if(!encounterKindToPrefab.TryGetValue(encounter.EncounterDefinition.Kind, out var encounterReference)) {
|
||||
encounterReference = Object.Instantiate(encounterPrefabs.encounterSets.FirstOrDefault(e => e.encounterKind == encounterKind)?.encounterReference);
|
||||
encounterKindToPrefab.Add(encounter.EncounterDefinition.Kind, encounterReference);
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize() { }
|
||||
|
||||
public void Show() {
|
||||
currentActiveKind = currentEncounter.EncounterDefinition.Kind;
|
||||
if(currentEncounter?.EncounterDefinition?.Kind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentReference) {
|
||||
currentReference.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
var kindType = currentEncounter.EncounterDefinition.Kind.GetType();
|
||||
var set = encounterPrefabs.encounterSets
|
||||
.FirstOrDefault(s => s.encounterKind != null && s.encounterKind.GetType() == kindType);
|
||||
if(set == null || !set.encounterReference) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!kindToReference.TryGetValue(kindType, out var reference) || !reference) {
|
||||
reference = UnityEngine.Object.Instantiate(set.encounterReference);
|
||||
kindToReference[kindType] = reference;
|
||||
}
|
||||
|
||||
currentReference = reference;
|
||||
currentAnswerPool = GetOrBuildAnswerPool(kindType, set);
|
||||
|
||||
PopulateEncounterReference();
|
||||
encounterKindToPrefab[currentActiveKind].gameObject.SetActive(true);
|
||||
}
|
||||
private void PopulateEncounterReference() {
|
||||
currentReference.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Hide() {
|
||||
encounterKindToPrefab[currentActiveKind].gameObject.SetActive(false);
|
||||
if(currentReference) {
|
||||
currentReference.gameObject.SetActive(false);
|
||||
}
|
||||
DeactivateAnswers(currentAnswerPool);
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
|
||||
private List<AnswerReference> GetOrBuildAnswerPool(Type kindType, EncounterSet set) {
|
||||
if(kindToAnswerPool.TryGetValue(kindType, out var pool) && pool != null) {
|
||||
return pool;
|
||||
}
|
||||
|
||||
pool = new List<AnswerReference>(MaxAnswers);
|
||||
kindToAnswerPool[kindType] = pool;
|
||||
|
||||
if(!set.answerReference || !currentReference.encounterOptionsContainer) {
|
||||
return pool;
|
||||
}
|
||||
|
||||
for(var i = 0; i < MaxAnswers; i++) {
|
||||
var answer = UnityEngine.Object.Instantiate(set.answerReference, currentReference.encounterOptionsContainer);
|
||||
answer.gameObject.SetActive(false);
|
||||
pool.Add(answer);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
private void PopulateEncounterReference() {
|
||||
var definition = currentEncounter.EncounterDefinition;
|
||||
var visuals = currentEncounter.EncounterVisuals;
|
||||
|
||||
if(currentReference.encounterName) {
|
||||
currentReference.encounterName.text = definition.name;
|
||||
}
|
||||
if(currentReference.encounterDescription) {
|
||||
currentReference.encounterDescription.text = definition.description;
|
||||
}
|
||||
if(currentReference.encounterArt && visuals != null) {
|
||||
currentReference.encounterArt.sprite = visuals.encounterArt;
|
||||
}
|
||||
|
||||
PopulateAnswers();
|
||||
}
|
||||
|
||||
private void PopulateAnswers() {
|
||||
DeactivateAnswers(currentAnswerPool);
|
||||
|
||||
var optionSet = currentEncounter.EncounterDialogOptionSet;
|
||||
if(currentAnswerPool == null || optionSet?.options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Mathf.Min(optionSet.options.Count, currentAnswerPool.Count);
|
||||
for(var i = 0; i < count; i++) {
|
||||
var option = optionSet.options[i];
|
||||
var answer = currentAnswerPool[i];
|
||||
if(option == null || !answer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(answer.number) {
|
||||
answer.number.text = (i + 1).ToString();
|
||||
}
|
||||
if(answer.dialogText) {
|
||||
answer.dialogText.text = option.text.Resolve(optionSet.library);
|
||||
}
|
||||
BindAnswerButton(answer, i);
|
||||
answer.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindAnswerButton(AnswerReference answer, int optionIndex) {
|
||||
var button = answer.button ? answer.button : answer.GetComponentInChildren<UnityEngine.UI.Button>(true);
|
||||
if(!button) {
|
||||
return;
|
||||
}
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => OptionSelected?.Invoke(optionIndex));
|
||||
}
|
||||
|
||||
private static void DeactivateAnswers(List<AnswerReference> pool) {
|
||||
if(pool == null) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < pool.Count; i++) {
|
||||
if(pool[i]) {
|
||||
pool[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach(var reference in kindToReference.Values) {
|
||||
if(reference) {
|
||||
UnityEngine.Object.Destroy(reference.gameObject);
|
||||
}
|
||||
}
|
||||
kindToReference.Clear();
|
||||
kindToAnswerPool.Clear();
|
||||
currentReference = null;
|
||||
currentAnswerPool = null;
|
||||
currentEncounter = null;
|
||||
OptionSelected = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,66 @@ MonoBehaviour:
|
||||
- encounterKind:
|
||||
rid: 1352971649185742937
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerPrefab: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610525
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610527
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610528
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610529
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610530
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610531
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610532
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
- encounterKind:
|
||||
rid: 696918149227610532
|
||||
encounterReference: {fileID: 3705563528526877357, guid: 62525e62adc83b84abde85d78828de2b, type: 3}
|
||||
answerReference: {fileID: -3146704326252051464, guid: cbecff27dee2cf7448a05a89ecb018b4, type: 3}
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: 696918149227610525
|
||||
type: {class: CombatKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 696918149227610527
|
||||
type: {class: QuestKind, ns: Jovian.EncounterSystem, asm: Jovian.EncounterSystem}
|
||||
data:
|
||||
nextEncounter:
|
||||
table: {fileID: 0}
|
||||
internalId:
|
||||
- rid: 696918149227610528
|
||||
type: {class: SocialKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 696918149227610529
|
||||
type: {class: TutorialKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 696918149227610530
|
||||
type: {class: HazardKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 696918149227610531
|
||||
type: {class: OtherKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 696918149227610532
|
||||
type: {class: PuzzleKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
- rid: 1352971649185742937
|
||||
type: {class: ExplorationKind, ns: Nox.Game, asm: Assembly-CSharp}
|
||||
data:
|
||||
|
||||
@@ -27,7 +27,7 @@ Transform:
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 6.8, y: 0.036, z: 3.2}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_LocalScale: {x: 5, y: 5, z: 5}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 561419057474875478}
|
||||
|
||||
@@ -27,7 +27,7 @@ Transform:
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_LocalScale: {x: 5, y: 5, z: 5}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2570389272920721074}
|
||||
@@ -160,7 +160,7 @@ BoxCollider:
|
||||
serializedVersion: 3
|
||||
m_Size: {x: 10, y: 0.1, z: 10}
|
||||
m_Center: {x: 0, y: -0.05, z: 0}
|
||||
--- !u!1 &6524685290382203959
|
||||
--- !u!1 &2956314392319795661
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -168,44 +168,44 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 150169694567605380}
|
||||
- component: {fileID: 2034157458128204794}
|
||||
- component: {fileID: 553836549156001377}
|
||||
- component: {fileID: 2776493244321975856}
|
||||
m_Layer: 0
|
||||
m_Name: Wilderness
|
||||
m_Name: South Road
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &150169694567605380
|
||||
--- !u!4 &553836549156001377
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6524685290382203959}
|
||||
m_GameObject: {fileID: 2956314392319795661}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_LocalPosition: {x: 6.95, y: 0, z: 1.7}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 926535160506351424}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &2034157458128204794
|
||||
--- !u!114 &2776493244321975856
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6524685290382203959}
|
||||
m_GameObject: {fileID: 2956314392319795661}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp-firstpass::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: b90063b33444d214fb0dd99845d20da0, type: 2}
|
||||
--- !u!1 &7700777388891455207
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 263de6ccb4b79c340883df4a1d555220, type: 2}
|
||||
--- !u!1 &6249518700168291789
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -213,43 +213,133 @@ GameObject:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6771033229695015998}
|
||||
- component: {fileID: 961106205653816651}
|
||||
- component: {fileID: 1941104781230840099}
|
||||
- component: {fileID: 2137196945385592953}
|
||||
m_Layer: 0
|
||||
m_Name: RedMist
|
||||
m_Name: Codrii Vasluiului
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &6771033229695015998
|
||||
--- !u!4 &1941104781230840099
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7700777388891455207}
|
||||
m_GameObject: {fileID: 6249518700168291789}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_LocalPosition: {x: 2.46, y: 0, z: 0.06}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 926535160506351424}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &961106205653816651
|
||||
--- !u!114 &2137196945385592953
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7700777388891455207}
|
||||
m_GameObject: {fileID: 6249518700168291789}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp-firstpass::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 6ae6794d84d30a64393cdac41f6bd89c, type: 2}
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 4eefe490fd756c947b300b6f3d697df4, type: 2}
|
||||
--- !u!1 &6403256874069463078
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2399577652422057350}
|
||||
- component: {fileID: 8103982541836116775}
|
||||
m_Layer: 0
|
||||
m_Name: RedMystEast_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &2399577652422057350
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6403256874069463078}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 7.289, y: 0, z: 3.48}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 926535160506351424}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &8103982541836116775
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6403256874069463078}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: b8ae323df4686334d91ad4d9b22f4159, type: 2}
|
||||
--- !u!1 &8299370824322124813
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2976819594879442214}
|
||||
- component: {fileID: 5245688421302175395}
|
||||
m_Layer: 0
|
||||
m_Name: Thievs Corener
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &2976819594879442214
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8299370824322124813}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 3.31, y: 0, z: 6.1}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 926535160506351424}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &5245688421302175395
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8299370824322124813}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 499c88e149852754ab0a9a5d551f7eea, type: 2}
|
||||
--- !u!1 &8990714869477060382
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -280,8 +370,10 @@ Transform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 150169694567605380}
|
||||
- {fileID: 6771033229695015998}
|
||||
- {fileID: 2399577652422057350}
|
||||
- {fileID: 553836549156001377}
|
||||
- {fileID: 1941104781230840099}
|
||||
- {fileID: 2976819594879442214}
|
||||
m_Father: {fileID: 3836152601157972426}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &2165519840843418706
|
||||
|
||||
@@ -11,7 +11,7 @@ GameObject:
|
||||
- component: {fileID: 3883127191764828975}
|
||||
- component: {fileID: 5653379054112650511}
|
||||
- component: {fileID: 6148308834396474161}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Panel
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -86,7 +86,7 @@ GameObject:
|
||||
- component: {fileID: 4012222738582012437}
|
||||
- component: {fileID: 3249071694086606283}
|
||||
- component: {fileID: 729116634618204491}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Text (TMP)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -222,7 +222,7 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 660494562578339291}
|
||||
- component: {fileID: 1480157237527998548}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Party
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -239,7 +239,7 @@ Transform:
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 1.07, y: 0.036, z: 1.95}
|
||||
m_LocalScale: {x: 0.5, y: 0.5, z: 0.5}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4401955371165223074}
|
||||
@@ -268,7 +268,7 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 4401955371165223074}
|
||||
- component: {fileID: 3916039184069698551}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Square
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -361,7 +361,7 @@ GameObject:
|
||||
- component: {fileID: 1438956083575921239}
|
||||
- component: {fileID: 846436570382128832}
|
||||
- component: {fileID: 3223447814727493591}
|
||||
m_Layer: 0
|
||||
m_Layer: 9
|
||||
m_Name: Canvas
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
|
||||
@@ -137,6 +137,126 @@ MonoBehaviour:
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &5634975795126131359
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7922870055581323619}
|
||||
- component: {fileID: 8449579385697103291}
|
||||
- component: {fileID: 680329226197598615}
|
||||
- component: {fileID: 4831999132806256115}
|
||||
m_Layer: 5
|
||||
m_Name: Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7922870055581323619
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5634975795126131359}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 5249586234698548752}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0.16499329, y: 0.000025749207}
|
||||
m_SizeDelta: {x: 0.32999, y: 0.046}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8449579385697103291
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5634975795126131359}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &680329226197598615
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5634975795126131359}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &4831999132806256115
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5634975795126131359}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 0}
|
||||
m_HighlightedColor: {r: 0, g: 0, b: 0, a: 0.31764707}
|
||||
m_PressedColor: {r: 0, g: 0, b: 0, a: 0.4627451}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 0}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 680329226197598615}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &5906514193138747816
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -150,7 +270,7 @@ GameObject:
|
||||
- component: {fileID: 1953891385698885950}
|
||||
- component: {fileID: -3146704326252051464}
|
||||
m_Layer: 5
|
||||
m_Name: Answers
|
||||
m_Name: AnswerReference
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -170,6 +290,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 1760747944163223242}
|
||||
- {fileID: 7428637175488290741}
|
||||
- {fileID: 7922870055581323619}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
@@ -219,6 +340,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.AnswerPrefab
|
||||
number: {fileID: 572009535857796838}
|
||||
dialogText: {fileID: 3412282869295099737}
|
||||
button: {fileID: 4831999132806256115}
|
||||
--- !u!1 &6684607543305325759
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -108,10 +108,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7039252932434566139}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.48286262}
|
||||
m_AnchorMax: {x: 0.6627312, y: 1}
|
||||
m_AnchoredPosition: {x: 0.9819946, y: -38.289}
|
||||
m_SizeDelta: {x: 1.62, y: -42.331}
|
||||
m_AnchorMin: {x: 0, y: 0.43427482}
|
||||
m_AnchorMax: {x: 0.6627312, y: 0.8264123}
|
||||
m_AnchoredPosition: {x: 0.9819946, y: 1.0250015}
|
||||
m_SizeDelta: {x: 1.62, y: 0.68299866}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &502023626210869990
|
||||
CanvasRenderer:
|
||||
@@ -169,12 +169,12 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 13.1
|
||||
m_fontSize: 15.55
|
||||
m_fontSizeBase: 36
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 7.8
|
||||
m_fontSizeMax: 13.1
|
||||
m_fontSizeMax: 15.55
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 1
|
||||
m_VerticalAlignment: 256
|
||||
@@ -208,7 +208,7 @@ MonoBehaviour:
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 26.800018, y: 6.367523, z: 6.807068, w: 0}
|
||||
m_margin: {x: 36.54596, y: 6.367523, z: 6.807068, w: 7.7981873}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
@@ -248,7 +248,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.6627312, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -14.5, y: 0.000015258789}
|
||||
m_AnchoredPosition: {x: -14.5, y: 0.000030517578}
|
||||
m_SizeDelta: {x: -32.578293, y: -65.75531}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2681516528620939688
|
||||
@@ -323,7 +323,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.8264123}
|
||||
m_AnchorMax: {x: 0.6627312, y: 1}
|
||||
m_AnchoredPosition: {x: -0.80999756, y: -15.64299}
|
||||
m_AnchoredPosition: {x: -0.80999756, y: -15.642975}
|
||||
m_SizeDelta: {x: 1.62, y: -33.7364}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3458576624150439327
|
||||
@@ -381,11 +381,11 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 26.9
|
||||
m_fontSize: 45.4
|
||||
m_fontSizeBase: 36
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 6.97
|
||||
m_fontSizeMin: 13.94
|
||||
m_fontSizeMax: 45.4
|
||||
m_fontStyle: 1
|
||||
m_HorizontalAlignment: 2
|
||||
@@ -458,10 +458,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7039252932434566139}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.057000004}
|
||||
m_AnchorMax: {x: 0.6627312, y: 0.43427482}
|
||||
m_AnchoredPosition: {x: 0.9819946, y: 13.1859}
|
||||
m_SizeDelta: {x: 1.62, y: -26.6202}
|
||||
m_AnchoredPosition: {x: 0.9819946, y: -0.81417847}
|
||||
m_SizeDelta: {x: 1.62, y: 1.3798008}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7326620177005582428
|
||||
CanvasRenderer:
|
||||
@@ -654,8 +654,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -510.3443, y: -262.6666}
|
||||
m_AnchoredPosition: {x: -12.132874, y: 7.188507}
|
||||
m_SizeDelta: {x: -534.61, y: -277.0435}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7810011449154573601
|
||||
CanvasRenderer:
|
||||
|
||||
@@ -1124,14 +1124,14 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 36
|
||||
m_fontSize: 32.83
|
||||
m_fontSizeBase: 36
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 4.7
|
||||
m_fontSizeMax: 32.83
|
||||
m_fontStyle: 33
|
||||
m_HorizontalAlignment: 4
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
@@ -1429,10 +1429,10 @@ RectTransform:
|
||||
- {fileID: 8599248206773371971}
|
||||
m_Father: {fileID: 8590246171855584120}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.10939136, y: 0.5}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0.32239145, y: 0.5}
|
||||
m_AnchoredPosition: {x: -23.35254, y: 0.80260015}
|
||||
m_SizeDelta: {x: -48.2798, y: 86.361}
|
||||
m_AnchoredPosition: {x: -24.620117, y: 2.125}
|
||||
m_SizeDelta: {x: -45.73, y: 60.911}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &542021207025892353
|
||||
CanvasRenderer:
|
||||
|
||||
@@ -374,141 +374,6 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &899987077
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 899987078}
|
||||
- component: {fileID: 899987079}
|
||||
m_Layer: 0
|
||||
m_Name: RedMystEast_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &899987078
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 899987077}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 7.289, y: 0, z: 3.48}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2065324670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &899987079
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 899987077}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: b8ae323df4686334d91ad4d9b22f4159, type: 2}
|
||||
--- !u!1 &1037239273
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1037239274}
|
||||
- component: {fileID: 1037239275}
|
||||
m_Layer: 0
|
||||
m_Name: Codrii Vasluiului
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1037239274
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1037239273}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 2.46, y: 0, z: 0.06}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2065324670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1037239275
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1037239273}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 4eefe490fd756c947b300b6f3d697df4, type: 2}
|
||||
--- !u!1 &1056456186
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1056456187}
|
||||
- component: {fileID: 1056456188}
|
||||
m_Layer: 0
|
||||
m_Name: Thievs Corener
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1056456187
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1056456186}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 3.31, y: 0, z: 6.1}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2065324670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1056456188
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1056456186}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 499c88e149852754ab0a9a5d551f7eea, type: 2}
|
||||
--- !u!1 &1342894008
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -555,51 +420,6 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1853369441
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1853369442}
|
||||
- component: {fileID: 1853369443}
|
||||
m_Layer: 0
|
||||
m_Name: South Road
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1853369442
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1853369441}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 6.95, y: 0, z: 1.7}
|
||||
m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2065324670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1853369443
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1853369441}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 95af4f7ff0649854598833eabd84f131, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Jovian.ZoneSystem::Jovian.ZoneSystem.ZoneInstance
|
||||
data: {fileID: 11400000, guid: 263de6ccb4b79c340883df4a1d555220, type: 2}
|
||||
--- !u!1001 &2065324669
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -608,34 +428,10 @@ PrefabInstance:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 150169694567605380, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 4.31
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 150169694567605380, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: -2.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 701269181302450206, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: MapRef
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2034157458128204794, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: data
|
||||
value:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3836152601157972426, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3836152601157972426, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3836152601157972426, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3836152601157972426, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 0
|
||||
@@ -676,42 +472,11 @@ PrefabInstance:
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6524685290382203959, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_IsActive
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6771033229695015998, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 4.91
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6771033229695015998, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: -2.08
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects:
|
||||
- {fileID: 7700777388891455207, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
- {fileID: 6524685290382203959, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
m_AddedGameObjects:
|
||||
- targetCorrespondingSourceObject: {fileID: 926535160506351424, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 899987078}
|
||||
- targetCorrespondingSourceObject: {fileID: 926535160506351424, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1853369442}
|
||||
- targetCorrespondingSourceObject: {fileID: 926535160506351424, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1037239274}
|
||||
- targetCorrespondingSourceObject: {fileID: 926535160506351424, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1056456187}
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
--- !u!4 &2065324670 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 926535160506351424, guid: e152c7d154fb4da419d61245d532e8ca, type: 3}
|
||||
m_PrefabInstance: {fileID: 2065324669}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1001 &2263217030056493631
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -724,18 +489,6 @@ PrefabInstance:
|
||||
propertyPath: m_Name
|
||||
value: Map_POI
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5146284940501144587, guid: a7f9155cd9331e542abcf7e18d5dc9ce, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5146284940501144587, guid: a7f9155cd9331e542abcf7e18d5dc9ce, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5146284940501144587, guid: a7f9155cd9331e542abcf7e18d5dc9ce, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5146284940501144587, guid: a7f9155cd9331e542abcf7e18d5dc9ce, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 0
|
||||
@@ -793,10 +546,6 @@ PrefabInstance:
|
||||
propertyPath: m_Name
|
||||
value: GUI
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3496454433051527820, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_Enabled
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5178544305442097730, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_Pivot.x
|
||||
value: 0
|
||||
@@ -877,50 +626,6 @@ PrefabInstance:
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSize
|
||||
value: 32.83
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSizeMax
|
||||
value: 32.83
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSizeMin
|
||||
value: 4.7
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_enableAutoSizing
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_HorizontalAlignment
|
||||
value: 2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 0.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: -45.73
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 60.911
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: -24.620117
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 2.125
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
@@ -934,18 +639,6 @@ PrefabInstance:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 0}
|
||||
m_Modifications:
|
||||
- target: {fileID: 660494562578339291, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 660494562578339291, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 660494562578339291, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 660494562578339291, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 36.49
|
||||
@@ -986,30 +679,10 @@ PrefabInstance:
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1024083357468631475, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 2537578862943088419, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4905655696927363011, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Party
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4905655696927363011, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4999717812512709540, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5077922024062410584, guid: 3a1b48d52adea3347acf44510bdb6fc3, type: 3}
|
||||
propertyPath: m_Layer
|
||||
value: 9
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
|
||||
24
Packages/com.jovian.tag-system/.gitignore
vendored
Normal file
24
Packages/com.jovian.tag-system/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
### Unity
|
||||
# Unity generated directories
|
||||
/[Ll]ibrary/
|
||||
/[Tt]emp/
|
||||
/[Oo]bj/
|
||||
/[Bb]uild/
|
||||
/[Bb]uilds/
|
||||
/[Ll]ogs/
|
||||
/[Uu]ser[Ss]ettings/
|
||||
/[Mm]emory[Cc]aptures/
|
||||
|
||||
# Asset meta data should only be ignored when the corresponding asset is also ignored
|
||||
!/[Aa]ssets/**/*.meta
|
||||
|
||||
# Build output
|
||||
*.apk
|
||||
*.aab
|
||||
*.unitypackage
|
||||
|
||||
# Autogenerated solution and project files
|
||||
*.csproj
|
||||
*.unityproj
|
||||
*.sln
|
||||
*.suo
|
||||
8
Packages/com.jovian.tag-system/Editor.meta
Normal file
8
Packages/com.jovian.tag-system/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0acbee4371cc244b8e5b10e1bbab803
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Jovian.TagSystem.Editor",
|
||||
"rootNamespace": "Jovian.TagSystem.Editor",
|
||||
"references": [
|
||||
"Jovian.TagSystem"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 941a740b30b4595478b5e69393ffa045
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
[InitializeOnLoad]
|
||||
public class JovianTagsEditorHistory {
|
||||
private readonly int numRecentTags = 20;
|
||||
private readonly string saveKey = "EditorSave";
|
||||
private readonly string saveDelimiter = "%£&";
|
||||
|
||||
public static JovianTagsEditorHistory Instance { get; }
|
||||
|
||||
public List<string> RecentTags { get; private set; } = new();
|
||||
|
||||
static JovianTagsEditorHistory() {
|
||||
Instance = new JovianTagsEditorHistory();
|
||||
}
|
||||
|
||||
private JovianTagsEditorHistory() {
|
||||
EditorApplication.delayCall += Load;
|
||||
}
|
||||
|
||||
public void AddRecentTag(string tag) {
|
||||
RecentTags.Insert(0, tag);
|
||||
while(RecentTags.Count > numRecentTags) {
|
||||
RecentTags.RemoveAt(RecentTags.Count - 1);
|
||||
}
|
||||
Save();
|
||||
}
|
||||
|
||||
private void Load() {
|
||||
var saves = EditorPrefs.GetString(Application.productName + saveKey);
|
||||
RecentTags = saves.Split(saveDelimiter).ToList();
|
||||
if(RecentTags == null) {
|
||||
RecentTags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Save() {
|
||||
EditorPrefs.SetString(Application.productName + saveKey, string.Join(saveDelimiter, RecentTags));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45efc4b96d295b84498b0fed0c4130d1
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
public static class JovianTagsEditorUtility {
|
||||
public static JovianTagsSettings[] GetSettings() {
|
||||
var settingsPaths = AssetDatabase.FindAssets("t:JovianTagsSettings");
|
||||
if(settingsPaths.Length == 0) {
|
||||
return Array.Empty<JovianTagsSettings>();
|
||||
}
|
||||
|
||||
var tagSettings = new List<JovianTagsSettings>();
|
||||
for(int i = 0, n = settingsPaths.Length; i < n; i++) {
|
||||
var guid = settingsPaths[i];
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var setting = AssetDatabase.LoadAssetAtPath<JovianTagsSettings>(path);
|
||||
tagSettings.Add(setting);
|
||||
}
|
||||
|
||||
return tagSettings.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2a32b341a059664bb365498df0d6bdf
|
||||
630
Packages/com.jovian.tag-system/Editor/JovianTagsEditorWindow.cs
Normal file
630
Packages/com.jovian.tag-system/Editor/JovianTagsEditorWindow.cs
Normal file
@@ -0,0 +1,630 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
public class JovianTagsEditorWindow : EditorWindow {
|
||||
|
||||
// --- Colors ---
|
||||
private static readonly Color DividerColor = new(1f, 1f, 1f, 0.12f);
|
||||
private static readonly Color GreenButton = new(0.3f, 0.8f, 0.3f);
|
||||
private static readonly Color HeaderBg = new(0.18f, 0.18f, 0.18f, 1f);
|
||||
private static readonly Color RowEven = new(0f, 0f, 0f, 0f);
|
||||
private static readonly Color RowOdd = new(1f, 1f, 1f, 0.03f);
|
||||
private static readonly Color RowHover = new(0.3f, 0.5f, 0.8f, 0.12f);
|
||||
private static readonly Color ChildIndentLine = new(1f, 1f, 1f, 0.08f);
|
||||
|
||||
// --- Constants ---
|
||||
private const int RowHeight = 22;
|
||||
private const int HeaderHeight = 26;
|
||||
private const int DepthIndent = 20;
|
||||
|
||||
// --- Cached layout options ---
|
||||
private static readonly GUILayoutOption RowHeightOpt = GUILayout.Height(RowHeight);
|
||||
private static readonly GUILayoutOption HeaderHeightOpt = GUILayout.Height(HeaderHeight);
|
||||
private static readonly GUILayoutOption ExpandWidth = GUILayout.ExpandWidth(true);
|
||||
private static readonly GUILayoutOption SmallBtnWidth = GUILayout.Width(22);
|
||||
private static readonly GUILayoutOption SmallBtnHeight = GUILayout.Height(18);
|
||||
|
||||
// --- State ---
|
||||
private Vector2 scrollPosition;
|
||||
private string searchFilter = "";
|
||||
private string newTagName = "";
|
||||
private string outputPath;
|
||||
private readonly Dictionary<string, bool> foldoutState = new();
|
||||
private bool scrollToBottom;
|
||||
private JovianTagsSettings[] allSettings;
|
||||
private int rowIndex;
|
||||
|
||||
// --- Styles (lazy init) ---
|
||||
private GUIStyle _tagStyle;
|
||||
|
||||
private GUIStyle TagStyle => _tagStyle ??= new GUIStyle(EditorStyles.label) {
|
||||
fontSize = 12, fixedHeight = RowHeight, padding = new RectOffset(2, 2, 2, 2)
|
||||
};
|
||||
|
||||
private GUIStyle _headerStyle;
|
||||
|
||||
private GUIStyle HeaderStyle => _headerStyle ??= new GUIStyle(EditorStyles.boldLabel) {
|
||||
fontSize = 12, fixedHeight = HeaderHeight, padding = new RectOffset(2, 2, 4, 4)
|
||||
};
|
||||
|
||||
private GUIStyle _pathStyle;
|
||||
|
||||
private GUIStyle PathStyle => _pathStyle ??= new GUIStyle(EditorStyles.miniLabel) {
|
||||
fontSize = 10, fixedHeight = RowHeight,
|
||||
normal = { textColor = new Color(1f, 1f, 1f, 0.3f) },
|
||||
padding = new RectOffset(4, 4, 4, 2)
|
||||
};
|
||||
|
||||
// --- Tree structure ---
|
||||
private class TagTreeNode {
|
||||
public string Name; // segment name (e.g., "Fire")
|
||||
public string FullPath; // full dotted path (e.g., "Damage.Fire")
|
||||
public bool IsRegistered; // exists in GameTagSettings
|
||||
public List<TagTreeNode> Children = new();
|
||||
}
|
||||
|
||||
[MenuItem("Jovian/Tag System/Tag Editor...")]
|
||||
public static void ShowWindow() {
|
||||
var window = GetWindow<JovianTagsEditorWindow>(true, "Tag Editor");
|
||||
window.minSize = new Vector2(480, 380);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable() { RefreshData(); }
|
||||
private void OnFocus() { RefreshData(); }
|
||||
|
||||
private void RefreshData() {
|
||||
allSettings = JovianTagsEditorUtility.GetSettings();
|
||||
outputPath = JovianTagsGenerator.FindOrDefaultOutputPath();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.BeginVertical();
|
||||
GUILayout.Space(8);
|
||||
|
||||
DrawToolbar();
|
||||
|
||||
if(scrollToBottom) {
|
||||
scrollPosition.y = float.MaxValue;
|
||||
scrollToBottom = false;
|
||||
}
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUIStyle.none, GUI.skin.verticalScrollbar);
|
||||
DrawContent();
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
DrawDivider();
|
||||
DrawAddTagRow();
|
||||
GUILayout.Space(4);
|
||||
DrawFooter();
|
||||
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndVertical();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ===== Toolbar =====
|
||||
|
||||
private void DrawToolbar() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Output:", GUILayout.Width(50));
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.TextField(outputPath);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
if(GUILayout.Button("...", GUILayout.Width(26), GUILayout.Height(18)))
|
||||
ChangeOutputPath();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(2);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Search:", GUILayout.Width(50));
|
||||
searchFilter = EditorGUILayout.TextField(searchFilter);
|
||||
if(!string.IsNullOrEmpty(searchFilter) && GUILayout.Button("✕", GUILayout.Width(20), GUILayout.Height(18))) {
|
||||
searchFilter = "";
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4);
|
||||
DrawDivider();
|
||||
}
|
||||
|
||||
// ===== Content =====
|
||||
|
||||
private void DrawContent() {
|
||||
if(allSettings.Length == 0) {
|
||||
GUILayout.Space(40);
|
||||
DrawCenteredMessage("No Tag Settings asset found.");
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("Create Tag Settings", GUILayout.Height(28), GUILayout.Width(180))) {
|
||||
CreateSettingsAsset();
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
return;
|
||||
}
|
||||
|
||||
var allTags = CollectAllTags();
|
||||
if(allTags.Count == 0) {
|
||||
GUILayout.Space(40);
|
||||
DrawCenteredMessage("No tags defined.\nAdd tags using the field below.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build tree
|
||||
var roots = BuildTree(allTags);
|
||||
|
||||
// Filter
|
||||
if(!string.IsNullOrEmpty(searchFilter)) {
|
||||
roots = FilterTree(roots, searchFilter);
|
||||
}
|
||||
|
||||
rowIndex = 0;
|
||||
foreach(var root in roots.OrderBy(r => r.Name, System.StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawTreeNode(root, 0);
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
var totalTags = allSettings.Sum(s => s.gameTags.Length);
|
||||
GUILayout.Label($"{totalTags} tags total", EditorStyles.centeredGreyMiniLabel);
|
||||
}
|
||||
|
||||
private void DrawTreeNode(TagTreeNode node, int depth) {
|
||||
var hasChildren = node.Children.Count > 0;
|
||||
var isRoot = depth == 0;
|
||||
|
||||
// --- Row ---
|
||||
if(isRoot) {
|
||||
// Root header
|
||||
var headerRect = EditorGUILayout.BeginHorizontal(HeaderHeightOpt);
|
||||
EditorGUI.DrawRect(headerRect, HeaderBg);
|
||||
GUILayout.Space(6);
|
||||
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
GUILayout.Label(node.Name, HeaderStyle, ExpandWidth);
|
||||
GUILayout.Label($"{CountDescendants(node)}", new GUIStyle(EditorStyles.miniLabel) {
|
||||
alignment = TextAnchor.MiddleRight,
|
||||
normal = { textColor = new Color(1, 1, 1, 0.4f) }
|
||||
}, GUILayout.Width(30));
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
GUILayout.Label(node.Name, HeaderStyle, ExpandWidth);
|
||||
}
|
||||
|
||||
DrawAddChildButton(node);
|
||||
DrawDeleteButton(node.FullPath);
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
else {
|
||||
// Child row
|
||||
var bgColor = rowIndex % 2 == 0 ? RowEven : RowOdd;
|
||||
var rect = EditorGUILayout.BeginHorizontal(RowHeightOpt);
|
||||
if(rect.Contains(Event.current.mousePosition)) bgColor = RowHover;
|
||||
if(bgColor.a > 0) EditorGUI.DrawRect(rect, bgColor);
|
||||
|
||||
// Indent with vertical guide lines
|
||||
GUILayout.Space(6 + depth * DepthIndent);
|
||||
if(Event.current.type == EventType.Repaint) {
|
||||
for(int d = 1; d <= depth; d++) {
|
||||
var lineX = 6 + d * DepthIndent - 10;
|
||||
EditorGUI.DrawRect(new Rect(lineX, rect.y, 1, rect.height), ChildIndentLine);
|
||||
}
|
||||
}
|
||||
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
}
|
||||
|
||||
GUILayout.Label(node.Name, TagStyle, ExpandWidth);
|
||||
|
||||
// Full path hint for deep tags
|
||||
if(depth > 1) {
|
||||
GUILayout.Label(node.FullPath, PathStyle,
|
||||
GUILayout.Width(Mathf.Min(250, position.width * 0.3f)));
|
||||
}
|
||||
|
||||
DrawAddChildButton(node);
|
||||
DrawDeleteButton(node.FullPath);
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
// --- Children ---
|
||||
if(!hasChildren) return;
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
if(!foldoutState[node.FullPath]) return;
|
||||
|
||||
foreach(var child in node.Children.OrderBy(c => c.Name, System.StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawTreeNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddChildButton(TagTreeNode parent) {
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("+", SmallBtnWidth, SmallBtnHeight)) {
|
||||
// Open a small input for the child name
|
||||
AddChildTagPopup.Show(parent.FullPath, childName => {
|
||||
var fullTag = parent.FullPath + "." + childName;
|
||||
AddTag(fullTag);
|
||||
});
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
}
|
||||
|
||||
private void DrawDeleteButton(string tag) {
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f);
|
||||
if(GUILayout.Button("✕", SmallBtnWidth, SmallBtnHeight)) {
|
||||
DeleteTag(tag);
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
}
|
||||
|
||||
// ===== Add Tag Row =====
|
||||
|
||||
private void DrawAddTagRow() {
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("New Tag:", GUILayout.Width(60));
|
||||
newTagName = EditorGUILayout.TextField(newTagName, GUILayout.Height(20));
|
||||
|
||||
string validationError = null;
|
||||
if(!string.IsNullOrWhiteSpace(newTagName)) {
|
||||
validationError = JovianTagsGenerator.ValidateTagName(newTagName.Trim());
|
||||
if(validationError == null && CollectAllTags().Contains(newTagName.Trim()))
|
||||
validationError = $"Tag '{newTagName.Trim()}' already exists.";
|
||||
}
|
||||
var canAdd = !string.IsNullOrWhiteSpace(newTagName) && validationError == null;
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!canAdd);
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("+ Add", GUILayout.Width(60), GUILayout.Height(20))) {
|
||||
AddTag(newTagName.Trim());
|
||||
newTagName = "";
|
||||
GUI.FocusControl(null);
|
||||
scrollToBottom = true;
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(newTagName) && validationError != null)
|
||||
EditorGUILayout.HelpBox(validationError, MessageType.Error);
|
||||
if(canAdd && newTagName.Contains(JovianTagsHandler.tagDelimiter))
|
||||
EditorGUILayout.HelpBox("Parent tags are created automatically.", MessageType.Info);
|
||||
|
||||
GUILayout.Space(4);
|
||||
}
|
||||
|
||||
// ===== Footer =====
|
||||
|
||||
private void DrawFooter() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("Save & Generate", GUILayout.Height(28), GUILayout.MinWidth(150)))
|
||||
SaveAndGenerate();
|
||||
GUI.backgroundColor = prevBg;
|
||||
if(GUILayout.Button("Clean Deleted In Assets", GUILayout.Height(28))) {
|
||||
CleanStaleTagsFromAllAssets();
|
||||
}
|
||||
if(GUILayout.Button("Refresh", GUILayout.Height(28), GUILayout.Width(70))) {
|
||||
RefreshData();
|
||||
Repaint();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ===== Tree Building =====
|
||||
|
||||
private List<TagTreeNode> BuildTree(List<string> allTags) {
|
||||
var rootNodes = new Dictionary<string, TagTreeNode>();
|
||||
var allNodes = new Dictionary<string, TagTreeNode>();
|
||||
|
||||
foreach(var tag in allTags) {
|
||||
var parts = tag.Split(JovianTagsHandler.tagDelimiter);
|
||||
var accumulated = "";
|
||||
TagTreeNode parent = null;
|
||||
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
accumulated = i == 0 ? parts[i] : accumulated + "." + parts[i];
|
||||
|
||||
if(!allNodes.TryGetValue(accumulated, out var node)) {
|
||||
node = new TagTreeNode {
|
||||
Name = parts[i],
|
||||
FullPath = accumulated,
|
||||
IsRegistered = allTags.Contains(accumulated)
|
||||
};
|
||||
allNodes[accumulated] = node;
|
||||
|
||||
if(parent != null)
|
||||
parent.Children.Add(node);
|
||||
else
|
||||
rootNodes[accumulated] = node;
|
||||
}
|
||||
parent = node;
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes.Values.ToList();
|
||||
}
|
||||
|
||||
private List<TagTreeNode> FilterTree(List<TagTreeNode> roots, string filter) {
|
||||
var result = new List<TagTreeNode>();
|
||||
foreach(var root in roots) {
|
||||
var filtered = FilterNode(root, filter);
|
||||
if(filtered != null) result.Add(filtered);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private TagTreeNode FilterNode(TagTreeNode node, string filter) {
|
||||
var matchesSelf = node.FullPath.IndexOf(filter, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var filteredChildren = new List<TagTreeNode>();
|
||||
|
||||
foreach(var child in node.Children) {
|
||||
var fc = FilterNode(child, filter);
|
||||
if(fc != null) filteredChildren.Add(fc);
|
||||
}
|
||||
|
||||
if(!matchesSelf && filteredChildren.Count == 0) return null;
|
||||
|
||||
return new TagTreeNode {
|
||||
Name = node.Name,
|
||||
FullPath = node.FullPath,
|
||||
IsRegistered = node.IsRegistered,
|
||||
Children = matchesSelf ? node.Children : filteredChildren
|
||||
};
|
||||
}
|
||||
|
||||
private int CountDescendants(TagTreeNode node) {
|
||||
int count = 0;
|
||||
foreach(var child in node.Children) {
|
||||
count++;
|
||||
count += CountDescendants(child);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ===== Actions =====
|
||||
|
||||
private void AddTag(string tagName) {
|
||||
if(allSettings.Length == 0) {
|
||||
EditorUtility.DisplayDialog("No Settings", "Create a JovianTagsSettings asset first.", "OK");
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(allSettings[0], "Add Tag");
|
||||
allSettings[0].AddGameTag(tagName);
|
||||
}
|
||||
|
||||
private void DeleteTag(string tag) {
|
||||
foreach(var s in allSettings) {
|
||||
if(!s.gameTags.Any(t => t.tag == tag)) continue;
|
||||
var hasChildren = s.gameTags.Any(t => t.tag != tag && t.tag.StartsWith(tag + "."));
|
||||
var message = hasChildren ? $"Delete '{tag}' and all children?" : $"Delete '{tag}'?";
|
||||
if(EditorUtility.DisplayDialog("Delete Tag", message, "Delete", "Cancel")) {
|
||||
Undo.RecordObject(s, "Remove Tag");
|
||||
s.RemoveTag(tag);
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSettingsAsset() {
|
||||
var folder = EditorUtility.OpenFolderPanel("Choose folder for Tag Settings", "Assets", "");
|
||||
if(string.IsNullOrEmpty(folder)) return;
|
||||
|
||||
var dataPath = Application.dataPath.Replace('\\', '/');
|
||||
folder = folder.Replace('\\', '/');
|
||||
if(folder.StartsWith(dataPath))
|
||||
folder = "Assets" + folder.Substring(dataPath.Length);
|
||||
|
||||
var asset = ScriptableObject.CreateInstance<JovianTagsSettings>();
|
||||
var path = $"{folder}/JovianTagsSettings.asset";
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.FocusProjectWindow();
|
||||
Selection.activeObject = asset;
|
||||
Debug.Log($"[Tag System] Created Tag Settings at {path}");
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void CleanStaleTagsFromAllAssets() {
|
||||
var validTags = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
foreach(var s in allSettings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
validTags.Add(t.tag);
|
||||
|
||||
int assetsScanned = 0;
|
||||
int assetsCleaned = 0;
|
||||
int tagsRemoved = 0;
|
||||
|
||||
// Scan all prefabs and ScriptableObjects
|
||||
var guids = AssetDatabase.FindAssets("t:GameObject t:ScriptableObject");
|
||||
// FindAssets with multiple types uses OR, but let's do them separately for clarity
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
var soGuids = AssetDatabase.FindAssets("t:ScriptableObject");
|
||||
var allGuids = new HashSet<string>(prefabGuids);
|
||||
foreach(var g in soGuids) allGuids.Add(g);
|
||||
|
||||
foreach(var guid in allGuids) {
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
assetsScanned++;
|
||||
|
||||
foreach(var asset in assets) {
|
||||
if(asset == null) continue;
|
||||
var so = new SerializedObject(asset);
|
||||
var cleaned = CleanSerializedObject(so, validTags);
|
||||
if(cleaned > 0) {
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(asset);
|
||||
assetsCleaned++;
|
||||
tagsRemoved += cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
EditorUtility.DisplayDialog("Clean Complete",
|
||||
$"Scanned {assetsScanned} assets.\n" +
|
||||
$"Cleaned {assetsCleaned} assets.\n" +
|
||||
$"Removed {tagsRemoved} stale tag references.",
|
||||
"OK");
|
||||
}
|
||||
|
||||
private static int CleanSerializedObject(SerializedObject so, HashSet<string> validTags) {
|
||||
int totalRemoved = 0;
|
||||
var prop = so.GetIterator();
|
||||
|
||||
while(prop.NextVisible(true)) {
|
||||
// Look for string arrays named "tags" inside JovianTagsGroup structs
|
||||
if(prop.isArray && prop.propertyType == SerializedPropertyType.String) continue;
|
||||
if(prop.propertyType != SerializedPropertyType.Generic) continue;
|
||||
if(!prop.type.Contains("JovianTagsGroup")) continue;
|
||||
|
||||
var tagsProp = prop.FindPropertyRelative("tags");
|
||||
if(tagsProp == null || !tagsProp.isArray) continue;
|
||||
|
||||
for(int i = tagsProp.arraySize - 1; i >= 0; i--) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val) && !validTags.Contains(val)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
totalRemoved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalRemoved;
|
||||
}
|
||||
|
||||
private void SaveAndGenerate() {
|
||||
foreach(var s in allSettings) EditorUtility.SetDirty(s);
|
||||
AssetDatabase.SaveAssets();
|
||||
JovianTagsGenerator.Regenerate(outputPath);
|
||||
}
|
||||
|
||||
private void ChangeOutputPath() {
|
||||
var newPath = EditorUtility.SaveFilePanel("Choose Generated File Location",
|
||||
System.IO.Path.GetDirectoryName(outputPath) ?? "Assets",
|
||||
System.IO.Path.GetFileName(outputPath) ?? "GameTags.cs", "cs");
|
||||
if(string.IsNullOrEmpty(newPath)) return;
|
||||
var dataPath = Application.dataPath.Replace('\\', '/');
|
||||
newPath = newPath.Replace('\\', '/');
|
||||
if(newPath.StartsWith(dataPath))
|
||||
newPath = "Assets" + newPath.Substring(dataPath.Length);
|
||||
outputPath = newPath;
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
private List<string> CollectAllTags() {
|
||||
var tags = new SortedSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||||
foreach(var s in allSettings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
tags.Add(t.tag);
|
||||
return tags.ToList();
|
||||
}
|
||||
|
||||
private static void DrawDivider() {
|
||||
var rect = EditorGUILayout.GetControlRect(false, 1);
|
||||
EditorGUI.DrawRect(rect, DividerColor);
|
||||
}
|
||||
|
||||
private static void DrawCenteredMessage(string message) {
|
||||
var style = new GUIStyle(EditorStyles.label) {
|
||||
alignment = TextAnchor.MiddleCenter, wordWrap = true, fontSize = 12,
|
||||
normal = { textColor = new Color(1, 1, 1, 0.4f) }
|
||||
};
|
||||
EditorGUILayout.LabelField(message, style, GUILayout.Height(60));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Add Child Tag Popup =====
|
||||
|
||||
public class AddChildTagPopup : EditorWindow {
|
||||
private string parentPath;
|
||||
private string childName = "";
|
||||
private System.Action<string> onConfirm;
|
||||
|
||||
public static void Show(string parentPath, System.Action<string> onConfirm) {
|
||||
var popup = CreateInstance<AddChildTagPopup>();
|
||||
popup.parentPath = parentPath;
|
||||
popup.onConfirm = onConfirm;
|
||||
popup.titleContent = new GUIContent("Add Child Tag");
|
||||
var size = new Vector2(300, 90);
|
||||
var mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
|
||||
popup.position = new Rect(mousePos.x - size.x * 0.5f, mousePos.y, size.x, size.y);
|
||||
popup.ShowUtility();
|
||||
popup.Focus();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label($"Parent: {parentPath}", EditorStyles.boldLabel);
|
||||
GUILayout.Space(4);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Name:", GUILayout.Width(42));
|
||||
GUI.SetNextControlName("ChildNameField");
|
||||
childName = EditorGUILayout.TextField(childName);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUI.FocusTextInControl("ChildNameField");
|
||||
|
||||
var error = string.IsNullOrWhiteSpace(childName)
|
||||
? null
|
||||
: JovianTagsGenerator.ValidateTagSegment(childName.Trim());
|
||||
var canAdd = !string.IsNullOrWhiteSpace(childName) && error == null;
|
||||
|
||||
GUILayout.Space(4);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUI.BeginDisabledGroup(!canAdd);
|
||||
if(GUILayout.Button("Add", GUILayout.Height(22)) || (canAdd && Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return)) {
|
||||
onConfirm?.Invoke(childName.Trim());
|
||||
Close();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
if(GUILayout.Button("Cancel", GUILayout.Height(22))) {
|
||||
Close();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if(Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9190068b05141d74aae0f630a8080f23
|
||||
300
Packages/com.jovian.tag-system/Editor/JovianTagsGenerator.cs
Normal file
300
Packages/com.jovian.tag-system/Editor/JovianTagsGenerator.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
/// <summary>
|
||||
/// Generates a C# file with static GameTag fields organized in a nested class hierarchy.
|
||||
/// Fields are resolved at runtime via JovianTagsHandler. Manual additions are preserved.
|
||||
/// </summary>
|
||||
public static class JovianTagsGenerator {
|
||||
|
||||
private const string DefaultClassName = "GameTags";
|
||||
private const string DefaultOutputPath = "Assets/GameTags.cs";
|
||||
private const string TagFieldPostfix = "_Tag";
|
||||
|
||||
/// <summary>
|
||||
/// Collects all tag names from all GameTagSettings assets in the project.
|
||||
/// </summary>
|
||||
public static List<string> CollectAllTags() {
|
||||
var allTags = new HashSet<string>(StringComparer.Ordinal);
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
foreach(var s in settings) {
|
||||
foreach(var t in s.gameTags) {
|
||||
if(!string.IsNullOrEmpty(t.tag)) {
|
||||
allTags.Add(t.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allTags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the C# source content for the strong-typed tag fields.
|
||||
/// </summary>
|
||||
public static string GenerateFileContent(List<string> tagNames, string className) {
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header
|
||||
sb.AppendLine("//------------------------------------------------------------------------------");
|
||||
sb.AppendLine("// Generated by Jovian Game Tag System");
|
||||
sb.AppendLine("// Manual edits are preserved — fields added here will be kept on regeneration.");
|
||||
sb.AppendLine($"// Last generated on {DateTime.Now:yyyy-MM-dd HH:mm}");
|
||||
sb.AppendLine("//------------------------------------------------------------------------------");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using Jovian.TagSystem;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"public static partial class {className} {{");
|
||||
|
||||
// Build tree structure from dot-delimited names
|
||||
var root = new TagNode("", "");
|
||||
foreach(var fullName in tagNames) {
|
||||
var parts = fullName.Split(JovianTagsHandler.tagDelimiter);
|
||||
var current = root;
|
||||
var accumulated = "";
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
var part = parts[i];
|
||||
accumulated = i == 0 ? part : accumulated + "." + part;
|
||||
if(!current.Children.TryGetValue(part, out var child)) {
|
||||
child = new TagNode(part, accumulated);
|
||||
current.Children[part] = child;
|
||||
}
|
||||
current = child;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate nested classes from tree
|
||||
GenerateNode(sb, root, 1);
|
||||
|
||||
// Generate initializer methods
|
||||
var allNodes = FlattenNodes(root);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" private static readonly string[] AllTags = {");
|
||||
for(int i = 0; i < allNodes.Count; i++) {
|
||||
var comma = i < allNodes.Count - 1 ? "," : "";
|
||||
sb.AppendLine($" \"{allNodes[i].FullName}\"{comma}");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]");
|
||||
sb.AppendLine(" private static void RuntimeInitialize() {");
|
||||
sb.AppendLine(" JovianTagsHandler.Initialize();");
|
||||
sb.AppendLine(" JovianTagsHandler.RegisterTags(AllTags);");
|
||||
foreach(var node in allNodes) {
|
||||
sb.AppendLine($" {node.FieldPath} = JovianTagsHandler.GetTag(\"{node.FullName}\");");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#if UNITY_EDITOR");
|
||||
sb.AppendLine(" [UnityEditor.InitializeOnLoadMethod]");
|
||||
sb.AppendLine(" private static void EditorInitialize() {");
|
||||
sb.AppendLine(" RuntimeInitialize();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("#endif");
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void GenerateNode(StringBuilder sb, TagNode node, int depth) {
|
||||
var indent = new string(' ', depth * 4);
|
||||
|
||||
foreach(var child in node.Children.Values.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
var safeName = SanitizeIdentifier(child.Name);
|
||||
|
||||
if(child.Children.Count > 0) {
|
||||
// Node with children → nested class + field
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"{indent}public static class {safeName} {{");
|
||||
sb.AppendLine($"{indent} public static JovianTag {safeName}{TagFieldPostfix};");
|
||||
GenerateNode(sb, child, depth + 1);
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
else {
|
||||
// Leaf → just a field
|
||||
sb.AppendLine($"{indent}public static JovianTag {safeName}{TagFieldPostfix};");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TagNode> FlattenNodes(TagNode root) {
|
||||
var result = new List<TagNode>();
|
||||
foreach(var child in root.Children.Values.OrderBy(c => c.FullName, StringComparer.OrdinalIgnoreCase)) {
|
||||
FlattenRecursive(child, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void FlattenRecursive(TagNode node, List<TagNode> result) {
|
||||
result.Add(node);
|
||||
foreach(var child in node.Children.Values.OrderBy(c => c.FullName, StringComparer.OrdinalIgnoreCase)) {
|
||||
FlattenRecursive(child, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the generated file and triggers AssetDatabase refresh.
|
||||
/// </summary>
|
||||
public static void WriteAndRefresh(string content, string outputPath) {
|
||||
outputPath = outputPath.Replace('\\', '/');
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(outputPath, content);
|
||||
Debug.Log($"[Game Tag System] Generated tag file at: {outputPath}");
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full regeneration from current GameTagSettings.
|
||||
/// </summary>
|
||||
public static void Regenerate(string outputPath = null) {
|
||||
if(string.IsNullOrEmpty(outputPath)) {
|
||||
outputPath = FindOrDefaultOutputPath();
|
||||
}
|
||||
var className = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var tags = CollectAllTags();
|
||||
if(tags.Count == 0) {
|
||||
Debug.LogWarning("[Game Tag System] No tags found in any GameTagSettings asset.");
|
||||
return;
|
||||
}
|
||||
var content = GenerateFileContent(tags, className);
|
||||
WriteAndRefresh(content, outputPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for an existing generated file or returns a default path.
|
||||
/// </summary>
|
||||
public static string FindOrDefaultOutputPath() {
|
||||
var guids = AssetDatabase.FindAssets($"{DefaultClassName} t:MonoScript");
|
||||
if(guids.Length > 0) {
|
||||
return AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
}
|
||||
return DefaultOutputPath;
|
||||
}
|
||||
|
||||
[MenuItem("Jovian/Tag System/Regenerate Tag Constants")]
|
||||
private static void MenuRegenerate() {
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal) {
|
||||
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char",
|
||||
"checked", "class", "const", "continue", "decimal", "default", "delegate", "do",
|
||||
"double", "else", "enum", "event", "explicit", "extern", "false", "finally",
|
||||
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int",
|
||||
"interface", "internal", "is", "lock", "long", "namespace", "new", "null",
|
||||
"object", "operator", "out", "override", "params", "private", "protected",
|
||||
"public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
|
||||
"stackalloc", "static", "string", "struct", "switch", "this", "throw", "true",
|
||||
"try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
|
||||
"virtual", "void", "volatile", "while"
|
||||
};
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex ValidIdentifierRegex =
|
||||
new(@"^[A-Za-z][A-Za-z0-9_]*$");
|
||||
|
||||
/// <summary>
|
||||
/// Validates a single tag segment (one part between dots).
|
||||
/// Returns null if valid, or an error message if invalid.
|
||||
/// </summary>
|
||||
public static string ValidateTagSegment(string segment) {
|
||||
if(string.IsNullOrWhiteSpace(segment))
|
||||
return "Tag name cannot be empty.";
|
||||
|
||||
if(!ValidIdentifierRegex.IsMatch(segment))
|
||||
return $"'{segment}' must start with a letter and contain only letters, digits, or underscores.";
|
||||
|
||||
if(CSharpKeywords.Contains(segment))
|
||||
return $"'{segment}' is a C# reserved keyword.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a full dot-delimited tag name. Checks each segment.
|
||||
/// Returns null if valid, or an error message for the first invalid segment.
|
||||
/// </summary>
|
||||
public static string ValidateTagName(string tagName) {
|
||||
if(string.IsNullOrWhiteSpace(tagName))
|
||||
return "Tag name cannot be empty.";
|
||||
|
||||
var segments = tagName.Split(JovianTagsHandler.tagDelimiter);
|
||||
foreach(var segment in segments) {
|
||||
var error = ValidateTagSegment(segment);
|
||||
if(error != null) return error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string SanitizeIdentifier(string name) {
|
||||
if(string.IsNullOrWhiteSpace(name)) return "_Empty";
|
||||
var sanitized = System.Text.RegularExpressions.Regex.Replace(name.Trim(), @"[^A-Za-z0-9_]", "_");
|
||||
if(char.IsDigit(sanitized[0])) sanitized = "_" + sanitized;
|
||||
if(CSharpKeywords.Contains(sanitized)) sanitized = "_" + sanitized;
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private class TagNode {
|
||||
public string Name;
|
||||
public string FullName;
|
||||
public Dictionary<string, TagNode> Children = new(StringComparer.Ordinal);
|
||||
|
||||
public TagNode(string name, string fullName) {
|
||||
Name = name;
|
||||
FullName = fullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the fully qualified C# field path for this node's tag field.
|
||||
/// Nodes with children are nested classes, so the path includes the class hierarchy.
|
||||
/// Leaf nodes at root level are just field names.
|
||||
/// E.g., "Character.Player" → "Character.Player.Player_Tag"
|
||||
/// E.g., "big" (leaf, no children) → "big_Tag"
|
||||
/// </summary>
|
||||
public string FieldPath {
|
||||
get {
|
||||
var parts = FullName.Split(JovianTagsHandler.tagDelimiter);
|
||||
var safeParts = parts.Select(p =>
|
||||
System.Text.RegularExpressions.Regex.Replace(p.Trim(), @"[^A-Za-z0-9_]", "_")).ToArray();
|
||||
var lastSafe = safeParts[^1];
|
||||
|
||||
// Nodes with children become classes; their _Tag field lives inside the class.
|
||||
// Leaf nodes are just fields inside their parent class.
|
||||
//
|
||||
// Examples (→ = generates):
|
||||
// "big" (root leaf) → big_Tag
|
||||
// "Damage" (root with children) → Damage.Damage_Tag
|
||||
// "Damage.Fire" (child leaf) → Damage.Fire_Tag
|
||||
// "Damage.Fire.AOE" (deep leaf) → Damage.Fire.AOE_Tag
|
||||
// "Damage.Fire" (with children) → Damage.Fire.Fire_Tag
|
||||
|
||||
if(Children.Count > 0) {
|
||||
// This node is a class — its _Tag field is inside itself
|
||||
// Class path = all segments joined, field = lastSegment_Tag
|
||||
return string.Join(".", safeParts) + "." + lastSafe + TagFieldPostfix;
|
||||
}
|
||||
|
||||
if(safeParts.Length == 1) {
|
||||
// Root leaf — field directly in outer class
|
||||
return lastSafe + TagFieldPostfix;
|
||||
}
|
||||
|
||||
// Non-root leaf — field inside parent class
|
||||
// Parent class path = all segments except last
|
||||
var parentPath = string.Join(".", safeParts.Take(safeParts.Length - 1));
|
||||
return parentPath + "." + lastSafe + TagFieldPostfix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 126771008dabad6429c6a5408b5f204d
|
||||
287
Packages/com.jovian.tag-system/Editor/JovianTagsPickerPopup.cs
Normal file
287
Packages/com.jovian.tag-system/Editor/JovianTagsPickerPopup.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
public class JovianTagsPickerPopup : EditorWindow {
|
||||
|
||||
private static readonly Color DividerColor = new(1f, 1f, 1f, 0.12f);
|
||||
private static readonly Color RowOdd = new(1f, 1f, 1f, 0.03f);
|
||||
private static readonly Color RowHover = new(0.3f, 0.5f, 0.8f, 0.12f);
|
||||
private static readonly Color HeaderBg = new(0.18f, 0.18f, 0.18f, 1f);
|
||||
private static readonly Color ChildIndentLine = new(1f, 1f, 1f, 0.08f);
|
||||
|
||||
private const int RowHeight = 22;
|
||||
private const int DepthIndent = 20;
|
||||
|
||||
private HashSet<string> selectedTags = new();
|
||||
private Action<HashSet<string>> onConfirm;
|
||||
private string searchFilter = "";
|
||||
private Vector2 scrollPosition;
|
||||
private List<string> allTags = new();
|
||||
private readonly Dictionary<string, bool> foldoutState = new();
|
||||
private int rowIndex;
|
||||
|
||||
private GUIStyle _tagStyle;
|
||||
|
||||
private GUIStyle TagStyle => _tagStyle ??= new GUIStyle(EditorStyles.label) {
|
||||
fontSize = 12, fixedHeight = RowHeight
|
||||
};
|
||||
|
||||
private GUIStyle _headerStyle;
|
||||
|
||||
private GUIStyle HeaderStyle => _headerStyle ??= new GUIStyle(EditorStyles.boldLabel) {
|
||||
fontSize = 12, fixedHeight = 24
|
||||
};
|
||||
|
||||
private class PickerNode {
|
||||
public string Name;
|
||||
public string FullPath;
|
||||
public List<PickerNode> Children = new();
|
||||
}
|
||||
|
||||
public static void Show(HashSet<string> currentTags, Action<HashSet<string>> onConfirm) {
|
||||
var window = CreateInstance<JovianTagsPickerPopup>();
|
||||
window.selectedTags = new HashSet<string>(currentTags);
|
||||
window.onConfirm = onConfirm;
|
||||
window.titleContent = new GUIContent("Select Tags");
|
||||
window.RefreshTags();
|
||||
|
||||
var size = new Vector2(380, 440);
|
||||
var mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
|
||||
window.position = new Rect(mousePos.x - size.x * 0.5f, mousePos.y, size.x, size.y);
|
||||
window.minSize = new Vector2(300, 300);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void RefreshTags() {
|
||||
allTags.Clear();
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
var tagSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach(var s in settings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
tagSet.Add(t.tag);
|
||||
allTags = tagSet.ToList();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
EditorGUILayout.BeginVertical();
|
||||
GUILayout.Space(6);
|
||||
|
||||
// Search
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label("Search:", GUILayout.Width(50));
|
||||
searchFilter = EditorGUILayout.TextField(searchFilter);
|
||||
if(!string.IsNullOrEmpty(searchFilter) && GUILayout.Button("✕", GUILayout.Width(20), GUILayout.Height(18))) {
|
||||
searchFilter = "";
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4);
|
||||
|
||||
// Selection count
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label($"{selectedTags.Count} selected", EditorStyles.centeredGreyMiniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
if(selectedTags.Count > 0 && GUILayout.Button("Clear All", EditorStyles.miniButton, GUILayout.Width(60)))
|
||||
selectedTags.Clear();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(2);
|
||||
DrawDivider();
|
||||
|
||||
// Tree
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
DrawTagTree();
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
DrawDivider();
|
||||
GUILayout.Space(4);
|
||||
|
||||
// Buttons
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||||
if(GUILayout.Button("Confirm", GUILayout.Height(26))) {
|
||||
onConfirm?.Invoke(selectedTags);
|
||||
Close();
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
if(GUILayout.Button("Cancel", GUILayout.Height(26)))
|
||||
Close();
|
||||
if(GUILayout.Button("Edit Tags", GUILayout.Height(26), GUILayout.Width(70)))
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(6);
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void DrawTagTree() {
|
||||
if(allTags.Count == 0) {
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("No tags defined.", EditorStyles.centeredGreyMiniLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
var roots = BuildTree();
|
||||
|
||||
// Filter
|
||||
if(!string.IsNullOrEmpty(searchFilter)) {
|
||||
roots = FilterTree(roots, searchFilter);
|
||||
}
|
||||
|
||||
rowIndex = 0;
|
||||
foreach(var root in roots.OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawPickerNode(root, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPickerNode(PickerNode node, int depth) {
|
||||
var hasChildren = node.Children.Count > 0;
|
||||
var isSelected = selectedTags.Contains(node.FullPath);
|
||||
|
||||
var bgColor = rowIndex % 2 != 0 ? RowOdd : Color.clear;
|
||||
var rect = EditorGUILayout.BeginHorizontal(GUILayout.Height(RowHeight));
|
||||
if(rect.Contains(Event.current.mousePosition)) bgColor = RowHover;
|
||||
if(depth == 0) bgColor = HeaderBg;
|
||||
if(bgColor.a > 0) EditorGUI.DrawRect(rect, bgColor);
|
||||
|
||||
// Indent + guide lines
|
||||
GUILayout.Space(6 + depth * DepthIndent);
|
||||
if(depth > 0 && Event.current.type == EventType.Repaint) {
|
||||
for(int d = 1; d <= depth; d++) {
|
||||
var lineX = 6 + d * DepthIndent - 10;
|
||||
EditorGUI.DrawRect(new Rect(lineX, rect.y, 1, rect.height), ChildIndentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
var newSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Width(16), GUILayout.Height(RowHeight));
|
||||
if(newSelected != isSelected) {
|
||||
if(newSelected) selectedTags.Add(node.FullPath);
|
||||
else selectedTags.Remove(node.FullPath);
|
||||
}
|
||||
|
||||
// Foldout for parents
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
}
|
||||
|
||||
// Label
|
||||
var style = depth == 0 ? HeaderStyle : TagStyle;
|
||||
GUILayout.Label(node.Name, style, GUILayout.ExpandWidth(true));
|
||||
|
||||
// Select all children button
|
||||
if(hasChildren) {
|
||||
var allChildTags = CollectAllPaths(node);
|
||||
var allChildrenSelected = allChildTags.All(t => selectedTags.Contains(t));
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = new Color(1, 1, 1, 0.5f);
|
||||
if(GUILayout.Button(allChildrenSelected ? "−all" : "+all", EditorStyles.miniButton,
|
||||
GUILayout.Width(32), GUILayout.Height(16))) {
|
||||
foreach(var t in allChildTags) {
|
||||
if(allChildrenSelected) selectedTags.Remove(t);
|
||||
else selectedTags.Add(t);
|
||||
}
|
||||
}
|
||||
GUI.color = prevColor;
|
||||
}
|
||||
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
rowIndex++;
|
||||
|
||||
// Children
|
||||
if(!hasChildren) return;
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
if(!foldoutState[node.FullPath]) return;
|
||||
|
||||
foreach(var child in node.Children.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawPickerNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Tree helpers =====
|
||||
|
||||
private List<PickerNode> BuildTree() {
|
||||
var rootNodes = new Dictionary<string, PickerNode>();
|
||||
var allNodes = new Dictionary<string, PickerNode>();
|
||||
|
||||
foreach(var tag in allTags) {
|
||||
var parts = tag.Split(JovianTagsHandler.tagDelimiter);
|
||||
PickerNode parent = null;
|
||||
var accumulated = "";
|
||||
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
accumulated = i == 0 ? parts[i] : accumulated + "." + parts[i];
|
||||
if(!allNodes.TryGetValue(accumulated, out var node)) {
|
||||
node = new PickerNode { Name = parts[i], FullPath = accumulated };
|
||||
allNodes[accumulated] = node;
|
||||
if(parent != null) parent.Children.Add(node);
|
||||
else rootNodes[accumulated] = node;
|
||||
}
|
||||
parent = node;
|
||||
}
|
||||
}
|
||||
return rootNodes.Values.ToList();
|
||||
}
|
||||
|
||||
private List<PickerNode> FilterTree(List<PickerNode> roots, string filter) {
|
||||
var result = new List<PickerNode>();
|
||||
foreach(var root in roots) {
|
||||
var filtered = FilterNode(root, filter);
|
||||
if(filtered != null) result.Add(filtered);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private PickerNode FilterNode(PickerNode node, string filter) {
|
||||
var matchesSelf = node.FullPath.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var filteredChildren = new List<PickerNode>();
|
||||
foreach(var child in node.Children) {
|
||||
var fc = FilterNode(child, filter);
|
||||
if(fc != null) filteredChildren.Add(fc);
|
||||
}
|
||||
if(!matchesSelf && filteredChildren.Count == 0) return null;
|
||||
return new PickerNode {
|
||||
Name = node.Name,
|
||||
FullPath = node.FullPath,
|
||||
Children = matchesSelf ? node.Children : filteredChildren
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> CollectAllPaths(PickerNode node) {
|
||||
var result = new List<string>();
|
||||
CollectPathsRecursive(node, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CollectPathsRecursive(PickerNode node, List<string> result) {
|
||||
result.Add(node.FullPath);
|
||||
foreach(var child in node.Children) CollectPathsRecursive(child, result);
|
||||
}
|
||||
|
||||
private static void DrawDivider() {
|
||||
var rect = EditorGUILayout.GetControlRect(false, 1);
|
||||
EditorGUI.DrawRect(rect, new Color(1f, 1f, 1f, 0.12f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 819384bf19a494342ba5e5f4319e34fd
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
[CustomPropertyDrawer(typeof(JovianTagsGroup), true)]
|
||||
public class JovianTagsSelectionDrawer : PropertyDrawer {
|
||||
|
||||
private const float RowHeight = 20f;
|
||||
private const float Spacing = 2f;
|
||||
private const float ButtonHeight = 20f;
|
||||
private const float FoldoutHeight = 20f;
|
||||
|
||||
// Per-property foldout state keyed by property path
|
||||
private static readonly Dictionary<string, bool> FoldoutStates = new();
|
||||
|
||||
private static bool GetFoldout(SerializedProperty property) {
|
||||
FoldoutStates.TryAdd(property.propertyPath, true);
|
||||
return FoldoutStates[property.propertyPath];
|
||||
}
|
||||
|
||||
private static void SetFoldout(SerializedProperty property, bool value) {
|
||||
FoldoutStates[property.propertyPath] = value;
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var tagsProp = property.FindPropertyRelative("tags");
|
||||
var count = tagsProp != null ? tagsProp.arraySize : 0;
|
||||
var expanded = GetFoldout(property);
|
||||
|
||||
// Foldout row (always)
|
||||
var height = FoldoutHeight + Spacing;
|
||||
|
||||
if(!expanded) return height;
|
||||
|
||||
// Tag rows
|
||||
height += count * (RowHeight + Spacing);
|
||||
// Bottom button (always)
|
||||
height += ButtonHeight + Spacing;
|
||||
height += 2; // padding
|
||||
return height;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
var tagsProp = property.FindPropertyRelative("tags");
|
||||
if(tagsProp == null) {
|
||||
EditorGUI.LabelField(position, label, new GUIContent("Error: 'tags' field not found"));
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-remove stale tags that no longer exist in any settings
|
||||
CleanStaleTags(tagsProp, property);
|
||||
|
||||
var count = tagsProp.arraySize;
|
||||
var expanded = GetFoldout(property);
|
||||
|
||||
// Foldout row with tag count
|
||||
var y = position.y;
|
||||
var foldoutRect = new Rect(position.x, y, position.width, FoldoutHeight);
|
||||
var foldoutLabel = count > 0 ? $"{label.text} ({count})" : label.text;
|
||||
var newExpanded = EditorGUI.Foldout(foldoutRect, expanded, foldoutLabel, true);
|
||||
if(newExpanded != expanded) SetFoldout(property, newExpanded);
|
||||
y += FoldoutHeight + Spacing;
|
||||
|
||||
if(!newExpanded) {
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
var indent = EditorGUI.indentLevel * 15f + 14f;
|
||||
var contentX = position.x + indent;
|
||||
var contentWidth = position.width - indent;
|
||||
|
||||
// Tag rows
|
||||
for(int i = 0; i < tagsProp.arraySize; i++) {
|
||||
var elementProp = tagsProp.GetArrayElementAtIndex(i);
|
||||
var tagValue = elementProp.stringValue;
|
||||
|
||||
var rowRect = new Rect(contentX, y, contentWidth, RowHeight);
|
||||
var tagRect = new Rect(rowRect.x + 4, rowRect.y, rowRect.width - 30, RowHeight);
|
||||
var removeBtnRect = new Rect(rowRect.xMax - 22, rowRect.y + 1, 20, RowHeight - 2);
|
||||
|
||||
// Row background
|
||||
var bgColor = i % 2 == 0 ? new Color(0, 0, 0, 0.08f) : Color.clear;
|
||||
EditorGUI.DrawRect(rowRect, bgColor);
|
||||
|
||||
// Tag label
|
||||
var displayName = string.IsNullOrEmpty(tagValue) ? "(empty)" : tagValue;
|
||||
EditorGUI.LabelField(tagRect, displayName, EditorStyles.label);
|
||||
|
||||
// Remove button
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f);
|
||||
if(GUI.Button(removeBtnRect, "✕", EditorStyles.miniButton)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
|
||||
y += RowHeight + Spacing;
|
||||
}
|
||||
|
||||
// Bottom Select Tags button (always)
|
||||
DrawAddButton(contentX, y, contentWidth, tagsProp, property);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
private float DrawAddButton(float x, float y, float width, SerializedProperty tagsProp, SerializedProperty property) {
|
||||
var gap = 4f;
|
||||
var editBtnWidth = 70f;
|
||||
var addBtnRect = new Rect(x, y, width - editBtnWidth - gap, ButtonHeight);
|
||||
var editBtnRect = new Rect(x + width - editBtnWidth, y, editBtnWidth, ButtonHeight);
|
||||
|
||||
// Edit Tags button — opens Tag Editor window
|
||||
if(GUI.Button(editBtnRect, "Edit Tags")) {
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.7f, 0.3f);
|
||||
if(GUI.Button(addBtnRect, "+ Select Tags")) {
|
||||
var currentTags = new HashSet<string>();
|
||||
for(int i = 0; i < tagsProp.arraySize; i++) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val)) currentTags.Add(val);
|
||||
}
|
||||
|
||||
JovianTagsPickerPopup.Show(currentTags, selectedTags => {
|
||||
tagsProp.ClearArray();
|
||||
foreach(var tag in selectedTags.OrderBy(t => t)) {
|
||||
tagsProp.InsertArrayElementAtIndex(tagsProp.arraySize);
|
||||
tagsProp.GetArrayElementAtIndex(tagsProp.arraySize - 1).stringValue = tag;
|
||||
}
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
foreach(var tag in selectedTags)
|
||||
JovianTagsEditorHistory.Instance.AddRecentTag(tag);
|
||||
});
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
return y + ButtonHeight + Spacing;
|
||||
}
|
||||
private static HashSet<string> cachedValidTags;
|
||||
private static double lastCacheTime;
|
||||
|
||||
private static void CleanStaleTags(SerializedProperty tagsProp, SerializedProperty property) {
|
||||
// Refresh valid tags cache every 2 seconds to avoid scanning settings every frame
|
||||
if(cachedValidTags == null || EditorApplication.timeSinceStartup - lastCacheTime > 2.0) {
|
||||
cachedValidTags = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
foreach(var s in settings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
cachedValidTags.Add(t.tag);
|
||||
lastCacheTime = EditorApplication.timeSinceStartup;
|
||||
}
|
||||
|
||||
bool removed = false;
|
||||
for(int i = tagsProp.arraySize - 1; i >= 0; i--) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val) && !cachedValidTags.Contains(val)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(removed) {
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6c5df6e64308804287ab931e990df7b
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
[CustomEditor(typeof(JovianTagsSettings))]
|
||||
public class JovianTagsSettingsInspector : UnityEditor.Editor {
|
||||
public override void OnInspectorGUI() {
|
||||
serializedObject.Update();
|
||||
DrawDefaultInspector();
|
||||
|
||||
var settings = (JovianTagsSettings)target;
|
||||
|
||||
// Validate and show errors
|
||||
var errors = ValidateTags(settings);
|
||||
if(errors.Count > 0) {
|
||||
GUILayout.Space(4);
|
||||
foreach(var error in errors) {
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||||
if(GUILayout.Button("Save & Generate Tags", GUILayout.Height(28))) {
|
||||
CleanAndSave(settings);
|
||||
JovianTagsGenerator.Regenerate();
|
||||
}
|
||||
|
||||
GUI.backgroundColor = prevBg;
|
||||
|
||||
if(GUILayout.Button("Open Tag Editor", GUILayout.Height(28))) {
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private static List<string> ValidateTags(JovianTagsSettings settings) {
|
||||
var errors = new List<string>();
|
||||
var seen = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
|
||||
foreach(var t in settings.gameTags) {
|
||||
if(string.IsNullOrWhiteSpace(t.tag)) {
|
||||
errors.Add("Empty tag name found.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentError = JovianTagsGenerator.ValidateTagName(t.tag);
|
||||
if(segmentError != null) {
|
||||
errors.Add($"'{t.tag}': {segmentError}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!seen.Add(t.tag)) {
|
||||
errors.Add($"Duplicate tag '{t.tag}' — will be removed on Save & Generate.");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void CleanAndSave(JovianTagsSettings settings) {
|
||||
var seen = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
var cleaned = new List<RegisteredTag>();
|
||||
var removed = 0;
|
||||
|
||||
foreach(var t in settings.gameTags) {
|
||||
if(string.IsNullOrWhiteSpace(t.tag)) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(JovianTagsGenerator.ValidateTagName(t.tag) != null) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!seen.Add(t.tag)) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
cleaned.Add(t);
|
||||
}
|
||||
|
||||
if(removed > 0) {
|
||||
Undo.RecordObject(settings, "Clean Game Tags");
|
||||
settings.gameTags = cleaned.ToArray();
|
||||
Debug.Log($"[GameTagSettings] Cleaned {removed} invalid/duplicate tag(s).");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(settings);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa5b2c3fc4ef68498c7852291a4e8a0
|
||||
21
Packages/com.jovian.tag-system/LICENSE
Normal file
21
Packages/com.jovian.tag-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.tag-system/LICENSE.meta
Normal file
7
Packages/com.jovian.tag-system/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51e674b2adc94084caaa472190fca829
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
423
Packages/com.jovian.tag-system/README.md
Normal file
423
Packages/com.jovian.tag-system/README.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# Jovian Tag System
|
||||
|
||||
A hierarchical, strongly-typed tag system for Unity. Define tags as dot-delimited hierarchies (`Damage.Fire.AOE`), auto-generate type-safe C# constants, and query relationships like ancestor/descendant/sibling at runtime with zero allocations.
|
||||
|
||||
## Table of Contents
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [**Important: Tags Are Not Inherited**](#important-tags-are-not-inherited)
|
||||
- [Hierarchical Tags](#hierarchical-tags)
|
||||
- [Tag Editor Window](#tag-editor-window)
|
||||
- [Using Tags in Code](#using-tags-in-code)
|
||||
- [Inspector Integration](#inspector-integration)
|
||||
- [Tag Picker Popup](#tag-picker-popup)
|
||||
- [Code Generation](#code-generation)
|
||||
- [Tag Containers](#tag-containers)
|
||||
- [Tag Containers with Data](#tag-containers-with-data)
|
||||
- [Runtime API Reference](#runtime-api-reference)
|
||||
- [Performance](#performance)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Tag Settings asset
|
||||
Right-click in the Project window > `Create` > `Jovian` > `Tag System` > `Tag Settings`
|
||||
|
||||
### 2. Add tags
|
||||
Open the Tag Editor via `Jovian` > `Tag System` > `Tag Editor...`
|
||||
|
||||
Add tags like:
|
||||
```
|
||||
Character
|
||||
Character.Player
|
||||
Character.Enemy
|
||||
Character.Enemy.Boss
|
||||
Damage
|
||||
Damage.Fire
|
||||
Damage.Ice
|
||||
Status
|
||||
Status.Burning
|
||||
Status.Frozen
|
||||
```
|
||||
|
||||
### 3. Click Save & Generate
|
||||
This creates a `GameTags.cs` file with strongly-typed constants:
|
||||
|
||||
```csharp
|
||||
// Auto-generated
|
||||
public static partial class GameTags {
|
||||
public static class Character {
|
||||
public static JovianTag Character_Tag;
|
||||
public static class Enemy {
|
||||
public static JovianTag Enemy_Tag;
|
||||
public static JovianTag Boss_Tag;
|
||||
}
|
||||
public static JovianTag Player_Tag;
|
||||
}
|
||||
public static class Damage {
|
||||
public static JovianTag Damage_Tag;
|
||||
public static JovianTag Fire_Tag;
|
||||
public static JovianTag Ice_Tag;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Use in code
|
||||
```csharp
|
||||
if (projectile.IsDescendantOf(GameTags.Damage.Damage_Tag)) {
|
||||
// This is any damage type (Fire, Ice, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important: Tags Are Not Inherited
|
||||
|
||||
> **TL;DR — Tagging an object with `Enemy.Name.Butcher.Size.Large` does NOT automatically assign `Enemy.Name.Butcher` or `Enemy` to that object.**
|
||||
|
||||
Each dot-delimited path is a **distinct, independent tag**. The hierarchy is **structural** — it enables tree queries — but it does **not** imply membership.
|
||||
|
||||
### Concrete Example
|
||||
|
||||
Register this tag in the Tag Editor:
|
||||
```
|
||||
Enemy.Name.Butcher.Size.Large
|
||||
```
|
||||
|
||||
The system creates **5 separate tag definitions** (one per segment), parented in the tree:
|
||||
- `Enemy`
|
||||
- `Enemy.Name`
|
||||
- `Enemy.Name.Butcher`
|
||||
- `Enemy.Name.Butcher.Size`
|
||||
- `Enemy.Name.Butcher.Size.Large`
|
||||
|
||||
Now tag a game object with `Enemy.Name.Butcher.Size.Large`. On that object, **only that one tag is set.** The ancestor tags exist as definitions, but they are not active on the object.
|
||||
|
||||
### What This Means in Practice
|
||||
|
||||
| You want to match... | Do this |
|
||||
|----------------------|---------|
|
||||
| Only Large Butcher | Tag object with `Enemy.Name.Butcher.Size.Large`, query with `Contains` |
|
||||
| Any Butcher (any size) via separate tag | Add a second tag `Enemy.Name.Butcher` to the object |
|
||||
| Any Butcher (any size) via hierarchy | Tag with `Enemy.Name.Butcher.Size.Large`, query with `ContainsDescendantOf(GameTags.Enemy.Name.Butcher_Tag)` |
|
||||
|
||||
### Exact vs Hierarchy Queries
|
||||
|
||||
| Query | Behavior |
|
||||
|-------|----------|
|
||||
| `Contains(tag)` | **Exact match only** — matches the literal tag you assigned |
|
||||
| `ContainsDescendantOf(tag)` | Matches any descendant of `tag`, including `tag` itself |
|
||||
| `ContainsAncestorOf(tag)` | Matches any ancestor of `tag` |
|
||||
|
||||
**The tag itself does not decide matching behavior — the query does.** Choose the query type based on your intent: `Contains` for "this exact thing," `ContainsDescendantOf` for "anything in this category."
|
||||
|
||||
---
|
||||
|
||||
## Hierarchical Tags
|
||||
|
||||
Tags are organized in a tree using dot-delimited names. Each segment creates a level in the hierarchy.
|
||||
|
||||
```
|
||||
Damage ← root tag (depth 0)
|
||||
├── Damage.Fire ← child of Damage (depth 1)
|
||||
│ └── Damage.Fire.AOE ← child of Fire (depth 2)
|
||||
├── Damage.Ice ← child of Damage (depth 1)
|
||||
└── Damage.Lightning ← child of Damage (depth 1)
|
||||
```
|
||||
|
||||
### Hierarchy Queries
|
||||
|
||||
Every tag knows its parent. This enables three types of queries:
|
||||
|
||||
**IsDescendantOf** — "Is this tag a child/grandchild/etc. of another?"
|
||||
```csharp
|
||||
var fire = GameTags.Damage.Fire_Tag;
|
||||
var damage = GameTags.Damage.Damage_Tag;
|
||||
var aoe = GameTags.Damage.Fire.AOE_Tag;
|
||||
|
||||
fire.IsDescendantOf(damage); // true — Fire is under Damage
|
||||
aoe.IsDescendantOf(damage); // true — AOE is under Damage (via Fire)
|
||||
aoe.IsDescendantOf(fire); // true — AOE is directly under Fire
|
||||
damage.IsDescendantOf(fire); // false — Damage is above Fire
|
||||
fire.IsDescendantOf(fire); // true — a tag is a descendant of itself
|
||||
```
|
||||
|
||||
**IsAncestorOf** — "Is this tag a parent/grandparent/etc. of another?"
|
||||
```csharp
|
||||
damage.IsAncestorOf(fire); // true
|
||||
damage.IsAncestorOf(aoe); // true
|
||||
fire.IsAncestorOf(damage); // false
|
||||
```
|
||||
|
||||
**IsSiblingTo** — "Do these tags share the same parent?"
|
||||
```csharp
|
||||
var fire = GameTags.Damage.Fire_Tag;
|
||||
var ice = GameTags.Damage.Ice_Tag;
|
||||
var player = GameTags.Character.Player_Tag;
|
||||
|
||||
fire.IsSiblingTo(ice); // true — both under Damage
|
||||
fire.IsSiblingTo(player); // false — different parents
|
||||
```
|
||||
|
||||
### Practical Use Cases
|
||||
|
||||
**Category matching** — check if something belongs to a broad category:
|
||||
```csharp
|
||||
// Does this entity have ANY damage tag?
|
||||
if (entity.tags.ContainsDescendantOf(GameTags.Damage.Damage_Tag)) {
|
||||
ApplyDamageEffect();
|
||||
}
|
||||
```
|
||||
|
||||
**Specific matching** — check for an exact tag:
|
||||
```csharp
|
||||
if (entity.tags.Contains(GameTags.Damage.Fire_Tag)) {
|
||||
ApplyBurnEffect();
|
||||
}
|
||||
```
|
||||
|
||||
**Resistance system** — use hierarchy for type matching:
|
||||
```csharp
|
||||
// Entity is immune to all fire damage (Fire, Fire.AOE, etc.)
|
||||
if (incomingDamage.IsDescendantOf(GameTags.Damage.Fire_Tag) && entity.HasResistance(GameTags.Damage.Fire_Tag)) {
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Tag Editor Window
|
||||
|
||||
Open via `Jovian` > `Tag System` > `Tag Editor...`
|
||||
|
||||
### Features
|
||||
- **Hierarchical tree view** — tags displayed as an expandable/collapsible tree
|
||||
- **Add root tags** — type a name in the "New Tag" field at the bottom and click "+ Add"
|
||||
- **Add child tags** — click the green **+** button on any tag to add a child under it
|
||||
- **Delete tags** — click the red **✕** button. If the tag has children, you'll be asked to confirm deletion of the entire branch
|
||||
- **Search** — filter tags by name
|
||||
- **Save & Generate** — saves all changes and regenerates the C# constants file
|
||||
- **Vertical indent guides** — visual lines showing parent-child relationships
|
||||
|
||||
### Adding hierarchical tags
|
||||
You can type full paths in the "New Tag" field:
|
||||
```
|
||||
Damage.Fire.AOE
|
||||
```
|
||||
This automatically creates `Damage` and `Damage.Fire` as parents if they don't exist.
|
||||
|
||||
Or use the **+** button on an existing tag to add a child — a popup asks for just the child name.
|
||||
|
||||
### Validation
|
||||
Tags must:
|
||||
- Start with a letter
|
||||
- Contain only letters, digits, or underscores
|
||||
- Not be a C# reserved keyword (`class`, `int`, `static`, etc.)
|
||||
|
||||
Invalid names are rejected with an error message. Duplicates are detected and shown as warnings.
|
||||
|
||||
## Using Tags in Code
|
||||
|
||||
### Generated Constants
|
||||
After clicking **Save & Generate**, use the generated constants:
|
||||
|
||||
```csharp
|
||||
using Jovian.TagSystem;
|
||||
|
||||
public class Projectile : MonoBehaviour {
|
||||
public void OnHit(JovianTag targetTag) {
|
||||
if (targetTag.IsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) {
|
||||
DealDamage();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JovianTag Struct
|
||||
The core type — 8 bytes, zero heap allocation:
|
||||
|
||||
```csharp
|
||||
JovianTag tag = GameTags.Damage.Fire_Tag;
|
||||
|
||||
tag.IsValid(); // true (not the empty/None tag)
|
||||
tag.IsNone(); // false
|
||||
tag.Id; // unique int identifier
|
||||
tag.ParentId; // parent's int identifier (0 = root)
|
||||
tag.ToString(); // "Damage.Fire" (when manager is initialized)
|
||||
tag.IsDescendantOf(otherTag); // hierarchy query
|
||||
tag.IsAncestorOf(otherTag); // hierarchy query
|
||||
tag.IsSiblingTo(otherTag); // same parent check
|
||||
tag == otherTag; // equality by ID
|
||||
```
|
||||
|
||||
## Inspector Integration
|
||||
|
||||
### JovianTagsGroup
|
||||
Use `JovianTagsGroup` on any MonoBehaviour or ScriptableObject to select tags in the inspector:
|
||||
|
||||
```csharp
|
||||
public class Enemy : MonoBehaviour {
|
||||
public JovianTagsGroup tags;
|
||||
|
||||
private void Start() {
|
||||
// Check tags at runtime
|
||||
if (tags.ContainsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) {
|
||||
Debug.Log("This is an enemy!");
|
||||
}
|
||||
|
||||
// Convert to a runtime container for efficient repeated queries
|
||||
var container = tags.ToContainer();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inspector Display
|
||||
- **Collapsible** — shows `Tags (3)` when collapsed, full list when expanded
|
||||
- **Tag list** — each tag shown with a red ✕ remove button
|
||||
- **+ Add Tags** button — opens the tag picker popup
|
||||
- **Edit Tags** button — opens the Tag Editor window
|
||||
|
||||
## Tag Picker Popup
|
||||
|
||||
The picker popup opens when you click **+ Add Tags** in the inspector. Features:
|
||||
|
||||
- **Hierarchical tree with checkboxes** — check/uncheck any tag at any depth
|
||||
- **Search** — filter by name
|
||||
- **+all / -all buttons** — bulk select/deselect all children of a parent tag
|
||||
- **Already-selected tags** are pre-checked when the popup opens
|
||||
- **Confirm / Cancel** — apply or discard the selection
|
||||
- **Edit Tags** button — jump to the Tag Editor window
|
||||
|
||||
## Code Generation
|
||||
|
||||
### How It Works
|
||||
The Tag Editor generates a C# file with:
|
||||
1. **Nested static classes** mirroring the tag hierarchy
|
||||
2. **Static `JovianTag` fields** for each tag (postfixed with `_Tag`)
|
||||
3. **`[RuntimeInitializeOnLoadMethod]`** that registers all tags and resolves the fields at startup
|
||||
4. **`[InitializeOnLoadMethod]`** (editor-only) for editor play mode
|
||||
|
||||
### Manual Edits
|
||||
You can add fields by hand to the generated file. They will be preserved on regeneration as long as the file compiles.
|
||||
|
||||
### Regenerate Without the Window
|
||||
Use `Jovian` > `Tag System` > `Regenerate Tag Constants` to regenerate from the menu without opening the Tag Editor.
|
||||
|
||||
### Settings Inspector
|
||||
The `JovianTagsSettings` ScriptableObject inspector has:
|
||||
- Default array editor for direct tag editing
|
||||
- **Save & Generate Tags** button
|
||||
- **Open Tag Editor** button
|
||||
- Validation errors shown inline (invalid names, duplicates)
|
||||
|
||||
## Tag Containers
|
||||
|
||||
### JovianTagsContainer (tags only)
|
||||
A runtime collection for holding and querying multiple tags:
|
||||
|
||||
```csharp
|
||||
var container = new JovianTagsContainer(4);
|
||||
container.Add(GameTags.Damage.Fire_Tag);
|
||||
container.Add(GameTags.Status.Burning_Tag);
|
||||
|
||||
container.Contains(GameTags.Damage.Fire_Tag); // true
|
||||
container.ContainsDescendantOf(GameTags.Damage.Damage_Tag); // true
|
||||
container.ContainsAncestorOf(GameTags.Damage.Fire.AOE_Tag); // true
|
||||
container.ContainsSibling(GameTags.Damage.Ice_Tag); // true (Fire and Ice are siblings)
|
||||
container.Count; // 2
|
||||
|
||||
container.Remove(GameTags.Damage.Fire_Tag);
|
||||
container.Clear();
|
||||
```
|
||||
|
||||
### From JovianTagsGroup
|
||||
Convert a serialized selection to a runtime container:
|
||||
```csharp
|
||||
public JovianTagsGroup selectedTags;
|
||||
|
||||
void Start() {
|
||||
var container = selectedTags.ToContainer();
|
||||
// Use container for efficient queries
|
||||
}
|
||||
```
|
||||
|
||||
## Tag Containers with Data
|
||||
|
||||
### JovianTagsContainer\<T\> (tags + values)
|
||||
Pair tags with typed data — like a dictionary with hierarchy-aware queries:
|
||||
|
||||
```csharp
|
||||
// Damage resistances
|
||||
var resistances = new JovianTagsContainer<float>(4);
|
||||
resistances.Add(GameTags.Damage.Fire_Tag, 0.5f); // 50% fire resistance
|
||||
resistances.Add(GameTags.Damage.Ice_Tag, 0.75f); // 75% ice resistance
|
||||
|
||||
// Exact lookup
|
||||
if (resistances.TryGetValue(GameTags.Damage.Fire_Tag, out float fireRes)) {
|
||||
Debug.Log($"Fire resistance: {fireRes}"); // 0.5
|
||||
}
|
||||
|
||||
// Hierarchy query — do I resist ANY damage type?
|
||||
resistances.ContainsDescendantOf(GameTags.Damage.Damage_Tag); // true
|
||||
|
||||
// Get all resistances under a parent
|
||||
var results = new List<TagEntry<float>>();
|
||||
resistances.GetByAncestor(GameTags.Damage.Damage_Tag, results);
|
||||
// results contains Fire(0.5) and Ice(0.75)
|
||||
```
|
||||
|
||||
### TagEntry\<T\>
|
||||
Each entry in a generic container:
|
||||
```csharp
|
||||
TagEntry<float> entry = ...;
|
||||
entry.Tag; // the JovianTag
|
||||
entry.Value; // the float value
|
||||
entry.IsDescendantOf(someAncestor); // hierarchy query on the tag
|
||||
entry.Is(someTag); // exact match
|
||||
```
|
||||
|
||||
## Runtime API Reference
|
||||
|
||||
### JovianTagsHandler (static)
|
||||
The central tag registry. Initialized automatically by the generated code.
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `Initialize()` | Reset and initialize the registry |
|
||||
| `RegisterTag(string)` | Register a dot-delimited tag and all parents |
|
||||
| `GetTag(string)` | Get tag by full name |
|
||||
| `GetTag(int)` | Get tag by ID |
|
||||
| `TryGetGameTag(string, out JovianTag)` | Safe lookup by name |
|
||||
| `TagToString(JovianTag)` | Get the full name of a tag |
|
||||
| `DisplayName(string)` | Get the last segment (`"Damage.Fire"` → `"Fire"`) |
|
||||
| `IsInitialized` | Whether the registry is ready |
|
||||
|
||||
### JovianTag (struct, 8 bytes)
|
||||
|
||||
| Member | Description |
|
||||
|--------|-------------|
|
||||
| `Id` | Unique integer identifier |
|
||||
| `ParentId` | Parent's ID (0 = root) |
|
||||
| `IsValid()` | Not the empty tag |
|
||||
| `IsNone()` | Is the empty tag |
|
||||
| `IsDescendantOf(tag)` | Hierarchy: child/grandchild check |
|
||||
| `IsAncestorOf(tag)` | Hierarchy: parent/grandparent check |
|
||||
| `IsSiblingTo(tag)` | Same parent check |
|
||||
| `==`, `!=`, `Equals()` | Equality by ID |
|
||||
|
||||
### JovianTagsGroup (serializable struct)
|
||||
|
||||
| Member | Description |
|
||||
|--------|-------------|
|
||||
| `tags` | `string[]` of tag names (serialized) |
|
||||
| `ToContainer()` | Resolve to `JovianTagsContainer` |
|
||||
| `Contains(tag)` | Check if any tag matches |
|
||||
| `ContainsDescendantOf(tag)` | Hierarchy query |
|
||||
| `ContainsAncestorOf(tag)` | Hierarchy query |
|
||||
| `HasAny()` | True if any tags selected |
|
||||
|
||||
## Performance
|
||||
|
||||
- **JovianTag** — 8 bytes (`int id` + `int parentId`), no heap allocation
|
||||
- **Hierarchy queries** — O(depth) parent chain walk via `JovianTagsHandler.GetTag(int)` dictionary lookup. Typical depth is 1-4 hops.
|
||||
- **Equality** — O(1) integer comparison
|
||||
- **Container queries** — O(n) linear scan, optimal for typical small tag counts (<10 per entity)
|
||||
- **ToString** — uses cached static `StringBuilder` to avoid per-call allocation
|
||||
- **Registration** — uses `ReadOnlySpan<char>` for segment parsing to minimize string allocations
|
||||
7
Packages/com.jovian.tag-system/README.md.meta
Normal file
7
Packages/com.jovian.tag-system/README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95d784d03badc334eb57943d0da95b0e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.tag-system/Runtime.meta
Normal file
8
Packages/com.jovian.tag-system/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a504e32ce3c16c4ba10a51b9444669a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/com.jovian.tag-system/Runtime/AssemblyInfo.cs
Normal file
3
Packages/com.jovian.tag-system/Runtime/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Jovian.TagSystem.Editor"), InternalsVisibleTo("Jovian.TagSystem.Tests")]
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 925b98fc2eb4d8f49900afdee32991a1
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Jovian.TagSystem",
|
||||
"rootNamespace": "Jovian.TagSystem",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f71bff532e6b8e24a8fc3e2761205890
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Packages/com.jovian.tag-system/Runtime/JovianTag.cs
Normal file
85
Packages/com.jovian.tag-system/Runtime/JovianTag.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
/// <summary>
|
||||
/// Lightweight tag identity. 8 bytes, no heap allocation.
|
||||
/// Hierarchy queries are resolved via GameTagManager static lookups.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct JovianTag : IEquatable<JovianTag>, IComparable<JovianTag> {
|
||||
[SerializeField] private int id;
|
||||
[SerializeField] private int parentId;
|
||||
|
||||
public int Id => id;
|
||||
public int ParentId => parentId;
|
||||
|
||||
public JovianTag(int id, int parentId) {
|
||||
this.id = id;
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsNone() => id == 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsValid() => id > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag is a descendant of the given ancestor.
|
||||
/// Walks the parent chain via GameTagManager.
|
||||
/// </summary>
|
||||
public readonly bool IsDescendantOf(JovianTag ancestor) {
|
||||
if(id == 0 || ancestor.id == 0) return false;
|
||||
if(id == ancestor.id) return true;
|
||||
|
||||
// Walk up from this tag's parent chain
|
||||
var current = this;
|
||||
while(current.parentId != 0) {
|
||||
if(current.parentId == ancestor.id) return true;
|
||||
current = JovianTagsHandler.GetTag(current.parentId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag is an ancestor of the given descendant.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsAncestorOf(JovianTag descendant) {
|
||||
return descendant.IsDescendantOf(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag shares the same parent as the given tag.
|
||||
/// </summary>
|
||||
public readonly bool IsSiblingTo(JovianTag sibling) {
|
||||
if(id == 0 || sibling.id == 0) return false;
|
||||
return parentId == sibling.parentId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(JovianTag x, JovianTag y) => x.id == y.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(JovianTag x, JovianTag y) => x.id != y.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Equals(JovianTag other) => id == other.id;
|
||||
|
||||
public readonly int CompareTo(JovianTag other) => id.CompareTo(other.id);
|
||||
|
||||
public override readonly bool Equals(object obj) => obj is JovianTag other && id == other.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly int GetHashCode() => id;
|
||||
|
||||
public override readonly string ToString() {
|
||||
if(JovianTagsHandler.IsInitialized) {
|
||||
return JovianTagsHandler.TagToString(this);
|
||||
}
|
||||
return id == 0 ? "None" : $"Tag({id})";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.tag-system/Runtime/JovianTag.cs.meta
Normal file
2
Packages/com.jovian.tag-system/Runtime/JovianTag.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f7edc6a4a42107499ed2b46654d63f3
|
||||
163
Packages/com.jovian.tag-system/Runtime/JovianTagsContainer.cs
Normal file
163
Packages/com.jovian.tag-system/Runtime/JovianTagsContainer.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
|
||||
/// <summary>
|
||||
/// Entry in a JovianTagContainer — pairs a tag with an optional typed value.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct TagEntry<T> {
|
||||
public JovianTag Tag;
|
||||
public T Value;
|
||||
|
||||
public TagEntry(JovianTag tag, T value) {
|
||||
Tag = tag;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public TagEntry(JovianTag tag) {
|
||||
Tag = tag;
|
||||
Value = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Is(JovianTag other) => Tag.Equals(other);
|
||||
public readonly bool IsDescendantOf(JovianTag ancestor) => Tag.IsDescendantOf(ancestor);
|
||||
public readonly bool IsAncestorOf(JovianTag descendant) => Tag.IsAncestorOf(descendant);
|
||||
public readonly bool IsSiblingTo(JovianTag sibling) => Tag.IsSiblingTo(sibling);
|
||||
public readonly bool IsValid() => Tag.IsValid();
|
||||
|
||||
public readonly override string ToString() => $"{Tag}: {Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic tag container — holds tag+value pairs with hierarchy-aware queries.
|
||||
/// For tags-only, use non-generic JovianTagContainer.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JovianTagsContainer<T> {
|
||||
public readonly List<TagEntry<T>> entries;
|
||||
|
||||
public JovianTagsContainer(int capacity) {
|
||||
entries = new List<TagEntry<T>>(capacity);
|
||||
}
|
||||
|
||||
public int Count => entries.Count;
|
||||
|
||||
public void Add(JovianTag tag, T value) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) return; // no duplicates
|
||||
}
|
||||
entries.Add(new TagEntry<T>(tag, value));
|
||||
}
|
||||
|
||||
public bool Remove(JovianTag tag) {
|
||||
for(int i = entries.Count - 1; i >= 0; i--) {
|
||||
if(entries[i].Tag.Id == tag.Id) {
|
||||
entries.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear() => entries.Clear();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Contains(JovianTag tag) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetValue(JovianTag tag, out T value) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) {
|
||||
value = entries[i].Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsDescendantOf(JovianTag ancestor) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsDescendantOf(ancestor)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsAncestorOf(JovianTag descendant) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsAncestorOf(descendant)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsSibling(JovianTag sibling) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsSiblingTo(sibling)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void GetByAncestor(JovianTag ancestor, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsDescendantOf(ancestor)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetByDescendant(JovianTag descendant, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsAncestorOf(descendant)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetBySibling(JovianTag sibling, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsSiblingTo(sibling)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly StringBuilder sb = new(256);
|
||||
|
||||
public override string ToString() {
|
||||
sb.Clear();
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(i > 0) sb.Append(" | ");
|
||||
sb.Append(entries[i].ToString());
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sentinel type for tag-only containers (no payload).
|
||||
/// </summary>
|
||||
public struct NoValue { }
|
||||
|
||||
/// <summary>
|
||||
/// Non-generic tag container — tags only, no data payload.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JovianTagsContainer : JovianTagsContainer<NoValue> {
|
||||
private static readonly JovianTagsContainer empty = new(0);
|
||||
public static JovianTagsContainer Empty => empty;
|
||||
|
||||
public JovianTagsContainer(int capacity) : base(capacity) { }
|
||||
|
||||
public void Add(JovianTag tag) => Add(tag, default);
|
||||
|
||||
/// <summary>
|
||||
/// Access tags directly for backwards compatibility.
|
||||
/// </summary>
|
||||
public JovianTag this[int index] => entries[index].Tag;
|
||||
|
||||
public JovianTag GetTag(int index) => entries[index].Tag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb368346014a3644995688d0a325abcd
|
||||
102
Packages/com.jovian.tag-system/Runtime/JovianTagsGroup.cs
Normal file
102
Packages/com.jovian.tag-system/Runtime/JovianTagsGroup.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
/// <summary>
|
||||
/// Serializable tag selection for use in MonoBehaviours and ScriptableObjects.
|
||||
/// Always supports multiple tags. Use the property drawer to select tags in the inspector.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct JovianTagsGroup {
|
||||
[SerializeField] public string[] tags;
|
||||
|
||||
public JovianTagsGroup(params string[] tags) {
|
||||
this.tags = tags ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int Count => tags?.Length ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all selected tags as resolved GameTags.
|
||||
/// Stale/unregistered tag names are silently skipped.
|
||||
/// </summary>
|
||||
public JovianTagsContainer ToContainer() {
|
||||
if(tags == null || tags.Length == 0) {
|
||||
return JovianTagsContainer.Empty;
|
||||
}
|
||||
var container = new JovianTagsContainer(tags.Length);
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTagThatIsNotNone(tag, out var resolved)) {
|
||||
container.Add(resolved);
|
||||
}
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag matches the given tag exactly.
|
||||
/// Stale/unregistered tag names count as no match (no error log).
|
||||
/// </summary>
|
||||
public bool Contains(JovianTag jovianTag) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.Equals(jovianTag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is a descendant of the given ancestor.
|
||||
/// </summary>
|
||||
public bool ContainsDescendantOf(JovianTag ancestor) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsDescendantOf(ancestor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is an ancestor of the given descendant.
|
||||
/// </summary>
|
||||
public bool ContainsAncestorOf(JovianTag descendant) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsAncestorOf(descendant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is a sibling of the given tag.
|
||||
/// </summary>
|
||||
public bool ContainsSiblingOf(JovianTag sibling) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsSiblingTo(sibling)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasAny() {
|
||||
return tags != null && tags.Length > 0;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if(tags == null || tags.Length == 0) return "None";
|
||||
return string.Join(", ", tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 140c6dc4e1c289e48b7441ac6ee5e20f
|
||||
151
Packages/com.jovian.tag-system/Runtime/JovianTagsHandler.cs
Normal file
151
Packages/com.jovian.tag-system/Runtime/JovianTagsHandler.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
|
||||
[Serializable]
|
||||
public struct RegisteredTag : IEquatable<RegisteredTag> {
|
||||
public string tag;
|
||||
|
||||
public RegisteredTag(string tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
public bool Equals(RegisteredTag other) => tag == other.tag;
|
||||
public override bool Equals([CanBeNull] object obj) => obj is RegisteredTag other && Equals(other);
|
||||
public override int GetHashCode() => tag != null ? tag.GetHashCode() : 0;
|
||||
}
|
||||
|
||||
public static class JovianTagsHandler {
|
||||
public const char tagDelimiter = '.';
|
||||
public const string emptyTagName = "None";
|
||||
public const int emptyTagId = 0;
|
||||
public static readonly JovianTag emptyTag = new(emptyTagId, 0);
|
||||
|
||||
// Primary lookups — no allocations on query
|
||||
private static Dictionary<string, JovianTag> tagsByName = new();
|
||||
private static Dictionary<int, JovianTag> tagsById = new();
|
||||
private static Dictionary<int, string> tagNames = new();
|
||||
private static int idCounter;
|
||||
private static bool initialized;
|
||||
|
||||
public static bool IsInitialized => initialized;
|
||||
|
||||
public static void Initialize() {
|
||||
tagsByName = new Dictionary<string, JovianTag>(64) { { emptyTagName, emptyTag } };
|
||||
tagsById = new Dictionary<int, JovianTag>(64) { { emptyTagId, emptyTag } };
|
||||
tagNames = new Dictionary<int, string>(64) { { emptyTagId, emptyTagName } };
|
||||
idCounter = 0;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public static void EnsureInitialized() {
|
||||
if(!initialized) Initialize();
|
||||
}
|
||||
|
||||
public static void Reset() {
|
||||
tagsByName = new Dictionary<string, JovianTag>();
|
||||
tagsById = new Dictionary<int, JovianTag>();
|
||||
tagNames = new Dictionary<int, string>();
|
||||
idCounter = 0;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
public static void RegisterTags(RegisteredTag[] serializedTags) {
|
||||
EnsureInitialized();
|
||||
for(int i = 0, n = serializedTags.Length; i < n; i++) {
|
||||
RegisterTag(serializedTags[i].tag);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterTags(string[] tagNames) {
|
||||
EnsureInitialized();
|
||||
for(int i = 0, n = tagNames.Length; i < n; i++) {
|
||||
RegisterTag(tagNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterTag(string tagToRegister) {
|
||||
EnsureInitialized();
|
||||
|
||||
// Walk segments without allocating a string[] — use ReadOnlySpan
|
||||
var span = tagToRegister.AsSpan();
|
||||
int parentId = 0;
|
||||
|
||||
for(int i = 0; i <= span.Length; i++) {
|
||||
if(i < span.Length && span[i] != tagDelimiter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// span[segStart..i] is the current segment
|
||||
// Full tag name is tagToRegister[0..i]
|
||||
var fullName = tagToRegister.Substring(0, i);
|
||||
|
||||
if(!tagsByName.TryGetValue(fullName, out var tag)) {
|
||||
idCounter++;
|
||||
tag = new JovianTag(idCounter, parentId);
|
||||
tagsByName[fullName] = tag;
|
||||
tagsById[idCounter] = tag;
|
||||
tagNames[idCounter] = fullName;
|
||||
}
|
||||
|
||||
parentId = tag.Id;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static JovianTag GetTag(string tagName) {
|
||||
if(string.IsNullOrEmpty(tagName)) {
|
||||
return emptyTag;
|
||||
}
|
||||
if(tagsByName.TryGetValue(tagName, out var tag)) {
|
||||
return tag;
|
||||
}
|
||||
Debug.LogError($"[TagManager] Trying to get unregistered Tag: {tagName}");
|
||||
return emptyTag;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static JovianTag GetTag(int id) {
|
||||
return tagsById.GetValueOrDefault(id, emptyTag);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryGetGameTag(string tagName, out JovianTag tag) {
|
||||
tag = emptyTag;
|
||||
return !string.IsNullOrEmpty(tagName) && tagsByName.TryGetValue(tagName, out tag);
|
||||
}
|
||||
|
||||
public static bool TryGetGameTagThatIsNotNone(string tagName, out JovianTag tag) {
|
||||
if(!TryGetGameTag(tagName, out tag)) {
|
||||
return false;
|
||||
}
|
||||
return tag.Id != emptyTagId;
|
||||
}
|
||||
|
||||
public static void GetAllTags(List<JovianTag> results) {
|
||||
foreach(var kvp in tagsByName) {
|
||||
if(kvp.Value.Id != emptyTagId)
|
||||
results.Add(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string TagToString(JovianTag jovianTag) {
|
||||
return tagNames.TryGetValue(jovianTag.Id, out var text) ? text : string.Empty;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string TagToString(int tagId) {
|
||||
return tagNames.TryGetValue(tagId, out var text) ? text : string.Empty;
|
||||
}
|
||||
|
||||
public static string DisplayName(string name) {
|
||||
var lastDot = name.LastIndexOf(tagDelimiter);
|
||||
return lastDot < 0 ? name : name.Substring(lastDot + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e700d92d5f8326b4aacff3563881ed6f
|
||||
90
Packages/com.jovian.tag-system/Runtime/JovianTagsSettings.cs
Normal file
90
Packages/com.jovian.tag-system/Runtime/JovianTagsSettings.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
[CreateAssetMenu(menuName = "Jovian/Tag System/JovianTagSettings")]
|
||||
public class JovianTagsSettings : ScriptableObject {
|
||||
public RegisteredTag[] gameTags = Array.Empty<RegisteredTag>();
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex ValidSegment =
|
||||
new(@"^[A-Za-z][A-Za-z0-9_]*$");
|
||||
|
||||
private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal) {
|
||||
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char",
|
||||
"checked", "class", "const", "continue", "decimal", "default", "delegate", "do",
|
||||
"double", "else", "enum", "event", "explicit", "extern", "false", "finally",
|
||||
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int",
|
||||
"interface", "internal", "is", "lock", "long", "namespace", "new", "null",
|
||||
"object", "operator", "out", "override", "params", "private", "protected",
|
||||
"public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
|
||||
"stackalloc", "static", "string", "struct", "switch", "this", "throw", "true",
|
||||
"try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
|
||||
"virtual", "void", "volatile", "while"
|
||||
};
|
||||
|
||||
public void AddGameTag(string newTag) {
|
||||
List<string> tagHierarchy = newTag.Split(JovianTagsHandler.tagDelimiter).ToList();
|
||||
tagHierarchy.Remove("");
|
||||
newTag = string.Join(JovianTagsHandler.tagDelimiter, tagHierarchy);
|
||||
|
||||
// Validate each segment
|
||||
foreach(var segment in tagHierarchy) {
|
||||
if(!ValidSegment.IsMatch(segment)) {
|
||||
Debug.LogError($"Invalid tag segment '{segment}': must start with a letter and contain only letters, digits, or underscores.");
|
||||
return;
|
||||
}
|
||||
if(CSharpKeywords.Contains(segment)) {
|
||||
Debug.LogError($"Invalid tag segment '{segment}': is a C# reserved keyword.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(gameTags.Contains(new(newTag))) {
|
||||
Debug.LogError($"{newTag} is already added to the game");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] tagsSplit = newTag.Split(JovianTagsHandler.tagDelimiter);
|
||||
string tagToAdd = "";
|
||||
for(int i = 0, n = tagsSplit.Length; i < n; i++) {
|
||||
tagToAdd += tagsSplit[i];
|
||||
if(gameTags.Any((a) => a.tag == tagToAdd)) {
|
||||
tagToAdd += ".";
|
||||
continue;
|
||||
}
|
||||
|
||||
var previous = gameTags;
|
||||
gameTags = new RegisteredTag[gameTags.Length + 1];
|
||||
previous.CopyTo(gameTags, 0);
|
||||
gameTags[^1] = new RegisteredTag(tagToAdd);
|
||||
tagToAdd += ".";
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static bool IsValidSegment(string segment) {
|
||||
return !string.IsNullOrWhiteSpace(segment)
|
||||
&& ValidSegment.IsMatch(segment)
|
||||
&& !CSharpKeywords.Contains(segment);
|
||||
}
|
||||
|
||||
private static bool IsValidTag(string tag) {
|
||||
if(string.IsNullOrWhiteSpace(tag)) return false;
|
||||
return tag.Split(JovianTagsHandler.tagDelimiter).All(IsValidSegment);
|
||||
}
|
||||
|
||||
public void RemoveTag(string gameTagToRemove) {
|
||||
var tagSearch = gameTags.ToList();
|
||||
tagSearch.RemoveAll((a) => a.tag.StartsWith(gameTagToRemove));
|
||||
gameTags = tagSearch.ToArray();
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e658688044a8f469e0c311f9c4facb
|
||||
8
Packages/com.jovian.tag-system/Tests.meta
Normal file
8
Packages/com.jovian.tag-system/Tests.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e15e55a8a150db444b90c0394baf332f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.tag-system/Tests/Editor.meta
Normal file
8
Packages/com.jovian.tag-system/Tests/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 973227b88de0b1e4ba64c1955c03f09a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using Jovian.TagSystem;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Jovian.TagSystem.Tests {
|
||||
public class TestGameTagUtility {
|
||||
private const string TagBase = "Test";
|
||||
private const string TagOne = "Test.One";
|
||||
private const string TagTwo = "Test.Two";
|
||||
private const string TagThree = "Test.Three";
|
||||
private const string TagFour = "Test.One.Four";
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
var tags = new RegisteredTag[] { new(TagOne), new(TagTwo), new(TagThree), new(TagFour), new(TagBase) };
|
||||
JovianTagsHandler.Initialize();
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCreateGameTagContainer() {
|
||||
var selection = new JovianTagsGroup(TagOne, TagTwo, TagFour);
|
||||
var container = CreateGameTagContainer(selection);
|
||||
Assert.AreEqual(3, container.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCreateGameTagContainerWithCapacity() {
|
||||
var selection = new JovianTagsGroup(TagOne, TagTwo, TagFour);
|
||||
var container = CreateGameTagContainer(selection, 10);
|
||||
Assert.AreEqual(3, container.Count);
|
||||
Assert.AreEqual(10, container.entries.Capacity);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void PostTest() {
|
||||
JovianTagsHandler.Reset();
|
||||
}
|
||||
|
||||
private JovianTagsContainer CreateGameTagContainer(JovianTagsGroup group) {
|
||||
return group.ToContainer();
|
||||
}
|
||||
|
||||
private JovianTagsContainer CreateGameTagContainer(JovianTagsGroup group, int capacity) {
|
||||
if(!group.HasAny()) return JovianTagsContainer.Empty;
|
||||
var container = new JovianTagsContainer(capacity);
|
||||
foreach(var tag in group.tags) {
|
||||
var resolved = JovianTagsHandler.GetTag(tag);
|
||||
if(resolved.IsValid()) container.Add(resolved);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9c31310bf551eb49bc72359408c5dee
|
||||
109
Packages/com.jovian.tag-system/Tests/Editor/TestJovianTag.cs
Normal file
109
Packages/com.jovian.tag-system/Tests/Editor/TestJovianTag.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Jovian.TagSystem.Tests {
|
||||
public class TestJovianTag {
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
JovianTagsHandler.Initialize();
|
||||
JovianTagsHandler.RegisterTag("A.B.C.D");
|
||||
JovianTagsHandler.RegisterTag("A.B.E");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() {
|
||||
JovianTagsHandler.Reset();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenTwoEqualTags_WhenCompared_ThenEqual() {
|
||||
var tag1 = JovianTagsHandler.GetTag("A.B");
|
||||
var tag2 = JovianTagsHandler.GetTag("A.B");
|
||||
Assert.AreEqual(tag1, tag2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenNoneTag_WhenChecked_ThenIsNone() {
|
||||
Assert.IsTrue(JovianTagsHandler.emptyTag.IsNone());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenValidTag_WhenChecked_ThenIsValid() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B");
|
||||
Assert.IsTrue(tag.IsValid());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSameTag_WhenIsDescendantOf_ThenTrue() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B");
|
||||
Assert.IsTrue(tag.IsDescendantOf(tag));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenChild_WhenIsDescendantOfParent_ThenTrue() {
|
||||
var parent = JovianTagsHandler.GetTag("A.B");
|
||||
var child = JovianTagsHandler.GetTag("A.B.C");
|
||||
Assert.IsTrue(child.IsDescendantOf(parent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenGrandchild_WhenIsDescendantOfRoot_ThenTrue() {
|
||||
var root = JovianTagsHandler.GetTag("A");
|
||||
var grandchild = JovianTagsHandler.GetTag("A.B.C.D");
|
||||
Assert.IsTrue(grandchild.IsDescendantOf(root));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenParent_WhenIsDescendantOfChild_ThenFalse() {
|
||||
var parent = JovianTagsHandler.GetTag("A.B");
|
||||
var child = JovianTagsHandler.GetTag("A.B.C");
|
||||
Assert.IsFalse(parent.IsDescendantOf(child));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenParent_WhenIsAncestorOfChild_ThenTrue() {
|
||||
var parent = JovianTagsHandler.GetTag("A");
|
||||
var child = JovianTagsHandler.GetTag("A.B.C.D");
|
||||
Assert.IsTrue(parent.IsAncestorOf(child));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenChild_WhenIsAncestorOfParent_ThenFalse() {
|
||||
var parent = JovianTagsHandler.GetTag("A");
|
||||
var child = JovianTagsHandler.GetTag("A.B.C.D");
|
||||
Assert.IsFalse(child.IsAncestorOf(parent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSiblings_WhenIsSiblingTo_ThenTrue() {
|
||||
var c = JovianTagsHandler.GetTag("A.B.C");
|
||||
var e = JovianTagsHandler.GetTag("A.B.E");
|
||||
// Both have parent A.B
|
||||
Assert.IsTrue(c.IsSiblingTo(e));
|
||||
Assert.IsTrue(e.IsSiblingTo(c));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenNonSiblings_WhenIsSiblingTo_ThenFalse() {
|
||||
var d = JovianTagsHandler.GetTag("A.B.C.D");
|
||||
var e = JovianTagsHandler.GetTag("A.B.E");
|
||||
// D's parent is C, E's parent is B — not siblings
|
||||
Assert.IsFalse(d.IsSiblingTo(e));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenUnrelatedBranches_WhenCrossChecked_ThenFalse() {
|
||||
var c = JovianTagsHandler.GetTag("A.B.C");
|
||||
var e = JovianTagsHandler.GetTag("A.B.E");
|
||||
Assert.IsFalse(c.IsDescendantOf(e));
|
||||
Assert.IsFalse(e.IsDescendantOf(c));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenTwoDifferentTags_WhenCompared_ThenNotEqual() {
|
||||
var tag1 = JovianTagsHandler.GetTag("A.B.C");
|
||||
var tag2 = JovianTagsHandler.GetTag("A.B.E");
|
||||
Assert.AreNotEqual(tag1, tag2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 743efdf77fd571944a6ae11fff741bb5
|
||||
@@ -0,0 +1,93 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Jovian.TagSystem.Tests {
|
||||
public class TestJovianTagsContainer {
|
||||
private JovianTagsContainer container;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
JovianTagsHandler.Initialize();
|
||||
JovianTagsHandler.RegisterTag("A.B.C.D");
|
||||
JovianTagsHandler.RegisterTag("A.B.E");
|
||||
JovianTagsHandler.RegisterTag("X.Y");
|
||||
container = new JovianTagsContainer(8);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() {
|
||||
JovianTagsHandler.Reset();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenEmptyContainer_WhenAdd_ThenContainsTag() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B");
|
||||
container.Add(tag);
|
||||
Assert.IsTrue(container.Contains(tag));
|
||||
Assert.AreEqual(1, container.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainer_WhenAddDuplicate_ThenNotAdded() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B");
|
||||
container.Add(tag);
|
||||
container.Add(tag);
|
||||
Assert.AreEqual(1, container.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainer_WhenRemove_ThenNotContained() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B");
|
||||
container.Add(tag);
|
||||
container.Remove(tag);
|
||||
Assert.IsFalse(container.Contains(tag));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainer_WhenClear_ThenEmpty() {
|
||||
container.Add(JovianTagsHandler.GetTag("A"));
|
||||
container.Add(JovianTagsHandler.GetTag("A.B"));
|
||||
container.Clear();
|
||||
Assert.AreEqual(0, container.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainer_WhenIndexer_ThenCorrectTag() {
|
||||
var tagA = JovianTagsHandler.GetTag("A");
|
||||
var tagB = JovianTagsHandler.GetTag("A.B");
|
||||
container.Add(tagA);
|
||||
container.Add(tagB);
|
||||
Assert.AreEqual(tagA, container[0]);
|
||||
Assert.AreEqual(tagB, container[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainerWithChild_WhenContainsDescendantOf_ThenTrue() {
|
||||
var parent = JovianTagsHandler.GetTag("A.B");
|
||||
container.Add(JovianTagsHandler.GetTag("A.B.C.D"));
|
||||
container.Add(JovianTagsHandler.GetTag("X.Y"));
|
||||
Assert.IsTrue(container.ContainsDescendantOf(parent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainerWithParent_WhenContainsAncestorOf_ThenTrue() {
|
||||
var child = JovianTagsHandler.GetTag("A.B.C");
|
||||
container.Add(JovianTagsHandler.GetTag("A.B"));
|
||||
container.Add(JovianTagsHandler.GetTag("X.Y"));
|
||||
Assert.IsTrue(container.ContainsAncestorOf(child));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainerWithSibling_WhenContainsSibling_ThenTrue() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B.C");
|
||||
container.Add(JovianTagsHandler.GetTag("A.B.E")); // sibling of C (both under A.B)
|
||||
Assert.IsTrue(container.ContainsSibling(tag));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenContainerWithoutSibling_WhenContainsSibling_ThenFalse() {
|
||||
var tag = JovianTagsHandler.GetTag("A.B.C");
|
||||
container.Add(JovianTagsHandler.GetTag("X.Y")); // unrelated
|
||||
Assert.IsFalse(container.ContainsSibling(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56948cc0d1c038549b8f0c3a42aec365
|
||||
@@ -0,0 +1,48 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Jovian.TagSystem.Tests {
|
||||
public class TestJovianTagsGroup {
|
||||
private const string TAG_ONE = "Test";
|
||||
private const int EXPECTED_TAG_ID = 1;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
var tags = new RegisteredTag[] { new(TAG_ONE) };
|
||||
JovianTagsHandler.Initialize();
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSelectionWithTag_WhenContains_ThenTrue() {
|
||||
var selection = new JovianTagsGroup(TAG_ONE);
|
||||
var resolved = JovianTagsHandler.GetTag(TAG_ONE);
|
||||
Assert.IsTrue(selection.Contains(resolved));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSelectionWithoutTag_WhenContains_ThenFalse() {
|
||||
var selection = new JovianTagsGroup("NonExistent");
|
||||
var resolved = JovianTagsHandler.GetTag(TAG_ONE);
|
||||
Assert.IsFalse(selection.Contains(resolved));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenMultipleTags_WhenToContainer_ThenAllResolved() {
|
||||
JovianTagsHandler.RegisterTag("Other");
|
||||
var selection = new JovianTagsGroup(TAG_ONE, "Other");
|
||||
var container = selection.ToContainer();
|
||||
Assert.AreEqual(2, container.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSelection_WhenHasAny_ThenCorrect() {
|
||||
Assert.IsTrue(new JovianTagsGroup(TAG_ONE).HasAny());
|
||||
Assert.IsFalse(new JovianTagsGroup().HasAny());
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Dispose() {
|
||||
JovianTagsHandler.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecc875077acb6d7499197f892e33d948
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Jovian.TagSystem.Tests {
|
||||
public class TestJovianTagsHandler {
|
||||
private const string TagBase = "Test";
|
||||
private const string TagOne = "Test.One";
|
||||
private const string TagTwo = "Test.Two";
|
||||
private const string TagThree = "Test.Three";
|
||||
private const string TagFour = "Test.One.Four";
|
||||
|
||||
private RegisteredTag[] tags;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
tags = new RegisteredTag[] { new(TagOne), new(TagTwo), new(TagThree), new(TagFour), new(TagBase) };
|
||||
JovianTagsHandler.Initialize();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() {
|
||||
JovianTagsHandler.Reset();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenUnregisteredTag_WhenGetTag_ThenReturnsNone() {
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
var tag = JovianTagsHandler.GetTag(TagOne);
|
||||
Assert.IsTrue(tag.IsNone());
|
||||
LogAssert.ignoreFailingMessages = false;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenRegisteredTags_WhenGetTag_ThenReturnsValidTag() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
var tagThree = JovianTagsHandler.GetTag(TagThree);
|
||||
Assert.IsTrue(tagOne.IsValid());
|
||||
Assert.IsTrue(tagThree.IsValid());
|
||||
Assert.AreNotEqual(tagOne.Id, tagThree.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(TagOne, ExpectedResult = true)]
|
||||
[TestCase(null, ExpectedResult = false)]
|
||||
[TestCase("", ExpectedResult = false)]
|
||||
[TestCase("None", ExpectedResult = true)]
|
||||
public bool GivenTryGetGameTag_ThenReturnsExpected(string tagName) {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
return JovianTagsHandler.TryGetGameTag(tagName, out _);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(TagOne, ExpectedResult = true)]
|
||||
[TestCase(null, ExpectedResult = false)]
|
||||
[TestCase("", ExpectedResult = false)]
|
||||
[TestCase("None", ExpectedResult = false)]
|
||||
public bool GivenTryGetGameTagNotNone_ThenReturnsExpected(string tagName) {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
return JovianTagsHandler.TryGetGameTagThatIsNotNone(tagName, out _);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenRegisteredTag_WhenTryGet_ThenTagIsCorrect() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
JovianTagsHandler.TryGetGameTag(TagOne, out var tag);
|
||||
Assert.AreEqual(JovianTagsHandler.GetTag(TagOne), tag);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenChildTag_WhenRegistered_ThenParentIdIsSet() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagFour = JovianTagsHandler.GetTag(TagFour);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
// TagFour is "Test.One.Four" — its parent should be "Test.One"
|
||||
Assert.AreEqual(tagOne.Id, tagFour.ParentId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenChildTag_WhenChecked_ThenIsDescendantOfParent() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagBase = JovianTagsHandler.GetTag(TagBase);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
var tagFour = JovianTagsHandler.GetTag(TagFour);
|
||||
|
||||
Assert.IsTrue(tagOne.IsDescendantOf(tagBase));
|
||||
Assert.IsTrue(tagFour.IsDescendantOf(tagBase));
|
||||
Assert.IsTrue(tagFour.IsDescendantOf(tagOne));
|
||||
Assert.IsFalse(tagBase.IsDescendantOf(tagOne));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenParentTag_WhenChecked_ThenIsAncestorOfChild() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagBase = JovianTagsHandler.GetTag(TagBase);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
var tagFour = JovianTagsHandler.GetTag(TagFour);
|
||||
|
||||
Assert.IsTrue(tagBase.IsAncestorOf(tagOne));
|
||||
Assert.IsTrue(tagBase.IsAncestorOf(tagFour));
|
||||
Assert.IsTrue(tagOne.IsAncestorOf(tagFour));
|
||||
Assert.IsFalse(tagFour.IsAncestorOf(tagBase));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSiblingTags_WhenChecked_ThenAreSiblings() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
var tagTwo = JovianTagsHandler.GetTag(TagTwo);
|
||||
var tagThree = JovianTagsHandler.GetTag(TagThree);
|
||||
var tagFour = JovianTagsHandler.GetTag(TagFour);
|
||||
|
||||
// One, Two, Three are all children of Test — siblings
|
||||
Assert.IsTrue(tagOne.IsSiblingTo(tagTwo));
|
||||
Assert.IsTrue(tagTwo.IsSiblingTo(tagThree));
|
||||
// Four is child of Test.One — not a sibling of Two
|
||||
Assert.IsFalse(tagFour.IsSiblingTo(tagTwo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenRegisteredTag_WhenTagToString_ThenCorrectName() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagThree = JovianTagsHandler.GetTag(TagThree);
|
||||
Assert.AreEqual(TagThree, JovianTagsHandler.TagToString(tagThree));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenTagName_WhenDisplayName_ThenReturnsLastSegment() {
|
||||
Assert.AreEqual("Four", JovianTagsHandler.DisplayName(TagFour));
|
||||
Assert.AreEqual("Test", JovianTagsHandler.DisplayName(TagBase));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenRegisteredTags_WhenGetById_ThenReturnsCorrectTag() {
|
||||
JovianTagsHandler.RegisterTags(tags);
|
||||
var tagOne = JovianTagsHandler.GetTag(TagOne);
|
||||
var tagById = JovianTagsHandler.GetTag(tagOne.Id);
|
||||
Assert.AreEqual(tagOne, tagById);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenEmptyTag_WhenChecked_ThenIsNone() {
|
||||
Assert.IsTrue(JovianTagsHandler.emptyTag.IsNone());
|
||||
Assert.IsFalse(JovianTagsHandler.emptyTag.IsValid());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSameTag_WhenRegisteredTwice_ThenSameId() {
|
||||
JovianTagsHandler.RegisterTag("Foo.Bar");
|
||||
var first = JovianTagsHandler.GetTag("Foo.Bar");
|
||||
JovianTagsHandler.RegisterTag("Foo.Bar");
|
||||
var second = JovianTagsHandler.GetTag("Foo.Bar");
|
||||
Assert.AreEqual(first.Id, second.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd13f72302bceda4b8025e613532c78c
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Jovian.TagSystem.Tests",
|
||||
"rootNamespace": "Jovian.TagSystem.Tests",
|
||||
"references": [
|
||||
"Jovian.TagSystem"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edb98aafe9b3b37478eb078aae34a5ce
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
6
Packages/com.jovian.tag-system/package.json
Normal file
6
Packages/com.jovian.tag-system/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "com.jovian.tag-system",
|
||||
"displayName": "Jovian Tag System",
|
||||
"version": "1.0.0",
|
||||
"description": "Strongly typed and hierarchical game tag system"
|
||||
}
|
||||
7
Packages/com.jovian.tag-system/package.json.meta
Normal file
7
Packages/com.jovian.tag-system/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb03f6dd77cb80e4a870bbdf375fc85c
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"com.unity.2d.animation": "13.0.4",
|
||||
"com.unity.2d.psdimporter": "12.0.1",
|
||||
"com.unity.addressables": "2.8.0",
|
||||
"com.unity.ai.navigation": "2.0.10",
|
||||
"com.unity.collab-proxy": "2.11.3",
|
||||
"com.unity.2d.animation": "13.0.5",
|
||||
"com.unity.2d.psdimporter": "12.0.2",
|
||||
"com.unity.addressables": "2.8.1",
|
||||
"com.unity.ai.navigation": "2.0.12",
|
||||
"com.unity.collab-proxy": "2.11.4",
|
||||
"com.unity.editorcoroutines": "1.0.1",
|
||||
"com.unity.ide.rider": "3.0.39",
|
||||
"com.unity.ide.visualstudio": "2.0.26",
|
||||
"com.unity.ide.rider": "3.0.40",
|
||||
"com.unity.ide.visualstudio": "2.0.27",
|
||||
"com.unity.inputsystem": "1.18.0",
|
||||
"com.unity.localization": "1.5.9",
|
||||
"com.unity.memoryprofiler": "1.1.9",
|
||||
"com.unity.localization": "1.5.11",
|
||||
"com.unity.memoryprofiler": "1.1.12",
|
||||
"com.unity.multiplayer.center": "1.0.1",
|
||||
"com.unity.render-pipelines.universal": "17.3.0",
|
||||
"com.unity.terrain-tools": "5.3.1",
|
||||
"com.unity.terrain-tools": "5.3.2",
|
||||
"com.unity.test-framework": "1.6.0",
|
||||
"com.unity.timeline": "1.8.11",
|
||||
"com.unity.timeline": "1.8.12",
|
||||
"com.unity.ugui": "2.0.0",
|
||||
"com.unity.ui.test-framework": "1.0.0",
|
||||
"com.unity.visualscripting": "1.9.9",
|
||||
"com.unity.visualscripting": "1.9.11",
|
||||
"com.unity.modules.accessibility": "1.0.0",
|
||||
"com.unity.modules.adaptiveperformance": "1.0.0",
|
||||
"com.unity.modules.ai": "1.0.0",
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1"
|
||||
}
|
||||
},
|
||||
"com.jovian.tag-system": {
|
||||
"version": "file:com.jovian.tag-system",
|
||||
"depth": 0,
|
||||
"source": "embedded",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.jovian.unitypackagesync": {
|
||||
"version": "file:com.jovian.unitypackagesync",
|
||||
"depth": 0,
|
||||
@@ -93,11 +99,11 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.2d.animation": {
|
||||
"version": "13.0.4",
|
||||
"version": "13.0.5",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.2d.common": "12.0.2",
|
||||
"com.unity.2d.common": "12.0.3",
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.collections": "2.4.3",
|
||||
"com.unity.modules.animation": "1.0.0",
|
||||
@@ -106,7 +112,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.2d.common": {
|
||||
"version": "12.0.2",
|
||||
"version": "12.0.3",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -121,11 +127,11 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.2d.psdimporter": {
|
||||
"version": "12.0.1",
|
||||
"version": "12.0.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.2d.common": "12.0.1",
|
||||
"com.unity.2d.common": "12.0.3",
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.2d.tilemap": "1.0.0"
|
||||
},
|
||||
@@ -147,7 +153,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.addressables": {
|
||||
"version": "2.8.0",
|
||||
"version": "2.8.1",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -157,13 +163,13 @@
|
||||
"com.unity.modules.jsonserialize": "1.0.0",
|
||||
"com.unity.modules.imageconversion": "1.0.0",
|
||||
"com.unity.modules.unitywebrequest": "1.0.0",
|
||||
"com.unity.scriptablebuildpipeline": "2.5.1",
|
||||
"com.unity.scriptablebuildpipeline": "2.5.2",
|
||||
"com.unity.modules.unitywebrequestassetbundle": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ai.navigation": {
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.12",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -182,7 +188,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.collab-proxy": {
|
||||
"version": "2.11.3",
|
||||
"version": "2.11.4",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
@@ -215,7 +221,7 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.ide.rider": {
|
||||
"version": "3.0.39",
|
||||
"version": "3.0.40",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -224,7 +230,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ide.visualstudio": {
|
||||
"version": "2.0.26",
|
||||
"version": "2.0.27",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -242,7 +248,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.localization": {
|
||||
"version": "1.5.9",
|
||||
"version": "1.5.11",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -259,7 +265,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.memoryprofiler": {
|
||||
"version": "1.1.9",
|
||||
"version": "1.1.12",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -333,7 +339,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.scriptablebuildpipeline": {
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.2",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -359,7 +365,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.terrain-tools": {
|
||||
"version": "5.3.1",
|
||||
"version": "5.3.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -397,7 +403,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.timeline": {
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.12",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@@ -428,7 +434,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.visualscripting": {
|
||||
"version": "1.9.9",
|
||||
"version": "1.9.11",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user