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.Snapshots; 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 SnapshotUtilityNonStatic _snapshotUtility = new(); private BattleActionDecisionPanel _battleActionDecisionPanel; private CameraStateMachine _cameraStateMachine; private SemaphoreSlim _preview = new(1,1); 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) { foreach (var entityId in readyEntities) { Debug.Log($"Entity {entityId} is real ready"); _battleActions[_actionDecisionResults[entityId].ActionType] .Finish( _entities[entityId], _battleActionDatas[entityId], _actionDecisionResults[entityId]); _battleActionDatas.Remove(entityId); _actionDecisionResults.Remove(entityId); } return readyEntities; } } } return null; } // TODO ЗАПУСКАТЬ ДВА ЦИКЛА ПОЛНЫЙ И ДО ПРЕРЫВАНИЯ private async UniTask PreviewBattleLoopAsync(int frameCount, Dictionary entities, CancellationToken ctx) { if (frameCount <= 0) { _preview.Release(1); throw new Exception("FrameCount must be > 0"); } _snapshotUtility.GetSnapshots( entities, _actionDecisionResults, _battleActionDatas); try { while (!ctx.IsCancellationRequested) { _snapshotUtility.RestoreFromSnapshots( entities, _actionDecisionResults, _battleActionDatas); await PlayTimelineForCertainFrames(frameCount, ctx).AttachExternalCancellation(ctx); } } finally { _snapshotUtility.RestoreFromSnapshots( entities, _actionDecisionResults, _battleActionDatas); foreach (var (_, entity) in entities) { entity.EntityPreviewVisuals.HidePreviewState(); } _preview.Release(1); } } private async UniTask PreviewBattleLoopAsync(EntityId entityId, Dictionary entities, CancellationToken ctx) { _snapshotUtility.GetSnapshots( entities, _actionDecisionResults, _battleActionDatas); try { while (!ctx.IsCancellationRequested) { _snapshotUtility.RestoreFromSnapshots( entities, _actionDecisionResults, _battleActionDatas); await PlayTimelineUntilThatSpecific(entityId, ctx).AttachExternalCancellation(ctx); } } finally { _snapshotUtility.RestoreFromSnapshots( entities, _actionDecisionResults, _battleActionDatas); foreach (var (_, entity) in entities) { entity.EntityPreviewVisuals.HidePreviewState(); } _preview.Release(1); } } private async UniTask PlayTimelineForCertainFrames(int frameCount, CancellationToken ctx) { float accumulator = 0f; int counter = frameCount; while (counter > 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 && counter > 0 && !ctx.IsCancellationRequested) { // Debug.Log($"Simulating {counter}/{frameCount}"); accumulator -= TickDelta; counter--; var readyEntities = SimulateSingleFrame(_entities); if (readyEntities.Length > 0) { foreach (var entityId in readyEntities) { var battleActionType = _actionDecisionResults[entityId].ActionType; Debug.Log($"Entity {entityId} is preview ready {battleActionType}"); _battleActions[battleActionType] .Finish( _entities[entityId], _battleActionDatas[entityId], _actionDecisionResults[entityId]); _battleActionDatas.Remove(entityId); _actionDecisionResults.Remove(entityId); } // return; } } } } private async UniTask PlayTimelineUntilThatSpecific(EntityId specificEntityId, CancellationToken ctx) { float accumulator = 0f; while (!ctx.IsCancellationRequested) { bool isCanceled = await UniTask.Yield(PlayerLoopTiming.Update, ctx).SuppressCancellationThrow(); if (isCanceled) break; accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f); while (accumulator >= TickDelta && !ctx.IsCancellationRequested) { // Debug.Log($"Simulating {counter}/{frameCount}"); accumulator -= TickDelta; var readyEntities = SimulateSingleFrame(_entities); if (readyEntities.Length > 0) { foreach (var entityId in readyEntities) { var battleActionType = _actionDecisionResults[entityId].ActionType; Debug.Log($"Entity {entityId} is preview ready {battleActionType}"); _battleActions[_actionDecisionResults[entityId].ActionType] .Finish( _entities[entityId], _battleActionDatas[entityId], _actionDecisionResults[entityId]); _battleActionDatas.Remove(entityId); _actionDecisionResults.Remove(entityId); } if(readyEntities.Contains(specificEntityId)) return; } } } } private EntityId[] SimulateSingleFrame(Dictionary inEntities) { foreach (var (entityId, entity) in inEntities) { if (!_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult) || !_battleActionDatas.TryGetValue(entityId, out var battleActionData)) continue; _battleActions[actionDecisionResult.ActionType] .Tick( TickDelta, entity, actionDecisionResult, battleActionData); entity.animator.Update(TickDelta); } KinematicCharacterController.KinematicCharacterSystem.Simulate( TickDelta, KinematicCharacterController.KinematicCharacterSystem.CharacterMotors, KinematicCharacterController.KinematicCharacterSystem.PhysicsMovers ); 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; previewCancellationToken.Cancel(); previewCancellationToken.Dispose(); await _preview.WaitAsync(ctx); _preview.Release(); actionData = _battleActions[decidedActionType].MountAndGetFrames(entity, decisionResult); if (decisionResult.ResultCode == BattleActionDecisionResultCode.Deciding) { _actionDecisionResults[entityId] = decisionResult; _battleActionDatas[entityId] = actionData; previewCancellationToken = new(); await _preview.WaitAsync(previewCancellationToken.Token); PreviewBattleLoopAsync(entityId, _entities, previewCancellationToken.Token) .Forget(); } else if (decisionResult.ResultCode == BattleActionDecisionResultCode.CancelDecision) { decidedActionType = BattleActionType.None; _battleActionDatas.Remove(entityId); _actionDecisionResults.Remove(entityId); break; } else break; } _battleActionDeciders[deciderType].UnmountDecider(); } _battleActionDatas[entityId] = entityActionDataSnapshot; _actionDecisionResults[entityId] = entityResultSnapshot; if (decisionResult == null || actionData == null) throw new ArgumentNullException(nameof(decisionResult)); return (decisionResult, actionData); } public void ForceDecideAction( EntityId entityId, BattleActionData forcedBattleActionData, BattleActionDecisionResult forcedBattleActionDataResult) { if(_battleActionDatas.TryGetValue(entityId, out var battleActionData) && _actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult)) _battleActions[_actionDecisionResults[entityId].ActionType] .Finish( _entities[entityId], battleActionData, actionDecisionResult); _battleActionDatas[entityId] = forcedBattleActionData; _actionDecisionResults[entityId] = forcedBattleActionDataResult; Debug.Log($"Задано действие {forcedBattleActionDataResult.ActionType} для {entityId} with {forcedBattleActionData.GetType()}"); } } }