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

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 (48) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +6 -0
  3. package/HyperLoader.unity +2 -2
  4. package/Runtime/Internal/Core/HyperRuntime.cs +104 -9
  5. package/Runtime/Internal/Managers/DataManager.cs +2 -1
  6. package/Runtime/Internal/Managers/EventManager.cs +7 -1
  7. package/Runtime/Internal/Managers/GameManager.cs +88 -51
  8. package/Runtime/Internal/Managers/ReplayManager.cs +5 -1
  9. package/Runtime/Internal/Managers/TimeManager.cs +25 -8
  10. package/Runtime/Internal/Replay/ReplaySessionState.cs +66 -6
  11. package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +4 -0
  12. package/Runtime/Internal/UI/Components/ReplayController.cs +134 -11
  13. package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs +18 -0
  14. package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs.meta +2 -0
  15. package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs +145 -0
  16. package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs.meta +2 -0
  17. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +1 -0
  18. package/Runtime/Public/Core/HyperSDK.cs +16 -0
  19. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +127 -3
  20. package/Runtime/Resources/Fonts/Nunito/Nunito-Bold SDF.asset +500 -457
  21. package/Runtime/Resources/Fonts/Wonder Boys/Wonder Boys.asset +394 -379
  22. package/Runtime/Resources/HyperSDKSettings.asset +1 -0
  23. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png +0 -0
  24. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png.meta +156 -0
  25. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png +0 -0
  26. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png.meta +156 -0
  27. package/Runtime/Resources/Sprites/Circle Loader/Property 1=1.png.meta +7 -7
  28. package/Runtime/Resources/Sprites/Circle Loader/Property 1=2.png.meta +7 -7
  29. package/Runtime/Resources/Sprites/Circle Loader/Property 1=3.png.meta +7 -7
  30. package/Runtime/Resources/Sprites/Circle Loader/Property 1=4.png.meta +7 -7
  31. package/Runtime/Resources/Sprites/Circle Loader/Property 1=5.png.meta +7 -7
  32. package/Runtime/Resources/Sprites/Replays/Replay.png +0 -0
  33. package/Runtime/Resources/Sprites/Replays/{Recording.png.meta → Replay.png.meta} +9 -9
  34. package/Runtime/Resources/UIPrefabs/ControlBar.prefab +5 -5
  35. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +305 -183
  36. package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab +2194 -0
  37. package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab.meta +7 -0
  38. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +291 -6
  39. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +3 -0
  40. package/Samples~/LaneRunner/LaneRunnerSample.unity +4 -3
  41. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +1 -1
  42. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +1 -1
  43. package/Samples~/LaneRunner/README.md +8 -1
  44. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +18 -4
  45. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +3 -1
  46. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +8 -0
  47. package/package.json +1 -1
  48. package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
@@ -32,6 +32,9 @@ namespace Hyper.Internal.Managers
32
32
  private float _battleTimer;
33
33
  private bool _isPracticeRunning;
34
34
  private bool _isBattleRunning;
35
+ private bool _resumePracticeAfterPause;
36
+ private bool _resumeBattleAfterPause;
37
+ private bool _hasPausedGameplayTimerState;
35
38
 
36
39
  private float _fiveSecondTimer = 5f;
37
40
  private bool _isFiveSecondTimerRunning;
@@ -111,6 +114,7 @@ namespace Hyper.Internal.Managers
111
114
  public void Reset()
112
115
  {
113
116
  _practiceTimer = 0;
117
+ ClearPausedGameplayTimerState();
114
118
  _lastUnscaledTime = Time.unscaledTime;
115
119
  }
116
120
 
@@ -120,6 +124,7 @@ namespace Hyper.Internal.Managers
120
124
  _battleTimer = _gameManager != null ? _gameManager.Config.TimeLimit : 0f;
121
125
  _isPracticeRunning = false;
122
126
  _isBattleRunning = false;
127
+ ClearPausedGameplayTimerState();
123
128
  _timeUpTriggered = false;
124
129
  _timeUpPending = false;
125
130
  _hasGameEnded = false;
