feat(demo): 改造 Lucky Dice 交互演示

This commit is contained in:
JSD\13999
2026-06-23 16:10:44 +08:00
parent c8b0687b0a
commit 97fe14fd31
5 changed files with 330 additions and 92 deletions

View File

@@ -2,7 +2,8 @@
"name": "FishDice.Demo",
"rootNamespace": "FishDice.Demo",
"references": [
"FishDice.Runtime"
"FishDice.Runtime",
"UnityEngine.UI"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -1,142 +1,307 @@
using System.Collections.Generic;
using System;
using System.Collections;
using FishDice.LuckyDice;
using UnityEngine.EventSystems;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
namespace FishDice.Demo
{
public sealed class LuckyDiceDemoController : MonoBehaviour
{
private static readonly LuckyDiceDemoScenario[] Scenarios =
{
new LuckyDiceDemoScenario("Normal Reward", "Numbers pay the base reward.", 0, 1),
new LuckyDiceDemoScenario("Clover Bonus", "A number plus Clover grants reward and bonus.", 0, 5),
new LuckyDiceDemoScenario("Lucky Dice: Rocket", "Two Clovers enter Lucky Dice and resolve Slap Down.", 5, 5, 0),
new LuckyDiceDemoScenario("Lucky Dice: Thief", "Two Clovers enter Lucky Dice and resolve Treasure Heist.", 5, 5, 1)
};
private static readonly string[] NormalPreviewFaces = { "2", "3", "4", "5", "6", "Clover" };
private static readonly string[] LuckyPreviewFaces = { "Rocket", "Thief" };
public const string BuiltinFontResourcePath = "LegacyRuntime.ttf";
private readonly List<LuckyDiceDemoSnapshot> _history = new List<LuckyDiceDemoSnapshot>();
private Vector2 _scroll;
private LuckyDiceDemoSnapshot _current;
private GUIStyle _titleStyle;
private GUIStyle _labelStyle;
private GUIStyle _summaryStyle;
private readonly SystemRandomSource _flowRandom = new SystemRandomSource(new System.Random());
private Text _titleText;
private Text _statusText;
private Text _detailText;
private Text _rewardText;
private Text[] _normalDiceTexts;
private Text[] _luckyDiceTexts;
private Button _primaryButton;
private Text _primaryButtonText;
private Coroutine _flowRoutine;
private LuckyDiceDemoSnapshot _lastSnapshot;
private Font _uiFont;
private void Awake()
{
RunScenario(Scenarios[0]);
BuildUi();
ShowReady();
}
private void OnGUI()
private void StartRoll()
{
EnsureStyles();
var margin = 24f;
var width = Mathf.Min(780f, Screen.width - margin * 2f);
var height = Mathf.Min(620f, Screen.height - margin * 2f);
GUILayout.BeginArea(new Rect(margin, margin, width, height), GUI.skin.box);
GUILayout.Label("FishDice Lucky Dice Demo", _titleStyle);
GUILayout.Label("Runtime path: normal roll -> combo facts -> rule actions -> optional Lucky Dice target mode.", _labelStyle);
GUILayout.Space(10f);
GUILayout.BeginHorizontal();
if (GUILayout.Button("Random Roll", GUILayout.Height(34f)))
if (_flowRoutine != null)
{
RunRandom();
StopCoroutine(_flowRoutine);
}
foreach (var scenario in Scenarios)
{
if (GUILayout.Button(scenario.Title, GUILayout.Height(34f)))
{
RunScenario(scenario);
}
}
GUILayout.EndHorizontal();
GUILayout.Space(12f);
DrawSnapshot(_current);
GUILayout.Space(12f);
DrawHistory();
GUILayout.EndArea();
_flowRoutine = StartCoroutine(PlayRollFlow());
}
private void RunScenario(LuckyDiceDemoScenario scenario)
private IEnumerator PlayRollFlow()
{
SetCurrent(LuckyDiceDemoRunner.RunScenario(scenario));
_primaryButton.interactable = false;
_primaryButtonText.text = "投掷中";
_rewardText.text = string.Empty;
_detailText.text = string.Empty;
SetLuckyDiceVisible(false);
SetStatus("摇动普通骰子");
_lastSnapshot = LuckyDiceDemoRunner.RunRandom(_flowRandom);
yield return AnimateDice(_normalDiceTexts, NormalPreviewFaces, 0.9f);
ApplyDiceTexts(_normalDiceTexts, _lastSnapshot.NormalFaces);
SetStatus("组合判定");
_detailText.text = _lastSnapshot.ComboLine;
yield return new WaitForSeconds(0.55f);
if (_lastSnapshot.OutcomeKind == "lucky_dice")
{
SetStatus("触发 Lucky Dice");
_rewardText.text = "双 Clover 命中";
yield return new WaitForSeconds(0.45f);
SetLuckyDiceVisible(true);
SetStatus("揭示特殊骰子");
yield return AnimateDice(_luckyDiceTexts, LuckyPreviewFaces, 1.0f);
ApplyDiceTexts(_luckyDiceTexts, _lastSnapshot.LuckyDiceSlotKeys);
}
private void RunRandom()
{
SetCurrent(LuckyDiceDemoRunner.RunRandom());
ShowResult(_lastSnapshot);
_flowRoutine = null;
}
private void SetCurrent(LuckyDiceDemoSnapshot snapshot)
private IEnumerator AnimateDice(Text[] diceTexts, string[] previewFaces, float duration)
{
_current = snapshot;
_history.Insert(0, snapshot);
if (_history.Count > 8)
var elapsed = 0f;
while (elapsed < duration)
{
_history.RemoveAt(_history.Count - 1);
for (var i = 0; i < diceTexts.Length; i++)
{
diceTexts[i].text = previewFaces[Random.Range(0, previewFaces.Length)];
}
yield return new WaitForSeconds(0.08f);
elapsed += 0.08f;
}
}
private void DrawSnapshot(LuckyDiceDemoSnapshot snapshot)
private void ShowReady()
{
if (snapshot == null)
_titleText.text = "Lucky Dice";
SetStatus("准备投骰");
_detailText.text = "普通骰子会随机落点,命中双 Clover 后进入 Lucky Dice。";
_rewardText.text = string.Empty;
ApplyDiceTexts(_normalDiceTexts, new[] { "?", "?" });
ApplyDiceTexts(_luckyDiceTexts, new[] { "?", "?", "?" });
SetLuckyDiceVisible(false);
_primaryButton.interactable = true;
_primaryButtonText.text = "投骰";
}
private void ShowResult(LuckyDiceDemoSnapshot snapshot)
{
SetStatus(ResolveResultStatus(snapshot));
_detailText.text = $"{snapshot.FaceLine}\n{snapshot.RuleLine}\n{snapshot.EffectLine}";
_rewardText.text = snapshot.TargetModeLine == "Target Mode: -"
? snapshot.Summary
: $"{snapshot.Summary}\n{snapshot.TargetModeLine}";
_primaryButton.interactable = true;
_primaryButtonText.text = "再投一次";
}
private static string ResolveResultStatus(LuckyDiceDemoSnapshot snapshot)
{
switch (snapshot.OutcomeKind)
{
case "lucky_dice":
return "进入目标玩法";
case "clover_bonus":
return "获得 Clover Bonus";
case "normal_reward":
return "获得普通奖励";
default:
return "本轮结束";
}
}
private void BuildUi()
{
EnsureEventSystem();
var canvasObject = new GameObject("LuckyDiceCanvas");
canvasObject.transform.SetParent(transform, false);
var canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.pixelPerfect = true;
var scaler = canvasObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
scaler.scaleFactor = 1f;
scaler.referencePixelsPerUnit = 100f;
canvasObject.AddComponent<GraphicRaycaster>();
var root = CreatePanel("Root", canvasObject.transform, new Color(0.06f, 0.09f, 0.13f, 1f));
Stretch(root.rectTransform);
var board = CreatePanel("Board", root.transform, new Color(0.12f, 0.16f, 0.22f, 0.96f));
Center(board.rectTransform, new Vector2(760f, 600f));
_titleText = CreateText("Title", board.transform, "Lucky Dice", 34, FontStyle.Bold, Color.white);
SetRect(_titleText.rectTransform, new Vector2(0f, 194f), new Vector2(680f, 48f));
_statusText = CreateText("Status", board.transform, string.Empty, 24, FontStyle.Bold, new Color(1f, 0.84f, 0.38f));
SetRect(_statusText.rectTransform, new Vector2(0f, 142f), new Vector2(680f, 40f));
_normalDiceTexts = new[]
{
CreateDiceText("NormalDiceA", board.transform, new Vector2(-96f, 64f), new Color(0.96f, 0.97f, 1f)),
CreateDiceText("NormalDiceB", board.transform, new Vector2(96f, 64f), new Color(0.96f, 0.97f, 1f))
};
_luckyDiceTexts = new[]
{
CreateDiceText("LuckySlotA", board.transform, new Vector2(-150f, -48f), new Color(1f, 0.86f, 0.46f)),
CreateDiceText("LuckySlotB", board.transform, new Vector2(0f, -48f), new Color(1f, 0.86f, 0.46f)),
CreateDiceText("LuckySlotC", board.transform, new Vector2(150f, -48f), new Color(1f, 0.86f, 0.46f))
};
_detailText = CreateText("Detail", board.transform, string.Empty, 16, FontStyle.Normal, new Color(0.82f, 0.88f, 0.94f));
SetRect(_detailText.rectTransform, new Vector2(0f, -142f), new Vector2(650f, 86f));
_rewardText = CreateText("Reward", board.transform, string.Empty, 19, FontStyle.Bold, new Color(0.55f, 0.92f, 0.75f));
SetRect(_rewardText.rectTransform, new Vector2(0f, -210f), new Vector2(650f, 56f));
_primaryButton = CreateButton("PrimaryButton", board.transform, "投骰", new Vector2(0f, -254f), new Vector2(190f, 48f));
_primaryButton.onClick.AddListener(StartRoll);
}
private static void EnsureEventSystem()
{
if (FindObjectOfType<EventSystem>() != null)
{
return;
}
GUILayout.Label(snapshot.Title, _summaryStyle);
GUILayout.Label(snapshot.Description, _labelStyle);
GUILayout.Space(6f);
GUILayout.Label(snapshot.FaceLine, _labelStyle);
GUILayout.Label(snapshot.ComboLine, _labelStyle);
GUILayout.Label(snapshot.RuleLine, _labelStyle);
GUILayout.Label(snapshot.EffectLine, _labelStyle);
GUILayout.Label(snapshot.LuckyDiceLine, _labelStyle);
GUILayout.Label(snapshot.TargetModeLine, _labelStyle);
GUILayout.Space(6f);
GUILayout.Label(snapshot.Summary, _summaryStyle);
var eventSystemObject = new GameObject("EventSystem");
eventSystemObject.AddComponent<EventSystem>();
eventSystemObject.AddComponent<StandaloneInputModule>();
}
private void DrawHistory()
private Text CreateDiceText(string name, Transform parent, Vector2 anchoredPosition, Color faceColor)
{
GUILayout.Label("Recent Rolls", _summaryStyle);
_scroll = GUILayout.BeginScrollView(_scroll, GUILayout.Height(180f));
foreach (var snapshot in _history)
{
GUILayout.Label($"{snapshot.Title} | {snapshot.FaceLine} | {snapshot.Summary}", _labelStyle);
}
GUILayout.EndScrollView();
var background = CreatePanel(name, parent, new Color(0.2f, 0.25f, 0.33f, 1f));
SetRect(background.rectTransform, anchoredPosition, new Vector2(116f, 84f));
var text = CreateText(name + "Text", background.transform, "?", 24, FontStyle.Bold, faceColor);
Stretch(text.rectTransform);
return text;
}
private void EnsureStyles()
private Button CreateButton(string name, Transform parent, string label, Vector2 anchoredPosition, Vector2 size)
{
if (_titleStyle != null)
{
return;
var image = CreatePanel(name, parent, new Color(0.92f, 0.56f, 0.16f, 1f));
SetRect(image.rectTransform, anchoredPosition, size);
var button = image.gameObject.AddComponent<Button>();
_primaryButtonText = CreateText(name + "Text", image.transform, label, 20, FontStyle.Bold, Color.white);
Stretch(_primaryButtonText.rectTransform);
return button;
}
_titleStyle = new GUIStyle(GUI.skin.label)
private Image CreatePanel(string name, Transform parent, Color color)
{
fontSize = 26,
fontStyle = FontStyle.Bold,
normal = { textColor = Color.white },
wordWrap = true
};
_labelStyle = new GUIStyle(GUI.skin.label)
var gameObject = new GameObject(name, typeof(RectTransform), typeof(Image));
gameObject.transform.SetParent(parent, false);
var image = gameObject.GetComponent<Image>();
image.color = color;
return image;
}
private Text CreateText(string name, Transform parent, string text, int fontSize, FontStyle fontStyle, Color color)
{
fontSize = 15,
normal = { textColor = new Color(0.86f, 0.9f, 0.95f) },
wordWrap = true
};
_summaryStyle = new GUIStyle(GUI.skin.label)
var gameObject = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Text));
gameObject.transform.SetParent(parent, false);
var label = gameObject.GetComponent<Text>();
label.font = GetUiFont();
label.text = text;
label.fontSize = fontSize;
label.fontStyle = fontStyle;
label.color = color;
label.alignment = TextAnchor.MiddleCenter;
label.horizontalOverflow = HorizontalWrapMode.Wrap;
label.verticalOverflow = VerticalWrapMode.Overflow;
label.alignByGeometry = true;
label.raycastTarget = false;
return label;
}
private Font GetUiFont()
{
fontSize = 18,
fontStyle = FontStyle.Bold,
normal = { textColor = new Color(1f, 0.86f, 0.35f) },
wordWrap = true
};
if (_uiFont == null)
{
_uiFont = Resources.GetBuiltinResource<Font>(BuiltinFontResourcePath);
}
return _uiFont;
}
private void SetStatus(string text)
{
_statusText.text = text;
}
private void ApplyDiceTexts(Text[] diceTexts, string[] values)
{
for (var i = 0; i < diceTexts.Length; i++)
{
diceTexts[i].text = i < values.Length ? FormatFace(values[i]) : "-";
}
}
private static string FormatFace(string value)
{
if (string.IsNullOrEmpty(value))
{
return "-";
}
return char.ToUpperInvariant(value[0]) + value.Substring(1);
}
private void SetLuckyDiceVisible(bool visible)
{
foreach (var text in _luckyDiceTexts)
{
text.transform.parent.gameObject.SetActive(visible);
}
}
private static void Center(RectTransform rectTransform, Vector2 size)
{
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.sizeDelta = size;
rectTransform.anchoredPosition = Vector2.zero;
}
private static void SetRect(RectTransform rectTransform, Vector2 position, Vector2 size)
{
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.sizeDelta = size;
rectTransform.anchoredPosition = position;
}
private static void Stretch(RectTransform rectTransform)
{
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
}
}
}

