using System.Collections; using System.Collections.Generic; using Definition.Enum; using Event; using UI; using UnityEngine; using UnityGameFramework.Runtime; namespace CustomComponent { [DisallowMultipleComponent] public class StoryDirectorComponent : GameFrameworkComponent { [SerializeField] [Tooltip("按顺序执行。每个元素是一个 StoryDirectiveAsset 资产引用。")] private List _directives = new List(); [SerializeField] private bool _allowRepeatTrigger = false; [SerializeField] private bool _verboseLog = true; [SerializeField] private string _backgroundAssetNamePrefix = string.Empty; private readonly HashSet _consumedDirectiveTokens = new HashSet(); private readonly HashSet _queuedDirectiveTokens = new HashSet(); private readonly Queue _pendingDirectives = new Queue(); private BgFormController _bgFormController; private Coroutine _directiveExecutionCoroutine; private int _nextBackgroundRequestId = 1; private int _lastBackgroundRequestId; private int _completedBackgroundRequestId; private struct PendingDirectiveInvocation { public StoryDirectiveAsset Directive; public StoryTriggerType TriggerType; public int TriggerId; public string DirectiveToken; } private void Start() { GameEntry.Event.Subscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted); GameEntry.Event.Subscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted); GameEntry.Event.Subscribe(BgTransitionCompletedEventArgs.EventId, OnBgTransitionCompleted); } private void OnDestroy() { GameEntry.Event.Unsubscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted); GameEntry.Event.Unsubscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted); GameEntry.Event.Unsubscribe(BgTransitionCompletedEventArgs.EventId, OnBgTransitionCompleted); if (_directiveExecutionCoroutine != null) { StopCoroutine(_directiveExecutionCoroutine); _directiveExecutionCoroutine = null; } _bgFormController?.CloseUI(); _bgFormController = null; } public void ResetConsumedDirectives() { _consumedDirectiveTokens.Clear(); _queuedDirectiveTokens.Clear(); _pendingDirectives.Clear(); } private void OnDialogCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is DialogCompletedEventArgs args)) { return; } ExecuteDirectives(StoryTriggerType.DialogCompleted, args.DialogId); } private void OnCombineCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is CombineCompletedEventArgs)) { return; } ExecuteDirectives(StoryTriggerType.CombineCompleted, 0); } private void ExecuteDirectives(StoryTriggerType triggerType, int triggerId) { bool hasEnqueuedDirective = false; for (int i = 0; i < _directives.Count; i++) { StoryDirectiveAsset directive = _directives[i]; if (directive == null || !directive.IsMatch(triggerType, triggerId)) { continue; } string directiveToken = BuildDirectiveToken(directive, i); if (!_allowRepeatTrigger && (_consumedDirectiveTokens.Contains(directiveToken) || _queuedDirectiveTokens.Contains(directiveToken))) { continue; } _pendingDirectives.Enqueue(new PendingDirectiveInvocation { Directive = directive, TriggerType = triggerType, TriggerId = triggerId, DirectiveToken = directiveToken }); hasEnqueuedDirective = true; if (!_allowRepeatTrigger) { _queuedDirectiveTokens.Add(directiveToken); } } if (!hasEnqueuedDirective || _directiveExecutionCoroutine != null) { return; } _directiveExecutionCoroutine = StartCoroutine(ProcessDirectiveQueue()); } private static string BuildDirectiveToken(StoryDirectiveAsset directive, int directiveIndex) { return $"{directive.GetInstanceID()}:{directiveIndex}"; } private void ExecuteDirective(StoryDirectiveAsset directive, StoryTriggerType triggerType, int triggerId) { directive.Execute(this); if (_verboseLog) { Log.Info("StoryDirector executed action '{0}' by trigger '{1}' ({2}).", directive.ActionName, triggerType.ToString(), triggerId.ToString()); } } private IEnumerator ProcessDirectiveQueue() { while (_pendingDirectives.Count > 0) { PendingDirectiveInvocation invocation = _pendingDirectives.Dequeue(); if (!_allowRepeatTrigger) { _queuedDirectiveTokens.Remove(invocation.DirectiveToken); } ExecuteDirective(invocation.Directive, invocation.TriggerType, invocation.TriggerId); if (invocation.Directive.RequiresCompletion) { yield return WaitForDirectiveCompletion(invocation.Directive); } if (!_allowRepeatTrigger) { _consumedDirectiveTokens.Add(invocation.DirectiveToken); } } _directiveExecutionCoroutine = null; } private IEnumerator WaitForDirectiveCompletion(StoryDirectiveAsset directive) { if (!(directive is StoryChangeBackgroundDirectiveAsset)) { yield break; } int requestId = _lastBackgroundRequestId; if (requestId <= 0) { yield break; } const float timeoutSeconds = 10f; float elapsed = 0f; while (_completedBackgroundRequestId < requestId) { elapsed += Time.unscaledDeltaTime; if (elapsed >= timeoutSeconds) { Log.Warning("StoryDirector wait background transition timeout. requestId={0}", requestId.ToString()); break; } yield return null; } } public void ExecuteStartDialog(int dialogId) { if (dialogId <= 0) { Log.Warning("StoryDirector start dialog skipped. dialogId must be positive."); return; } if (GameEntry.Dialog == null) { Log.Warning("StoryDirector start dialog failed. Dialog component is missing."); return; } GameEntry.Dialog.StartDialog(dialogId); } public void ExecuteStartCombine(StoryCombineConfig config) { if (GameEntry.Combine == null) { Log.Warning("StoryDirector start combine failed. Combine component is missing."); return; } CombineFormContext context = BuildCombineFormContext(config); int? formSerialId = GameEntry.Combine.StartLevel(context); if (!formSerialId.HasValue) { Log.Warning("StoryDirector start combine failed. Open combine UI failed."); return; } } public void ExecuteChangeBackground(string backgroundAssetName) { _lastBackgroundRequestId = 0; string assetName = string.IsNullOrWhiteSpace(backgroundAssetName) ? string.Empty : backgroundAssetName.Trim(); if (string.IsNullOrEmpty(assetName)) { Log.Warning("StoryDirector change background skipped. assetName is empty."); return; } if (_bgFormController == null) { _bgFormController = new BgFormController(); } int requestId = _nextBackgroundRequestId++; _lastBackgroundRequestId = requestId; int? formSerialId = _bgFormController.OpenUI(new BgFormContext { BackgroundAssetName = _backgroundAssetNamePrefix + assetName, TransitionRequestId = requestId }); if (!formSerialId.HasValue) { Log.Warning("StoryDirector change background failed. BgForm open returned null."); _completedBackgroundRequestId = Mathf.Max(_completedBackgroundRequestId, requestId); } } public void ExecuteEndChapter(int chapterId) { int finalChapterId = chapterId > 0 ? chapterId : (GameEntry.Dialog != null ? GameEntry.Dialog.CurrentChapterId : 0); GameEntry.Event.Fire(this, StoryChapterEndedEventArgs.Create(finalChapterId)); } private void OnBgTransitionCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is BgTransitionCompletedEventArgs args)) { return; } if (args.RequestId <= 0) { return; } _completedBackgroundRequestId = Mathf.Max(_completedBackgroundRequestId, args.RequestId); } private static CombineFormContext BuildCombineFormContext(StoryCombineConfig config) { if (config != null && config.Slots != null && config.Slots.Count > 0) { return new CombineFormContext { Slots = CloneSlots(config.Slots), Parts = CloneParts(config.Parts), AutoStart = config.AutoStart }; } return BuildDefaultCombineContext(); } private static List CloneSlots(List source) { var result = new List(); if (source == null) { return result; } for (int i = 0; i < source.Count; i++) { CombineSlotContext slot = source[i]; if (slot == null) { continue; } result.Add(new CombineSlotContext { RequiredPartType = slot.RequiredPartType, BuildOrder = slot.BuildOrder, RequireStrictOrder = slot.RequireStrictOrder, AnchoredPosition = slot.AnchoredPosition, SizeDelta = slot.SizeDelta, MechanicsExplanation = slot.MechanicsExplanation, MismatchHint = slot.MismatchHint }); } return result; } private static List CloneParts(List source) { var result = new List(); if (source == null) { return result; } for (int i = 0; i < source.Count; i++) { CombinePartContext part = source[i]; if (part == null) { continue; } result.Add(new CombinePartContext { PartType = part.PartType, PartDisplayName = part.PartDisplayName, MechanicsExplanation = part.MechanicsExplanation, LockAfterPlaced = part.LockAfterPlaced }); } return result; } private static CombineFormContext BuildDefaultCombineContext() { List slots = new List { new CombineSlotContext { RequiredPartType = CombinePartType.Dou, BuildOrder = 0, RequireStrictOrder = true, AnchoredPosition = new Vector2(-320f, -160f), SizeDelta = new Vector2(120f, 120f), MechanicsExplanation = "Dou transfers upper load and works as the base node." }, new CombineSlotContext { RequiredPartType = CombinePartType.Sheng, BuildOrder = 1, RequireStrictOrder = true, AnchoredPosition = new Vector2(-320f, -20f), SizeDelta = new Vector2(120f, 120f), MechanicsExplanation = "Sheng raises layer height to form the bracket hierarchy." }, new CombineSlotContext { RequiredPartType = CombinePartType.Gong, BuildOrder = 2, RequireStrictOrder = true, AnchoredPosition = new Vector2(-320f, 120f), SizeDelta = new Vector2(120f, 120f), MechanicsExplanation = "Gong spreads force laterally through overhang." }, new CombineSlotContext { RequiredPartType = CombinePartType.Qiao, BuildOrder = 3, RequireStrictOrder = true, AnchoredPosition = new Vector2(-160f, 120f), SizeDelta = new Vector2(120f, 120f), MechanicsExplanation = "Qiao continues force transfer to the outer side." }, new CombineSlotContext { RequiredPartType = CombinePartType.Ang, BuildOrder = 4, RequireStrictOrder = true, AnchoredPosition = new Vector2(0f, 120f), SizeDelta = new Vector2(120f, 120f), MechanicsExplanation = "Ang uses leverage to redirect eave load inward." } }; List parts = new List { new CombinePartContext { PartType = CombinePartType.Dou, PartDisplayName = "Dou", LockAfterPlaced = true }, new CombinePartContext { PartType = CombinePartType.Sheng, PartDisplayName = "Sheng", LockAfterPlaced = true }, new CombinePartContext { PartType = CombinePartType.Gong, PartDisplayName = "Gong", LockAfterPlaced = true }, new CombinePartContext { PartType = CombinePartType.Qiao, PartDisplayName = "Qiao", LockAfterPlaced = true }, new CombinePartContext { PartType = CombinePartType.Ang, PartDisplayName = "Ang", LockAfterPlaced = true } }; return new CombineFormContext { Slots = slots, Parts = parts, AutoStart = true }; } } }