@@ -129,18 +134,17 @@ namespace Hyper.Internal.Managers
129
134
 
130
135
  public void ResumeGameTimersAfterPause()
131
136
  {
132
- if (_gameManager.IsBattleMode)
133
- {
134
- StartBattle();
135
- }
136
- else
137
- {
138
- StartPractice();
139
- }
137
+ if (!_hasPausedGameplayTimerState) return;
138
+
139
+ _isPracticeRunning = _resumePracticeAfterPause;
140
+ _isBattleRunning = _resumeBattleAfterPause;
141
+ ClearPausedGameplayTimerState();
140
142
  }
141
143
 
142
144
  public void StartActiveGameTimer()
143
145
  {
146
+ ClearPausedGameplayTimerState();
147
+
144
148
  if (_gameManager.IsBattleMode)
145
149
  {
146
150
  StartSessionTimer();
@@ -158,6 +162,7 @@ namespace Hyper.Internal.Managers
158
162
  {
159
163
  _hasGameEnded = true;
160
164
  _timeUpPending = false;
165
+ ClearPausedGameplayTimerState();
161
166
  StopBattle();
162
167
  StopPractice();
163
168
  }
@@ -168,6 +173,11 @@ namespace Hyper.Internal.Managers
168
173
  /// </summary>
169
174
  public void PauseGameplayTimers()
170
175
  {
176
+ if (_hasPausedGameplayTimerState) return;
177
+
178
+ _resumePracticeAfterPause = _isPracticeRunning;
179
+ _resumeBattleAfterPause = _isBattleRunning;
180
+ _hasPausedGameplayTimerState = true;
171
181
  StopBattle();
172
182
  StopPractice();
173
183
  }
@@ -208,6 +218,13 @@ namespace Hyper.Internal.Managers
208
218
  internal void StartBattle() => _isBattleRunning = true;
209
219
  internal void StopBattle() => _isBattleRunning = false;
210
220
 
221
+ private void ClearPausedGameplayTimerState()
222
+ {
223
+ _resumePracticeAfterPause = false;
224
+ _resumeBattleAfterPause = false;
225
+ _hasPausedGameplayTimerState = false;
226
+ }
227
+
211
228
  internal bool TrySetSessionDurationFromBackendRemainingTime(float remainingTimeSeconds)
212
229
  {
213
230
  if (_hasBackendSessionDuration)
@@ -24,6 +24,7 @@ namespace Hyper.Internal
24
24
  public static bool IsSeekRestartPending => IsSeeking && !_seekPlaybackReady;
25
25
  public static float SeekTarget01 { get; private set; }
26
26
  public static bool HasEnded { get; private set; }
27
+ public static float GameplayStartProgress01 { get; private set; }
27
28
 
28
29
  internal static string EditorReplayFilePath =>
29
30
  Path.Combine(Application.persistentDataPath, "hyper-replay.ndjson");
@@ -31,6 +32,9 @@ namespace Hyper.Internal
31
32
  public static event Action<float> SeekCompleted;
32
33
 
33
34
  private static bool _seekPlaybackReady;
35
+ private static bool _watchAgainAutoStartPending;
36
+ private static bool _isSeekAudioMuted;
37
+ private static float _volumeBeforeSeek;
34
38
 
35
39
  public static void Configure(
36
40
  string playerName,
@@ -38,6 +42,7 @@ namespace Hyper.Internal
38
42
  long originalRandomSeed,
39
43
  string entryId = null)
40
44
  {
45
+ RestoreAudioAfterSeek();
41
46
  IsActive = true;
42
47
  PlayerName = playerName ?? string.Empty;
43
48
  AvatarUrl = avatarUrl ?? string.Empty;
@@ -51,7 +56,9 @@ namespace Hyper.Internal
51
56
  IsSeeking = false;
52
57
  SeekTarget01 = 0f;
53
58
  _seekPlaybackReady = false;
59
+ _watchAgainAutoStartPending = false;
54
60
  HasEnded = false;
61
+ GameplayStartProgress01 = 0f;
55
62
  }
56
63
 
57
64
  public static void SetMetadata(string playerName, string avatarUrl, long originalRandomSeed)
@@ -76,19 +83,28 @@ namespace Hyper.Internal
76
83
  AvatarTexture = texture;
77
84
  }
78
85
 
79
- public static void SetProgress(float progress)
86
+ public static bool SetProgress(float progress)
80
87
  {
81
- if (!IsActive) return;
82
- if (IsSeeking && !_seekPlaybackReady) return;
83
- Progress01 = Math.Max(0f, Math.Min(1f, progress));
84
-
85
- if (IsSeeking && Progress01 >= SeekTarget01)
88
+ if (!IsActive) return false;
89
+ if (IsSeeking && !_seekPlaybackReady) return false;
90
+ float rawProgress = Math.Max(0f, Math.Min(1f, progress));
91
+ Progress01 = GameplayStartProgress01 >= 1f
92
+ ? 0f
93
+ : Math.Max(0f, Math.Min(1f,
94
+ (rawProgress - GameplayStartProgress01) / (1f - GameplayStartProgress01)));
95
+
96
+ bool hasReachedGameplayStart = rawProgress + 0.000001f >= GameplayStartProgress01;
97
+ if (IsSeeking && hasReachedGameplayStart && Progress01 >= SeekTarget01)
86
98
  {
87
99
  IsSeeking = false;
88
100
  _seekPlaybackReady = false;
89
101
  PlaybackSpeed = UserPlaybackSpeed;
102
+ RestoreAudioAfterSeek();
90
103
  SeekCompleted?.Invoke(PlaybackSpeed);
104
+ return true;
91
105
  }
106
+
107
+ return false;
92
108
  }
93
109
 
94
110
  public static void SetPlaybackSpeed(float speed)
@@ -105,6 +121,7 @@ namespace Hyper.Internal
105
121
  Progress01 = 1f;
106
122
  if (IsSeeking) PlaybackSpeed = UserPlaybackSpeed;
107
123
  IsSeeking = false;
124
+ RestoreAudioAfterSeek();
108
125
  HasEnded = true;
109
126
  }
110
127
 
@@ -117,6 +134,7 @@ namespace Hyper.Internal
117
134
  PlaybackSpeed = SanitizeSpeed(seekSpeed);
118
135
  IsSeeking = true;
119
136
  _seekPlaybackReady = false;
137
+ MuteAudioForSeek();
120
138
  }
