using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Code.Game.Entities; using Code.Game.OfficeGame.BattleMode; using Code.Infrastructure.Camera; using Code.OfficeGame.BattleMode.Actions; using Code.OfficeGame.BattleMode.Actions.Deciders; using Code.OfficeGame.BattleMode.UI; using Cysharp.Threading.Tasks; using R3; using UnityEngine; namespace Code.OfficeGame.BattleMode.Timeline { public class BattleTimeline { public const int TicksPerSecond = 60; public const float TickDelta = 1f / TicksPerSecond; private readonly ReactiveProperty _currentBattleFrame = new(0); public ReadOnlyReactiveProperty CurrentBattleFrame => _currentBattleFrame; private readonly BattleTimelinePanel _timelinePanel; private DisposableBag _battleSubscriptions; private Dictionary _battleActions; private Dictionary _battleActionDeciders; private Dictionary _entities; private Dictionary _battleActionDatas = new(); private Dictionary _actionDecisionResults = new(); private BattleActionDecisionPanel _battleActionDecisionPanel; private CameraStateMachine _cameraStateMachine; public BattleTimeline( BattleTimelinePanel timelinePanel, IEnumerable battleActions, IEnumerable battleActionDeciders, BattleActionDecisionPanel battleActionDecisionPanel, CameraStateMachine cameraStateMachine) { _cameraStateMachine = cameraStateMachine; _battleActionDecisionPanel = battleActionDecisionPanel; _timelinePanel = timelinePanel; _battleActions = new(); foreach (var battleAction in battleActions) { battleAction.SetBattleTimeline(this); _battleActions.Add(battleAction.ActionType, battleAction); } _battleActionDeciders = battleActionDeciders.ToDictionary(x => x.DeciderType); } public async UniTask RunBattleLoopAsync(List entities, CancellationToken ctx) { _entities = entities.ToDictionary(x => x.GetEntityId()); _currentBattleFrame.Value = 0; _battleSubscriptions = new DisposableBag(); try { _currentBattleFrame .Subscribe(_timelinePanel.OnFrameTick) .AddTo(ref _battleSubscriptions); await DecideActions(_entities.Keys.ToArray(), ctx); while (!ctx.IsCancellationRequested) { EntityId[] entitiesNeedOrders = await PlayTimelineUntilPause(ctx); await DecideActions(entitiesNeedOrders, ctx); } } finally { _battleSubscriptions.Dispose(); } } private async UniTask PlayTimelineUntilPause(CancellationToken ctx) { float accumulator = 0f; while (!ctx.IsCancellationRequested) { await UniTask.Yield(PlayerLoopTiming.Update, ctx); accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f); while (accumulator >= TickDelta) { accumulator -= TickDelta; _currentBattleFrame.Value++; var readyEntities = SimulateSingleFrame(_entities); if (readyEntities.Length > 0) return readyEntities; } } return null; } // TODO ЗАПУСКАТЬ ДВА ЦИКЛА ПОЛНЫЙ И ДО ПРЕРЫВАНИЯ private async UniTask PreviewBattleLoopAsync(int frameCount, Dictionary entities, CancellationToken ctx) { Dictionary actionDataSnapshots = new(); Dictionary resultsSnapshots = new(); Dictionary entitySnapshots = new(); foreach (var (entityId, entity) in entities) { if (!_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult) || !_battleActionDatas.TryGetValue(entityId, out var battleActionData)) { entity.EntityPreviewVisuals.ShowPreviewState(spawnStaticSnapshotGhost: false); continue; } entity.EntityPreviewVisuals.ShowPreviewState(spawnStaticSnapshotGhost: true); resultsSnapshots[entityId] = actionDecisionResult.GetSnapshot(); actionDataSnapshots[entityId] = battleActionData.GetSnapshot(); entitySnapshots[entityId] = entity.GetSnapshot(); } try { while (!ctx.IsCancellationRequested) { RestoreFromSnapshots(entities, resultsSnapshots, actionDataSnapshots, entitySnapshots); await PlayTimelineForCertainFrames(frameCount, ctx).AttachExternalCancellation(ctx); } } finally { RestoreFromSnapshots(entities, resultsSnapshots, actionDataSnapshots, entitySnapshots); foreach (var (_, entity) in entities) { entity.EntityPreviewVisuals.HidePreviewState(); } } } private void RestoreFromSnapshots(Dictionary entities, Dictionary resultsSnapshots, Dictionary actionDataSnapshots, Dictionary entitySnapshots) { foreach (var (entityId, entity) in entities) { if (!resultsSnapshots.TryGetValue(entityId, out var actionDecisionResult) || !actionDataSnapshots.TryGetValue(entityId, out var battleActionData)) continue; bool actionChanged = false; if (!_actionDecisionResults.TryAdd(entityId, actionDecisionResult)) { actionChanged = actionDecisionResult.ActionType != _actionDecisionResults[entityId].ActionType; if (actionChanged) _actionDecisionResults[entityId] = actionDecisionResult.GetSnapshot(); else _actionDecisionResults[entityId]?.RestoreSnapshot(actionDecisionResult); } if (!_battleActionDatas.TryAdd(entityId, battleActionData)) { if (actionChanged) _battleActionDatas[entityId] = battleActionData.GetSnapshot(); else _battleActionDatas[entityId]?.RestoreSnapshot(battleActionData); } entity.RestoreSnapshot(entitySnapshots[entityId]); } } private async UniTask PlayTimelineForCertainFrames(int frameCount, CancellationToken ctx) { float accumulator = 0f; while (frameCount > 0 && !ctx.IsCancellationRequested) { bool isCanceled = await UniTask.Yield(PlayerLoopTiming.Update, ctx).SuppressCancellationThrow(); if (isCanceled) break; accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f); while (accumulator >= TickDelta && frameCount > 0 && !ctx.IsCancellationRequested) { accumulator -= TickDelta; frameCount--; var readyEntities = SimulateSingleFrame(_entities); if (readyEntities.Length > 0) { foreach (var entityId in readyEntities) { // _battleActionDatas.Remove(entityID); // _actionDecisionResults.Remove(entityID); _battleActionDatas[entityId] = new WaitBattleActionData { TotalFrames = frameCount, WaitFrames = frameCount }; _actionDecisionResults[entityId] = new FrameBattleActionDeciderResult { ActionType = BattleActionType.Wait, FrameCount = frameCount, ResultCode = BattleActionDecisionResultCode.Decided }; } // return; } } } } private EntityId[] SimulateSingleFrame(Dictionary inEntities) { foreach (var entity in inEntities) { var entityId = entity.Key; if (!_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult) || !_battleActionDatas.TryGetValue(entityId, out var battleActionData)) continue; _battleActions[actionDecisionResult.ActionType] .Tick( TickDelta, entity.Value, actionDecisionResult, battleActionData); entity.Value.animator.Update(TickDelta); } Physics.Simulate(TickDelta); Physics.SyncTransforms(); var readyEntities = _battleActionDatas .Where(x => x.Value.Finished) .Select(x => x.Key) .ToArray(); return readyEntities; } private async UniTask DecideActions(EntityId[] entities, CancellationToken ctx) { foreach (var entityId in entities) { var (decisionResult, battleActionData) = await DecideAction(ctx, _entities[entityId]); _actionDecisionResults[entityId] = decisionResult; _battleActionDatas[entityId] = battleActionData; } } private async UniTask<(BattleActionDecisionResult, BattleActionData)> DecideAction(CancellationToken ctx, EntityProperties entity) { _battleActionDecisionPanel.SetEntityInfo(entity.battleActionTypes); BattleActionType decidedActionType = BattleActionType.None; BattleActionDecisionResult decisionResult = null; BattleActionData actionData = null; var entityId = entity.GetEntityId(); BattleActionData entityActionDataSnapshot = null; BattleActionDecisionResult entityResultSnapshot = null; if (_battleActionDatas.TryGetValue(entityId, out var battleActionData)) entityActionDataSnapshot = battleActionData.GetSnapshot(); if (_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult)) { entityResultSnapshot = actionDecisionResult.GetSnapshot(); _timelinePanel.SetPreviousActionText(entityResultSnapshot.ActionType.ToString()); } while (decidedActionType == BattleActionType.None || ctx.IsCancellationRequested) { Cursor.lockState = CursorLockMode.Confined; ((CameraBattleModeState)_cameraStateMachine.CurrentBaseState).battleDeciding = true; await _battleActionDecisionPanel.Open(ctx); decidedActionType = await _battleActionDecisionPanel.GetDecision(ctx); if (decidedActionType == BattleActionType.None) continue; var deciderType = _battleActions[decidedActionType].DeciderType; _battleActionDeciders[deciderType].MountDecider(entity); CancellationTokenSource previewCancellationToken = new(); while (!ctx.IsCancellationRequested) { Cursor.lockState = CursorLockMode.Locked; ((CameraBattleModeState)_cameraStateMachine.CurrentBaseState).battleDeciding = false; await _battleActionDecisionPanel.Close(ctx); decisionResult = await _battleActionDeciders[deciderType].GetDecision(); decisionResult.ActionType = decidedActionType; actionData = _battleActions[decidedActionType].MountAndGetFrames(entity, decisionResult); previewCancellationToken.Cancel(); previewCancellationToken.Dispose(); if (decisionResult.ResultCode == BattleActionDecisionResultCode.Deciding) { _actionDecisionResults[entityId] = decisionResult; _battleActionDatas[entityId] = actionData; previewCancellationToken = new(); PreviewBattleLoopAsync(actionData.TotalFrames, _entities, previewCancellationToken.Token) .Forget(); } else if (decisionResult.ResultCode == BattleActionDecisionResultCode.CancelDecision) { decidedActionType = BattleActionType.None; break; } else break; } _battleActionDeciders[deciderType].UnmountDecider(); } if (entityResultSnapshot != null && !_actionDecisionResults.TryAdd(entityId, entityResultSnapshot)) _actionDecisionResults[entityId]?.RestoreSnapshot(entityResultSnapshot); if (entityActionDataSnapshot != null && !_battleActionDatas.TryAdd(entityId, entityActionDataSnapshot)) _battleActionDatas[entityId]?.RestoreSnapshot(entityActionDataSnapshot); if (decisionResult == null || actionData == null) throw new ArgumentNullException(nameof(decisionResult)); return (decisionResult, actionData); } public void ForceDecideAction( EntityId entityId, BattleActionData forcedBattleActionData, BattleActionDecisionResult forcedBattleActionDataResult) { _battleActionDatas[entityId] = forcedBattleActionData; _actionDecisionResults[entityId] = forcedBattleActionDataResult; Debug.Log($"Задано действие {forcedBattleActionDataResult.ActionType} для {entityId}"); } } }