2026-04-25 18:20:16 +07:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.UIElements;
|
2026-04-28 00:07:42 +07:00
|
|
|
using PrimeTween;
|
|
|
|
|
using System.Threading.Tasks;
|
2026-04-25 18:20:16 +07:00
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
namespace Hallucinate.UI
|
2026-04-25 18:20:16 +07:00
|
|
|
{
|
2026-04-28 00:07:42 +07:00
|
|
|
public class MainMenuController : BaseUIController
|
2026-04-25 18:20:16 +07:00
|
|
|
{
|
2026-04-28 00:07:42 +07:00
|
|
|
public enum MenuState { Idle, Ribbon }
|
|
|
|
|
private MenuState _currentState = MenuState.Idle;
|
|
|
|
|
|
2026-04-26 04:39:59 +07:00
|
|
|
private VisualElement _logo;
|
|
|
|
|
private VisualElement _ribbon;
|
2026-04-28 10:11:28 +07:00
|
|
|
private VisualElement _logoSpace;
|
2026-04-26 05:20:47 +07:00
|
|
|
|
|
|
|
|
private float _lastInteractionTime;
|
2026-04-28 00:07:42 +07:00
|
|
|
private const float IDLE_TIMEOUT = 5.0f;
|
2026-04-29 01:04:28 +07:00
|
|
|
private bool _isFirstLoad = true;
|
2026-04-25 18:20:16 +07:00
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
private Tween _pulseTween;
|
2026-04-28 10:11:28 +07:00
|
|
|
private Tween _rotationTween;
|
2026-04-28 10:44:22 +07:00
|
|
|
private Texture2D _currentIcon;
|
2026-04-26 04:39:59 +07:00
|
|
|
|
2026-05-01 21:58:20 +07:00
|
|
|
private bool _isBusy = false;
|
|
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
|
|
|
|
|
{
|
|
|
|
|
base.Initialize(uxmlRoot, manager);
|
2026-04-26 04:39:59 +07:00
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
_logo = root.Q<VisualElement>("Logo");
|
|
|
|
|
_ribbon = root.Q<VisualElement>("Ribbon");
|
2026-04-28 10:11:28 +07:00
|
|
|
_logoSpace = root.Q<VisualElement>("LogoSpace");
|
2026-04-25 18:20:16 +07:00
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
if (_logo == null)
|
2026-04-27 15:48:17 +07:00
|
|
|
{
|
2026-04-28 10:11:28 +07:00
|
|
|
Debug.LogError($"[MainMenuController] Element 'Logo' not found in UXML!");
|
2026-04-28 00:07:42 +07:00
|
|
|
return;
|
2026-04-27 15:48:17 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
// Lắng nghe sự kiện thay đổi kích thước toàn màn hình
|
|
|
|
|
root.RegisterCallback<GeometryChangedEvent>(OnScreenResize);
|
|
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
_logo.RegisterCallback<ClickEvent>(OnLogoClicked);
|
2026-04-26 05:20:47 +07:00
|
|
|
|
2026-04-28 13:07:52 +07:00
|
|
|
var settingsBtn = root.Q<Button>("SettingsBtn");
|
2026-05-01 21:58:20 +07:00
|
|
|
if (settingsBtn != null) settingsBtn.clicked += () => { if (_isBusy) return; uiManager.ToggleSettings(); };
|
2026-04-28 13:07:52 +07:00
|
|
|
|
2026-05-01 21:58:20 +07:00
|
|
|
root.Q<Button>("JoinBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<LobbyController>(); };
|
|
|
|
|
root.Q<Button>("CreateBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<LobbyController>(); };
|
|
|
|
|
root.Q<Button>("ProfileBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<ProfileController>(); };
|
2026-04-28 10:11:28 +07:00
|
|
|
root.Q<Button>("ExitBtn").clicked += () => Application.Quit();
|
2026-04-26 05:20:47 +07:00
|
|
|
|
2026-05-01 17:57:07 +07:00
|
|
|
// Đăng ký Localization
|
|
|
|
|
if (LocalizationManager.Instance != null)
|
|
|
|
|
{
|
|
|
|
|
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
|
|
|
|
}
|
|
|
|
|
ApplyLocalization();
|
|
|
|
|
|
2026-04-28 10:44:22 +07:00
|
|
|
ResetLogoPosition();
|
2026-04-28 00:07:42 +07:00
|
|
|
StartPulse();
|
|
|
|
|
_lastInteractionTime = Time.time;
|
2026-04-26 05:20:47 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 17:57:07 +07:00
|
|
|
private void ApplyLocalization()
|
|
|
|
|
{
|
|
|
|
|
if (LocalizationManager.Instance == null) return;
|
|
|
|
|
|
|
|
|
|
var joinBtn = root.Q<Button>("JoinBtn");
|
|
|
|
|
if (joinBtn != null) joinBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_JOIN");
|
|
|
|
|
|
|
|
|
|
var createBtn = root.Q<Button>("CreateBtn");
|
|
|
|
|
if (createBtn != null) createBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_CREATE");
|
|
|
|
|
|
|
|
|
|
var settingsBtn = root.Q<Button>("SettingsBtn");
|
|
|
|
|
if (settingsBtn != null) settingsBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_SETTINGS");
|
|
|
|
|
|
|
|
|
|
var profileBtn = root.Q<Button>("ProfileBtn");
|
|
|
|
|
if (profileBtn != null) profileBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_PROFILE");
|
|
|
|
|
|
|
|
|
|
var exitBtn = root.Q<Button>("ExitBtn");
|
|
|
|
|
if (exitBtn != null) exitBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_EXIT");
|
|
|
|
|
|
|
|
|
|
var playLabel = _logo.Q<Label>();
|
|
|
|
|
if (playLabel != null) playLabel.text = LocalizationManager.Instance.GetLocalizedString("MENU_PLAY");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
private void OnScreenResize(GeometryChangedEvent evt)
|
|
|
|
|
{
|
|
|
|
|
// Khi màn hình thay đổi kích thước, nếu đang ở Ribbon thì cập nhật theo LogoSpace
|
|
|
|
|
if (_currentState == MenuState.Ribbon)
|
|
|
|
|
{
|
|
|
|
|
UpdateLogoToSpace();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ResetLogoPosition();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
private void ResetLogoPosition()
|
2026-04-26 05:20:47 +07:00
|
|
|
{
|
2026-04-28 10:25:34 +07:00
|
|
|
if (_logo == null) return;
|
2026-04-29 02:31:15 +07:00
|
|
|
|
|
|
|
|
// Sử dụng phần trăm để luôn ở giữa bất kể độ phân giải
|
|
|
|
|
_logo.style.left = Length.Percent(50);
|
|
|
|
|
_logo.style.top = Length.Percent(50);
|
|
|
|
|
_logo.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
|
|
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
_logo.style.width = 200;
|
|
|
|
|
_logo.style.height = 200;
|
2026-04-26 05:20:47 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
public void SetGameIcon(Texture2D icon)
|
|
|
|
|
{
|
|
|
|
|
if (icon == null || _logo == null) return;
|
2026-04-28 10:44:22 +07:00
|
|
|
_currentIcon = icon;
|
2026-04-28 10:11:28 +07:00
|
|
|
_logo.style.backgroundImage = icon;
|
|
|
|
|
|
|
|
|
|
var radius = new StyleLength(new Length(50, LengthUnit.Percent));
|
|
|
|
|
_logo.style.borderTopLeftRadius = radius;
|
|
|
|
|
_logo.style.borderTopRightRadius = radius;
|
|
|
|
|
_logo.style.borderBottomLeftRadius = radius;
|
|
|
|
|
_logo.style.borderBottomRightRadius = radius;
|
|
|
|
|
_logo.style.overflow = Overflow.Hidden;
|
|
|
|
|
|
|
|
|
|
var label = _logo.Q<Label>();
|
|
|
|
|
if (label != null) label.style.display = DisplayStyle.None;
|
|
|
|
|
|
2026-04-28 10:44:22 +07:00
|
|
|
StartRotation();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StartRotation()
|
|
|
|
|
{
|
|
|
|
|
if (_currentIcon == null) return;
|
2026-04-28 10:11:28 +07:00
|
|
|
if (_rotationTween.isAlive) _rotationTween.Stop();
|
2026-04-28 10:44:22 +07:00
|
|
|
|
2026-04-29 01:04:28 +07:00
|
|
|
_rotationTween = Tween.Custom(0f, 360f, duration: 4f,
|
|
|
|
|
onValueChange: val => _logo.style.rotate = new StyleRotate(new Rotate(Angle.Degrees(val))),
|
|
|
|
|
cycles: -1,
|
|
|
|
|
ease: Ease.Linear);
|
2026-04-28 10:11:28 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async Task PlayTransitionIn()
|
2026-04-26 04:39:59 +07:00
|
|
|
{
|
2026-05-01 21:58:20 +07:00
|
|
|
_isBusy = false;
|
2026-04-28 10:44:22 +07:00
|
|
|
_lastInteractionTime = Time.time;
|
2026-04-28 10:25:34 +07:00
|
|
|
_currentState = MenuState.Idle;
|
|
|
|
|
ResetLogoPosition();
|
2026-04-28 10:44:22 +07:00
|
|
|
|
2026-04-28 10:25:34 +07:00
|
|
|
if (_ribbon != null)
|
|
|
|
|
{
|
|
|
|
|
_ribbon.style.display = DisplayStyle.None;
|
|
|
|
|
_ribbon.style.opacity = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 10:44:22 +07:00
|
|
|
StartRotation();
|
2026-04-28 10:25:34 +07:00
|
|
|
await base.PlayTransitionIn();
|
|
|
|
|
|
2026-04-29 01:04:28 +07:00
|
|
|
if (!_isFirstLoad) TransitionToRibbon();
|
|
|
|
|
else _isFirstLoad = false;
|
2026-04-28 00:07:42 +07:00
|
|
|
}
|
2026-04-27 13:27:40 +07:00
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
public override async Task PlayTransitionOut()
|
2026-04-28 00:07:42 +07:00
|
|
|
{
|
2026-04-28 10:11:28 +07:00
|
|
|
if (_rotationTween.isAlive) _rotationTween.Stop();
|
|
|
|
|
await base.PlayTransitionOut();
|
2026-04-27 15:48:17 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 01:04:28 +07:00
|
|
|
private async void OnLogoClicked(ClickEvent evt)
|
2026-04-27 15:48:17 +07:00
|
|
|
{
|
2026-05-01 21:58:20 +07:00
|
|
|
if (_isBusy) return;
|
2026-04-28 00:07:42 +07:00
|
|
|
_lastInteractionTime = Time.time;
|
2026-04-28 10:11:28 +07:00
|
|
|
if (_currentState == MenuState.Idle) TransitionToRibbon();
|
2026-05-01 21:58:20 +07:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_isBusy = true;
|
|
|
|
|
await uiManager.Push<LobbyController>();
|
|
|
|
|
}
|
2026-04-26 04:39:59 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 10:44:22 +07:00
|
|
|
private void TransitionToRibbon()
|
2026-04-26 05:20:47 +07:00
|
|
|
{
|
2026-04-28 10:44:22 +07:00
|
|
|
if (_currentState == MenuState.Ribbon && _ribbon.resolvedStyle.display == DisplayStyle.Flex) return;
|
2026-04-28 00:07:42 +07:00
|
|
|
_currentState = MenuState.Ribbon;
|
2026-04-28 10:44:22 +07:00
|
|
|
_lastInteractionTime = Time.time;
|
2026-04-26 05:20:47 +07:00
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
_ribbon.style.display = DisplayStyle.Flex;
|
2026-04-28 10:11:28 +07:00
|
|
|
Tween.Custom(0f, 1f, duration: 0.3f, onValueChange: val => _ribbon.style.opacity = val);
|
|
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
// Đợi một frame để Ribbon layout xong rồi mới lấy vị trí LogoSpace
|
2026-04-28 10:44:22 +07:00
|
|
|
_logoSpace.RegisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
|
|
|
|
|
}
|
2026-04-28 10:11:28 +07:00
|
|
|
|
2026-04-28 10:44:22 +07:00
|
|
|
private void OnLogoSpaceReady(GeometryChangedEvent evt)
|
|
|
|
|
{
|
|
|
|
|
_logoSpace.UnregisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
|
2026-04-29 02:31:15 +07:00
|
|
|
UpdateLogoToSpace(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateLogoToSpace(bool animate = false)
|
|
|
|
|
{
|
2026-04-28 10:11:28 +07:00
|
|
|
Rect targetBounds = _logoSpace.worldBound;
|
2026-04-28 10:44:22 +07:00
|
|
|
if (targetBounds.width <= 0) return;
|
|
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
// Chuyển đổi tọa độ world của LogoSpace sang tọa độ local của root
|
|
|
|
|
Vector2 localPos = root.WorldToLocal(new Vector2(targetBounds.x, targetBounds.y));
|
2026-04-29 01:04:28 +07:00
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
float targetX = localPos.x + (targetBounds.width / 2f);
|
|
|
|
|
float targetY = localPos.y + (targetBounds.height / 2f);
|
2026-04-28 10:11:28 +07:00
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
// Khi ở Ribbon, chúng ta bỏ translate -50% để tính toán chính xác tâm
|
|
|
|
|
_logo.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
|
2026-04-28 10:11:28 +07:00
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
if (animate)
|
|
|
|
|
{
|
|
|
|
|
Tween.Custom(_logo.resolvedStyle.left, targetX, duration: 0.5f,
|
|
|
|
|
onValueChange: val => _logo.style.left = val,
|
|
|
|
|
ease: Ease.OutQuad);
|
|
|
|
|
|
|
|
|
|
Tween.Custom(_logo.resolvedStyle.top, targetY, duration: 0.5f,
|
|
|
|
|
onValueChange: val => _logo.style.top = val,
|
|
|
|
|
ease: Ease.OutQuad);
|
|
|
|
|
|
|
|
|
|
Tween.Custom(_logo.resolvedStyle.width, 100f, duration: 0.5f,
|
|
|
|
|
onValueChange: val => _logo.style.width = val,
|
|
|
|
|
ease: Ease.OutQuad);
|
|
|
|
|
|
|
|
|
|
Tween.Custom(_logo.resolvedStyle.height, 100f, duration: 0.5f,
|
|
|
|
|
onValueChange: val => _logo.style.height = val,
|
|
|
|
|
ease: Ease.OutQuad);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logo.style.left = targetX;
|
|
|
|
|
_logo.style.top = targetY;
|
|
|
|
|
_logo.style.width = 100;
|
|
|
|
|
_logo.style.height = 100;
|
|
|
|
|
}
|
2026-04-28 10:25:34 +07:00
|
|
|
|
|
|
|
|
_lastInteractionTime = Time.time;
|
2026-04-27 13:27:40 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 00:07:42 +07:00
|
|
|
private void TransitionToIdle()
|
2026-04-26 05:20:47 +07:00
|
|
|
{
|
2026-04-28 10:25:34 +07:00
|
|
|
if (_currentState == MenuState.Idle) return;
|
2026-04-28 00:07:42 +07:00
|
|
|
_currentState = MenuState.Idle;
|
2026-04-28 10:11:28 +07:00
|
|
|
|
2026-04-29 02:31:15 +07:00
|
|
|
// Quay lại dùng phần trăm để tự động căn giữa
|
2026-04-29 01:04:28 +07:00
|
|
|
Tween.Custom(_logo.resolvedStyle.width, 200f, duration: 0.5f, onValueChange: val => _logo.style.width = val, ease: Ease.OutQuad);
|
|
|
|
|
Tween.Custom(_logo.resolvedStyle.height, 200f, duration: 0.5f, onValueChange: val => _logo.style.height = val, ease: Ease.OutQuad);
|
2026-04-29 02:31:15 +07:00
|
|
|
|
|
|
|
|
// Animate left/top về 50%
|
|
|
|
|
float startLeft = _logo.resolvedStyle.left;
|
|
|
|
|
float startTop = _logo.resolvedStyle.top;
|
|
|
|
|
float targetLeft = root.resolvedStyle.width / 2f;
|
|
|
|
|
float targetTop = root.resolvedStyle.height / 2f;
|
|
|
|
|
|
|
|
|
|
Tween.Custom(0f, 1f, duration: 0.5f, ease: Ease.OutQuad, onValueChange: t => {
|
|
|
|
|
_logo.style.left = Mathf.Lerp(startLeft, targetLeft, t);
|
|
|
|
|
_logo.style.top = Mathf.Lerp(startTop, targetTop, t);
|
|
|
|
|
}).OnComplete(() => {
|
|
|
|
|
ResetLogoPosition(); // Đảm bảo cuối cùng dùng đơn vị Percent
|
|
|
|
|
});
|
2026-04-28 00:07:42 +07:00
|
|
|
|
|
|
|
|
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _ribbon.style.opacity = val)
|
|
|
|
|
.OnComplete(() => _ribbon.style.display = DisplayStyle.None);
|
2026-04-26 05:20:47 +07:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 13:10:00 +07:00
|
|
|
public override void Update()
|
2026-04-26 04:39:59 +07:00
|
|
|
{
|
2026-04-28 10:44:22 +07:00
|
|
|
if (Input.GetAxis("Mouse X") != 0 || Input.GetAxis("Mouse Y") != 0 || Input.anyKey)
|
|
|
|
|
{
|
|
|
|
|
_lastInteractionTime = Time.time;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
if (_currentState == MenuState.Ribbon && Time.time - _lastInteractionTime > IDLE_TIMEOUT)
|
2026-04-28 00:07:42 +07:00
|
|
|
{
|
2026-04-28 10:11:28 +07:00
|
|
|
TransitionToIdle();
|
2026-04-28 00:07:42 +07:00
|
|
|
}
|
2026-04-26 04:39:59 +07:00
|
|
|
}
|
2026-04-26 05:20:47 +07:00
|
|
|
|
2026-04-28 10:11:28 +07:00
|
|
|
private void StartPulse()
|
2026-04-26 05:20:47 +07:00
|
|
|
{
|
2026-04-29 01:04:28 +07:00
|
|
|
if (_pulseTween.isAlive) _pulseTween.Stop();
|
|
|
|
|
_pulseTween = Tween.Custom(Vector3.one, Vector3.one * 1.1f, duration: 0.8f,
|
|
|
|
|
onValueChange: val => _logo.style.scale = new StyleScale(new Scale(val)),
|
|
|
|
|
cycles: -1,
|
|
|
|
|
cycleMode: CycleMode.Yoyo,
|
|
|
|
|
ease: Ease.InOutSine);
|
2026-04-26 05:20:47 +07:00
|
|
|
}
|
2026-05-01 17:57:07 +07:00
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
{
|
|
|
|
|
if (LocalizationManager.Instance != null)
|
|
|
|
|
{
|
|
|
|
|
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-25 18:20:16 +07:00
|
|
|
}
|
|
|
|
|
}
|