121
139
 
122
140
  public static void ResetForSeekRestart()
@@ -124,6 +142,7 @@ namespace Hyper.Internal
124
142
  if (!IsActive || !IsSeeking) return;
125
143
  Progress01 = 0f;
126
144
  _seekPlaybackReady = false;
145
+ _watchAgainAutoStartPending = false;
127
146
  HasEnded = false;
128
147
  }
129
148
 
@@ -133,20 +152,42 @@ namespace Hyper.Internal
133
152
  _seekPlaybackReady = true;
134
153
  }
135
154
 
155
+ internal static void EnsureAudioMutedDuringSeek()
156
+ {
157
+ if (_isSeekAudioMuted)
158
+ AudioListener.volume = 0f;
159
+ }
160
+
136
161
  public static void ResetForWatchAgain()
137
162
  {
138
163
  if (!IsActive) return;
164
+ RestoreAudioAfterSeek();
139
165
  Progress01 = 0f;
140
166
  PlaybackSpeed = 1f;
141
167
  UserPlaybackSpeed = 1f;
142
168
  IsSeeking = false;
143
169
  SeekTarget01 = 0f;
144
170
  _seekPlaybackReady = false;
171
+ _watchAgainAutoStartPending = true;
145
172
  HasEnded = false;
146
173
  }
147
174
 
175
+ public static void SetGameplayStartProgress(float progress)
176
+ {
177
+ if (!IsActive) return;
178
+ GameplayStartProgress01 = Math.Max(0f, Math.Min(1f, progress));
179
+ }
180
+
181
+ public static bool ConsumeWatchAgainAutoStart()
182
+ {
183
+ bool pending = _watchAgainAutoStartPending;
184
+ _watchAgainAutoStartPending = false;
185
+ return pending;
186
+ }
187
+
148
188
  public static void Clear()
