feat: implement AI Director & Narrative Voting System (Phase 9)
Add complete AI Director system that transforms the survival simulation into a user-driven interactive story with audience voting. Backend: - Add DirectorService for LLM-powered plot generation with fallback templates - Add VoteManager for dual-channel voting (Twitch + Unity) - Integrate 4-phase game loop: Simulation → Narrative → Voting → Resolution - Add vote command parsing (!1, !2, !A, !B) in Twitch service - Add type-safe LLM output handling with _coerce_int() helper - Normalize voter IDs for case-insensitive duplicate prevention Unity Client: - Add NarrativeUI for cinematic event cards and voting progress bars - Add 7 new event types and data models for director/voting events - Add delayed subscription coroutine for NetworkManager timing - Sync client timer with server's remaining_seconds to prevent drift Documentation: - Update README.md with AI Director features, voting commands, and event types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -362,6 +362,15 @@ namespace TheIsland.Models
|
||||
|
||||
// Phase 8: VFX
|
||||
public const string VFX_EVENT = "vfx_event";
|
||||
|
||||
// AI Director & Narrative Voting (Phase 9)
|
||||
public const string MODE_CHANGE = "mode_change";
|
||||
public const string NARRATIVE_PLOT = "narrative_plot";
|
||||
public const string VOTE_STARTED = "vote_started";
|
||||
public const string VOTE_UPDATE = "vote_update";
|
||||
public const string VOTE_ENDED = "vote_ended";
|
||||
public const string VOTE_RESULT = "vote_result";
|
||||
public const string RESOLUTION_APPLIED = "resolution_applied";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -449,4 +458,108 @@ namespace TheIsland.Models
|
||||
public int target_id; // Optional: if -1 or 0, might mean global or specific position logic
|
||||
public string message;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AI Director & Narrative Voting (Phase 9)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Mode change event data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ModeChangeData
|
||||
{
|
||||
public string mode; // "simulation", "narrative", "voting", "resolution"
|
||||
public string old_mode;
|
||||
public string message;
|
||||
public double ends_at; // Timestamp when this mode ends (for voting)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Choice option in a plot point.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PlotChoiceData
|
||||
{
|
||||
public string choice_id;
|
||||
public string text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Narrative plot event data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class NarrativePlotData
|
||||
{
|
||||
public string plot_id;
|
||||
public string title;
|
||||
public string description;
|
||||
public List<PlotChoiceData> choices;
|
||||
public int ttl_seconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vote started event data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VoteStartedData
|
||||
{
|
||||
public string vote_id;
|
||||
public List<PlotChoiceData> choices;
|
||||
public int duration_seconds;
|
||||
public double ends_at;
|
||||
public string source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Real-time vote update data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VoteUpdateData
|
||||
{
|
||||
public string vote_id;
|
||||
public List<int> tallies;
|
||||
public List<float> percentages;
|
||||
public int total_votes;
|
||||
public float remaining_seconds;
|
||||
public double ends_at;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vote ended event data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VoteEndedData
|
||||
{
|
||||
public string vote_id;
|
||||
public int total_votes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final voting result data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VoteResultData
|
||||
{
|
||||
public string vote_id;
|
||||
public string winning_choice_id;
|
||||
public string winning_choice_text;
|
||||
public int winning_index;
|
||||
public List<int> tallies;
|
||||
public List<float> percentages;
|
||||
public int total_votes;
|
||||
public bool is_tie;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolution applied event data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ResolutionAppliedData
|
||||
{
|
||||
public string plot_id;
|
||||
public string choice_id;
|
||||
public string message;
|
||||
public string effects_json;
|
||||
}
|
||||
}
|
||||
|
||||
588
unity-client/Assets/Scripts/NarrativeUI.cs
Normal file
588
unity-client/Assets/Scripts/NarrativeUI.cs
Normal file
@@ -0,0 +1,588 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using TheIsland.Models;
|
||||
using TheIsland.Network;
|
||||
|
||||
namespace TheIsland.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Cinematic narrative UI overlay for AI Director events and voting.
|
||||
/// Handles plot cards, voting bars, and resolution displays.
|
||||
/// </summary>
|
||||
public class NarrativeUI : MonoBehaviour
|
||||
{
|
||||
#region Singleton
|
||||
private static NarrativeUI _instance;
|
||||
public static NarrativeUI Instance => _instance;
|
||||
#endregion
|
||||
|
||||
#region UI References
|
||||
[Header("Main Panel")]
|
||||
[SerializeField] private CanvasGroup mainPanel;
|
||||
[SerializeField] private Image backgroundOverlay;
|
||||
|
||||
[Header("Event Card")]
|
||||
[SerializeField] private RectTransform eventCard;
|
||||
[SerializeField] private TextMeshProUGUI titleText;
|
||||
[SerializeField] private TextMeshProUGUI descriptionText;
|
||||
|
||||
[Header("Voting Panel")]
|
||||
[SerializeField] private RectTransform votingPanel;
|
||||
[SerializeField] private TextMeshProUGUI timerText;
|
||||
[SerializeField] private TextMeshProUGUI totalVotesText;
|
||||
|
||||
[Header("Choice A")]
|
||||
[SerializeField] private RectTransform choiceAContainer;
|
||||
[SerializeField] private TextMeshProUGUI choiceAText;
|
||||
[SerializeField] private Image choiceABar;
|
||||
[SerializeField] private TextMeshProUGUI choiceAPercentText;
|
||||
|
||||
[Header("Choice B")]
|
||||
[SerializeField] private RectTransform choiceBContainer;
|
||||
[SerializeField] private TextMeshProUGUI choiceBText;
|
||||
[SerializeField] private Image choiceBBar;
|
||||
[SerializeField] private TextMeshProUGUI choiceBPercentText;
|
||||
|
||||
[Header("Result Panel")]
|
||||
[SerializeField] private RectTransform resultPanel;
|
||||
[SerializeField] private TextMeshProUGUI resultTitleText;
|
||||
[SerializeField] private TextMeshProUGUI resultMessageText;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private float fadeInDuration = 0.5f;
|
||||
[SerializeField] private float fadeOutDuration = 0.3f;
|
||||
[SerializeField] private float cardSlideDistance = 100f;
|
||||
[SerializeField] private float barAnimationSpeed = 5f;
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
private bool isActive = false;
|
||||
private string currentPlotId;
|
||||
private float targetChoiceAPercent = 0f;
|
||||
private float targetChoiceBPercent = 0f;
|
||||
private float currentChoiceAPercent = 0f;
|
||||
private float currentChoiceBPercent = 0f;
|
||||
private double votingEndsAt = 0;
|
||||
private Coroutine timerCoroutine;
|
||||
private bool isSubscribed = false;
|
||||
private Coroutine subscribeCoroutine;
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
|
||||
// Initialize UI state
|
||||
if (mainPanel != null) mainPanel.alpha = 0;
|
||||
if (mainPanel != null) mainPanel.blocksRaycasts = false;
|
||||
HideAllPanels();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Start coroutine to subscribe when NetworkManager is ready
|
||||
subscribeCoroutine = StartCoroutine(SubscribeWhenReady());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Stop subscribe coroutine if running
|
||||
if (subscribeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(subscribeCoroutine);
|
||||
subscribeCoroutine = null;
|
||||
}
|
||||
|
||||
// Unsubscribe from network events
|
||||
UnsubscribeFromNetwork();
|
||||
}
|
||||
|
||||
private IEnumerator SubscribeWhenReady()
|
||||
{
|
||||
// Wait until NetworkManager is available
|
||||
while (NetworkManager.Instance == null)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
SubscribeToNetwork();
|
||||
}
|
||||
|
||||
private void SubscribeToNetwork()
|
||||
{
|
||||
if (isSubscribed) return;
|
||||
|
||||
var network = NetworkManager.Instance;
|
||||
if (network == null) return;
|
||||
|
||||
network.OnModeChange += HandleModeChange;
|
||||
network.OnNarrativePlot += HandleNarrativePlot;
|
||||
network.OnVoteStarted += HandleVoteStarted;
|
||||
network.OnVoteUpdate += HandleVoteUpdate;
|
||||
network.OnVoteResult += HandleVoteResult;
|
||||
network.OnResolutionApplied += HandleResolutionApplied;
|
||||
isSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeFromNetwork()
|
||||
{
|
||||
if (!isSubscribed) return;
|
||||
|
||||
var network = NetworkManager.Instance;
|
||||
if (network == null)
|
||||
{
|
||||
isSubscribed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
network.OnModeChange -= HandleModeChange;
|
||||
network.OnNarrativePlot -= HandleNarrativePlot;
|
||||
network.OnVoteStarted -= HandleVoteStarted;
|
||||
network.OnVoteUpdate -= HandleVoteUpdate;
|
||||
network.OnVoteResult -= HandleVoteResult;
|
||||
network.OnResolutionApplied -= HandleResolutionApplied;
|
||||
isSubscribed = false;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Smoothly animate voting bars
|
||||
if (isActive && votingPanel != null && votingPanel.gameObject.activeSelf)
|
||||
{
|
||||
AnimateVotingBars();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
private void HandleModeChange(ModeChangeData data)
|
||||
{
|
||||
Debug.Log($"[NarrativeUI] Mode changed: {data.old_mode} -> {data.mode}");
|
||||
|
||||
switch (data.mode)
|
||||
{
|
||||
case "simulation":
|
||||
// Fade out UI when returning to simulation
|
||||
if (isActive)
|
||||
{
|
||||
StartCoroutine(FadeOutUI());
|
||||
}
|
||||
break;
|
||||
|
||||
case "narrative":
|
||||
case "voting":
|
||||
// Ensure UI is visible
|
||||
if (!isActive)
|
||||
{
|
||||
StartCoroutine(FadeInUI());
|
||||
}
|
||||
break;
|
||||
|
||||
case "resolution":
|
||||
// Keep UI visible for resolution display
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNarrativePlot(NarrativePlotData data)
|
||||
{
|
||||
Debug.Log($"[NarrativeUI] Narrative plot: {data.title}");
|
||||
currentPlotId = data.plot_id;
|
||||
|
||||
// Show event card
|
||||
ShowEventCard(data.title, data.description);
|
||||
|
||||
// Prepare voting choices
|
||||
if (data.choices != null && data.choices.Count >= 2)
|
||||
{
|
||||
SetupVotingChoices(data.choices[0].text, data.choices[1].text);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleVoteStarted(VoteStartedData data)
|
||||
{
|
||||
Debug.Log($"[NarrativeUI] Vote started: {data.vote_id}");
|
||||
|
||||
votingEndsAt = data.ends_at;
|
||||
|
||||
// Setup choices if not already done
|
||||
if (data.choices != null && data.choices.Count >= 2)
|
||||
{
|
||||
SetupVotingChoices(data.choices[0].text, data.choices[1].text);
|
||||
}
|
||||
|
||||
// Show voting panel
|
||||
ShowVotingPanel();
|
||||
|
||||
// Start countdown timer
|
||||
if (timerCoroutine != null) StopCoroutine(timerCoroutine);
|
||||
timerCoroutine = StartCoroutine(UpdateTimer());
|
||||
}
|
||||
|
||||
private void HandleVoteUpdate(VoteUpdateData data)
|
||||
{
|
||||
// Update target percentages for smooth animation
|
||||
if (data.percentages != null && data.percentages.Count >= 2)
|
||||
{
|
||||
targetChoiceAPercent = data.percentages[0];
|
||||
targetChoiceBPercent = data.percentages[1];
|
||||
}
|
||||
|
||||
// Sync timer with server's remaining_seconds to avoid clock drift
|
||||
if (data.remaining_seconds > 0)
|
||||
{
|
||||
double now = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
votingEndsAt = now + data.remaining_seconds;
|
||||
}
|
||||
|
||||
// Update total votes display
|
||||
if (totalVotesText != null)
|
||||
{
|
||||
totalVotesText.text = $"{data.total_votes} votes";
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleVoteResult(VoteResultData data)
|
||||
{
|
||||
Debug.Log($"[NarrativeUI] Vote result: {data.winning_choice_text}");
|
||||
|
||||
// Stop timer
|
||||
if (timerCoroutine != null)
|
||||
{
|
||||
StopCoroutine(timerCoroutine);
|
||||
timerCoroutine = null;
|
||||
}
|
||||
|
||||
// Flash winning choice
|
||||
StartCoroutine(FlashWinningChoice(data.winning_index));
|
||||
|
||||
// Show result briefly
|
||||
ShowResult($"The Audience Has Spoken!", data.winning_choice_text);
|
||||
}
|
||||
|
||||
private void HandleResolutionApplied(ResolutionAppliedData data)
|
||||
{
|
||||
Debug.Log($"[NarrativeUI] Resolution: {data.message}");
|
||||
|
||||
// Update result display with full resolution message
|
||||
ShowResult("Consequence", data.message);
|
||||
|
||||
// Auto-hide after delay
|
||||
StartCoroutine(HideAfterDelay(5f));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Control Methods
|
||||
private void HideAllPanels()
|
||||
{
|
||||
if (eventCard != null) eventCard.gameObject.SetActive(false);
|
||||
if (votingPanel != null) votingPanel.gameObject.SetActive(false);
|
||||
if (resultPanel != null) resultPanel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ShowEventCard(string title, string description)
|
||||
{
|
||||
if (eventCard == null) return;
|
||||
|
||||
// Set content
|
||||
if (titleText != null) titleText.text = title;
|
||||
if (descriptionText != null) descriptionText.text = description;
|
||||
|
||||
// Show card with animation
|
||||
eventCard.gameObject.SetActive(true);
|
||||
StartCoroutine(SlideInCard(eventCard));
|
||||
}
|
||||
|
||||
private void SetupVotingChoices(string choiceA, string choiceB)
|
||||
{
|
||||
if (choiceAText != null) choiceAText.text = $"!1 {choiceA}";
|
||||
if (choiceBText != null) choiceBText.text = $"!2 {choiceB}";
|
||||
|
||||
// Reset percentages
|
||||
targetChoiceAPercent = 50f;
|
||||
targetChoiceBPercent = 50f;
|
||||
currentChoiceAPercent = 50f;
|
||||
currentChoiceBPercent = 50f;
|
||||
|
||||
UpdateVotingBarsImmediate();
|
||||
}
|
||||
|
||||
private void ShowVotingPanel()
|
||||
{
|
||||
if (votingPanel == null) return;
|
||||
|
||||
votingPanel.gameObject.SetActive(true);
|
||||
StartCoroutine(SlideInCard(votingPanel));
|
||||
}
|
||||
|
||||
private void ShowResult(string title, string message)
|
||||
{
|
||||
if (resultPanel == null) return;
|
||||
|
||||
// Hide other panels
|
||||
if (eventCard != null) eventCard.gameObject.SetActive(false);
|
||||
if (votingPanel != null) votingPanel.gameObject.SetActive(false);
|
||||
|
||||
// Set content
|
||||
if (resultTitleText != null) resultTitleText.text = title;
|
||||
if (resultMessageText != null) resultMessageText.text = message;
|
||||
|
||||
// Show result
|
||||
resultPanel.gameObject.SetActive(true);
|
||||
StartCoroutine(SlideInCard(resultPanel));
|
||||
}
|
||||
|
||||
private void AnimateVotingBars()
|
||||
{
|
||||
// Smoothly interpolate bar widths
|
||||
currentChoiceAPercent = Mathf.Lerp(
|
||||
currentChoiceAPercent,
|
||||
targetChoiceAPercent,
|
||||
Time.deltaTime * barAnimationSpeed
|
||||
);
|
||||
currentChoiceBPercent = Mathf.Lerp(
|
||||
currentChoiceBPercent,
|
||||
targetChoiceBPercent,
|
||||
Time.deltaTime * barAnimationSpeed
|
||||
);
|
||||
|
||||
UpdateVotingBarsImmediate();
|
||||
}
|
||||
|
||||
private void UpdateVotingBarsImmediate()
|
||||
{
|
||||
// Update bar fill amounts (assuming horizontal fill)
|
||||
if (choiceABar != null)
|
||||
{
|
||||
choiceABar.fillAmount = currentChoiceAPercent / 100f;
|
||||
}
|
||||
if (choiceBBar != null)
|
||||
{
|
||||
choiceBBar.fillAmount = currentChoiceBPercent / 100f;
|
||||
}
|
||||
|
||||
// Update percentage texts
|
||||
if (choiceAPercentText != null)
|
||||
{
|
||||
choiceAPercentText.text = $"{Mathf.RoundToInt(currentChoiceAPercent)}%";
|
||||
}
|
||||
if (choiceBPercentText != null)
|
||||
{
|
||||
choiceBPercentText.text = $"{Mathf.RoundToInt(currentChoiceBPercent)}%";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Coroutines
|
||||
private IEnumerator FadeInUI()
|
||||
{
|
||||
isActive = true;
|
||||
if (mainPanel == null) yield break;
|
||||
|
||||
mainPanel.blocksRaycasts = true;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < fadeInDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
mainPanel.alpha = Mathf.Lerp(0f, 1f, elapsed / fadeInDuration);
|
||||
yield return null;
|
||||
}
|
||||
mainPanel.alpha = 1f;
|
||||
|
||||
// Darken background
|
||||
if (backgroundOverlay != null)
|
||||
{
|
||||
Color c = backgroundOverlay.color;
|
||||
c.a = 0.6f;
|
||||
backgroundOverlay.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FadeOutUI()
|
||||
{
|
||||
if (mainPanel == null)
|
||||
{
|
||||
isActive = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
float elapsed = 0f;
|
||||
float startAlpha = mainPanel.alpha;
|
||||
|
||||
while (elapsed < fadeOutDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
mainPanel.alpha = Mathf.Lerp(startAlpha, 0f, elapsed / fadeOutDuration);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
mainPanel.alpha = 0f;
|
||||
mainPanel.blocksRaycasts = false;
|
||||
isActive = false;
|
||||
|
||||
HideAllPanels();
|
||||
}
|
||||
|
||||
private IEnumerator SlideInCard(RectTransform card)
|
||||
{
|
||||
if (card == null) yield break;
|
||||
|
||||
Vector2 startPos = card.anchoredPosition;
|
||||
Vector2 targetPos = startPos;
|
||||
startPos.y -= cardSlideDistance;
|
||||
|
||||
card.anchoredPosition = startPos;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < fadeInDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / fadeInDuration;
|
||||
// Ease out cubic
|
||||
t = 1f - Mathf.Pow(1f - t, 3f);
|
||||
card.anchoredPosition = Vector2.Lerp(startPos, targetPos, t);
|
||||
yield return null;
|
||||
}
|
||||
card.anchoredPosition = targetPos;
|
||||
}
|
||||
|
||||
private IEnumerator UpdateTimer()
|
||||
{
|
||||
while (votingEndsAt > 0)
|
||||
{
|
||||
double now = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
double remaining = votingEndsAt - now;
|
||||
|
||||
if (remaining <= 0)
|
||||
{
|
||||
if (timerText != null) timerText.text = "0s";
|
||||
break;
|
||||
}
|
||||
|
||||
if (timerText != null)
|
||||
{
|
||||
timerText.text = $"{Mathf.CeilToInt((float)remaining)}s";
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FlashWinningChoice(int winningIndex)
|
||||
{
|
||||
// Flash the winning choice bar
|
||||
Image winningBar = winningIndex == 0 ? choiceABar : choiceBBar;
|
||||
if (winningBar == null) yield break;
|
||||
|
||||
Color originalColor = winningBar.color;
|
||||
Color flashColor = Color.yellow;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
winningBar.color = flashColor;
|
||||
yield return new WaitForSeconds(0.15f);
|
||||
winningBar.color = originalColor;
|
||||
yield return new WaitForSeconds(0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator HideAfterDelay(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
// The mode will change to simulation, which will trigger fade out
|
||||
// But we can also force it here as a fallback
|
||||
if (isActive)
|
||||
{
|
||||
StartCoroutine(FadeOutUI());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Force show the narrative UI (for testing).
|
||||
/// </summary>
|
||||
public void ForceShow()
|
||||
{
|
||||
StartCoroutine(FadeInUI());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force hide the narrative UI.
|
||||
/// </summary>
|
||||
public void ForceHide()
|
||||
{
|
||||
StartCoroutine(FadeOutUI());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test the UI with sample data.
|
||||
/// </summary>
|
||||
[ContextMenu("Test Narrative UI")]
|
||||
public void TestUI()
|
||||
{
|
||||
// Create test data
|
||||
var plotData = new NarrativePlotData
|
||||
{
|
||||
plot_id = "test_001",
|
||||
title = "Mysterious Footprints",
|
||||
description = "Strange footprints appear on the beach. Someone has been watching...",
|
||||
choices = new List<PlotChoiceData>
|
||||
{
|
||||
new PlotChoiceData { choice_id = "investigate", text = "Follow the tracks" },
|
||||
new PlotChoiceData { choice_id = "fortify", text = "Strengthen defenses" }
|
||||
},
|
||||
ttl_seconds = 60
|
||||
};
|
||||
|
||||
// Simulate events
|
||||
HandleModeChange(new ModeChangeData { mode = "narrative", old_mode = "simulation", message = "Director intervenes..." });
|
||||
HandleNarrativePlot(plotData);
|
||||
|
||||
// Simulate vote start after delay
|
||||
StartCoroutine(SimulateVoting());
|
||||
}
|
||||
|
||||
private IEnumerator SimulateVoting()
|
||||
{
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
HandleVoteStarted(new VoteStartedData
|
||||
{
|
||||
vote_id = "test_vote",
|
||||
duration_seconds = 30,
|
||||
ends_at = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 30,
|
||||
choices = new List<PlotChoiceData>
|
||||
{
|
||||
new PlotChoiceData { choice_id = "investigate", text = "Follow the tracks" },
|
||||
new PlotChoiceData { choice_id = "fortify", text = "Strengthen defenses" }
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate vote updates
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
yield return new WaitForSeconds(1f);
|
||||
HandleVoteUpdate(new VoteUpdateData
|
||||
{
|
||||
vote_id = "test_vote",
|
||||
tallies = new List<int> { Random.Range(10, 50), Random.Range(10, 50) },
|
||||
percentages = new List<float> { Random.Range(30f, 70f), Random.Range(30f, 70f) },
|
||||
total_votes = Random.Range(20, 100),
|
||||
remaining_seconds = 30 - i
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
unity-client/Assets/Scripts/NarrativeUI.cs.meta
Normal file
2
unity-client/Assets/Scripts/NarrativeUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7748b2f53bcb247f8a0e6707b1ecb1ce
|
||||
@@ -75,6 +75,15 @@ namespace TheIsland.Network
|
||||
public event Action<GiveItemEventData> OnGiveItem; // Phase 23: Item Exchange
|
||||
public event Action<GroupActivityEventData> OnGroupActivity; // Phase 24: Group Activities
|
||||
public event Action<VFXEventData> OnVFXEvent; // Phase 8: VFX
|
||||
|
||||
// AI Director & Narrative Voting (Phase 9)
|
||||
public event Action<ModeChangeData> OnModeChange;
|
||||
public event Action<NarrativePlotData> OnNarrativePlot;
|
||||
public event Action<VoteStartedData> OnVoteStarted;
|
||||
public event Action<VoteUpdateData> OnVoteUpdate;
|
||||
public event Action<VoteEndedData> OnVoteEnded;
|
||||
public event Action<VoteResultData> OnVoteResult;
|
||||
public event Action<ResolutionAppliedData> OnResolutionApplied;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
@@ -380,8 +389,6 @@ namespace TheIsland.Network
|
||||
var randomEventData = JsonUtility.FromJson<RandomEventData>(dataJson);
|
||||
OnRandomEvent?.Invoke(randomEventData);
|
||||
Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}");
|
||||
OnRandomEvent?.Invoke(randomEventData);
|
||||
Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}");
|
||||
break;
|
||||
|
||||
case EventTypes.GIVE_ITEM:
|
||||
@@ -399,6 +406,42 @@ namespace TheIsland.Network
|
||||
OnVFXEvent?.Invoke(vfxData);
|
||||
break;
|
||||
|
||||
// AI Director & Narrative Voting (Phase 9)
|
||||
case EventTypes.MODE_CHANGE:
|
||||
var modeData = JsonUtility.FromJson<ModeChangeData>(dataJson);
|
||||
OnModeChange?.Invoke(modeData);
|
||||
break;
|
||||
|
||||
case EventTypes.NARRATIVE_PLOT:
|
||||
var plotData = JsonUtility.FromJson<NarrativePlotData>(dataJson);
|
||||
OnNarrativePlot?.Invoke(plotData);
|
||||
break;
|
||||
|
||||
case EventTypes.VOTE_STARTED:
|
||||
var voteStarted = JsonUtility.FromJson<VoteStartedData>(dataJson);
|
||||
OnVoteStarted?.Invoke(voteStarted);
|
||||
break;
|
||||
|
||||
case EventTypes.VOTE_UPDATE:
|
||||
var voteUpdate = JsonUtility.FromJson<VoteUpdateData>(dataJson);
|
||||
OnVoteUpdate?.Invoke(voteUpdate);
|
||||
break;
|
||||
|
||||
case EventTypes.VOTE_ENDED:
|
||||
var voteEnded = JsonUtility.FromJson<VoteEndedData>(dataJson);
|
||||
OnVoteEnded?.Invoke(voteEnded);
|
||||
break;
|
||||
|
||||
case EventTypes.VOTE_RESULT:
|
||||
var voteResult = JsonUtility.FromJson<VoteResultData>(dataJson);
|
||||
OnVoteResult?.Invoke(voteResult);
|
||||
break;
|
||||
|
||||
case EventTypes.RESOLUTION_APPLIED:
|
||||
var resolution = JsonUtility.FromJson<ResolutionAppliedData>(dataJson);
|
||||
OnResolutionApplied?.Invoke(resolution);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user