app.hypergames.hypersdk 1.3.4 → 1.4.0
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.
- package/CHANGELOG.md +8 -0
- package/Editor/{HyperSDK.Editor.asmdef → Hyper.Editor.asmdef} +1 -1
- package/Editor/Resources/ControlBarPreviews/Dark/Default.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Dark/Default.png.meta +143 -0
- package/Editor/Resources/ControlBarPreviews/Dark/HealthBarLeft.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Dark/HealthBarLeft.png.meta +143 -0
- package/Editor/Resources/ControlBarPreviews/Dark/HeartsLeft.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Dark/HeartsLeft.png.meta +169 -0
- package/Editor/Resources/ControlBarPreviews/Dark.meta +8 -0
- package/Editor/Resources/ControlBarPreviews/Light/Default.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Light/Default.png.meta +143 -0
- package/Editor/Resources/ControlBarPreviews/Light/HealthBarLeft.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Light/HealthBarLeft.png.meta +143 -0
- package/Editor/Resources/ControlBarPreviews/Light/HeartsLeft.png +0 -0
- package/Editor/Resources/ControlBarPreviews/Light/HeartsLeft.png.meta +143 -0
- package/Editor/Resources/ControlBarPreviews/Light.meta +8 -0
- package/Editor/Resources/ControlBarPreviews.meta +8 -0
- package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +206 -0
- package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs.meta +2 -0
- package/Editor/Scripts/Inspectors.meta +8 -0
- package/Editor/Scripts/Windows/HyperCoreSettingsWindow.cs +0 -8
- package/Editor/Scripts/Windows/HyperGameContentWindow.cs +0 -8
- package/Editor/Scripts/Windows/HyperSettingsWindow.cs +3 -3
- package/HyperLoader.unity +4 -49
- package/Runtime/Internal/AssemblyInfo.cs +1 -1
- package/Runtime/Internal/Managers/EventManager.cs +4 -0
- package/Runtime/Internal/Managers/GameManager.cs +135 -2
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +16 -10
- package/Runtime/Internal/UI/Animation/BlinkingText.cs +1 -1
- package/Runtime/Internal/UI/Animation/HyperTween.cs +88 -0
- package/Runtime/Internal/UI/Components/ControlBar.cs +220 -40
- package/Runtime/Internal/UI/Components/HealthBarDisplay.cs +269 -0
- package/Runtime/Internal/UI/Components/HealthBarDisplay.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/LivesDisplay.cs +327 -0
- package/Runtime/Internal/UI/Components/LivesDisplay.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +202 -3
- package/Runtime/Internal/Utils/HyperConstants.cs +27 -0
- package/Runtime/Public/Core/HyperSDK.cs +68 -0
- package/Runtime/Resources/Fonts/Wonder Boys/Wonder Boys + Hyper BIG.mat +1 -1
- package/Runtime/Resources/HyperSDKSettings.asset +3 -2
- package/Runtime/Resources/Sprites/Control Bar/Ellipse 3.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Ellipse 3.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar - Green Fill.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar - Green Fill.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar - Underlay.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar - Underlay.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Health Bar.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/heart.empty.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/heart.empty.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/heart.fill.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/heart.fill.png.meta +169 -0
- package/Runtime/Resources/Sprites/Frames/{Game Over P.png.meta → Game Over.png.meta } +4 -4
- package/Runtime/Resources/Sprites/HYPERGAMES_Logo.png.meta +3 -3
- package/Runtime/Resources/Sprites/Hyper Atlas.spriteatlasv2 +6 -0
- package/Runtime/Resources/Sprites/Start CTAs/Hand.PNG +0 -0
- package/Runtime/Resources/Sprites/Start CTAs/Hand.PNG.meta +169 -0
- package/Runtime/Resources/Sprites/Start CTAs/Line .PNG +0 -0
- package/Runtime/Resources/Sprites/Start CTAs/Line .PNG.meta +169 -0
- package/Runtime/Resources/Sprites/Start CTAs/Swipe Gesture GIF.controller +130 -0
- package/Runtime/Resources/Sprites/Start CTAs/Swipe Gesture GIF.controller.meta +8 -0
- package/Runtime/Resources/Sprites/Start CTAs/Swipe Gesture.anim +158 -0
- package/Runtime/Resources/Sprites/Start CTAs/Swipe Gesture.anim.meta +8 -0
- package/Runtime/Resources/Sprites/Start CTAs.meta +8 -0
- package/Runtime/Resources/UIPrefabs/Control Bar Objects/Health Bar Display.prefab +361 -0
- package/Runtime/Resources/UIPrefabs/Control Bar Objects/Health Bar Display.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/Control Bar Objects/Lives Display .prefab +246 -0
- package/Runtime/Resources/UIPrefabs/Control Bar Objects/Lives Display .prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/Control Bar Objects.meta +8 -0
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +131 -17
- package/Runtime/Resources/UIPrefabs/StartingInstructionModal.prefab +271 -57
- package/Runtime/Resources/UIPrefabs/TutorialLandscapeModal.prefab +1 -1
- package/Runtime/Resources/UIPrefabs/TutorialPortraitModal.prefab +1 -1
- package/Runtime/Shared/DTOs/Enums.cs +22 -6
- package/package.json +1 -1
- /package/Editor/{HyperSDK.Editor.asmdef.meta → Hyper.Editor.asmdef.meta} +0 -0
- /package/Runtime/Resources/Sprites/Frames/{Game Over P.png → Game Over.png} +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
using System.Collections;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
using UnityEngine.UI;
|
|
4
|
+
using Hyper.Internal.Animation;
|
|
5
|
+
using System;
|
|
6
|
+
|
|
7
|
+
namespace Hyper.Internal
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Manages the display of health bar in the control bar.
|
|
11
|
+
/// Uses Image.fillAmount to represent health from 0-1.
|
|
12
|
+
/// </summary>
|
|
13
|
+
internal class HealthBarDisplay : MonoBehaviour
|
|
14
|
+
{
|
|
15
|
+
[Header("UI Components")]
|
|
16
|
+
[SerializeField] private Image healthFillImage;
|
|
17
|
+
[SerializeField] private Image underlayFillImage;
|
|
18
|
+
|
|
19
|
+
[Header("Configuration")]
|
|
20
|
+
[SerializeField] private float lowHealthThreshold = 0.3f;
|
|
21
|
+
|
|
22
|
+
[Header("Animation")]
|
|
23
|
+
[SerializeField] private float fillDelay = 0.1f;
|
|
24
|
+
[SerializeField] private float fillAnimationDuration = 0.3f;
|
|
25
|
+
[SerializeField] private bool usePulseOnLowHealth = false;
|
|
26
|
+
[SerializeField] private float pulseDuration = 0.5f;
|
|
27
|
+
|
|
28
|
+
private float _currentHealth = 1f;
|
|
29
|
+
private float _maxHealth = 1f;
|
|
30
|
+
private HyperTween.TweenHandle _fillTweenHandle;
|
|
31
|
+
private Coroutine _pulseCoroutine;
|
|
32
|
+
private bool _healthExhaustedTriggered;
|
|
33
|
+
|
|
34
|
+
private void Awake()
|
|
35
|
+
{
|
|
36
|
+
if (healthFillImage == null)
|
|
37
|
+
{
|
|
38
|
+
Debug.LogError("[HealthBarDisplay] Health fill image is not assigned!");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initialize to full health
|
|
42
|
+
if (healthFillImage != null && underlayFillImage != null)
|
|
43
|
+
{
|
|
44
|
+
healthFillImage.fillAmount = 1f;
|
|
45
|
+
underlayFillImage.fillAmount = 1f;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// <summary>
|
|
50
|
+
/// Initializes the health bar with a maximum health value.
|
|
51
|
+
/// </summary>
|
|
52
|
+
/// <param name="maxHealth">Maximum health value</param>
|
|
53
|
+
/// <param name="initialHealth">Initial health (defaults to max, or pass 0-1 for normalized)</param>
|
|
54
|
+
/// <param name="isNormalized">If true, initialHealth is treated as 0-1, otherwise as absolute value</param>
|
|
55
|
+
public void Initialize(float maxHealth, float initialHealth = -1f, bool isNormalized = false)
|
|
56
|
+
{
|
|
57
|
+
if (maxHealth <= 0)
|
|
58
|
+
{
|
|
59
|
+
Debug.LogWarning("[HealthBarDisplay] Max health must be greater than 0. Setting to 1.");
|
|
60
|
+
maxHealth = 1f;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_maxHealth = maxHealth;
|
|
64
|
+
|
|
65
|
+
if (initialHealth < 0)
|
|
66
|
+
{
|
|
67
|
+
_currentHealth = maxHealth;
|
|
68
|
+
}
|
|
69
|
+
else if (isNormalized)
|
|
70
|
+
{
|
|
71
|
+
_currentHealth = Mathf.Clamp01(initialHealth) * maxHealth;
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
{
|
|
75
|
+
_currentHealth = Mathf.Clamp(initialHealth, 0f, maxHealth);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_healthExhaustedTriggered = false;
|
|
79
|
+
UpdateDisplay();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// <summary>
|
|
83
|
+
/// Sets the current health value.
|
|
84
|
+
/// </summary>
|
|
85
|
+
/// <param name="health">Health value (absolute)</param>
|
|
86
|
+
public void SetHealth(float health)
|
|
87
|
+
{
|
|
88
|
+
float previousHealth = _currentHealth;
|
|
89
|
+
_currentHealth = Mathf.Clamp(health, 0f, _maxHealth);
|
|
90
|
+
AnimateHealthChange(previousHealth, _currentHealth);
|
|
91
|
+
|
|
92
|
+
// Check if health reached 0
|
|
93
|
+
if (previousHealth > 0 && _currentHealth <= 0)
|
|
94
|
+
{
|
|
95
|
+
CheckHealthExhausted();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// <summary>
|
|
100
|
+
/// Sets the current health as a normalized value (0-1).
|
|
101
|
+
/// </summary>
|
|
102
|
+
/// <param name="normalizedHealth">Health value from 0 to 1</param>
|
|
103
|
+
public void SetHealthNormalized(float normalizedHealth)
|
|
104
|
+
{
|
|
105
|
+
SetHealth(Mathf.Clamp01(normalizedHealth) * _maxHealth);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// <summary>
|
|
109
|
+
/// Adds health.
|
|
110
|
+
/// </summary>
|
|
111
|
+
/// <param name="amount">Amount of health to add</param>
|
|
112
|
+
public void AddHealth(float amount)
|
|
113
|
+
{
|
|
114
|
+
if (amount > 0)
|
|
115
|
+
{
|
|
116
|
+
SetHealth(_currentHealth + amount);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// <summary>
|
|
121
|
+
/// Removes health.
|
|
122
|
+
/// </summary>
|
|
123
|
+
/// <param name="amount">Amount of health to remove</param>
|
|
124
|
+
public void RemoveHealth(float amount)
|
|
125
|
+
{
|
|
126
|
+
if (amount > 0)
|
|
127
|
+
{
|
|
128
|
+
SetHealth(_currentHealth - amount);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// <summary>
|
|
133
|
+
/// Gets the current health value.
|
|
134
|
+
/// </summary>
|
|
135
|
+
public float GetCurrentHealth() => _currentHealth;
|
|
136
|
+
|
|
137
|
+
/// <summary>
|
|
138
|
+
/// Gets the maximum health value.
|
|
139
|
+
/// </summary>
|
|
140
|
+
public float GetMaxHealth() => _maxHealth;
|
|
141
|
+
|
|
142
|
+
/// <summary>
|
|
143
|
+
/// Gets the current health as a normalized value (0-1).
|
|
144
|
+
/// </summary>
|
|
145
|
+
public float GetHealthNormalized() => _maxHealth > 0 ? _currentHealth / _maxHealth : 0f;
|
|
146
|
+
|
|
147
|
+
private void AnimateHealthChange(float fromHealth, float toHealth)
|
|
148
|
+
{
|
|
149
|
+
if (healthFillImage == null) return;
|
|
150
|
+
|
|
151
|
+
// Stop existing animation
|
|
152
|
+
if (_fillTweenHandle != null)
|
|
153
|
+
{
|
|
154
|
+
_fillTweenHandle.Kill();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
float fromFill = _maxHealth > 0 ? fromHealth / _maxHealth : 0f;
|
|
158
|
+
float toFill = _maxHealth > 0 ? toHealth / _maxHealth : 0f;
|
|
159
|
+
|
|
160
|
+
healthFillImage.fillAmount -= Math.Abs(fromHealth - toHealth) / _maxHealth;
|
|
161
|
+
|
|
162
|
+
HyperTween.DelayedCall(fillDelay, () =>
|
|
163
|
+
{
|
|
164
|
+
// Animate fill amount
|
|
165
|
+
_fillTweenHandle = underlayFillImage.DOFillAmount(toFill, fillAnimationDuration)
|
|
166
|
+
.SetEase(HyperTween.Ease.OutQuad)
|
|
167
|
+
.OnComplete(() =>
|
|
168
|
+
{
|
|
169
|
+
_fillTweenHandle = null;
|
|
170
|
+
|
|
171
|
+
// Start pulse if health is low
|
|
172
|
+
if (usePulseOnLowHealth && toFill <= lowHealthThreshold && toFill > 0)
|
|
173
|
+
{
|
|
174
|
+
StartPulseAnimation();
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
{
|
|
178
|
+
StopPulseAnimation();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private void StartPulseAnimation()
|
|
185
|
+
{
|
|
186
|
+
if (_pulseCoroutine != null) return;
|
|
187
|
+
_pulseCoroutine = StartCoroutine(PulseAnimation());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private void StopPulseAnimation()
|
|
191
|
+
{
|
|
192
|
+
if (_pulseCoroutine != null)
|
|
193
|
+
{
|
|
194
|
+
StopCoroutine(_pulseCoroutine);
|
|
195
|
+
_pulseCoroutine = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (healthFillImage != null)
|
|
199
|
+
{
|
|
200
|
+
Color color = healthFillImage.color;
|
|
201
|
+
color.a = 1f;
|
|
202
|
+
healthFillImage.color = color;
|
|
203
|
+
underlayFillImage.color = color;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private IEnumerator PulseAnimation()
|
|
208
|
+
{
|
|
209
|
+
if (healthFillImage == null) yield break;
|
|
210
|
+
|
|
211
|
+
while (true)
|
|
212
|
+
{
|
|
213
|
+
if (healthFillImage == null) yield break;
|
|
214
|
+
|
|
215
|
+
float normalizedHealth = GetHealthNormalized();
|
|
216
|
+
if (normalizedHealth > lowHealthThreshold || normalizedHealth <= 0)
|
|
217
|
+
{
|
|
218
|
+
StopPulseAnimation();
|
|
219
|
+
yield break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Pulse alpha
|
|
223
|
+
float elapsed = 0f;
|
|
224
|
+
Color startColor = healthFillImage.color;
|
|
225
|
+
Color pulseColor = new Color(startColor.r, startColor.g, startColor.b, 0.5f);
|
|
226
|
+
|
|
227
|
+
while (elapsed < pulseDuration)
|
|
228
|
+
{
|
|
229
|
+
if (healthFillImage == null) yield break;
|
|
230
|
+
elapsed += Time.deltaTime;
|
|
231
|
+
float t = Mathf.PingPong(elapsed / pulseDuration, 1f);
|
|
232
|
+
Color currentColor = Color.Lerp(startColor, pulseColor, t);
|
|
233
|
+
healthFillImage.color = currentColor;
|
|
234
|
+
underlayFillImage.color = currentColor;
|
|
235
|
+
yield return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private void UpdateDisplay()
|
|
241
|
+
{
|
|
242
|
+
if (healthFillImage == null) return;
|
|
243
|
+
|
|
244
|
+
float normalizedHealth = GetHealthNormalized();
|
|
245
|
+
healthFillImage.fillAmount = normalizedHealth;
|
|
246
|
+
underlayFillImage.fillAmount = normalizedHealth;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private void CheckHealthExhausted()
|
|
250
|
+
{
|
|
251
|
+
if (_currentHealth <= 0 && !_healthExhaustedTriggered)
|
|
252
|
+
{
|
|
253
|
+
_healthExhaustedTriggered = true;
|
|
254
|
+
HyperRuntime.EventManager?.InvokeHealthExhaustedEvent();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void OnDestroy()
|
|
259
|
+
{
|
|
260
|
+
if (_fillTweenHandle != null)
|
|
261
|
+
{
|
|
262
|
+
_fillTweenHandle.Kill();
|
|
263
|
+
}
|
|
264
|
+
HyperTween.Kill(underlayFillImage);
|
|
265
|
+
StopPulseAnimation();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
using System.Collections;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
using TMPro;
|
|
4
|
+
using UnityEngine.UI;
|
|
5
|
+
using Hyper.Internal.Animation;
|
|
6
|
+
|
|
7
|
+
namespace Hyper.Internal
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Manages the display of lives/hearts in the control bar.
|
|
11
|
+
/// Dynamically creates and manages heart icons based on current lives.
|
|
12
|
+
/// </summary>
|
|
13
|
+
internal class LivesDisplay : MonoBehaviour
|
|
14
|
+
{
|
|
15
|
+
[Header("Configuration")]
|
|
16
|
+
[SerializeField] private Sprite heartFullSprite;
|
|
17
|
+
[SerializeField] private Sprite heartEmptySprite;
|
|
18
|
+
[SerializeField] private float heartSize = 50f;
|
|
19
|
+
[SerializeField] private float spacing = 10f;
|
|
20
|
+
[SerializeField, Min(1)] private int maxVisibleHearts = 3;
|
|
21
|
+
|
|
22
|
+
[Header("Animation")]
|
|
23
|
+
[SerializeField] private float fadeDuration = 0.3f;
|
|
24
|
+
[SerializeField] private bool useScaleAnimation = false;
|
|
25
|
+
[SerializeField] private float scaleAmount = 0.8f;
|
|
26
|
+
[SerializeField] private float scaleDuration = 0.15f;
|
|
27
|
+
|
|
28
|
+
[Header("Container")]
|
|
29
|
+
[SerializeField] private RectTransform container;
|
|
30
|
+
[SerializeField] private TextMeshProUGUI overflowLabel;
|
|
31
|
+
|
|
32
|
+
private int _currentLives;
|
|
33
|
+
private int _maxLives;
|
|
34
|
+
private Image[] _heartImages;
|
|
35
|
+
private bool _livesExhaustedTriggered;
|
|
36
|
+
|
|
37
|
+
private void Awake()
|
|
38
|
+
{
|
|
39
|
+
if (container == null)
|
|
40
|
+
{
|
|
41
|
+
container = GetComponent<RectTransform>();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ConfigureLayoutGroup();
|
|
45
|
+
|
|
46
|
+
if (overflowLabel != null)
|
|
47
|
+
{
|
|
48
|
+
overflowLabel.gameObject.SetActive(false);
|
|
49
|
+
overflowLabel.text = string.Empty;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private void ConfigureLayoutGroup()
|
|
54
|
+
{
|
|
55
|
+
HorizontalLayoutGroup layoutGroup = container.GetComponent<HorizontalLayoutGroup>();
|
|
56
|
+
if (layoutGroup == null)
|
|
57
|
+
{
|
|
58
|
+
layoutGroup = container.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
layoutGroup.spacing = spacing;
|
|
62
|
+
layoutGroup.childAlignment = TextAnchor.MiddleLeft;
|
|
63
|
+
layoutGroup.childControlWidth = false;
|
|
64
|
+
layoutGroup.childControlHeight = false;
|
|
65
|
+
layoutGroup.childForceExpandWidth = false;
|
|
66
|
+
layoutGroup.childForceExpandHeight = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// <summary>
|
|
70
|
+
/// Initializes the lives display with a maximum number of lives.
|
|
71
|
+
/// Creates heart icons based on the max value.
|
|
72
|
+
/// </summary>
|
|
73
|
+
/// <param name="maxLives">Maximum number of lives to display</param>
|
|
74
|
+
/// <param name="initialLives">Initial number of lives (defaults to max)</param>
|
|
75
|
+
public void Initialize(int maxLives, int initialLives = -1)
|
|
76
|
+
{
|
|
77
|
+
if (maxLives < 0)
|
|
78
|
+
{
|
|
79
|
+
Debug.LogWarning("[LivesDisplay] Max lives cannot be negative. Setting to 0.");
|
|
80
|
+
maxLives = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_maxLives = maxLives;
|
|
84
|
+
_currentLives = initialLives >= 0 ? initialLives : maxLives;
|
|
85
|
+
|
|
86
|
+
if (_currentLives > _maxLives)
|
|
87
|
+
{
|
|
88
|
+
_currentLives = _maxLives;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_livesExhaustedTriggered = false;
|
|
92
|
+
CreateHeartIcons();
|
|
93
|
+
UpdateDisplay();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// <summary>
|
|
97
|
+
/// Sets the current number of lives and updates the display.
|
|
98
|
+
/// </summary>
|
|
99
|
+
/// <param name="lives">Number of lives to set</param>
|
|
100
|
+
public void SetLives(int lives)
|
|
101
|
+
{
|
|
102
|
+
int previousLives = _currentLives;
|
|
103
|
+
_currentLives = Mathf.Clamp(lives, 0, _maxLives);
|
|
104
|
+
UpdateDisplay();
|
|
105
|
+
|
|
106
|
+
// Only check if lives decreased to 0
|
|
107
|
+
if (previousLives > 0 && _currentLives == 0)
|
|
108
|
+
{
|
|
109
|
+
CheckLivesExhausted();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// <summary>
|
|
114
|
+
/// Adds one life (up to max).
|
|
115
|
+
/// </summary>
|
|
116
|
+
public void AddLife()
|
|
117
|
+
{
|
|
118
|
+
if (_currentLives < _maxLives)
|
|
119
|
+
{
|
|
120
|
+
_currentLives++;
|
|
121
|
+
UpdateDisplay();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// <summary>
|
|
126
|
+
/// Removes one life (down to 0).
|
|
127
|
+
/// </summary>
|
|
128
|
+
public void RemoveLife()
|
|
129
|
+
{
|
|
130
|
+
if (_currentLives > 0)
|
|
131
|
+
{
|
|
132
|
+
int lostLifeIndex = _currentLives - 1;
|
|
133
|
+
_currentLives--;
|
|
134
|
+
UpdateDisplay();
|
|
135
|
+
AnimateLifeLost(lostLifeIndex);
|
|
136
|
+
CheckLivesExhausted();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// <summary>
|
|
141
|
+
/// Gets the current number of lives.
|
|
142
|
+
/// </summary>
|
|
143
|
+
public int GetCurrentLives() => _currentLives;
|
|
144
|
+
|
|
145
|
+
/// <summary>
|
|
146
|
+
/// Gets the maximum number of lives.
|
|
147
|
+
/// </summary>
|
|
148
|
+
public int GetMaxLives() => _maxLives;
|
|
149
|
+
|
|
150
|
+
private void CreateHeartIcons()
|
|
151
|
+
{
|
|
152
|
+
// Clear existing hearts
|
|
153
|
+
ClearHearts();
|
|
154
|
+
|
|
155
|
+
if (_maxLives <= 0)
|
|
156
|
+
{
|
|
157
|
+
_heartImages = new Image[0];
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
int heartSlots = maxVisibleHearts > 0
|
|
162
|
+
? Mathf.Min(_maxLives, maxVisibleHearts)
|
|
163
|
+
: _maxLives;
|
|
164
|
+
|
|
165
|
+
_heartImages = new Image[heartSlots];
|
|
166
|
+
|
|
167
|
+
for (int i = 0; i < heartSlots; i++)
|
|
168
|
+
{
|
|
169
|
+
GameObject heartObject = new GameObject($"Heart_{i}");
|
|
170
|
+
heartObject.transform.SetParent(container, false);
|
|
171
|
+
|
|
172
|
+
RectTransform heartRect = heartObject.AddComponent<RectTransform>();
|
|
173
|
+
heartRect.sizeDelta = new Vector2(heartSize, heartSize);
|
|
174
|
+
|
|
175
|
+
Image heartImage = heartObject.AddComponent<Image>();
|
|
176
|
+
heartImage.sprite = heartFullSprite;
|
|
177
|
+
heartImage.preserveAspect = true;
|
|
178
|
+
|
|
179
|
+
_heartImages[i] = heartImage;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private void UpdateDisplay()
|
|
184
|
+
{
|
|
185
|
+
if (_heartImages == null) return;
|
|
186
|
+
|
|
187
|
+
for (int i = 0; i < _heartImages.Length; i++)
|
|
188
|
+
{
|
|
189
|
+
if (_heartImages[i] != null)
|
|
190
|
+
{
|
|
191
|
+
bool isFull = i < Mathf.Min(_currentLives, _heartImages.Length);
|
|
192
|
+
_heartImages[i].sprite = isFull ? heartFullSprite : heartEmptySprite;
|
|
193
|
+
_heartImages[i].gameObject.SetActive(true);
|
|
194
|
+
|
|
195
|
+
// Reset color to full opacity for immediate updates
|
|
196
|
+
Color color = _heartImages[i].color;
|
|
197
|
+
color.a = 1f;
|
|
198
|
+
_heartImages[i].color = color;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
UpdateOverflowLabel();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private void AnimateLifeLost(int heartIndex)
|
|
206
|
+
{
|
|
207
|
+
if (_heartImages == null || heartIndex < 0 || heartIndex >= _heartImages.Length)
|
|
208
|
+
return;
|
|
209
|
+
|
|
210
|
+
Image heartImage = _heartImages[heartIndex];
|
|
211
|
+
if (heartImage == null) return;
|
|
212
|
+
|
|
213
|
+
// Update sprite immediately
|
|
214
|
+
Color color = heartImage.color;
|
|
215
|
+
color.a = 0.45f;
|
|
216
|
+
heartImage.color = color;
|
|
217
|
+
heartImage.sprite = heartEmptySprite;
|
|
218
|
+
|
|
219
|
+
// Stop any existing animations on this heart
|
|
220
|
+
HyperTween.Kill(heartImage);
|
|
221
|
+
if (heartImage.transform != null)
|
|
222
|
+
{
|
|
223
|
+
HyperTween.Kill(heartImage.transform);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fade animation: fade out then fade back in with empty sprite
|
|
227
|
+
AnimateHeartFade(heartImage);
|
|
228
|
+
|
|
229
|
+
// Optional scale animation
|
|
230
|
+
if (useScaleAnimation && heartImage.transform != null)
|
|
231
|
+
{
|
|
232
|
+
AnimateHeartScale(heartImage.transform);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private void AnimateHeartFade(Image heartImage)
|
|
237
|
+
{
|
|
238
|
+
if (heartImage == null) return;
|
|
239
|
+
|
|
240
|
+
Color startColor = heartImage.color;
|
|
241
|
+
Color fadeOutColor = new Color(startColor.r, startColor.g, startColor.b, 0f);
|
|
242
|
+
Color fadeInColor = new Color(startColor.r, startColor.g, startColor.b, 1f);
|
|
243
|
+
float halfDuration = fadeDuration * 0.5f;
|
|
244
|
+
|
|
245
|
+
// Fade out, then fade back in
|
|
246
|
+
heartImage.DOColor(fadeOutColor, halfDuration)
|
|
247
|
+
.SetEase(HyperTween.Ease.OutQuad)
|
|
248
|
+
.OnComplete(() =>
|
|
249
|
+
{
|
|
250
|
+
if (heartImage != null)
|
|
251
|
+
{
|
|
252
|
+
heartImage.DOColor(fadeInColor, halfDuration)
|
|
253
|
+
.SetEase(HyperTween.Ease.InQuad);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void AnimateHeartScale(Transform heartTransform)
|
|
259
|
+
{
|
|
260
|
+
if (heartTransform == null) return;
|
|
261
|
+
|
|
262
|
+
Vector3 originalScale = Vector3.one;
|
|
263
|
+
Vector3 scaledDown = originalScale * scaleAmount;
|
|
264
|
+
float halfDuration = scaleDuration * 0.5f;
|
|
265
|
+
|
|
266
|
+
// Scale down, then scale back up
|
|
267
|
+
heartTransform.DOScale(scaledDown, halfDuration)
|
|
268
|
+
.SetEase(HyperTween.Ease.OutQuad)
|
|
269
|
+
.OnComplete(() =>
|
|
270
|
+
{
|
|
271
|
+
if (heartTransform != null)
|
|
272
|
+
{
|
|
273
|
+
heartTransform.DOScale(originalScale, halfDuration)
|
|
274
|
+
.SetEase(HyperTween.Ease.InQuad);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private void CheckLivesExhausted()
|
|
280
|
+
{
|
|
281
|
+
if (_currentLives <= 0 && !_livesExhaustedTriggered)
|
|
282
|
+
{
|
|
283
|
+
_livesExhaustedTriggered = true;
|
|
284
|
+
HyperRuntime.EventManager?.InvokeLivesExhaustedEvent();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private void ClearHearts()
|
|
289
|
+
{
|
|
290
|
+
if (_heartImages == null) return;
|
|
291
|
+
|
|
292
|
+
foreach (Image heart in _heartImages)
|
|
293
|
+
{
|
|
294
|
+
if (heart != null)
|
|
295
|
+
{
|
|
296
|
+
Destroy(heart.gameObject);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_heartImages = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private void OnDestroy()
|
|
304
|
+
{
|
|
305
|
+
ClearHearts();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private void UpdateOverflowLabel()
|
|
309
|
+
{
|
|
310
|
+
if (overflowLabel == null) return;
|
|
311
|
+
|
|
312
|
+
int heartSlots = _heartImages != null ? _heartImages.Length : 0;
|
|
313
|
+
int overflow = Mathf.Max(0, _currentLives - heartSlots);
|
|
314
|
+
if (overflow > 0)
|
|
315
|
+
{
|
|
316
|
+
overflowLabel.gameObject.SetActive(true);
|
|
317
|
+
overflowLabel.text = $"+{overflow}";
|
|
318
|
+
}
|
|
319
|
+
else
|
|
320
|
+
{
|
|
321
|
+
overflowLabel.text = string.Empty;
|
|
322
|
+
overflowLabel.gameObject.SetActive(false);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|