2021-04-19 04:35:46 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
2026-06-25 18:09:33 +01:00
|
|
|
using System.Globalization;
|
2021-04-19 04:35:46 +01:00
|
|
|
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();
|
|
|
|
|
private AetheriaUnityObservedFacadeIndex _observedFacadeIndex;
|
|
|
|
|
|
|
|
|
|
public void SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)
|
|
|
|
|
{
|
|
|
|
|
_observedFacadeIndex = observedFacadeIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
AetheriaRuntimeLocalStorySurfaceBuilder.Build(ProjectStorySurface()),
|
|
|
|
|
HandleStorySurfaceCommand,
|
|
|
|
|
_surfaceChrome);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AetheriaRuntimeLocalStorySurfaceState ProjectStorySurface()
|
|
|
|
|
{
|
|
|
|
|
return AetheriaRuntimeLocalStorySurfaceBuilder.Project(
|
|
|
|
|
ResolveLocationLabel(),
|
|
|
|
|
_currentPath,
|
|
|
|
|
_activeStory?.currentText ?? "",
|
|
|
|
|
_activeStory?.canContinue == true,
|
|
|
|
|
_activeChoices.Select(choice => new AetheriaRuntimeLocalStoryChoiceState(
|
|
|
|
|
choice.Index,
|
|
|
|
|
choice.Choice?.text ?? "")),
|
|
|
|
|
DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 01:18:21 +01:00
|
|
|
private bool TryResolveDockingState(out AetheriaClientDockingSnapshot dockingState)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
2026-06-28 01:18:21 +01:00
|
|
|
dockingState = null;
|
2026-06-25 18:09:33 +01:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-06-28 01:18:21 +01:00
|
|
|
dockingState = ResolveClient()
|
2026-06-27 14:01:41 +01:00
|
|
|
.Aetheria()
|
2026-06-28 01:18:21 +01:00
|
|
|
.DockingState
|
|
|
|
|
.Latest();
|
2026-06-25 18:09:33 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-06-28 01:18:21 +01:00
|
|
|
Debug.LogWarning($"Failed to read Aetheria docking state for local story surface: {ex.Message}");
|
2026-06-25 18:09:33 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 01:18:21 +01:00
|
|
|
return dockingState != null;
|
2026-06-25 18:09:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryResolveDockedLocalStory(out LocationStory story)
|
|
|
|
|
{
|
|
|
|
|
story = null;
|
|
|
|
|
if (_observedFacadeIndex == null ||
|
2026-06-28 01:18:21 +01:00
|
|
|
!TryResolveDockingState(out var dockingState) ||
|
|
|
|
|
dockingState?.IsDocked != true ||
|
2026-06-28 02:08:17 +01:00
|
|
|
string.IsNullOrWhiteSpace(dockingState.DockParentEntityKey) ||
|
|
|
|
|
dockingState.DockingBayIndex < 0 ||
|
2026-06-25 18:09:33 +01:00
|
|
|
!_observedFacadeIndex.TryResolveDockingBayByRecordKey(
|
2026-06-28 02:08:17 +01:00
|
|
|
dockingState.DockParentEntityKey,
|
|
|
|
|
dockingState.DockingBayIndex,
|
2026-06-25 18:09:33 +01:00
|
|
|
out var dockingBay) ||
|
|
|
|
|
dockingBay?.Entity is not OrbitalEntity { Story: { } dockedStory })
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
story = dockedStory;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AetheriaClient ResolveClient()
|
|
|
|
|
{
|
2026-06-28 02:36:30 +01:00
|
|
|
return AetheriaUnityRuntimeClientProvider.ResolveClient(
|
|
|
|
|
AetheriaRuntimeStateBoot.Inspect(AetheriaUnityRuntimePaths.GameDataDirectory),
|
|
|
|
|
"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;
|
|
|
|
|
}
|
2021-05-16 05:24:51 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-19 04:35:46 +01:00
|
|
|
// Update is called once per frame
|
|
|
|
|
void Update()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|