View File

@@ -26,6 +26,15 @@ namespace FishDice.Demo
return Run(scenario, new SystemRandomSource());
}
public static LuckyDiceDemoSnapshot RunRandom(IRandomSource random)
{
var scenario = new LuckyDiceDemoScenario(
"Random Roll",
"Uses the default FishDice runtime config and the supplied random source.");
return Run(scenario, random);
}
private static LuckyDiceDemoSnapshot Run(LuckyDiceDemoScenario scenario, IRandomSource random)
{
var flow = LuckyDiceRuntimeFactory.CreateDefault(random).CreateDefaultFlow();
@@ -43,6 +52,11 @@ namespace FishDice.Demo
var luckyDiceLine = FormatLuckyDice(result.LuckyDiceResult);
var targetModeLine = "Target Mode: " + (result.TargetMode == null ? "-" : result.TargetMode.ModeKey);
var summary = FormatSummary(result);
var normalFaces = result.RollResult.Faces.Select(face => face.Key).ToArray();
var luckyDiceSlots = result.LuckyDiceResult == null || result.LuckyDiceResult.SlotResultKeys == null
? new string[0]
: result.LuckyDiceResult.SlotResultKeys.ToArray();
var outcomeKind = ResolveOutcomeKind(result);
return new LuckyDiceDemoSnapshot(
scenario.Title,
@@ -53,7 +67,10 @@ namespace FishDice.Demo
effectLine,
luckyDiceLine,
targetModeLine,
summary);
summary,
normalFaces,
luckyDiceSlots,
outcomeKind);
}
private static string FormatLuckyDice(LuckyDiceSelectionResult result)
@@ -86,6 +103,26 @@ namespace FishDice.Demo
return "No configured outcome.";
}
private static string ResolveOutcomeKind(RollFlowResult result)
{
if (result.TargetMode != null)
{
return "lucky_dice";
}
if (result.Effects.OfType<MultiplierEffect>().Any())
{
return "clover_bonus";
}
if (result.Effects.OfType<RewardEffect>().Any())
{
return "normal_reward";
}
return "none";
}
private static string JoinOrDash(IEnumerable<string> values)
{
var materialized = values.Where(value => !string.IsNullOrEmpty(value)).ToArray();

View File

@@ -11,7 +11,10 @@ namespace FishDice.Demo
string effectLine,
string luckyDiceLine,
string targetModeLine,
string summary)
string summary,
string[] normalFaces,
string[] luckyDiceSlotKeys,
string outcomeKind)
{
Title = title;
Description = description;
@@ -22,6 +25,9 @@ namespace FishDice.Demo
LuckyDiceLine = luckyDiceLine;
TargetModeLine = targetModeLine;
Summary = summary;
NormalFaces = normalFaces ?? new string[0];
LuckyDiceSlotKeys = luckyDiceSlotKeys ?? new string[0];
OutcomeKind = string.IsNullOrEmpty(outcomeKind) ? "none" : outcomeKind;
}
public string Title { get; }
@@ -41,5 +47,11 @@ namespace FishDice.Demo
public string TargetModeLine { get; }
public string Summary { get; }
public string[] NormalFaces { get; }
public string[] LuckyDiceSlotKeys { get; }
public string OutcomeKind { get; }
}
}