149
189
  {
190
+ RestoreAudioAfterSeek();
150
191
  IsActive = false;
151
192
  PlayerName = string.Empty;
152
193
  AvatarUrl = string.Empty;
@@ -160,7 +201,9 @@ namespace Hyper.Internal
160
201
  IsSeeking = false;
161
202
  SeekTarget01 = 0f;
162
203
  _seekPlaybackReady = false;
204
+ _watchAgainAutoStartPending = false;
163
205
  HasEnded = false;
206
+ GameplayStartProgress01 = 0f;
164
207
  SeekCompleted = null;
165
208
  }
166
209
 
@@ -169,6 +212,23 @@ namespace Hyper.Internal
169
212
  return float.IsNaN(speed) || float.IsInfinity(speed) || speed <= 0f ? 1f : speed;
170
213
  }
171
214
 
215
+ private static void MuteAudioForSeek()
216
+ {
217
+ if (_isSeekAudioMuted) return;
218
+
219
+ _volumeBeforeSeek = AudioListener.volume;
220
+ _isSeekAudioMuted = true;
221
+ AudioListener.volume = 0f;
222
+ }
223
+
224
+ private static void RestoreAudioAfterSeek()
225
+ {
226
+ if (!_isSeekAudioMuted) return;
227
+
228
+ AudioListener.volume = _volumeBeforeSeek;
229
+ _isSeekAudioMuted = false;
230
+ }
231
+
172
232
  private static void ReleaseAvatarTexture()
173
233
  {
174
234
  if (AvatarTexture != null)
@@ -76,6 +76,10 @@ namespace Hyper.Internal
76
76
  "instead of using default restart. Listen to HyperSDK.Event.OnCustomRestart in your game manager.")]
77
77
  public bool useCustomRestartFlow = false;
78
78
 
79
+ [Tooltip("Use the game-owned custom restart flow for replay Watch Again and seeking. " +
80
+ "Leave disabled to rebuild the game through HyperLoader for deterministic replay state.")]
81
+ public bool useCustomReplayRestartFlow = false;
82
+
79
83
  #endregion
80
84
 
81
85
  #region Editor Testing
@@ -15,12 +15,15 @@ namespace Hyper.Internal
15
15
  [SerializeField] private Slider progressSlider;
16
16
  [SerializeField] private TextMeshProUGUI replayLabel;
17
17
  [SerializeField] private RawImage playerAvatar;
18
- [SerializeField] private Button exitButton;
18
+ [SerializeField] private Button pauseButton;
19
19
  [SerializeField] private Button playbackSpeedButton;
20
20
  [SerializeField] private TextMeshProUGUI playbackSpeedLabel;
21
21
  [SerializeField] private float[] playbackSpeeds = { 1.5f, 2f, 3f, 4f, 5f, 1f };
22
- [SerializeField, Min(1f)] private float seekPlaybackSpeed = 30f;
22
+ [SerializeField, Min(1f)] private float seekPlaybackSpeed = 10f;
23
23
  [SerializeField, Range(0.25f, 1f)] private float seekSnapshotScale = 0.55f;
24
+ [SerializeField, Range(0f, 0.5f)] private float seekPreparationProgress = 0.2f;
25
+ [SerializeField, Range(0f, 0.15f)] private float seekPreparationProgressVariation = 0.05f;
26
+ [SerializeField, Min(0.1f)] private float seekPreparationDuration = 3f;
24
27
  [SerializeField] private GameObject seekLoadingOverlay;
25
28
  [SerializeField] private RawImage seekSnapshotImage;
26
29
 
@@ -32,6 +35,12 @@ namespace Hyper.Internal
32
35
  private Texture2D seekSnapshotTexture;
33
36
  private Image seekOverlayBackground;
34
37
  private Color seekOverlayBackgroundColor;
38
+ private ReplaySeekOverlay seekOverlay;
39
+ private static readonly System.Random SeekProgressRandom = new System.Random();
40
+ private static float displayedSeekProgress;
41
+ private static float firstSeekPreparationTarget;
42
+ private static float secondSeekPreparationTarget;
43
+ private static float seekPreparationCeiling;
35
44
 
