9 Commits

Author SHA1 Message Date
Sebastian Bularca
a49a21c0cb cleaning 2026-05-17 22:44:17 +02:00
Sebastian Bularca
7be3499f14 some work on wiring in the encounters 2026-05-17 19:49:42 +02:00
Sebastian Bularca
3d13dac256 added tag systempackage 2026-05-17 18:49:35 +02:00
Sebastian Bularca
a239e6286b ada 2026-05-17 18:38:30 +02:00
Sebastian Bularca
b9985eaa71 add tag system 2026-05-17 18:37:35 +02:00
Sebastian Bularca
e5771b113a package update and prefabs applied 2026-05-17 18:30:41 +02:00
Sebastian Bularca
50ec5d44a8 will handle editor cleaning later 2026-05-08 01:16:58 +02:00
Sebastian Bularca
1e4aeb0e5b Merge branch 'main' of https://git.quiludet.com/uzihead/trail-into-darkness 2026-05-08 01:13:33 +02:00
Sebastian Bularca
e7f7da985b minor fixes 2026-05-08 01:13:30 +02:00
73 changed files with 3913 additions and 467 deletions

1
.gitignore vendored
View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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:

View File

@@ -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}

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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: []

View 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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c0acbee4371cc244b8e5b10e1bbab803
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 941a740b30b4595478b5e69393ffa045
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 45efc4b96d295b84498b0fed0c4130d1

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a2a32b341a059664bb365498df0d6bdf

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9190068b05141d74aae0f630a8080f23

View 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;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 126771008dabad6429c6a5408b5f204d

View 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));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 819384bf19a494342ba5e5f4319e34fd

View File

@@ -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();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d6c5df6e64308804287ab931e990df7b

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6aa5b2c3fc4ef68498c7852291a4e8a0

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 51e674b2adc94084caaa472190fca829
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 95d784d03badc334eb57943d0da95b0e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9a504e32ce3c16c4ba10a51b9444669a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Jovian.TagSystem.Editor"), InternalsVisibleTo("Jovian.TagSystem.Tests")]

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 925b98fc2eb4d8f49900afdee32991a1

View File

@@ -0,0 +1,14 @@
{
"name": "Jovian.TagSystem",
"rootNamespace": "Jovian.TagSystem",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f71bff532e6b8e24a8fc3e2761205890
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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})";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2f7edc6a4a42107499ed2b46654d63f3

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cb368346014a3644995688d0a325abcd

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 140c6dc4e1c289e48b7441ac6ee5e20f

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e700d92d5f8326b4aacff3563881ed6f

View 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
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 32e658688044a8f469e0c311f9c4facb

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e15e55a8a150db444b90c0394baf332f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 973227b88de0b1e4ba64c1955c03f09a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9c31310bf551eb49bc72359408c5dee

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 743efdf77fd571944a6ae11fff741bb5

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 56948cc0d1c038549b8f0c3a42aec365

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ecc875077acb6d7499197f892e33d948

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cd13f72302bceda4b8025e613532c78c

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: edb98aafe9b3b37478eb078aae34a5ce
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cb03f6dd77cb80e4a870bbdf375fc85c
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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",

View File

@@ -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": {