2021-04-19 04:35:46 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
2026-06-25 18:09:33 +01:00
|
|
|
using GameCult.Aetheria.EveRuntime;
|
|
|
|
|
using GameCult.Aetheria.State.Verse;
|
|
|
|
|
using GameCult.Eve.Surface;
|
2021-04-19 04:35:46 +01:00
|
|
|
using Ink.Runtime;
|
|
|
|
|
using TMPro;
|
|
|
|
|
using UniRx;
|
|
|
|
|
using UniRx.Triggers;
|
|
|
|
|
using UnityEngine;
|
2026-06-25 18:09:33 +01:00
|
|
|
using UnityEngine.UIElements;
|
2021-04-19 04:35:46 +01:00
|
|
|
|
|
|
|
|
public class LocalMenu : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
public ObservablePointerClickTrigger ContinueTrigger;
|
|
|
|
|
public TextMeshProUGUI Output;
|
|
|
|
|
public RectTransform ChoiceParent;
|
|
|
|
|
public ChoicePrefab ChoicePrefab;
|
2021-05-16 05:24:51 +01:00
|
|
|
|
|
|
|
|
private string _currentPath;
|
|
|
|
|
private LocationStory _currentLocation;
|
|
|
|
|
private Story _activeStory;
|
2026-06-25 18:09:33 +01:00
|
|
|
private readonly List<ActiveStoryChoice> _activeChoices = new List<ActiveStoryChoice>();
|
|
|
|
|
private UIDocument _surfaceDocument;
|
|
|
|
|
private readonly AetheriaEveUnitySurfaceChrome _surfaceChrome = new AetheriaEveUnitySurfaceChrome();
|
2026-06-28 03:34:08 +01:00
|
|
|
private AetheriaUnityObservedEntityIndex _observedEntityIndex;
|
2026-06-28 12:30:16 +01:00
|
|
|
private AetheriaUnityObservedDockingIndex _observedDockingIndex;
|
2026-06-28 10:07:05 +01:00
|
|
|
private string _clientStatePath = "";
|
2026-06-25 18:09:33 +01:00
|
|
|
|
2026-06-28 03:34:08 +01:00
|
|
|
public void SetObservedEntityIndex(AetheriaUnityObservedEntityIndex observedEntityIndex)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
2026-06-28 12:30:16 +01:00
|
|
|
if (!ReferenceEquals(_observedEntityIndex, observedEntityIndex))
|
|
|
|
|
{
|
|
|
|
|
_observedDockingIndex?.Dispose();
|
|
|
|
|
_observedDockingIndex = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 03:34:08 +01:00
|
|
|
_observedEntityIndex = observedEntityIndex;
|
2026-06-25 18:09:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class ActiveStoryChoice
|
|
|
|
|
{
|
|
|
|
|
public ActiveStoryChoice(int index, Story story, Choice choice)
|
|
|
|
|
{
|
|
|
|
|
Index = index;
|
|
|
|
|
Story = story;
|
|
|
|
|
Choice = choice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int Index { get; }
|
|
|
|
|
public Story Story { get; }
|
|
|
|
|
public Choice Choice { get; }
|
|
|
|
|
}
|
2021-04-19 04:35:46 +01:00
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
if (!TryResolveDockedLocalStory(out _currentLocation))
|
2021-05-16 05:24:51 +01:00
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
HideStorySurface();
|
2026-06-21 19:10:53 +01:00
|
|
|
return;
|
2021-05-16 05:24:51 +01:00
|
|
|
}
|
2026-06-21 19:10:53 +01:00
|
|
|
|
|
|
|
|
_activeStory = _currentLocation.Story;
|
2021-04-19 04:35:46 +01:00
|
|
|
Continue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Start()
|
|
|
|
|
{
|
|
|
|
|
ContinueTrigger.OnPointerClickAsObservable().Subscribe(pointerEvent =>
|
|
|
|
|
{
|
2021-05-16 05:24:51 +01:00
|
|
|
if (_activeStory == null) return;
|
|
|
|
|
Continue();
|
2021-04-19 04:35:46 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Continue()
|
|
|
|
|
{
|
2021-05-16 05:24:51 +01:00
|
|
|
if (!_activeStory.state.previousPointer.isNull) _currentPath = _activeStory.state.previousPointer.path.head.name;
|
|
|
|
|
if(_activeStory.canContinue) _activeStory.Continue();
|
|
|
|
|
if (!_activeStory.state.previousPointer.isNull) _currentPath = _activeStory.state.previousPointer.path.head.name;
|
2026-06-25 18:09:33 +01:00
|
|
|
if (Output != null)
|
|
|
|
|
Output.text = _activeStory.currentText;
|
2021-04-19 04:35:46 +01:00
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
_activeChoices.Clear();
|
2021-04-19 04:35:46 +01:00
|
|
|
|
2021-05-16 05:24:51 +01:00
|
|
|
if(_activeStory.currentChoices.Any())
|
|
|
|
|
{
|
|
|
|
|
PresentCurrentChoices();
|
|
|
|
|
}
|
|
|
|
|
else if (!_activeStory.canContinue)
|
2021-04-19 04:35:46 +01:00
|
|
|
{
|
2021-05-16 05:24:51 +01:00
|
|
|
// There's no choices, but we also can't continue; indicates we hit an END
|
|
|
|
|
if (_activeStory == _currentLocation.Story)
|
|
|
|
|
{
|
|
|
|
|
// END inside location-based story thread, restart the story
|
|
|
|
|
_activeStory.ResetState();
|
|
|
|
|
Continue();
|
|
|
|
|
}
|
|
|
|
|
else
|
2021-04-19 04:35:46 +01:00
|
|
|
{
|
2021-05-16 05:24:51 +01:00
|
|
|
// END inside quest content, switch back to location thread and present choices
|
|
|
|
|
_activeStory = _currentLocation.Story;
|
2021-04-19 04:35:46 +01:00
|
|
|
Continue();
|
2021-05-16 05:24:51 +01:00
|
|
|
}
|
2021-04-19 04:35:46 +01:00
|
|
|
}
|
2026-06-25 18:09:33 +01:00
|
|
|
|
|
|
|
|
RenderStorySurface();
|
2021-04-19 04:35:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-05-16 05:24:51 +01:00
|
|
|
void PresentCurrentChoices()
|
|
|
|
|
{
|
|
|
|
|
Debug.Log($"Current Path: \"{_currentPath}\" in {(_activeStory == _currentLocation.Story ? "Location Story" : "Quest Story")}");
|
|
|
|
|
if(!string.IsNullOrEmpty(_currentPath) && _currentLocation.KnotQuests.ContainsKey(_currentPath))
|
|
|
|
|
{
|
|
|
|
|
foreach (var quest in _currentLocation.KnotQuests[_currentPath])
|
|
|
|
|
{
|
|
|
|
|
if (quest.Story == _activeStory) continue; // Don't repeat choices for active injected branch
|
|
|
|
|
|
|
|
|
|
quest.Story.ChoosePathString(_currentPath);
|
|
|
|
|
quest.Story.ContinueMaximally();
|
|
|
|
|
foreach (var choice in quest.Story.currentChoices) PresentChoice(quest.Story, choice);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach (var choice in _activeStory.currentChoices) PresentChoice(_activeStory, choice);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PresentChoice(Story story, Choice choice)
|
|
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
_activeChoices.Add(new ActiveStoryChoice(_activeChoices.Count, story, choice));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RenderStorySurface()
|
|
|
|
|
{
|
|
|
|
|
if (_activeStory == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (Output != null)
|
|
|
|
|
Output.gameObject.SetActive(false);
|
|
|
|
|
if (ChoiceParent != null)
|
|
|
|
|
ChoiceParent.gameObject.SetActive(false);
|
|
|
|
|
|
|
|
|
|
_surfaceDocument = AetheriaEveUnitySurfaceHost.RenderRuntime(
|
|
|
|
|
transform,
|
|
|
|
|
_surfaceDocument,
|
|
|
|
|
"Aetheria Runtime Local Story Surface",
|
2026-06-29 05:21:22 +01:00
|
|
|
AetheriaRuntimeLocalStorySurfaceBuilder.Build(
|
|
|
|
|
ResolveLocationLabel(),
|
|
|
|
|
_currentPath,
|
|
|
|
|
_activeStory?.currentText ?? "",
|
|
|
|
|
_activeStory?.canContinue == true,
|
|
|
|
|
_activeChoices.Select(choice => new AetheriaRuntimeLocalStoryChoiceState(
|
|
|
|
|
choice.Index,
|
|
|
|
|
choice.Choice?.text ?? "")),
|
|
|
|
|
DateTime.UtcNow.ToString("O")),
|
2026-06-25 18:09:33 +01:00
|
|
|
HandleStorySurfaceCommand,
|
|
|
|
|
_surfaceChrome);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleStorySurfaceCommand(EveSurfaceCommandRequest request)
|
|
|
|
|
{
|
|
|
|
|
if (!AetheriaRuntimeLocalStorySurfaceCommands.TryRead(request, out var command))
|
2021-05-16 05:24:51 +01:00
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
Debug.LogWarning($"Unknown runtime local story command: {request?.Command}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (command.Kind)
|
|
|
|
|
{
|
|
|
|
|
case AetheriaRuntimeLocalStoryCommandKind.Continue:
|
|
|
|
|
Continue();
|
|
|
|
|
return;
|
|
|
|
|
case AetheriaRuntimeLocalStoryCommandKind.Choose:
|
|
|
|
|
ChooseStoryOption(command.ChoiceIndex);
|
|
|
|
|
return;
|
|
|
|
|
default:
|
|
|
|
|
Debug.LogWarning($"Unknown runtime local story command: {request?.Command}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ChooseStoryOption(int choiceIndex)
|
|
|
|
|
{
|
|
|
|
|
var selected = _activeChoices.FirstOrDefault(choice => choice.Index == choiceIndex);
|
|
|
|
|
if (selected == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"Unknown runtime local story choice index: {choiceIndex}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_activeStory = selected.Story;
|
|
|
|
|
_activeStory.ChoosePath(selected.Choice.targetPath);
|
|
|
|
|
Continue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveLocationLabel()
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(_currentLocation?.Name))
|
|
|
|
|
return _currentLocation.Name;
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(_currentLocation?.FileName))
|
|
|
|
|
return _currentLocation.FileName;
|
|
|
|
|
return "Local";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryResolveDockedLocalStory(out LocationStory story)
|
|
|
|
|
{
|
|
|
|
|
story = null;
|
2026-06-28 03:34:08 +01:00
|
|
|
if (_observedEntityIndex == null ||
|
2026-06-28 12:30:16 +01:00
|
|
|
!TryResolveObservedDockingIndex(out var dockingIndex) ||
|
|
|
|
|
!dockingIndex.TryResolveCurrentDockingBay(out var dockingBay) ||
|
2026-06-25 18:09:33 +01:00
|
|
|
dockingBay?.Entity is not OrbitalEntity { Story: { } dockedStory })
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
story = dockedStory;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 12:30:16 +01:00
|
|
|
private bool TryResolveObservedDockingIndex(out AetheriaUnityObservedDockingIndex dockingIndex)
|
|
|
|
|
{
|
|
|
|
|
dockingIndex = null;
|
|
|
|
|
if (_observedEntityIndex == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
dockingIndex = _observedDockingIndex ??= new AetheriaUnityObservedDockingIndex(ResolveClient, _observedEntityIndex);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
private AetheriaClient ResolveClient()
|
|
|
|
|
{
|
2026-06-28 10:07:05 +01:00
|
|
|
var stateBoot = AetheriaRuntimeStateBoot.Inspect(AetheriaUnityRuntimePaths.GameDataDirectory);
|
|
|
|
|
if (!string.Equals(_clientStatePath, stateBoot.StateFilePath, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
_clientStatePath = stateBoot.StateFilePath;
|
2026-06-28 12:30:16 +01:00
|
|
|
_observedDockingIndex?.Dispose();
|
|
|
|
|
_observedDockingIndex = null;
|
2026-06-28 10:07:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AetheriaUnityRuntimeClientProvider.ResolveClient(stateBoot, "unity-runtime-local-story");
|
2026-06-25 18:09:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HideStorySurface()
|
|
|
|
|
{
|
|
|
|
|
if (_surfaceDocument == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
AetheriaEveUnitySurfaceHost.Hide(_surfaceDocument);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
{
|
|
|
|
|
HideStorySurface();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
{
|
|
|
|
|
if (_surfaceDocument != null)
|
|
|
|
|
{
|
|
|
|
|
AetheriaEveUnitySurfaceHost.DestroyDocument(_surfaceDocument);
|
|
|
|
|
_surfaceDocument = null;
|
|
|
|
|
}
|
2026-06-28 12:30:16 +01:00
|
|
|
_observedDockingIndex?.Dispose();
|
|
|
|
|
_observedDockingIndex = null;
|
2021-05-16 05:24:51 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-19 04:35:46 +01:00
|
|
|
// Update is called once per frame
|
|
|
|
|
void Update()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|