app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/Editor/Scripts/Setup/HyperSDKInstaller.cs +42 -0
  3. package/Editor/Scripts/Tools/HyperGameModeMenu.cs +27 -7
  4. package/Runtime/Internal/AssemblyInfo.cs +1 -0
  5. package/Runtime/Internal/Bootstrap/HyperLoader.cs +186 -8
  6. package/Runtime/Internal/Core/HyperRuntime.cs +82 -0
  7. package/Runtime/Internal/Core/HyperRuntimeHost.cs +6 -0
  8. package/Runtime/Internal/Data/URLData.cs +18 -1
  9. package/Runtime/Internal/Managers/BackendManager.cs +281 -24
  10. package/Runtime/Internal/Managers/DataManager.cs +207 -21
  11. package/Runtime/Internal/Managers/GameManager.cs +365 -17
  12. package/Runtime/Internal/Managers/ReplayManager.cs +117 -0
  13. package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
  14. package/Runtime/Internal/Managers/TimeManager.cs +54 -12
  15. package/Runtime/Internal/Replay/ReplayActivityRecorder.cs +182 -0
  16. package/Runtime/Internal/Replay/ReplayActivityRecorder.cs.meta +2 -0
  17. package/Runtime/Internal/Replay/ReplaySessionState.cs +181 -0
  18. package/Runtime/Internal/Replay/ReplaySessionState.cs.meta +2 -0
  19. package/Runtime/Internal/Replay.meta +8 -0
  20. package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +5 -0
  21. package/Runtime/Internal/UI/Animation/LoadingTextAnimation.cs +15 -1
  22. package/Runtime/Internal/UI/Components/ControlBar.cs +25 -2
  23. package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
  24. package/Runtime/Internal/UI/Components/ReplayController.cs +527 -0
  25. package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
  26. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
  27. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
  28. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
  29. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
  30. package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
  31. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +61 -0
  32. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
  33. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
  34. package/Runtime/Public/AssemblyInfo.cs +3 -0
  35. package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
  36. package/Runtime/Public/Core/HyperSDK.cs +305 -109
  37. package/Runtime/Public/Hyper.asmdef +2 -1
  38. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
  39. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
  40. package/Runtime/Public/Replay.meta +8 -0
  41. package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
  42. package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
  43. package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
  44. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
  45. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
  46. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
  47. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
  48. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
  49. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
  50. package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
  51. package/Runtime/Resources/Sprites/Replays/Recording.png.meta +156 -0
  52. package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
  53. package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
  54. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
  55. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
  56. package/Runtime/Resources/Sprites/Replays.meta +8 -0
  57. package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
  58. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  59. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
  60. package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
  61. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
  62. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
  63. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
  64. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
  65. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
  66. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
  67. package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
  68. package/Runtime/Shared/AssemblyInfo.cs +5 -0
  69. package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
  70. package/Runtime/Shared/DTOs/Enums.cs +8 -3
  71. package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
  72. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
  73. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
  74. package/Runtime/Shared/Replay.meta +8 -0
  75. package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
  76. package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
  77. package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
  78. package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
  79. package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
  80. package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
  81. package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
  82. package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
  83. package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
  84. package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
  85. package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
  86. package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
  87. package/Samples~/LaneRunner/Materials/Player.mat +84 -0
  88. package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
  89. package/Samples~/LaneRunner/Materials.meta +8 -0
  90. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
  91. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
  92. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
  93. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
  94. package/Samples~/LaneRunner/Prefabs.meta +8 -0
  95. package/Samples~/LaneRunner/README.md +35 -0
  96. package/Samples~/LaneRunner/README.md.meta +7 -0
  97. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
  98. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
  99. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
  100. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
  101. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
  102. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
  103. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
  104. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
  105. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
  106. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
  107. package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
  108. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
  109. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
  110. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
  111. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
  112. package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
  113. package/Samples~/LaneRunner/Scripts.meta +8 -0
  114. package/Tests.meta +8 -0
  115. package/package.json +8 -1