View File

@@ -32,5 +32,28 @@ namespace FishDice.Tests.LuckyDice
Assert.That(snapshot.LuckyDiceLine, Is.EqualTo("Lucky Dice: rocket x3"));
Assert.That(snapshot.TargetModeLine, Is.EqualTo("Target Mode: slap_down_normal"));
}
[Test]
public void DemoRunner_exposes_structured_faces_for_player_ui_flow()
{
var normal = LuckyDiceDemoRunner.RunScenario(
new LuckyDiceDemoScenario("Normal Reward", "Numbers pay the base reward.", 0, 1));
var lucky = LuckyDiceDemoRunner.RunScenario(
new LuckyDiceDemoScenario("Lucky Dice", "Clover pair enters the special dice flow.", 5, 5, 0));
Assert.That(normal.NormalFaces, Is.EqualTo(new[] { "2", "3" }));
Assert.That(normal.LuckyDiceSlotKeys, Is.Empty);
Assert.That(normal.OutcomeKind, Is.EqualTo("normal_reward"));
Assert.That(lucky.NormalFaces, Is.EqualTo(new[] { "clover", "clover" }));
Assert.That(lucky.LuckyDiceSlotKeys, Is.EqualTo(new[] { "rocket", "rocket", "rocket" }));
Assert.That(lucky.OutcomeKind, Is.EqualTo("lucky_dice"));
}
[Test]
public void DemoController_uses_current_unity_builtin_font_for_player_ui_text()
{
Assert.That(LuckyDiceDemoController.BuiltinFontResourcePath, Is.EqualTo("LegacyRuntime.ttf"));
}
}
}