36
45
  private void Awake()
37
46
  {
@@ -41,7 +50,7 @@ namespace Hyper.Internal
41
50
  ConfigureSeekSnapshot();
42
51
  ApplyInitialPlaybackSpeed();
43
52
  ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
44
- if (exitButton != null) exitButton.onClick.AddListener(ExitReplay);
53
+ if (pauseButton != null) pauseButton.onClick.AddListener(PauseReplay);
45
54
  if (playbackSpeedButton != null) playbackSpeedButton.onClick.AddListener(CyclePlaybackSpeed);
46
55
  }
47
56
 
@@ -79,6 +88,11 @@ namespace Hyper.Internal
79
88
  SetSeekingPresentation(isSeeking);
80
89
  }
81
90
 
91
+ if (isSeeking)
92
+ {
93
+ UpdateSeekProgress(true);
94
+ }
95
+
82
96
  if (progressSlider != null && (seekInput == null || !seekInput.IsDragging))
83
97
  {
84
98
  float displayedProgress = isCapturingSeek
@@ -94,7 +108,7 @@ namespace Hyper.Internal
94
108
 
95
109
  private void OnDestroy()
96
110
  {
97
- if (exitButton != null) exitButton.onClick.RemoveListener(ExitReplay);
111
+ if (pauseButton != null) pauseButton.onClick.RemoveListener(PauseReplay);
98
112
  if (playbackSpeedButton != null) playbackSpeedButton.onClick.RemoveListener(CyclePlaybackSpeed);
99
113
  ReplaySessionState.SeekCompleted -= RestorePlaybackAfterSeek;
100
114
  seekInput?.Clear();
@@ -124,7 +138,7 @@ namespace Hyper.Internal
124
138
 
125
139
  internal void SetReplayEnded()
126
140
  {
127
- if (exitButton != null) exitButton.interactable = false;
141
+ if (pauseButton != null) pauseButton.interactable = false;
128
142
  if (playbackSpeedButton != null) playbackSpeedButton.interactable = false;
129
143
  if (progressSlider != null) progressSlider.interactable = false;
130
144
  if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(false);
@@ -158,10 +172,9 @@ namespace Hyper.Internal
158
172
  SetSeekingPresentation(ReplaySessionState.IsSeeking);
159
173
  }
160
174
 
161
- private void ExitReplay()
175
+ private void PauseReplay()
162
176
  {
163
- if (exitButton != null) exitButton.interactable = false;
164
- HyperRuntime.GameManager?.ExitReplayEarly();
177
+ HyperRuntime.GameManager?.PauseReplay();
165
178
  }
166
179
 
167
180
  private void CyclePlaybackSpeed()
@@ -190,6 +203,7 @@ namespace Hyper.Internal
190
203
 
191
204
  float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
192
205
  float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
206
+ InitializeSeekProgressPresentation();
193
207
  StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
194
208
  }
195
209
 
@@ -239,7 +253,6 @@ namespace Hyper.Internal
239
253
  private void RestorePlaybackAfterSeek(float playbackSpeed)
240
254
  {
241
255
  float speed = SanitizePlaybackSpeed(playbackSpeed);
242
- Time.timeScale = speed;
243
256
  AlignPlaybackSpeedIndex(speed);
244
257
  UpdatePlaybackSpeedLabel(speed);
245
258
  SetSeekingPresentation(false);
@@ -261,6 +274,7 @@ namespace Hyper.Internal
261
274
  private void SetSeekingPresentation(bool isSeeking)
262
275
  {
263
276
  ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
277
+ UpdateSeekProgress(isSeeking);
264
278
  if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
265
279
  if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
266
280
  if (playbackSpeedButton != null)
@@ -268,7 +282,7 @@ namespace Hyper.Internal
268
282
  playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
269
283
  }
270
284
 
271
- if (exitButton != null) exitButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
285
+ if (pauseButton != null) pauseButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
272
286
  if (!isSeeking) ClearSeekSnapshot();
273
287
  }
274
288
 
@@ -341,7 +355,12 @@ namespace Hyper.Internal
341
355
  private void AutoBindReferences()
342
356
  {
343
357
  if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
344
- if (exitButton == null) exitButton = GetComponentInChildren<Button>(true);
358
+
359
+ Button[] buttons = GetComponentsInChildren<Button>(true);
360
+ for (int i = 0; i < buttons.Length; i++)
361
+ {
362
+ if (pauseButton == null && buttons[i].name == "PauseButton") pauseButton = buttons[i];
363
+ }
345
364
 
346
365
  TextMeshProUGUI[] labels = GetComponentsInChildren<TextMeshProUGUI>(true);
347
366
  for (int i = 0; replayLabel == null && i < labels.Length; i++)
@@ -400,10 +419,114 @@ namespace Hyper.Internal
400
419
  seekLoadingOverlay.transform.SetAsFirstSibling();
401
420
  seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
402
421
  if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
422
+ seekOverlay = seekLoadingOverlay.GetComponent<ReplaySeekOverlay>();
403
423
  seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
404
424
  wasSeeking = ReplaySessionState.IsSeeking;
405
425
  }
406
426
 
427
+ private void UpdateSeekProgress(bool isSeeking)
428
+ {
429
+ if (seekOverlay == null) return;
430
+
431
+ float progress;
432
+ if (!isSeeking)
433
+ {
434
+ progress = 1f;
435
+ }
436
+ else if (isCapturingSeek || ReplaySessionState.IsSeekRestartPending)
437
+ {
438
+ AdvanceSeekPreparationProgress();
439
+ progress = displayedSeekProgress;
440
+ }
441
+ else
442
+ {
443
+ EnsureSeekProgressPresentation();
444
+ float target = ReplaySessionState.SeekTarget01;
445
+ float reconstructionProgress = target <= Mathf.Epsilon
446
+ ? 1f
447
+ : ReplaySessionState.Progress01 / target;
448
+ float realProgress = Mathf.Lerp(
449
+ seekPreparationCeiling,
450
+ 1f,
451
+ Mathf.Clamp01(reconstructionProgress));
452
+ AdvanceSeekReconstructionProgress(realProgress);
453
+ progress = displayedSeekProgress;
454
+ }
455
+
456
+ seekOverlay.SetProgress(progress);
457
+ }
458
+
459
+ private void AdvanceSeekPreparationProgress()
460
+ {
461
+ EnsureSeekProgressPresentation();
462
+ float duration = Mathf.Max(0.1f, seekPreparationDuration);
463
+ float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
464
+
465
+ if (displayedSeekProgress < firstSeekPreparationTarget)
466
+ {
467
+ float speed = firstSeekPreparationTarget / (duration * 0.35f);
468
+ displayedSeekProgress = Mathf.MoveTowards(
469
+ displayedSeekProgress,
470
+ firstSeekPreparationTarget,
471
+ speed * deltaTime);
472
+ return;
473
+ }
474
+
475
+ if (displayedSeekProgress < secondSeekPreparationTarget)
476
+ {
477
+ float speed = (secondSeekPreparationTarget - firstSeekPreparationTarget) / (duration * 0.2f);
478
+ displayedSeekProgress = Mathf.MoveTowards(
479
+ displayedSeekProgress,
480
+ secondSeekPreparationTarget,
481
+ speed * deltaTime);
482
+ return;
483
+ }
484
+
485
+ float crawlResponse = 1f - Mathf.Exp(-deltaTime / (duration * 0.65f));
486
+ displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, seekPreparationCeiling, crawlResponse);
487
+ }
488
+
489
+ private void InitializeSeekProgressPresentation()
490
+ {
491
+ float center = Mathf.Clamp01(seekPreparationProgress);
492
+ float variation = Mathf.Max(0f, seekPreparationProgressVariation);
493
+ float minimum = Mathf.Clamp01(center - variation);
494
+ float maximum = Mathf.Clamp01(center + variation);
495
+
496
+ displayedSeekProgress = 0f;
497
+ seekPreparationCeiling = RandomRange(minimum, maximum);
498
+ firstSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.25f, 0.35f);
499
+ secondSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.62f, 0.76f);
500
+ }
501
+
502
+ private void EnsureSeekProgressPresentation()
503
+ {
504
+ if (seekPreparationCeiling > 0f) return;
505
+ InitializeSeekProgressPresentation();
506
+ }
507
+
508
+ private static float RandomRange(float minimum, float maximum)
509
+ {
510
+ if (maximum <= minimum) return minimum;
511
+ return minimum + ((float)SeekProgressRandom.NextDouble() * (maximum - minimum));
512
+ }
513
+
514
+ private void AdvanceSeekReconstructionProgress(float realProgress)
515
+ {
516
+ float target = Mathf.Max(displayedSeekProgress, realProgress);
517
+ if (target >= 0.999f)
518
+ {
519
+ displayedSeekProgress = 1f;
520
+ return;
521
+ }
522
+
523
+ float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
524
+ float gap = target - displayedSeekProgress;
525
+ float responseSpeed = Mathf.Lerp(5f, 14f, Mathf.Clamp01(gap * 4f));
526
+ float response = 1f - Mathf.Exp(-responseSpeed * deltaTime);
527
+ displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, target, response);
528
+ }
529
+
407
530
  private void ConfigureSeekSnapshot()