@@ -82,14 +82,20 @@ namespace Hyper.Internal.Managers
82
82
  {
83
83
  Destroy(_hyperCanvasObject);
84
84
  }
85
+
86
+ if (!ReplaySessionState.IsSeeking)
87
+ {
88
+ ReplayPresentationCanvas.DestroyCurrent();
89
+ }
85
90
  }
86
91
 
87
92
  #endregion
88
93
 
89
94
  #region Public API Implementation
90
95
 
91
- public bool IsBattleMode => Config.GameMode == GameMode.Battle;
92
- public bool IsPracticeMode => !IsBattleMode;
96
+ public bool IsBattleMode => Config.IsBattleGameplay;
97
+ public bool IsPracticeMode => Config.GameMode == GameMode.Practice;
98
+ public bool IsReplayMode => Config.GameMode == GameMode.Replay;
93
99
  public bool IsPaused => _isPaused;
94
100
  public bool HasGameStarted => _hasGameStarted;
95
101
  public bool HasGameEnded => _hasGameEnded;
@@ -114,6 +120,12 @@ namespace Hyper.Internal.Managers
114
120
  {
115
121
  if (!IsBattleMode) return;
116
122
 
123
+ if (IsReplayMode)
124
+ {
125
+ EndReplayGame();
126
+ return;
127
+ }
128
+
117
129
  var timeLeft = HyperRuntime.Timer.CurrentTime;
118
130
  if (timeLeft <= 0f) return;
119
131
 
@@ -124,6 +136,12 @@ namespace Hyper.Internal.Managers
124
136
  {
125
137
  if (IsBattleMode) return;
126
138
 
139
+ if (ReplaySessionState.IsActive)
140
+ {
141
+ EndReplayGame();
142
+ return;
143
+ }
144
+
127
145
  SetGameEnded();
128
146
  HyperRuntime.Timer.StopAllTimers();
129
147
 
@@ -158,6 +176,7 @@ namespace Hyper.Internal.Managers
158
176
 
159
177
  internal void PauseGame()
160
178
  {
179
+ if (IsReplayMode) return;
161
180
  if (!_hasGameStarted || _isPaused || _hasGameEnded) return;
162
181
 
163
182
  _isPaused = true;
@@ -190,11 +209,66 @@ namespace Hyper.Internal.Managers
190
209
 
191
210
  void OnApplicationFocus(bool hasFocus)
192
211
  {
212
+ if (IsReplayMode)
213
+ {
214
+ HandleReplayFocus(hasFocus);
215
+ return;
216
+ }
193
217
  #if !UNITY_EDITOR
194
218
  HandlePauseState(!hasFocus);
195
219
  #endif
196
220
  }
197
221
 
222
+ private void HandleReplayFocus(bool hasFocus)
223
+ {
224
+ if (!_hasGameStarted || _hasGameEnded) return;
225
+
226
+ if (!hasFocus && _replayFocusResumeCoroutine != null)
227
+ {
228
+ StopCoroutine(_replayFocusResumeCoroutine);
229
+ _replayFocusResumeCoroutine = null;
230
+ }
231
+
232
+ if (!hasFocus && !_replayFocusSuspended)
233
+ {
234
+ _replayFocusSuspended = true;
235
+ _savedReplayTimeScale = Time.timeScale;
236
+ Time.timeScale = 0f;
237
+ HyperRuntime.Timer.PauseGameplayTimers();
238
+
239
+ // WebGL may stop producing frames immediately after focus loss,
240
+ // so suspend replay timing here instead of waiting for Update.
241
+ _previousTimeScale = 0f;
242
+ _eventManager.InvokeTimeScaleFrozenEvent();
243
+ HyperDebug.Log("Time scale frozen event invoked");
244
+ }
245
+ else if (hasFocus && _replayFocusSuspended)
246
+ {
247
+ if (_replayFocusResumeCoroutine == null)
248
+ {
249
+ _replayFocusResumeCoroutine = StartCoroutine(ResumeReplayAfterFocus());
250
+ }
251
+ }
252
+ }
253
+
254
+ private IEnumerator ResumeReplayAfterFocus()
255
+ {
256
+ // WebGL reports focus before Unity refreshes its unscaled clock. Keep
257
+ // gameplay and replay frozen briefly while session time continues.
258
+ yield return new WaitForSecondsRealtime(1f);
259
+
260
+ _replayFocusResumeCoroutine = null;
261
+ if (!_replayFocusSuspended || _hasGameEnded) yield break;
262
+
263
+ _replayFocusSuspended = false;
264
+ Time.timeScale = _savedReplayTimeScale > 0f ? _savedReplayTimeScale : 1f;
265
+ HyperRuntime.Timer.ResumeGameTimersAfterPause();
266
+
267
+ _previousTimeScale = Time.timeScale;
268
+ _eventManager.InvokeTimeScaleResumedEvent();
269
+ HyperDebug.Log("Time scale resumed event invoked");
270
+ }
271
+
198
272
  void HandlePauseState(bool shouldPause)
199
273
  {
200
274
  if (shouldPause && !_isPaused)
@@ -214,7 +288,7 @@ namespace Hyper.Internal.Managers
214
288
  _eventManager.InvokeTimeScaleFrozenEvent();
215
289
  HyperDebug.Log("Time scale frozen event invoked");
216
290
 
217
- if (_backendManager.isSocketConnected)
291
+ if (!IsReplayMode && _backendManager.isSocketConnected)
218
292
  {
219
293
  _dataManager.StopCapturingScores();
220
294
  _backendManager.StopSendingScores();
@@ -227,13 +301,13 @@ namespace Hyper.Internal.Managers
227
301
 
228
302
  // Always restart capturing when game resumes, even if offline,
229
303
  // so scores accumulate for the offline batch and are flushed on reconnect.
230
- if (!_hasGameEnded && _hasGameStarted)
304
+ if (!IsReplayMode && !_hasGameEnded && _hasGameStarted)
231
305
  {
232
306
  _dataManager.StartCapturingScores();
233
307
  }
234
308
 
235
309
  // Only resume server session if connected
236
- if (_backendManager.isSocketConnected && !_hasGameEnded && _hasGameStarted)
310
+ if (!IsReplayMode && _backendManager.isSocketConnected && !_hasGameEnded && _hasGameStarted)
237
311
  {
238
312
  _backendManager.SendGameResumed();
239
313
  }
@@ -336,6 +410,7 @@ namespace Hyper.Internal.Managers
336
410
 
337
411
  internal void ShowConnectionLost()
338
412
  {
413
+ if (IsReplayMode) return;
339
414
  if (!IsInGameScene) return;
340
415
 
341
416
  EnsureConnectionStatusBar();
@@ -352,6 +427,7 @@ namespace Hyper.Internal.Managers
352
427
 
353
428
  internal void ShowConnectionRestored()
354
429
  {
430
+ if (IsReplayMode) return;
355
431
  if (!IsInGameScene) return;
356
432
 
357
433
  EnsureConnectionStatusBar();
@@ -374,7 +450,7 @@ namespace Hyper.Internal.Managers
374
450
  if (!_loadingFailedModal.activeInHierarchy)
375
451
  {
376
452
  _loadingFailedModal.SetActive(true);
377
- SetBlockerActive(true);
453
+ SetBlockerActive(true, 0.05f);
378
454
  }
379
455
  _eventManager.InvokeConnectionLostEvent();
380
456
  }
@@ -389,6 +465,59 @@ namespace Hyper.Internal.Managers
389
465
  }
390
466
  }
391
467
 
468
+ internal void RetryReplayLoading()
469
+ {
470
+ if (!IsReplayMode) return;
471
+
472
+ if (_replayConnectionCheckCoroutine != null)
473
+ {
474
+ return;
475
+ }
476
+
477
+ _replayConnectionCheckCoroutine = StartCoroutine(CheckReplayConnection());
478
+ }
479
+
480
+ private IEnumerator CheckReplayConnection()
481
+ {
482
+ const float timeoutSeconds = 5f;
483
+ float startedAt = Time.realtimeSinceStartup;
484
+ bool checkInProgress = false;
485
+ bool isConnected = false;
486
+
487
+ _loadingFailedModalComponent?.ShowRetryCountdown(Mathf.CeilToInt(timeoutSeconds));
488
+
489
+ while (Time.realtimeSinceStartup - startedAt < timeoutSeconds)
490
+ {
491
+ if (!isConnected && !checkInProgress)
492
+ {
493
+ checkInProgress = true;
494
+ StartCoroutine(HyperRuntime.BackendManager.CheckInternetConnection(connected =>
495
+ {
496
+ isConnected = connected;
497
+ checkInProgress = false;
498
+ }));
499
+ }
500
+
501
+ float remaining = timeoutSeconds - (Time.realtimeSinceStartup - startedAt);
502
+ _loadingFailedModalComponent?.ShowRetryCountdown(Mathf.CeilToInt(remaining));
503
+ yield return null;
504
+ }
505
+
506
+ _replayConnectionCheckCoroutine = null;
507
+
508
+ if (isConnected)
509
+ {
510
+ // This emits connection-found, allowing the existing loader to restart
511
+ // bootstrap and fetch/validate all replay data through its normal path.
512
+ ShowLoadingFailedModal(false);
513
+ yield break;
514
+ }
515
+
516
+ // The probe may finish early or reach the five-second deadline. In either
517
+ // case restore the modal CTA state instead of waiting at 00:00.
518
+ _loadingFailedModalComponent?.AllAttemptsHaveFailed();
519
+ }
520
+
392
521
  internal void ShowScoreSubmissionFailedModal()
393
522
  {
394
523
  if (IsPortrait())
@@ -484,6 +613,12 @@ namespace Hyper.Internal.Managers
484
613
 
485
614
  internal void ShowGameOverModal()
486
615
  {
616
+ if (IsReplayMode)
617
+ {
618
+ ShowReplayOverModal();
619
+ return;
620
+ }
621
+
487
622
  HidePauseModals();
488
623
 
489
624
  if (IsBattleMode)
@@ -585,12 +720,16 @@ namespace Hyper.Internal.Managers
585
720
  #region Private State
586
721
 
587
722
  private bool _hasGameStarted;
723
+ private bool _gameStartRequested;
588
724
  private bool _isPaused;
589
725
  private bool _hasGameEnded;
726
+ private bool _replayEndHandled;
590
727
  private bool _isFrozenBeforePause;
591
728
  private bool _isPortrait;
592
729
  private float _previousTimeScale = 1f;
593
730
  private float _savedTimeScaleBeforePause = 1f;
731
+ private bool _replayFocusSuspended;
732
+ private float _savedReplayTimeScale = 1f;
594
733
 
595
734
  #endregion
596
735
 
@@ -609,6 +748,9 @@ namespace Hyper.Internal.Managers
609
748
 
610
749
  [DllImport("__Internal")]
611
750
  private static extern void MarkGameLoadedOnHTML();
751
+
752
+ [DllImport("__Internal")]
753
+ private static extern void MarkGameStartedOnHTML();
612
754
  #endif
613
755
 
614
756
  private void HandleQueryParameters()
@@ -618,6 +760,8 @@ namespace Hyper.Internal.Managers
618
760
  if (queryParamsPtr == IntPtr.Zero)
619
761
  {
620
762
  HyperDebug.LogError("Failed to get query parameters");
763
+ Config = new URLData { GameMode = GameMode.Practice };
764
+ SyncReplayContext();
621
765
  return;
622
766
  }
623
767
 
@@ -637,23 +781,32 @@ namespace Hyper.Internal.Managers
637
781
  }
638
782
  #else
639
783
  Config = HyperRuntime.Settings._settings;
784
+ #if !UNITY_EDITOR
785
+ if (Config.GameMode == GameMode.Replay)
786
+ {
787
+ Config.GameMode = GameMode.Practice;
788
+ }
789
+ #endif
790
+ SyncReplayContext();
640
791
  #endif
641
792
  }
642
793
 
643
794
  private void ParseQueryParameters()
644
795
  {
645
- SetGameModeAndPVP();
796
+ SetGameMode();
646
797
  SetBackendGameStatus();
647
798
  SetOptionalParameters();
648
799
  SetSafeAreaBounds();
800
+ SyncReplayContext();
649
801
  }
650
802
 
651
- private void SetGameModeAndPVP()
803
+ private void SetGameMode()
652
804
  {
653
- bool isPVP = QueryParamsData.TryGetValue("pvp", out string pvpValue) && pvpValue == "true";
654
- Config.GameMode = isPVP ? GameMode.Battle : GameMode.Practice;
805
+ bool isReplay = IsTrueQueryFlag("replay");
806
+ bool isPVP = IsTrueQueryFlag("pvp");
807
+ Config.SetModeFromLaunchFlags(isPVP, isReplay);
655
808
 
656
- if (isPVP && QueryParamsData.TryGetValue("time", out string timeValue))
809
+ if (Config.IsBattleGameplay && QueryParamsData.TryGetValue("time", out string timeValue))
657
810
  {
658
811
  if (float.TryParse(timeValue, out float timeLimit))
659
812
  {
@@ -662,6 +815,35 @@ namespace Hyper.Internal.Managers
662
815
  }
663
816
  }
664
817
 
818
+ private bool IsTrueQueryFlag(string key)
819
+ {
820
+ return QueryParamsData.TryGetValue(key, out string value) &&
821
+ string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
822
+ }
823
+
824
+ private void SyncReplayContext()
825
+ {
826
+ if (IsReplayMode)
827
+ {
828
+ string entryId = Config.EntryId ?? string.Empty;
829
+ bool isNewReplay = !ReplaySessionState.IsActive ||
830
+ !string.Equals(ReplaySessionState.EntryId, entryId, StringComparison.Ordinal);
831
+
832
+ if (isNewReplay)
833
+ {
834
+ ReplaySessionState.Configure(
835
+ string.Empty,
836
+ string.Empty,
837
+ (int)Config.RandomSeed,
838
+ entryId);
839
+ }
840
+ }
841
+ else
842
+ {
843
+ ReplaySessionState.Clear();
844
+ }
845
+ }
846
+
665
847
  private void SetBackendGameStatus()
666
848
  {
667
849
  string[] requiredFields = { "tournamentId", "gameId", "entryId", "host", "embedUrl" };
@@ -680,6 +862,14 @@ namespace Hyper.Internal.Managers
680
862
 
681
863
  private void SetOptionalParameters()
682
864
  {
865
+ // Replay launches only require replay=true and entryId, so capture it
866
+ // independently of the full backend PvP launch contract.
867
+ if (QueryParamsData.TryGetValue("entryId", out string entryId) &&
868
+ !string.IsNullOrWhiteSpace(entryId))
869
+ {
870
+ Config.EntryId = entryId;
871
+ }
872
+
683
873
  if (QueryParamsData.TryGetValue("gameSlug", out string gameSlug))
684
874
  Config.GameSlug = gameSlug;
685
875
 
@@ -766,6 +956,7 @@ namespace Hyper.Internal.Managers
766
956
  {
767
957
  IsInGameScene = false;
768
958
  _hasGameStarted = false;
959
+ _gameStartRequested = false;
769
960
  _hasGameEnded = false;
770
961
  _isPaused = false;
771
962
  _isFrozenBeforePause = false;
@@ -790,7 +981,9 @@ namespace Hyper.Internal.Managers
790
981
  HideGameOverModal();
791
982
  IsInGameScene = true;
792
983
  _hasGameStarted = false;
984
+ _gameStartRequested = false;
793
985
  _hasGameEnded = false;
986
+ _replayEndHandled = false;
794
987
  _isPaused = false;
795
988
  _isFrozenBeforePause = false;
796
989
  _previousTimeScale = Time.timeScale; // Sync with actual time scale on scene load
@@ -798,7 +991,7 @@ namespace Hyper.Internal.Managers
798
991
 
799
992
  private void InitializeSceneUI()
800
993
  {
801
- if (Config.isPVP)
994
+ if (IsBattleMode)
802
995
  {
803
996
  HyperRuntime.Timer.StartPreSessionTimer();
804
997
  HyperRuntime.Timer.StartFiveSecondCountdown();
@@ -810,7 +1003,44 @@ namespace Hyper.Internal.Managers
810
1003
 
811
1004
  ShowControlBarElements();
812
1005
  AddTutorialModal();
813
- ShowTutorialAnnouncementOrStartingInstruction();
1006
+ if (IsReplayMode && ReplaySessionState.IsSeeking)
1007
+ {
1008
+ StartCoroutine(StartSeekingReplayWhenReady());
1009
+ }
1010
+ else
1011
+ {
1012
+ ShowTutorialAnnouncementOrStartingInstruction();
1013
+ }
1014
+ }
1015
+
1016
+ private IEnumerator StartSeekingReplayWhenReady()
1017
+ {
1018
+ // Allow scene scripts to subscribe to OnGameStart without presenting Tap to Start.
1019
+ yield return null;
1020
+ StartGameplay();
1021
+ }
1022
+
1023
+ internal void StartGameplay()
1024
+ {
1025
+ if (_gameStartRequested || _hasGameStarted || _hasGameEnded || !IsInGameScene) return;
1026
+ _gameStartRequested = true;
1027
+
1028
+ HideStartGameDeadlineSnackBar();
1029
+ _dataManager.UpdateScore(0);
1030
+ HyperRuntime.Timer.StartActiveGameTimer();
1031
+ _dataManager.StartCapturingScores();
1032
+ _backendManager.StartSendingScores();
1033
+
1034
+ if (_backendManager.isSocketConnected)
1035
+ {
1036
+ _backendManager.SendGameStarted();
1037
+ }
1038
+
1039
+ #if UNITY_WEBGL && !UNITY_EDITOR
1040
+ MarkGameStartedOnHTML();
1041
+ #endif
1042
+ _eventManager.InvokeGameStartEvent();
1043
+ HideStartingInstructionModal();
814
1044
  }
815
1045
 
816
1046
  private IEnumerator BootstrapAndStart()
@@ -838,19 +1068,33 @@ namespace Hyper.Internal.Managers
838
1068
  {
839
1069
  HideGameOverModal();
840
1070
  HidePauseModals();
1071
+ if (_replayOverModal != null) _replayOverModal.SetActive(false);
841
1072
 
842
1073
  // Reset game state flags
843
1074
  _hasGameStarted = false;
1075
+ _gameStartRequested = false;
844
1076
  _hasGameEnded = false;
1077
+ _replayEndHandled = false;
1078
+ _replayFocusSuspended = false;
845
1079
  _isPaused = false;
846
1080
  _isFrozenBeforePause = false;
847
1081
 
848
- HyperRuntime.Timer.Reset();
1082
+ HyperRuntime.Timer.ResetForGameRestart();
849
1083
  HyperRuntime.DataManager.UpdateScore(0);
850
1084
  HyperRuntime.DataManager.RestoreAudioAfterGameSessionEnd();
851
1085
 
852
1086
  ShowControlBarElements();
853
- ShowStartingInstructionModal();
1087
+ _replayControllerComponent?.ResetForReplayRestart();
1088
+
1089
+ if (IsReplayMode && ReplaySessionState.IsSeeking)
1090
+ {
1091
+ HideStartingInstructionModal();
1092
+ StartCoroutine(StartSeekingReplayWhenReady());
1093
+ }
1094
+ else
1095
+ {
1096
+ ShowStartingInstructionModal();
1097
+ }
854
1098
  }
855
1099
 
856
1100
  #endregion
@@ -859,6 +1103,12 @@ namespace Hyper.Internal.Managers
859
1103
 
860
1104
  private void EndGameInternal()
861
1105
  {
1106
+ if (IsReplayMode)
1107
+ {
1108
+ EndReplayGame();
1109
+ return;
1110
+ }
1111
+
862
1112
  SetGameEnded();
863
1113
 
864
1114
  _eventManager.InvokeGameEndedEvent();
@@ -878,6 +1128,57 @@ namespace Hyper.Internal.Managers
878
1128
  }
879
1129
  }
880
1130
 
1131
+ private void EndReplayGame()
1132
+ {
1133
+ if (ReplaySessionState.IsSeekRestartPending) return;
1134
+
1135
+ // Time Up marks _hasGameEnded before its two-second presentation finishes.
1136
+ // Track replay finalization separately so that delayed end still runs once.
1137
+ if (_replayEndHandled) return;
1138
+
1139
+ _replayEndHandled = true;
1140
+ _hasGameEnded = true;
1141
+ _replayFocusSuspended = false;
1142
+ Time.timeScale = 1f;
1143
+ _eventManager.InvokeGameEndedEvent();
1144
+ HyperRuntime.Timer.StopAllTimers();
1145
+ _dataManager.StopCapturingScores();
1146
+ HyperRuntime.BackendManager.StopSendingScores();
1147
+ ReplaySessionState.MarkEnded();
1148
+ _replayControllerComponent?.SetReplayEnded();
1149
+ ShowReplayOverModal();
1150
+ }
1151
+
1152
+ internal void ExitReplayEarly()
1153
+ {
1154
+ if (!IsReplayMode) return;
1155
+
1156
+ if (!_replayEndHandled)
1157
+ {
1158
+ _replayEndHandled = true;
1159
+ _hasGameEnded = true;
1160
+ _eventManager.InvokeGameEndedEvent();
1161
+ }
1162
+
1163
+ Time.timeScale = 1f;
1164
+ HyperRuntime.Timer.StopAllTimers();
1165
+ _dataManager.StopCapturingScores();
1166
+ HyperRuntime.BackendManager.StopSendingScores();
1167
+ _replayControllerComponent?.SetReplayEnded();
1168
+ ReplaySessionState.Clear();
1169
+ ReplayPresentationCanvas.DestroyCurrent();
1170
+ HyperRuntime.BackendManager.LogEvent(ConsoleEventType.PracticeGameEnd);
1171
+ }
1172
+
1173
+ internal void ExitCompletedReplay()
1174
+ {
1175
+ if (!IsReplayMode) return;
1176
+
1177
+ ReplaySessionState.Clear();
1178
+ ReplayPresentationCanvas.DestroyCurrent();
1179
+ HyperRuntime.BackendManager.LogEvent(ConsoleEventType.PracticeGameEnd);
1180
+ }
1181
+
881
1182
  internal void EndGame(string key)
882
1183
  {
883
1184
  if (key == "Hyper_Time_Up")
@@ -969,6 +1270,12 @@ namespace Hyper.Internal.Managers
969
1270
  private GameObject _blockerUI;
970
1271
  private Image _blockerUIImage;
971
1272
  private GameObject _safeArea;
1273
+ private GameObject _replayInputBlocker;
1274
+ private GameObject _replayControllerObject;
1275
+ private ReplayController _replayControllerComponent;
1276
+ private GameObject _replayOverModal;
1277
+ private Coroutine _replayConnectionCheckCoroutine;
1278
+ private Coroutine _replayFocusResumeCoroutine;
972
1279
 
973
1280
  private GameObject _connectionStatusBar;
974
1281
  private GameObject _startGameDeadlineStatusBar;
@@ -1085,7 +1392,7 @@ namespace Hyper.Internal.Managers
1085
1392
  #endif
1086
1393
  }
1087
1394
 
1088
- private void SetBlockerActive(bool isActive)
1395
+ private void SetBlockerActive(bool isActive, float opacity = 0.55f)
1089
1396
  {
1090
1397
  if (_blockerUI == null) return;
1091
1398
 
@@ -1093,7 +1400,7 @@ namespace Hyper.Internal.Managers
1093
1400
 
1094
1401
  if (isActive)
1095
1402
  {
1096
- _blockerUIImage.color = new Color(0f, 0f, 0f, 0.55f);
1403
+ _blockerUIImage.color = new Color(0f, 0f, 0f, Mathf.Clamp01(opacity));
1097
1404
  int lastActiveIndex = GetLastActiveChildIndex();
1098
1405
  _blockerUI.transform.SetSiblingIndex(lastActiveIndex - 1);
1099
1406
  }
@@ -1121,6 +1428,11 @@ namespace Hyper.Internal.Managers
1121
1428
  if (_controlBarElements == null) CreateControlBarElements();
1122
1429
 
1123
1430
  _controlBarElements.SetActive(true);
1431
+
1432
+ if (IsReplayMode)
1433
+ {
1434
+ EnsureReplayPresentation();
1435
+ }
1124
1436
  }
1125
1437
 
1126
1438
  private void HideTopBarElements()
@@ -1157,6 +1469,42 @@ namespace Hyper.Internal.Managers
1157
1469
  _controlBarModal = _controlBarElements.GetComponent<ControlBar>();
1158
1470
  }
1159
1471
 
1472
+ private void EnsureReplayPresentation()
1473
+ {
1474
+ ReplayPresentationCanvas presentation = ReplayPresentationCanvas.GetOrCreate(_isPortrait);
1475
+ _replayInputBlocker = presentation.InputBlocker;
1476
+ _replayControllerObject = presentation.ControllerObject;
1477
+ _replayControllerComponent = presentation.Controller;
1478
+ EventSystem.current?.SetSelectedGameObject(null);
1479
+ }
1480
+
1481
+ private void ShowReplayOverModal()
1482
+ {
1483
+ HidePauseModals();
1484
+
1485
+ if (_replayOverModal == null)
1486
+ {
1487
+ if (_canvas == null) CreateCanvas();
1488
+ GameObject prefab = Resources.Load<GameObject>("UIPrefabs/ReplayOverModal");
1489
+ if (prefab == null)
1490
+ {
1491
+ HyperDebug.LogWarning("[GameManager] ReplayOverModal prefab not found.");
1492
+ return;
1493
+ }
1494
+
1495
+ _replayOverModal = InstantiateModal(prefab);
1496
+ if (_replayOverModal.GetComponent<ReplayOverModal>() == null)
1497
+ {
1498
+ _replayOverModal.AddComponent<ReplayOverModal>();
1499
+ }
1500
+ }
1501
+
1502
+ _replayOverModal.SetActive(true);
1503
+ _replayOverModal.transform.SetAsLastSibling();
1504
+ SetBlockerActive(true);
1505
+ _replayOverModal.transform.SetAsLastSibling();
1506
+ }
1507
+
1160
1508
  private void EnsureConnectionStatusBar()
1161
1509
  {
1162
1510
  if (_connectionStatusBar != null) return;