This commit is contained in:
2026-04-29 02:31:15 +07:00
parent 21c999a904
commit ed86fface3
12 changed files with 433 additions and 79 deletions

View File

@@ -38,6 +38,13 @@ namespace Hallucinate.UI
}
}
protected string GetLoc(string key)
{
if (LocalizationManager.Instance != null)
return LocalizationManager.Instance.GetLocalizedString(key);
return key;
}
public virtual async Task PlayTransitionIn()
{
if (root == null) return;

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Hallucinate.UI
{
public class LocalizationManager : MonoBehaviour
{
public static LocalizationManager Instance { get; private set; }
private Dictionary<string, string> _localizedText = new Dictionary<string, string>();
private string _currentLanguage = "en";
public event Action OnLanguageChanged;
[Serializable]
private class LocalizationData
{
public List<LocalizationEntry> items;
}
[Serializable]
private class LocalizationEntry
{
public string key;
public string value;
}
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
LoadLanguage(PlayerPrefs.GetString("Language", "en"));
}
else
{
Destroy(gameObject);
}
}
public void LoadLanguage(string langCode)
{
_currentLanguage = langCode;
TextAsset jsonAsset = Resources.Load<TextAsset>($"Localization/{langCode}");
if (jsonAsset != null)
{
// Vì JsonUtility không hỗ trợ Dictionary, chúng ta parse tay một chút nếu file là JSON object phẳng
// Hoặc giả định file JSON có cấu trúc phù hợp.
// Ở đây tôi sẽ dùng giải pháp parse đơn giản cho JSON phẳng { "key": "value" }
ParseFlatJson(jsonAsset.text);
PlayerPrefs.SetString("Language", langCode);
PlayerPrefs.Save();
OnLanguageChanged?.Invoke();
Debug.Log($"[Localization] Loaded language: {langCode}");
}
else
{
Debug.LogError($"[Localization] Language file not found: Localization/{langCode}");
}
}
private void ParseFlatJson(string json)
{
_localizedText.Clear();
// Xóa các ký tự thừa
json = json.Trim().Trim('{', '}');
string[] pairs = json.Split(new[] { "\",", "\n" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
string[] kv = pair.Split(new[] { "\":\"" }, StringSplitOptions.None);
if (kv.Length == 2)
{
string key = kv[0].Trim().Trim('"', ' ', '\t', '\r');
string val = kv[1].Trim().Trim('"', ' ', '\t', '\r');
_localizedText[key] = val;
}
}
}
public string GetLocalizedString(string key)
{
if (_localizedText != null && _localizedText.TryGetValue(key, out string value))
{
return value;
}
return key;
}
public string CurrentLanguage => _currentLanguage;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ff7ac0ce8c8c98445b895ac53a4618f1

View File

@@ -36,6 +36,9 @@ namespace Hallucinate.UI
return;
}
// Lắng nghe sự kiện thay đổi kích thước toàn màn hình
root.RegisterCallback<GeometryChangedEvent>(OnScreenResize);
_logo.RegisterCallback<ClickEvent>(OnLogoClicked);
var settingsBtn = root.Q<Button>("SettingsBtn");
@@ -51,12 +54,28 @@ namespace Hallucinate.UI
_lastInteractionTime = Time.time;
}
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();
}
}
private void ResetLogoPosition()
{
if (_logo == null) return;
_logo.style.translate = new StyleTranslate(new Translate(0, 0));
_logo.style.left = (Screen.width / 2f) - 100;
_logo.style.top = (Screen.height / 2f) - 100;
// 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)));
_logo.style.width = 200;
_logo.style.height = 200;
}
@@ -132,34 +151,55 @@ namespace Hallucinate.UI
_ribbon.style.display = DisplayStyle.Flex;
Tween.Custom(0f, 1f, duration: 0.3f, onValueChange: val => _ribbon.style.opacity = val);
// Đợi một frame để Ribbon layout xong rồi mới lấy vị trí LogoSpace
_logoSpace.RegisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
}
private void OnLogoSpaceReady(GeometryChangedEvent evt)
{
_logoSpace.UnregisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
UpdateLogoToSpace(true);
}
private void UpdateLogoToSpace(bool animate = false)
{
Rect targetBounds = _logoSpace.worldBound;
if (targetBounds.width <= 0) return;
// Center logo in LogoSpace (within the centered Ribbon)
float targetX = targetBounds.x + (targetBounds.width / 2f) - 50f;
float targetY = targetBounds.y + (targetBounds.height / 2f) - 50f;
// 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));
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);
float targetX = localPos.x + (targetBounds.width / 2f);
float targetY = localPos.y + (targetBounds.height / 2f);
Tween.Custom(_logo.resolvedStyle.width, 100f, duration: 0.5f,
onValueChange: val => _logo.style.width = val,
ease: Ease.OutQuad);
// 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)));
Tween.Custom(_logo.resolvedStyle.height, 100f, duration: 0.5f,
onValueChange: val => _logo.style.height = val,
ease: Ease.OutQuad);
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;
}
_lastInteractionTime = Time.time;
}
@@ -169,13 +209,22 @@ namespace Hallucinate.UI
if (_currentState == MenuState.Idle) return;
_currentState = MenuState.Idle;
float targetX = (Screen.width / 2f) - 100;
float targetY = (Screen.height / 2f) - 100;
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);
// Quay lại dùng phần trăm để tự động căn giữa
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);
// 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
});
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _ribbon.style.opacity = val)
.OnComplete(() => _ribbon.style.display = DisplayStyle.None);

