app.hypergames.hypersdk 1.8.19 → 1.8.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 (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/HyperLoader.unity +3 -3
  3. package/Runtime/Internal/Managers/DataManager.cs +38 -2
  4. package/Runtime/Internal/Managers/GameManager.cs +34 -23
  5. package/Runtime/Internal/UI/Animation/SlideUp.cs +6 -1
  6. package/Runtime/Internal/UI/Components/RoundedRawImage.cs +146 -0
  7. package/Runtime/Internal/UI/Components/RoundedRawImage.cs.meta +11 -0
  8. package/Runtime/Internal/UI/Modals/GamePausedModal.cs +2 -0
  9. package/Runtime/Internal/UI/Modals/TutorialButton.cs +10 -0
  10. package/Runtime/Internal/UI/Modals/TutorialLoaderModal.cs +31 -4
  11. package/Runtime/Internal/UI/Modals/TutorialModal.cs +814 -47
  12. package/Runtime/Plugins/WebGL/HyperTutorialCachePlugin.jslib +35 -53
  13. package/Runtime/Resources/Materials/RoundedRawImage.mat +65 -0
  14. package/Runtime/Resources/Materials/RoundedRawImage.mat.meta +8 -0
  15. package/Runtime/Resources/Materials.meta +8 -0
  16. package/Runtime/Resources/RenderTextures/TutorialVideoTexture P.renderTexture +3 -3
  17. package/Runtime/Resources/Shaders/RoundedRawImage.shader +142 -0
  18. package/Runtime/Resources/Shaders/RoundedRawImage.shader.meta +10 -0
  19. package/Runtime/Resources/Shaders/RoundedStrip.shader +152 -0
  20. package/Runtime/Resources/Shaders/RoundedStrip.shader.meta +10 -0
  21. package/Runtime/Resources/Shaders.meta +8 -0
  22. package/Runtime/Resources/Sprites/Frames/Square.png +0 -0
  23. package/Runtime/Resources/Sprites/Frames/Square.png.meta +186 -0
  24. package/Runtime/Resources/Sprites/TutorialModal/Button_Back.png.meta +1 -1
  25. package/Runtime/Resources/Sprites/TutorialModal/Button_Next.png.meta +1 -1
  26. package/Runtime/Resources/Sprites/TutorialModal/Knob.png.meta +1 -1
  27. package/Runtime/Resources/Sprites/TutorialModal/Tutorial_Background_Portrait.png.meta +3 -3
  28. package/Runtime/Resources/UIPrefabs/GameOverModal_Battle.prefab +1 -1
  29. package/Runtime/Resources/UIPrefabs/GameOverModal_Practice.prefab +1 -1
  30. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  31. package/Runtime/Resources/UIPrefabs/IndicatorCircle.prefab +2 -2
  32. package/Runtime/Resources/UIPrefabs/TutorialLandscapeModal.prefab +1614 -1184
  33. package/Runtime/Resources/UIPrefabs/TutorialLandscapeModal.prefab.meta +1 -1
  34. package/Runtime/Resources/UIPrefabs/TutorialPortraitModal.prefab +1409 -948
  35. package/Runtime/Shared/DTOs/TutorialData.cs +18 -1
  36. package/package.json +1 -1
@@ -6,13 +6,24 @@ using UnityEngine;
6
6
  using UnityEngine.UI;
7
7
  using UnityEngine.Video;
8
8
  using UnityEngine.Networking;
9
+ using Hyper.Internal.Animation;
9
10
  using Hyper.Internal.Managers;
10
11
  using Hyper.Shared;
12
+ using TMPro;
11
13
 
12
14
  namespace Hyper.Internal
13
15
  {
14
16
  internal class TutorialModal : MonoBehaviour
15
17
  {
18
+ // Keep the seek wiring available while the tutorial video uses a passive progress bar.
19
+ private const bool VideoSeekingEnabled = false;
20
+ private const float LoaderFadeOutDelay = 2.15f;
21
+ private const float SlideInDuration = 0.33f;
22
+ private const float ContentFadeInDuration = 0.28f;
23
+ private const float ContentFadeOutDuration = 0.18f;
24
+ private const float MediaFadeInDuration = 0.18f;
25
+ private const float VideoRevealDelay = 0.05f;
26
+ private const float HiddenPreloadPlayDuration = 0.75f;
16
27
 
17
28
  #if UNITY_WEBGL && !UNITY_EDITOR
18
29
  [DllImport("__Internal")]
@@ -23,20 +34,25 @@ namespace Hyper.Internal
23
34
  private CanvasGroup tutorialCG;
24
35
 
25
36
  [SerializeField] TutorialLoaderModal tutorialLoaderModal;
37
+ [SerializeField] private CanvasGroup tutorialContentGroup;
26
38
 
27
39
  [Header("Display Components")]
28
40
  [SerializeField] private RawImage tutorialDisplayP;
29
41
  [SerializeField] private RawImage tutorialDisplayL;
30
42
  [SerializeField] private VideoPlayer videoPlayer;
43
+ [SerializeField] private RawImage tutorialImageDisplay;
44
+ [SerializeField] private TMP_Text descriptionText;
45
+
46
+ #if UNITY_EDITOR
47
+ [Header("Editor Tutorial Test")]
48
+ [SerializeField] private EditorTutorialCard[] editorTutorialCards;
49
+ [SerializeField] private Vector2 editorLoadDelayRange = new Vector2(1f, 2f);
50
+ #endif
31
51
 
32
52
  [Header("Video Textures")]
33
53
  [SerializeField] private RenderTexture tutorialVideoTextureP;
34
54
  [SerializeField] private RenderTexture tutorialVideoTextureL;
35
55
 
36
- [Header("Aspect Ratio Fitters")]
37
- [SerializeField] private AspectRatioFitter aspectRatioFitterP;
38
- [SerializeField] private AspectRatioFitter aspectRatioFitterL;
39
-
40
56
  [Header("UI Controls")]
41
57
  [SerializeField] private Slider timeSlider;
42
58
  [SerializeField] private Button backButton;
@@ -46,16 +62,39 @@ namespace Hyper.Internal
46
62
  [SerializeField] private Transform indicatorParent;
47
63
  [SerializeField] private GameObject indicatorPrefab;
48
64
  private Sprite[] tutorialImages;
65
+ private readonly List<TutorialCardData> tutorialCards = new List<TutorialCardData>();
49
66
  private RawImage currentDisplay;
50
67
  private int currentImageIndex = 0;
68
+ private int currentCardIndex = 0;
51
69
  private bool isDraggingSlider = false;
52
70
  private float cachedVideoLength = 0f;
53
71
  private Coroutine updateVideoTimeCo = null;
72
+ private Coroutine editorTutorialLoadCo = null;
73
+ private Coroutine hiddenPreloadCloseCo = null;
74
+ private Coroutine mediaDisplayFadeCo = null;
54
75
  private TutorialData tutorialData;
76
+ private bool isClosing;
77
+ private bool hasInvokedTutorialLoadComplete;
78
+ private bool hasPreparedTutorialContent;
79
+ private bool isDirectContentOpen;
80
+ private bool contentRevealScheduled;
81
+ private HyperTween.TweenHandle contentRevealDelayTween;
55
82
 
56
83
  private EventManager _eventManager;
57
84
  private DataManager _dataManager;
58
85
 
86
+ #if UNITY_EDITOR
87
+ [Serializable]
88
+ private class EditorTutorialCard
89
+ {
90
+ public TutorialType type = TutorialType.video;
91
+ public VideoClip videoClip;
92
+ public Sprite image;
93
+ [TextArea] public string description;
94
+ public int position;
95
+ }
96
+ #endif
97
+
59
98
  private void Awake()
60
99
  {
61
100
  _eventManager = HyperRuntime.EventManager;
@@ -63,31 +102,32 @@ namespace Hyper.Internal
63
102
 
64
103
  slideUp = GetComponent<SlideUp>();
65
104
  tutorialCG = GetComponent<CanvasGroup>();
105
+ Hide();
66
106
 
67
107
  tutorialLoaderModal.gameObject.SetActive(true);
68
108
 
69
109
  _eventManager.OnTutorialResourcesReferenced += ShowTutorialBasedOnType;
110
+ _eventManager.OnTutorialLoadingComplete += HandleTutorialLoadingComplete;
70
111
  }
71
112
 
72
113
  void OnEnable()
73
114
  {
74
115
  SetupSliderControls();
75
116
 
117
+ if (hasPreparedTutorialContent) return;
118
+
76
119
  if (_dataManager.HasReferencedTutorialResources)
77
120
  {
78
- // ShowTutorialBasedOnType();
79
- if (tutorialData != null && tutorialData.type == TutorialType.video)
80
- {
81
- PlayCurrentVideo();
82
- }
83
- else if(tutorialData != null && tutorialData.type == TutorialType.images)
84
- {
85
- ShowImagesTutorial(tutorialData);
86
- }
121
+ ShowTutorialData(_dataManager.TutorialData);
87
122
  }
88
123
  else
89
124
  {
90
- _eventManager.InvokeTutorialFetchFailedEvent();
125
+ #if UNITY_EDITOR
126
+ if (TryStartEditorTutorialLoad())
127
+ {
128
+ return;
129
+ }
130
+ #endif
91
131
  }
92
132
  }
93
133
 
@@ -99,11 +139,47 @@ namespace Hyper.Internal
99
139
 
100
140
  public void Show()
101
141
  {
142
+ isClosing = false;
143
+ isDirectContentOpen = false;
144
+ contentRevealScheduled = false;
145
+ CancelHiddenPreloadClose();
146
+ HideTutorialContent();
102
147
  tutorialCG.alpha = 1f;
103
148
  tutorialCG.interactable = true;
104
149
  tutorialCG.blocksRaycasts = true;
105
150
 
106
- slideUp.SlideUpAnimation();
151
+ slideUp.SlideUpAnimation(SlideInDuration);
152
+
153
+ if (CanShowPreparedTutorialImmediately())
154
+ {
155
+ ShowPreparedTutorialImmediately();
156
+ return;
157
+ }
158
+
159
+ if (tutorialLoaderModal != null)
160
+ {
161
+ tutorialLoaderModal.gameObject.SetActive(true);
162
+ }
163
+
164
+ if (_dataManager != null && _dataManager.HasReferencedTutorialResources)
165
+ {
166
+ ShowTutorialData(_dataManager.TutorialData);
167
+ }
168
+ #if UNITY_EDITOR
169
+ else
170
+ {
171
+ if (!TryStartEditorTutorialLoad())
172
+ {
173
+ _eventManager.InvokeTutorialFetchFailedEvent();
174
+ }
175
+ }
176
+ #endif
177
+ #if !UNITY_EDITOR
178
+ else
179
+ {
180
+ HyperRuntime.BackendManager.GetTutorial(() => { });
181
+ }
182
+ #endif
107
183
  }
108
184
 
109
185
  private void Hide()
@@ -111,6 +187,143 @@ namespace Hyper.Internal
111
187
  tutorialCG.alpha = 0f;
112
188
  tutorialCG.interactable = false;
113
189
  tutorialCG.blocksRaycasts = false;
190
+ // HideTutorialContent();
191
+ }
192
+
193
+ private bool CanShowPreparedTutorialImmediately()
194
+ {
195
+ return hasPreparedTutorialContent && tutorialData != null;
196
+ }
197
+
198
+ private void ShowPreparedTutorialImmediately()
199
+ {
200
+ isDirectContentOpen = true;
201
+
202
+ if (tutorialLoaderModal != null)
203
+ {
204
+ tutorialLoaderModal.gameObject.SetActive(false);
205
+ }
206
+
207
+ FadeTutorialContentIn(SlideInDuration);
208
+ ShowTutorialData(tutorialData);
209
+ }
210
+
211
+ private void ShowTutorialContentImmediate()
212
+ {
213
+ if (tutorialContentGroup == null) return;
214
+
215
+ tutorialContentGroup.DOKill();
216
+ tutorialContentGroup.gameObject.SetActive(true);
217
+ tutorialContentGroup.alpha = 1f;
218
+ tutorialContentGroup.interactable = true;
219
+ tutorialContentGroup.blocksRaycasts = true;
220
+ }
221
+
222
+ private void FadeTutorialContentIn(float delay = 0f)
223
+ {
224
+ if (tutorialContentGroup == null) return;
225
+
226
+ contentRevealScheduled = true;
227
+ contentRevealDelayTween?.Kill();
228
+ contentRevealDelayTween = null;
229
+ tutorialContentGroup.DOKill();
230
+ tutorialContentGroup.gameObject.SetActive(true);
231
+ tutorialContentGroup.alpha = 0f;
232
+ tutorialContentGroup.interactable = false;
233
+ tutorialContentGroup.blocksRaycasts = false;
234
+
235
+ if (delay > 0f)
236
+ {
237
+ contentRevealDelayTween = HyperTween.DelayedCall(delay, StartTutorialContentFade)
238
+ .SetUpdate(true);
239
+ return;
240
+ }
241
+
242
+ StartTutorialContentFade();
243
+ }
244
+
245
+ private void StartTutorialContentFade()
246
+ {
247
+ if (tutorialContentGroup == null) return;
248
+
249
+ contentRevealDelayTween = null;
250
+ tutorialContentGroup.DOKill();
251
+ tutorialContentGroup.gameObject.SetActive(true);
252
+ tutorialContentGroup.interactable = true;
253
+ tutorialContentGroup.blocksRaycasts = true;
254
+ var fadeTween = tutorialContentGroup.DOFade(1f, ContentFadeInDuration)
255
+ .SetEase(HyperTween.Ease.OutQuad)
256
+ .SetUpdate(true);
257
+ }
258
+
259
+ private void HandleTutorialLoadingComplete()
260
+ {
261
+ if (!isActiveAndEnabled || isClosing) return;
262
+
263
+ hasPreparedTutorialContent = true;
264
+
265
+ if (IsHiddenPreload())
266
+ {
267
+ ScheduleHiddenPreloadClose();
268
+ return;
269
+ }
270
+
271
+ if (isDirectContentOpen)
272
+ {
273
+ if (!contentRevealScheduled)
274
+ {
275
+ FadeTutorialContentIn(SlideInDuration);
276
+ }
277
+ return;
278
+ }
279
+
280
+ if (tutorialContentGroup == null) return;
281
+ if (contentRevealScheduled) return;
282
+
283
+ FadeTutorialContentIn(LoaderFadeOutDelay);
284
+ }
285
+
286
+ private void HideTutorialContent()
287
+ {
288
+ if (tutorialContentGroup == null) return;
289
+
290
+ contentRevealScheduled = false;
291
+ contentRevealDelayTween?.Kill();
292
+ contentRevealDelayTween = null;
293
+ tutorialContentGroup.DOKill();
294
+ tutorialContentGroup.alpha = 0f;
295
+ tutorialContentGroup.interactable = false;
296
+ tutorialContentGroup.blocksRaycasts = false;
297
+ }
298
+
299
+ private bool IsHiddenPreload()
300
+ {
301
+ return tutorialCG != null && tutorialCG.alpha <= 0.001f && !tutorialCG.blocksRaycasts && !tutorialCG.interactable;
302
+ }
303
+
304
+ private void ScheduleHiddenPreloadClose()
305
+ {
306
+ CancelHiddenPreloadClose();
307
+ hiddenPreloadCloseCo = StartCoroutine(CloseHiddenPreloadAfterPreview());
308
+ }
309
+
310
+ private IEnumerator CloseHiddenPreloadAfterPreview()
311
+ {
312
+ yield return new WaitForSecondsRealtime(HiddenPreloadPlayDuration);
313
+
314
+ hiddenPreloadCloseCo = null;
315
+
316
+ if (!isActiveAndEnabled || isClosing || !IsHiddenPreload()) yield break;
317
+
318
+ FinishClose();
319
+ }
320
+
321
+ private void CancelHiddenPreloadClose()
322
+ {
323
+ if (hiddenPreloadCloseCo == null) return;
324
+
325
+ StopCoroutine(hiddenPreloadCloseCo);
326
+ hiddenPreloadCloseCo = null;
114
327
  }
115
328
 
116
329
  private void SetupSliderControls()
@@ -119,19 +332,25 @@ namespace Hyper.Internal
119
332
 
120
333
  timeSlider.minValue = 0f;
121
334
  timeSlider.wholeNumbers = false;
122
- timeSlider.onValueChanged.AddListener(OnTimeSliderChanged);
335
+ timeSlider.interactable = VideoSeekingEnabled;
336
+ timeSlider.onValueChanged.RemoveListener(OnTimeSliderChanged);
123
337
 
124
- // Add event triggers for drag detection
125
338
  var eventTrigger = timeSlider.gameObject.GetComponent<UnityEngine.EventSystems.EventTrigger>();
339
+ if (eventTrigger != null)
340
+ {
341
+ eventTrigger.triggers.Clear();
342
+ }
343
+
344
+ if (!VideoSeekingEnabled) return;
345
+
346
+ timeSlider.onValueChanged.AddListener(OnTimeSliderChanged);
347
+
348
+ eventTrigger = timeSlider.gameObject.GetComponent<UnityEngine.EventSystems.EventTrigger>();
126
349
  if (eventTrigger == null)
127
350
  {
128
351
  eventTrigger = timeSlider.gameObject.AddComponent<UnityEngine.EventSystems.EventTrigger>();
129
352
  }
130
353
 
131
- // Clear existing triggers
132
- eventTrigger.triggers.Clear();
133
-
134
- // Pointer down (begin drag)
135
354
  var pointerDownEntry = new UnityEngine.EventSystems.EventTrigger.Entry
136
355
  {
137
356
  eventID = UnityEngine.EventSystems.EventTriggerType.PointerDown
@@ -139,7 +358,6 @@ namespace Hyper.Internal
139
358
  pointerDownEntry.callback.AddListener((_) => OnSliderDragStart());
140
359
  eventTrigger.triggers.Add(pointerDownEntry);
141
360
 
142
- // Pointer up (end drag)
143
361
  var pointerUpEntry = new UnityEngine.EventSystems.EventTrigger.Entry
144
362
  {
145
363
  eventID = UnityEngine.EventSystems.EventTriggerType.PointerUp
@@ -150,11 +368,15 @@ namespace Hyper.Internal
150
368
 
151
369
  private void OnSliderDragStart()
152
370
  {
371
+ if (!VideoSeekingEnabled) return;
372
+
153
373
  isDraggingSlider = true;
154
374
  }
155
375
 
156
376
  private void OnSliderDragEnd()
157
377
  {
378
+ if (!VideoSeekingEnabled) return;
379
+
158
380
  isDraggingSlider = false;
159
381
 
160
382
  if (videoPlayer != null && videoPlayer.isPrepared)
@@ -171,8 +393,24 @@ namespace Hyper.Internal
171
393
  {
172
394
  //Debug.Log("Showing tutorial based on type");
173
395
  tutorialData = _dataManager.TutorialData;
396
+ ShowTutorialData(tutorialData);
397
+ }
174
398
 
175
- if (tutorialData.type == TutorialType.video)
399
+ private void ShowTutorialData(TutorialData data)
400
+ {
401
+ if (data == null)
402
+ {
403
+ _eventManager.InvokeTutorialFetchFailedEvent();
404
+ return;
405
+ }
406
+
407
+ tutorialData = data;
408
+
409
+ if (tutorialData.cards != null && tutorialData.cards.Length > 0)
410
+ {
411
+ ShowMixedTutorial(tutorialData);
412
+ }
413
+ else if (tutorialData.type == TutorialType.video)
176
414
  {
177
415
  ShowVideoTutorial(tutorialData);
178
416
  }
@@ -187,6 +425,74 @@ namespace Hyper.Internal
187
425
  }
188
426
  }
189
427
 
428
+ #if UNITY_EDITOR
429
+ internal bool TryStartEditorTutorialLoad()
430
+ {
431
+ TutorialData editorData = CreateEditorTutorialData();
432
+ if (editorData == null) return false;
433
+
434
+ if (editorTutorialLoadCo != null)
435
+ {
436
+ StopCoroutine(editorTutorialLoadCo);
437
+ }
438
+
439
+ editorTutorialLoadCo = StartCoroutine(LoadEditorTutorialAfterDelay(editorData));
440
+ return true;
441
+ }
442
+
443
+ private TutorialData CreateEditorTutorialData()
444
+ {
445
+ if (editorTutorialCards == null || editorTutorialCards.Length == 0) return null;
446
+
447
+ List<TutorialCardData> editorCards = new List<TutorialCardData>();
448
+
449
+ for (int i = 0; i < editorTutorialCards.Length; i++)
450
+ {
451
+ EditorTutorialCard editorCard = editorTutorialCards[i];
452
+ if (editorCard == null || editorCard.type == TutorialType.None) continue;
453
+
454
+ if (editorCard.type == TutorialType.video && editorCard.videoClip == null) continue;
455
+ if (editorCard.type == TutorialType.images && editorCard.image == null) continue;
456
+
457
+ editorCards.Add(new TutorialCardData
458
+ {
459
+ type = editorCard.type,
460
+ videoClip = editorCard.videoClip,
461
+ image = editorCard.image,
462
+ description = editorCard.description,
463
+ position = editorCard.position > 0 ? editorCard.position : i + 1
464
+ });
465
+ }
466
+
467
+ if (editorCards.Count == 0) return null;
468
+
469
+ editorCards.Sort((a, b) => a.position.CompareTo(b.position));
470
+
471
+ return new TutorialData
472
+ {
473
+ type = editorCards[0].type,
474
+ cards = editorCards.ToArray()
475
+ };
476
+ }
477
+
478
+ private IEnumerator LoadEditorTutorialAfterDelay(TutorialData editorData)
479
+ {
480
+ tutorialLoaderModal.gameObject.SetActive(true);
481
+ tutorialDisplayP.gameObject.SetActive(false);
482
+ tutorialDisplayL.gameObject.SetActive(false);
483
+ videoPlayer.enabled = false;
484
+
485
+ float minDelay = Mathf.Max(0f, Mathf.Min(editorLoadDelayRange.x, editorLoadDelayRange.y));
486
+ float maxDelay = Mathf.Max(minDelay, Mathf.Max(editorLoadDelayRange.x, editorLoadDelayRange.y));
487
+ float delay = UnityEngine.Random.Range(minDelay, maxDelay);
488
+
489
+ yield return new WaitForSecondsRealtime(delay);
490
+
491
+ editorTutorialLoadCo = null;
492
+ ShowTutorialData(editorData);
493
+ }
494
+ #endif
495
+
190
496
  private RawImage SelectOptimalDisplay(float aspectRatio)
191
497
  {
192
498
  bool isLandscape = aspectRatio > 1.0f;
@@ -196,20 +502,393 @@ namespace Hyper.Internal
196
502
 
197
503
  if (isLandscape)
198
504
  {
199
- //Debug.Log($"Content is landscape (aspect: {aspectRatio:F2}), using landscape display");
200
505
  tutorialDisplayL.gameObject.SetActive(true);
201
506
  return tutorialDisplayL;
202
507
  }
203
508
  else
204
509
  {
205
- //Debug.Log($"Content is portrait (aspect: {aspectRatio:F2}), using portrait display");
206
510
  tutorialDisplayP.gameObject.SetActive(true);
207
511
  return tutorialDisplayP;
208
512
  }
209
513
  }
210
514
 
515
+ private void ShowMixedTutorial(TutorialData tutorialData)
516
+ {
517
+ if (tutorialData.cards == null || tutorialData.cards.Length == 0)
518
+ {
519
+ _eventManager.InvokeTutorialFetchFailedEvent();
520
+ return;
521
+ }
522
+
523
+ hasInvokedTutorialLoadComplete = false;
524
+ tutorialCards.Clear();
525
+ tutorialCards.AddRange(tutorialData.cards);
526
+ tutorialCards.Sort((a, b) => a.position.CompareTo(b.position));
527
+ currentCardIndex = 0;
528
+ currentImageIndex = 0;
529
+
530
+ if (tutorialDisplayP != null) tutorialDisplayP.gameObject.SetActive(false);
531
+ if (tutorialDisplayL != null) tutorialDisplayL.gameObject.SetActive(false);
532
+
533
+ if (TryLoadCardImagesFromUrls())
534
+ {
535
+ return;
536
+ }
537
+
538
+ SetupMixedTutorialCards();
539
+ }
540
+
541
+ private bool TryLoadCardImagesFromUrls()
542
+ {
543
+ bool hasRemoteImage = false;
544
+
545
+ for (int i = 0; i < tutorialCards.Count; i++)
546
+ {
547
+ TutorialCardData card = tutorialCards[i];
548
+ if (card.type == TutorialType.images && card.image == null && !string.IsNullOrEmpty(card.imageUrl))
549
+ {
550
+ hasRemoteImage = true;
551
+ break;
552
+ }
553
+ }
554
+
555
+ if (!hasRemoteImage) return false;
556
+
557
+ StartCoroutine(LoadCardImagesFromUrlsCoroutine(SetupMixedTutorialCards));
558
+ return true;
559
+ }
560
+
561
+ private IEnumerator LoadCardImagesFromUrlsCoroutine(Action onComplete)
562
+ {
563
+ for (int i = 0; i < tutorialCards.Count; i++)
564
+ {
565
+ TutorialCardData card = tutorialCards[i];
566
+ if (card.type != TutorialType.images || card.image != null || string.IsNullOrEmpty(card.imageUrl)) continue;
567
+
568
+ using (UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(card.imageUrl))
569
+ {
570
+ yield return webRequest.SendWebRequest();
571
+
572
+ if (webRequest.result == UnityWebRequest.Result.Success)
573
+ {
574
+ Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
575
+ texture.filterMode = FilterMode.Bilinear;
576
+ texture.Compress(false);
577
+
578
+ card.image = Sprite.Create(
579
+ texture,
580
+ new Rect(0, 0, texture.width, texture.height),
581
+ new Vector2(0.5f, 0.5f),
582
+ 100f,
583
+ 0,
584
+ SpriteMeshType.Tight
585
+ );
586
+ }
587
+ }
588
+ }
589
+
590
+ onComplete?.Invoke();
591
+ }
592
+
593
+ private void SetupMixedTutorialCards()
594
+ {
595
+ RemoveInvalidTutorialCards();
596
+
597
+ if (tutorialCards.Count == 0)
598
+ {
599
+ _eventManager.InvokeTutorialFetchFailedEvent();
600
+ return;
601
+ }
602
+
603
+ backButton.gameObject.SetActive(true);
604
+ nextButton.gameObject.SetActive(true);
605
+ indicatorParent.gameObject.SetActive(true);
606
+
607
+ UpdateIndicators(tutorialCards.Count);
608
+ StartCoroutine(ApplyInitialIndicatorHighlightNextFrame());
609
+ DisplayCurrentCard(true);
610
+ }
611
+
612
+ private void RemoveInvalidTutorialCards()
613
+ {
614
+ for (int i = tutorialCards.Count - 1; i >= 0; i--)
615
+ {
616
+ TutorialCardData card = tutorialCards[i];
617
+ bool hasVideo = card.type == TutorialType.video && (card.videoClip != null || !string.IsNullOrEmpty(card.videoUrl));
618
+ bool hasImage = card.type == TutorialType.images && card.image != null;
619
+
620
+ if (!hasVideo && !hasImage)
621
+ {
622
+ tutorialCards.RemoveAt(i);
623
+ }
624
+ }
625
+ }
626
+
627
+ private void ResetCardMediaState()
628
+ {
629
+ StopMediaDisplayFade();
630
+ ResetVideoProgressState();
631
+ HideMediaDisplay(tutorialDisplayP);
632
+ HideMediaDisplay(tutorialDisplayL);
633
+ HideMediaDisplay(tutorialImageDisplay);
634
+ }
635
+
636
+ private void ResetVideoProgressState()
637
+ {
638
+ cachedVideoLength = 0f;
639
+ isDraggingSlider = false;
640
+
641
+ if (timeSlider == null) return;
642
+
643
+ timeSlider.minValue = 0f;
644
+ timeSlider.maxValue = 1f;
645
+ timeSlider.value = 0f;
646
+ }
647
+
648
+ private void HideMediaDisplay(RawImage display)
649
+ {
650
+ if (display == null) return;
651
+
652
+ SetMediaDisplayAlpha(display, 0f);
653
+ display.gameObject.SetActive(false);
654
+ }
655
+
656
+ private void SetMediaDisplayAlpha(RawImage display, float alpha)
657
+ {
658
+ if (display == null) return;
659
+
660
+ Color color = display.color;
661
+ color.a = alpha;
662
+ display.color = color;
663
+ }
664
+
665
+ private void StopMediaDisplayFade()
666
+ {
667
+ if (mediaDisplayFadeCo == null) return;
668
+
669
+ StopCoroutine(mediaDisplayFadeCo);
670
+ mediaDisplayFadeCo = null;
671
+ }
672
+
673
+ private void FadeInMediaDisplay(RawImage display, float delay = 0f)
674
+ {
675
+ if (display == null) return;
676
+
677
+ StopMediaDisplayFade();
678
+ mediaDisplayFadeCo = StartCoroutine(FadeInMediaDisplayCoroutine(display, delay));
679
+ }
680
+
681
+ private IEnumerator FadeInMediaDisplayCoroutine(RawImage display, float delay)
682
+ {
683
+ display.gameObject.SetActive(true);
684
+ SetMediaDisplayAlpha(display, 0f);
685
+
686
+ if (delay > 0f)
687
+ {
688
+ yield return new WaitForSecondsRealtime(delay);
689
+ }
690
+
691
+ float elapsed = 0f;
692
+
693
+ while (elapsed < MediaFadeInDuration)
694
+ {
695
+ if (display == null) yield break;
696
+
697
+ elapsed += Time.unscaledDeltaTime;
698
+ float t = Mathf.Clamp01(elapsed / MediaFadeInDuration);
699
+ SetMediaDisplayAlpha(display, Mathf.SmoothStep(0f, 1f, t));
700
+ yield return null;
701
+ }
702
+
703
+ if (display != null)
704
+ {
705
+ SetMediaDisplayAlpha(display, 1f);
706
+ }
707
+
708
+ mediaDisplayFadeCo = null;
709
+ }
710
+
711
+
712
+
713
+ private void DisplayCurrentCard(bool isInitialDisplay)
714
+ {
715
+ if (currentCardIndex < 0 || currentCardIndex >= tutorialCards.Count) return;
716
+
717
+ StopVideoPlayback();
718
+ ResetCardMediaState();
719
+ TutorialCardData card = tutorialCards[currentCardIndex];
720
+
721
+ if (descriptionText != null)
722
+ {
723
+ descriptionText.text = card.description ?? string.Empty;
724
+ }
725
+
726
+ if (card.type == TutorialType.video)
727
+ {
728
+ DisplayVideoCard(card, isInitialDisplay);
729
+ }
730
+ else
731
+ {
732
+ DisplayImageCard(card, isInitialDisplay);
733
+ }
734
+
735
+ HighlightIndicator(currentCardIndex);
736
+ UpdateNavigationButtons(tutorialCards.Count, currentCardIndex);
737
+ }
738
+
739
+ private void DisplayVideoCard(TutorialCardData card, bool isInitialDisplay)
740
+ {
741
+ if (videoPlayer == null)
742
+ {
743
+ _eventManager.InvokeTutorialFetchFailedEvent();
744
+ return;
745
+ }
746
+
747
+ // Keep the media surface hidden while VideoPlayer prepares the next source.
748
+ currentDisplay = tutorialDisplayP;
749
+
750
+ videoPlayer.enabled = true;
751
+ videoPlayer.isLooping = true;
752
+ videoPlayer.prepareCompleted -= OnVideoPrepared;
753
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
754
+ videoPlayer.loopPointReached -= OnLoopPointReached;
755
+ videoPlayer.loopPointReached += OnLoopPointReached;
756
+
757
+ if (card.videoClip != null)
758
+ {
759
+ videoPlayer.source = VideoSource.VideoClip;
760
+ videoPlayer.clip = card.videoClip;
761
+ }
762
+ else if (!string.IsNullOrEmpty(card.videoUrl))
763
+ {
764
+ videoPlayer.source = VideoSource.Url;
765
+ videoPlayer.url = card.videoUrl;
766
+ }
767
+ else
768
+ {
769
+ _eventManager.InvokeTutorialFetchFailedEvent();
770
+ return;
771
+ }
772
+
773
+ videoPlayer.prepareCompleted += OnCardVideoPrepared;
774
+ videoPlayer.Prepare();
775
+
776
+ timeSlider.gameObject.SetActive(true);
777
+ timeSlider.interactable = VideoSeekingEnabled;
778
+ }
779
+
780
+ private void OnCardVideoPrepared(VideoPlayer vp)
781
+ {
782
+ vp.prepareCompleted -= OnCardVideoPrepared;
783
+ PreventIOSFullscreen();
784
+
785
+ if (currentDisplay == null)
786
+ {
787
+ _eventManager.InvokeTutorialFetchFailedEvent();
788
+ return;
789
+ }
790
+
791
+ bool isLandscape = vp.width > vp.height;
792
+ RawImage targetDisplay = isLandscape ? tutorialDisplayL : tutorialDisplayP;
793
+ RenderTexture targetTexture = isLandscape ? tutorialVideoTextureL : tutorialVideoTextureP;
794
+ if (targetTexture == null) targetTexture = isLandscape ? tutorialVideoTextureP : tutorialVideoTextureL;
795
+
796
+ // Switch to the correct display
797
+ tutorialDisplayP.gameObject.SetActive(targetDisplay == tutorialDisplayP);
798
+ tutorialDisplayL.gameObject.SetActive(targetDisplay == tutorialDisplayL);
799
+ currentDisplay = targetDisplay;
800
+ SetMediaDisplayAlpha(currentDisplay, 0f);
801
+
802
+ if (targetTexture != null)
803
+ {
804
+ currentDisplay.texture = targetTexture;
805
+ videoPlayer.targetTexture = targetTexture;
806
+ }
807
+
808
+ double len = videoPlayer.length;
809
+ if (len <= 0 && videoPlayer.clip != null) len = videoPlayer.clip.length;
810
+ cachedVideoLength = (float)Mathf.Max(0f, (float)len);
811
+
812
+ timeSlider.minValue = 0f;
813
+ timeSlider.maxValue = cachedVideoLength;
814
+ timeSlider.value = 0f;
815
+
816
+ if (updateVideoTimeCo != null) StopCoroutine(updateVideoTimeCo);
817
+ updateVideoTimeCo = StartCoroutine(UpdateVideoTime());
818
+
819
+ InvokeTutorialLoadingCompleteOnce();
820
+ videoPlayer.time = 0;
821
+ videoPlayer.Play();
822
+ FadeInMediaDisplay(currentDisplay, VideoRevealDelay);
823
+ }
824
+
825
+ private void DisplayImageCard(TutorialCardData card, bool isInitialDisplay)
826
+ {
827
+ videoPlayer.enabled = false;
828
+ timeSlider.gameObject.SetActive(false);
829
+
830
+ if (tutorialDisplayP != null)
831
+ tutorialDisplayP.gameObject.SetActive(false);
832
+ if (tutorialDisplayL != null)
833
+ tutorialDisplayL.gameObject.SetActive(false);
834
+
835
+ if (tutorialImageDisplay == null || card.image == null)
836
+ {
837
+ _eventManager.InvokeTutorialFetchFailedEvent();
838
+ return;
839
+ }
840
+
841
+ tutorialImageDisplay.gameObject.SetActive(true);
842
+ tutorialImageDisplay.texture = card.image.texture;
843
+ FadeInMediaDisplay(tutorialImageDisplay);
844
+
845
+ if (isInitialDisplay)
846
+ {
847
+ InvokeTutorialLoadingCompleteOnce();
848
+ }
849
+ }
850
+
851
+ private void InvokeTutorialLoadingCompleteOnce()
852
+ {
853
+ if (hasInvokedTutorialLoadComplete) return;
854
+
855
+ hasPreparedTutorialContent = true;
856
+ hasInvokedTutorialLoadComplete = true;
857
+ _eventManager.InvokeTutorialLoadingCompleteEvent();
858
+ }
859
+
860
+ private void StopVideoPlayback()
861
+ {
862
+ if (updateVideoTimeCo != null)
863
+ {
864
+ StopCoroutine(updateVideoTimeCo);
865
+ updateVideoTimeCo = null;
866
+ }
867
+
868
+ if (videoPlayer == null) return;
869
+
870
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
871
+ videoPlayer.loopPointReached -= OnLoopPointReached;
872
+ if (videoPlayer.isPrepared)
873
+ {
874
+ videoPlayer.time = 0;
875
+ }
876
+ videoPlayer.Stop();
877
+ videoPlayer.targetTexture = null;
878
+ }
879
+
880
+
881
+
211
882
  private void ShowVideoTutorial(TutorialData tutorialData)
212
883
  {
884
+ if (videoPlayer == null)
885
+ {
886
+ _eventManager.InvokeTutorialFetchFailedEvent();
887
+ return;
888
+ }
889
+
890
+ videoPlayer.enabled = true;
891
+
213
892
  bool videoFound = false;
214
893
 
215
894
  if (tutorialData.videoClip != null)
@@ -234,6 +913,7 @@ namespace Hyper.Internal
234
913
 
235
914
  else
236
915
  {
916
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
237
917
  videoPlayer.prepareCompleted += OnVideoPrepared;
238
918
  videoPlayer.loopPointReached += OnLoopPointReached;
239
919
  }
@@ -258,15 +938,6 @@ namespace Hyper.Internal
258
938
  currentDisplay.texture = targetTexture;
259
939
  videoPlayer.targetTexture = targetTexture;
260
940
 
261
- AspectRatioFitter fitter = (currentDisplay == tutorialDisplayL) ?
262
- aspectRatioFitterL : aspectRatioFitterP;
263
-
264
- if (fitter != null)
265
- {
266
- fitter.aspectRatio = Mathf.Max(0.01f, videoAspectRatio);
267
- fitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
268
- }
269
-
270
941
  timeSlider.gameObject.SetActive(true);
271
942
  backButton.gameObject.SetActive(false);
272
943
  nextButton.gameObject.SetActive(false);
@@ -278,6 +949,7 @@ namespace Hyper.Internal
278
949
  timeSlider.minValue = 0f;
279
950
  timeSlider.maxValue = cachedVideoLength;
280
951
  timeSlider.value = 0f;
952
+ timeSlider.interactable = VideoSeekingEnabled;
281
953
 
282
954
  if (updateVideoTimeCo != null) StopCoroutine(updateVideoTimeCo);
283
955
  updateVideoTimeCo = StartCoroutine(UpdateVideoTime());
@@ -324,6 +996,7 @@ namespace Hyper.Internal
324
996
  {
325
997
  tutorialImages = tutorialData.images;
326
998
  SetupImageTutorial();
999
+ _eventManager.InvokeTutorialLoadingCompleteEvent();
327
1000
  }
328
1001
  else if (tutorialData.imageUrls != null && tutorialData.imageUrls.Length > 0)
329
1002
  {
@@ -377,15 +1050,6 @@ namespace Hyper.Internal
377
1050
  float aspectRatio = (float)texture.width / texture.height;
378
1051
 
379
1052
  currentDisplay = SelectOptimalDisplay(aspectRatio);
380
-
381
- AspectRatioFitter fitter = (currentDisplay == tutorialDisplayL) ?
382
- aspectRatioFitterL : aspectRatioFitterP;
383
-
384
- if (fitter != null)
385
- {
386
- fitter.aspectRatio = Mathf.Max(0.01f, aspectRatio);
387
- fitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
388
- }
389
1053
  }
390
1054
 
391
1055
  private IEnumerator LoadImagesFromUrlsCoroutine(string[] urls, Action<Sprite[]> onComplete)
@@ -407,7 +1071,7 @@ namespace Hyper.Internal
407
1071
  if (webRequest.result == UnityWebRequest.Result.Success)
408
1072
  {
409
1073
  Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
410
- texture.filterMode = FilterMode.Point;
1074
+ texture.filterMode = FilterMode.Bilinear;
411
1075
  texture.Compress(false);
412
1076
 
413
1077
  Sprite sprite = Sprite.Create(
@@ -447,6 +1111,21 @@ namespace Hyper.Internal
447
1111
 
448
1112
  private void OnDisable()
449
1113
  {
1114
+ isDirectContentOpen = false;
1115
+ contentRevealScheduled = false;
1116
+ isClosing = false;
1117
+ CancelHiddenPreloadClose();
1118
+ StopMediaDisplayFade();
1119
+ HideTutorialContent();
1120
+
1121
+ #if UNITY_EDITOR
1122
+ if (editorTutorialLoadCo != null)
1123
+ {
1124
+ StopCoroutine(editorTutorialLoadCo);
1125
+ editorTutorialLoadCo = null;
1126
+ }
1127
+ #endif
1128
+
450
1129
  if (updateVideoTimeCo != null)
451
1130
  {
452
1131
  StopCoroutine(updateVideoTimeCo);
@@ -454,6 +1133,14 @@ namespace Hyper.Internal
454
1133
  }
455
1134
  }
456
1135
 
1136
+ private void OnDestroy()
1137
+ {
1138
+ if (_eventManager == null) return;
1139
+
1140
+ _eventManager.OnTutorialResourcesReferenced -= ShowTutorialBasedOnType;
1141
+ _eventManager.OnTutorialLoadingComplete -= HandleTutorialLoadingComplete;
1142
+ }
1143
+
457
1144
  private void DisplayImage(int index)
458
1145
  {
459
1146
  if (tutorialImages == null || index < 0 || index >= tutorialImages.Length)
@@ -471,6 +1158,12 @@ namespace Hyper.Internal
471
1158
 
472
1159
  public void NextImage()
473
1160
  {
1161
+ if (tutorialCards.Count > 0)
1162
+ {
1163
+ NextContent();
1164
+ return;
1165
+ }
1166
+
474
1167
  if (currentImageIndex < tutorialImages.Length - 1)
475
1168
  {
476
1169
  currentImageIndex++;
@@ -480,6 +1173,12 @@ namespace Hyper.Internal
480
1173
 
481
1174
  public void PreviousImage()
482
1175
  {
1176
+ if (tutorialCards.Count > 0)
1177
+ {
1178
+ PreviousContent();
1179
+ return;
1180
+ }
1181
+
483
1182
  if (currentImageIndex > 0)
484
1183
  {
485
1184
  currentImageIndex--;
@@ -487,19 +1186,77 @@ namespace Hyper.Internal
487
1186
  }
488
1187
  }
489
1188
 
1189
+ public void NextContent()
1190
+ {
1191
+ if (currentCardIndex >= tutorialCards.Count - 1) return;
1192
+
1193
+ currentCardIndex++;
1194
+ DisplayCurrentCard(false);
1195
+ }
1196
+
1197
+ public void PreviousContent()
1198
+ {
1199
+ if (currentCardIndex <= 0) return;
1200
+
1201
+ currentCardIndex--;
1202
+ DisplayCurrentCard(false);
1203
+ }
1204
+
490
1205
  public void Close()
491
1206
  {
1207
+ if (isClosing) return;
1208
+
1209
+ isClosing = true;
1210
+ CancelHiddenPreloadClose();
1211
+ tutorialCG.interactable = false;
1212
+ tutorialCG.blocksRaycasts = false;
1213
+
1214
+ if (tutorialContentGroup == null || tutorialContentGroup.alpha <= 0.001f)
1215
+ {
1216
+ FinishClose();
1217
+ return;
1218
+ }
1219
+
1220
+ tutorialContentGroup.DOKill();
1221
+ tutorialContentGroup.interactable = false;
1222
+ tutorialContentGroup.blocksRaycasts = false;
1223
+ tutorialContentGroup.DOFade(0f, ContentFadeOutDuration)
1224
+ .SetEase(HyperTween.Ease.InQuad)
1225
+ .SetUpdate(true)
1226
+ .OnComplete(FinishClose);
1227
+ }
1228
+
1229
+ private void FinishClose()
1230
+ {
1231
+ if (tutorialContentGroup != null)
1232
+ {
1233
+ tutorialContentGroup.alpha = 0f;
1234
+ }
1235
+
492
1236
  if (videoPlayer != null) videoPlayer.enabled = false;
493
1237
  slideUp.SlideDownAnimation();
494
1238
  }
495
1239
 
496
1240
  private void UpdateNavigationButtons()
497
1241
  {
498
- nextButton.interactable = currentImageIndex < tutorialImages.Length - 1;
499
- backButton.interactable = currentImageIndex > 0;
1242
+ UpdateNavigationButtons(tutorialImages != null ? tutorialImages.Length : 0, currentImageIndex);
1243
+ }
1244
+
1245
+ private void UpdateNavigationButtons(int totalCount, int currentIndex)
1246
+ {
1247
+ bool hasMultipleCards = totalCount > 1;
1248
+ nextButton.gameObject.SetActive(hasMultipleCards);
1249
+ backButton.gameObject.SetActive(hasMultipleCards);
1250
+ nextButton.interactable = currentIndex < totalCount - 1;
1251
+ backButton.interactable = currentIndex > 0;
500
1252
  }
501
1253
 
502
1254
  private void UpdateIndicators()
1255
+ {
1256
+ UpdateIndicators(tutorialImages != null ? tutorialImages.Length : 0);
1257
+ }
1258
+
1259
+ private void UpdateIndicators(int totalCount)
503
1260
  {
504
1261
 
505
1262
  foreach (Transform child in indicatorParent)
@@ -507,10 +1264,18 @@ namespace Hyper.Internal
507
1264
  Destroy(child.gameObject);
508
1265
  }
509
1266
 
510
- for (int i = 0; i < tutorialImages.Length; i++)
1267
+ for (int i = 0; i < totalCount; i++)
511
1268
  {
512
1269
  Instantiate(indicatorPrefab, indicatorParent);
513
1270
  }
1271
+
1272
+ indicatorParent.gameObject.SetActive(totalCount > 1);
1273
+ }
1274
+
1275
+ private IEnumerator ApplyInitialIndicatorHighlightNextFrame()
1276
+ {
1277
+ yield return null;
1278
+ HighlightIndicator(currentCardIndex);
514
1279
  }
515
1280
 
516
1281
  private void HighlightIndicator(int index)
@@ -559,6 +1324,8 @@ namespace Hyper.Internal
559
1324
 
560
1325
  private void OnTimeSliderChanged(float value)
561
1326
  {
1327
+ if (!VideoSeekingEnabled) return;
1328
+
562
1329
  // var tutorialData = HyperRuntime.GameManager.IsPortrait()
563
1330
  // ? HyperRuntime.DataManager.tutorialData
564
1331
  // : HyperRuntime.DataManager.landscapeTutorial;