408
531
  {
409
532
  if (seekSnapshotImage == null)
@@ -0,0 +1,18 @@
1
+ using TMPro;
2
+ using UnityEngine;
3
+
4
+ namespace Hyper.Internal
5
+ {
6
+ internal sealed class ReplaySeekOverlay : MonoBehaviour
7
+ {
8
+ [SerializeField] private TextMeshProUGUI progressText;
9
+
10
+ internal void SetProgress(float progress01)
11
+ {
12
+ if (progressText == null) return;
13
+
14
+ int percentage = Mathf.RoundToInt(Mathf.Clamp01(progress01) * 100f);
15
+ progressText.text = $"{percentage}%";
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: a7d0eb352f3c4d64a687b8c4509e1fa2
@@ -0,0 +1,145 @@
1
+ using TMPro;
2
+ using UnityEngine;
3
+ using UnityEngine.UI;
4
+
5
+ namespace Hyper.Internal
6
+ {
7
+ internal sealed class ReplayPausedModal : MonoBehaviour
8
+ {
9
+ [SerializeField] private TextMeshProUGUI scoreValue;
10
+ [SerializeField] private TextMeshProUGUI scoreOwnerName;
11
+ [SerializeField] private TextMeshProUGUI soundText;
12
+ [SerializeField] private Sprite soundIconOn;
13
+ [SerializeField] private Sprite soundIconOff;
14
+ [SerializeField] private Image soundIcon;
15
+ [SerializeField] private GameObject restartButton;
16
+ [SerializeField] private Button resumeButton;
17
+ [SerializeField] private Button exitButton;
18
+ [SerializeField] private Button soundButton;
19
+ [SerializeField] private Button restartActionButton;
20
+ [SerializeField] private GameObject exitLoadingCircle;
21
+
22
+ private bool handled;
23
+
24
+ private void Awake()
25
+ {
26
+ AutoBindButtons();
27
+ resumeButton?.onClick.AddListener(ResumeReplay);
28
+ exitButton?.onClick.AddListener(ExitReplay);
29
+ soundButton?.onClick.AddListener(ToggleSound);
30
+ restartActionButton?.onClick.AddListener(RestartReplay);
31
+ }
32
+
33
+ private void OnEnable()
34
+ {
35
+ handled = false;
36
+ SetButtonsInteractable(true);
37
+ exitLoadingCircle?.SetActive(false);
38
+
39
+ if (scoreValue != null)
40
+ {
41
+ scoreValue.text = (HyperRuntime.DataManager?.CurrentScore ?? 0).ToString("N0");
42
+ }
43
+
44
+ if (scoreOwnerName != null)
45
+ {
46
+ scoreOwnerName.text = FormatScoreOwner(ReplaySessionState.PlayerName);
47
+ }
48
+
49
+ UpdateSoundPresentation();
50
+ }
51
+
52
+ private void OnDestroy()
53
+ {
54
+ resumeButton?.onClick.RemoveListener(ResumeReplay);
55
+ exitButton?.onClick.RemoveListener(ExitReplay);
56
+ soundButton?.onClick.RemoveListener(ToggleSound);
57
+ restartActionButton?.onClick.RemoveListener(RestartReplay);
58
+ }
59
+
60
+ public void ResumeReplay()
61
+ {
62
+ if (handled) return;
63
+ handled = true;
64
+ SetButtonsInteractable(false);
65
+ HyperRuntime.GameManager?.ResumeReplay();
66
+ }
67
+
68
+ public void ExitReplay()
69
+ {
70
+ if (handled) return;
71
+ handled = true;
72
+ SetButtonsInteractable(false);
73
+ exitLoadingCircle?.SetActive(true);
74
+ HyperRuntime.GameManager?.ExitReplayEarly();
75
+ }
76
+
77
+ public void ToggleSound()
78
+ {
79
+ if (handled || HyperRuntime.DataManager == null) return;
80
+
81
+ int sound = HyperRuntime.DataManager.GetSound() == 1 ? 0 : 1;
82
+ HyperRuntime.DataManager.SetSound(sound);
83
+ UpdateSoundPresentation();
84
+ }
85
+
86
+ public void RestartReplay()
87
+ {
88
+ if (handled) return;
89
+ handled = true;
90
+ SetButtonsInteractable(false);
91
+ HyperRuntime.RestartReplaySession();
92
+ }
93
+
94
+ internal static string FormatScoreOwner(string playerName)
95
+ {
96
+ string normalizedName = (playerName ?? string.Empty).Trim().TrimStart('@');
97
+ return string.IsNullOrEmpty(normalizedName)
98
+ ? "PLAYER'S SCORE"
99
+ : $"@{normalizedName.ToUpperInvariant()}'S SCORE";
100
+ }
101
+
102
+ private void UpdateSoundPresentation()
103
+ {
104
+ bool isSoundOn = HyperRuntime.DataManager == null || HyperRuntime.DataManager.GetSound() == 1;
105
+ if (soundText != null) soundText.text = isSoundOn ? "Sound On" : "Sound Off";
106
+ if (soundIcon != null) soundIcon.sprite = isSoundOn ? soundIconOn : soundIconOff;
107
+ }
108
+
109
+ private void AutoBindButtons()
110
+ {
111
+ Button[] buttons = GetComponentsInChildren<Button>(true);
112
+ for (int i = 0; i < buttons.Length; i++)
113
+ {
114
+ switch (buttons[i].name)
115
+ {
116
+ case "Resume":
117
+ if (resumeButton == null) resumeButton = buttons[i];
118
+ break;
119
+ case "Exit":
120
+ if (exitButton == null) exitButton = buttons[i];
121
+ break;
122
+ case "SoundButton":
123
+ if (soundButton == null) soundButton = buttons[i];
124
+ break;
125
+ case "RestartButton":
126
+ if (restartActionButton == null) restartActionButton = buttons[i];
127
+ break;
128
+ }
129
+ }
130
+
131
+ if (restartButton == null && restartActionButton != null)
132
+ {
133
+ restartButton = restartActionButton.gameObject;
134
+ }
135
+ }
136
+
137
+ private void SetButtonsInteractable(bool interactable)
138
+ {
139
+ if (resumeButton != null) resumeButton.interactable = interactable;
140
+ if (exitButton != null) exitButton.interactable = interactable;
141
+ if (soundButton != null) soundButton.interactable = interactable;
142
+ if (restartActionButton != null) restartActionButton.interactable = interactable;
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: b26c59e3565391f4daa22e917b6156b4
@@ -59,6 +59,7 @@ namespace Hyper.Internal
59
59
  {
60
60
  float elapsed = 0f;
61
61
  float delay = Mathf.Max(0f, replayAutoStartDelay);
62
+ if (ReplaySessionState.ConsumeWatchAgainAutoStart()) delay *= 0.5f;
62
63
  while (elapsed < delay)
63
64
  {
64
65
  if (Application.isFocused) elapsed += Time.unscaledDeltaTime;