View File

@@ -6,7 +6,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine.SceneManagement;
using OnlyScove.Scripts; // Namespace của InputReader
using OnlyScove.Scripts;
namespace Hallucinate.UI
{
@@ -28,16 +28,12 @@ namespace Hallucinate.UI
_tabTitle = root.Q<Label>("TabTitle");
_content = root.Q<ScrollView>("SettingsContent");
// Ưu tiên 1: Lấy từ InputReader đã gán trong UIManager
_inputActions = uiManager.InputReader?.InputActions;
// Ưu tiên 2: Nếu null, thử tìm PlayerInput trong scene
if (_inputActions == null)
{
_inputActions = GameObject.FindAnyObjectByType<PlayerInput>()?.actions;
}
// Click outside to close
root.RegisterCallback<PointerDownEvent>(evt => {
if (evt.target == root)
{
@@ -53,7 +49,6 @@ namespace Hallucinate.UI
var closeBtn = root.Q<Button>("CloseSettingsBtn");
if (closeBtn != null) closeBtn.clicked += () => uiManager.ToggleSettings();
// Default tab
SwitchTab("GENERAL");
}
@@ -72,7 +67,6 @@ namespace Hallucinate.UI
_activeTab = tabId;
_tabTitle.text = tabId;
// Update tab styles
foreach (var kvp in _tabButtons)
{
if (kvp.Key == tabId) kvp.Value.AddToClassList("active-tab");
@@ -99,12 +93,13 @@ namespace Hallucinate.UI
private void RenderGeneralSettings()
{
var section = CreateSection("ACCOUNT & DATA");
// --- ACCOUNT ---
_content.Add(CreateSection(GetLoc("settings_general")));
var wipeBtn = new Button { text = "WIPE ALL USER DATA (TEST ONLY)" };
wipeBtn.AddToClassList("button-spring");
wipeBtn.AddToClassList("btn-exit");
wipeBtn.style.marginTop = 20;
wipeBtn.style.marginTop = 10;
wipeBtn.style.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 0.8f);
wipeBtn.clicked += () => {
@@ -116,17 +111,105 @@ namespace Hallucinate.UI
{
PlayerPrefs.DeleteAll();
PlayerPrefs.Save();
Debug.Log("[Settings] Data wiped. Restarting...");
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
};
_content.Add(section);
_content.Add(wipeBtn);
_content.Add(new Label("\nNote: This will force the game to ask for your name again on next launch.") {
style = { fontSize = 12, color = new Color(0.6f, 0.6f, 0.6f), whiteSpace = WhiteSpace.Normal, marginTop = 10 }
// --- LANGUAGE ---
_content.Add(CreateSection(GetLoc("label_language")));
var langContainer = new VisualElement();
langContainer.style.flexDirection = FlexDirection.Row;
langContainer.style.alignItems = Align.Center;
langContainer.style.marginTop = 10;
var langLabel = new Label(GetLoc("label_language"));
langLabel.AddToClassList("text-body");
langLabel.style.width = Length.Percent(40);
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" }, LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
langDropdown.style.flexGrow = 1;
langDropdown.RegisterValueChangedCallback(evt => {
string code = evt.newValue == "Tiếng Việt" ? "vi" : "en";
LocalizationManager.Instance?.LoadLanguage(code);
// Refresh current tab to update text
SwitchTab("GENERAL");
});
langContainer.Add(langLabel);
langContainer.Add(langDropdown);
_content.Add(langContainer);
// --- INTERFACE ---
_content.Add(CreateSection("INTERFACE"));
float currentScale = PlayerPrefs.GetFloat("UIScale", 1.0f);
var scaleRow = CreateSliderWithInput("UI SCALE", 0.5f, 2.0f, currentScale, (val) => {
uiManager.SetUIScale(val);
});
_content.Add(scaleRow);
_content.Add(new Label("\nNote: Some elements may require restart to align perfectly.") {
style = { fontSize = 12, color = new Color(0.6f, 0.6f, 0.6f), whiteSpace = WhiteSpace.Normal, marginTop = 20 }
});
}
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.marginTop = 10;
row.style.marginBottom = 10;
var label = new Label(labelText);
label.AddToClassList("text-body");
label.style.width = Length.Percent(35);
var slider = new Slider(min, max);
slider.style.flexGrow = 1;
slider.value = startVal;
var input = new TextField();
input.style.width = 60;
input.style.marginLeft = 15;
input.value = startVal.ToString("F1");
input.AddToClassList("input-field");
input.style.marginBottom = 0; // Override default margin
input.style.height = 30;
input.style.fontSize = 14;
// Sync Slider -> Input
slider.RegisterValueChangedCallback(evt => {
float val = Mathf.Round(evt.newValue * 10f) / 10f;
// Kiểm tra xem input có đang được focus không để tránh ghi đè khi người dùng đang nhập
bool isInputFocused = input.panel?.focusController?.focusedElement == input.ElementAt(0);
if (!isInputFocused) input.value = val.ToString("F1");
OnValueChanged?.Invoke(val);
});
// Sync Input -> Slider
input.RegisterValueChangedCallback(evt => {
if (float.TryParse(evt.newValue, out float val))
{
val = Mathf.Clamp(val, min, max);
slider.value = val;
OnValueChanged?.Invoke(val);
}
});
// Format on blur
input.RegisterCallback<BlurEvent>(evt => {
if (float.TryParse(input.value, out float val))
{
input.value = Mathf.Clamp(val, min, max).ToString("F1");
}
});
row.Add(label);
row.Add(slider);
row.Add(input);
return row;
}
private void RenderControlSettings()
@@ -142,7 +225,6 @@ namespace Hallucinate.UI
var playerMap = _inputActions.FindActionMap("Player");
if (playerMap == null) return;
// Categories
RenderSection("MOVEMENT", playerMap, new[] { "Move", "Jump", "Sprint", "Crouch" });
RenderSection("COMBAT", playerMap, new[] { "Attack" });
RenderSection("INTERACTION", playerMap, new[] { "Interact", "Next", "Previous" });
@@ -155,10 +237,7 @@ namespace Hallucinate.UI
foreach (var name in actionNames)
{
var action = map.FindAction(name);
if (action != null)
{
_content.Add(CreateRebindRow(action));
}
if (action != null) _content.Add(CreateRebindRow(action));
}
}
@@ -179,10 +258,7 @@ namespace Hallucinate.UI
var rebindBtn = new Button();
rebindBtn.AddToClassList("rebind-button");
// Get current binding text
UpdateBindingText(action, rebindBtn);
rebindBtn.clicked += () => StartRebind(action, rebindBtn);
row.Add(label);
@@ -201,19 +277,17 @@ namespace Hallucinate.UI
string oldText = btn.text;
btn.text = "...";
btn.style.color = Color.yellow;
action.Disable();
var rebindOperation = action.PerformInteractiveRebinding()
.WithControlsExcluding("<Mouse>/delta") // Don't bind to mouse movement
.WithControlsExcluding("<Mouse>/delta")
.WithControlsExcluding("<Mouse>/scroll")
.OnMatchWaitForAnother(0.1f)
.OnComplete(operation => {
btn.style.color = new Color(0f, 1f, 0.8f); // Reset color
btn.style.color = new Color(0f, 1f, 0.8f);
UpdateBindingText(action, btn);
action.Enable();
operation.Dispose();
// Save bindings here if you have a save system
})
.OnCancel(operation => {
btn.style.color = new Color(0f, 1f, 0.8f);
@@ -243,7 +317,6 @@ namespace Hallucinate.UI
{
await Tween.Custom(0f, -100f, duration: 0.4f, ease: Ease.InQuad,
onValueChange: val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
Hide();
}
}

View File

@@ -61,11 +61,32 @@ namespace Hallucinate.UI
private float _trailOpacity = 1f;
private bool _isSettingsOpen = false;
private const string UI_SCALE_KEY = "UIScale";
private void Awake()
{
Instance = this;
_uiDocument = GetComponent<UIDocument>();
UnityEngine.Cursor.visible = false;
ApplySavedUIScale();
}
public void SetUIScale(float scale)
{
if (_uiDocument == null || _uiDocument.panelSettings == null) return;
// Unity UI Toolkit dùng panelSettings để điều khiển tỉ lệ
// Chúng ta thay đổi scale multiplier
_uiDocument.panelSettings.scale = scale;
PlayerPrefs.SetFloat(UI_SCALE_KEY, scale);
PlayerPrefs.Save();
}
private void ApplySavedUIScale()
{
float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f);
SetUIScale(savedScale);
}
private void Start()
@@ -213,6 +234,13 @@ namespace Hallucinate.UI
for (int i = 0; i < trailLength * trailSpacing + 1; i++) _posHistory.Add(startPos);
}
private float GetCurrentScale()
{
if (_uiDocument != null && _uiDocument.panelSettings != null)
return _uiDocument.panelSettings.scale;
return 1.0f;
}
private void OnGlobalClick(PointerDownEvent evt)
{
if (!enableRipples || _cursorLayer == null) return;
@@ -238,6 +266,7 @@ namespace Hallucinate.UI
ripple.style.borderLeftWidth = 2;
ripple.style.borderRightWidth = 2;
// PointerDownEvent.localPosition đã được Unity tự động scale theo Panel
ripple.style.left = evt.localPosition.x;
ripple.style.top = evt.localPosition.y;
ripple.pickingMode = PickingMode.Ignore;
@@ -271,7 +300,10 @@ namespace Hallucinate.UI
}
_cursorLayer.style.display = DisplayStyle.Flex;
Vector2 uiPos = new Vector2(mousePos.x, Screen.height - mousePos.y);
// QUAN TRỌNG: Chia tọa độ pixel cho scale của UI để có tọa độ local chính xác
float scale = GetCurrentScale();
Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale);
float mouseSpeed = Vector2.Distance(uiPos, _lastMousePos);
_lastMousePos = uiPos;