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

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise">
<file url="file://$PROJECT_DIR$/Assets/Scripts/Elo_System_Spec.txt" charset="ISO-8859-1" />
<file url="file://$PROJECT_DIR$/Assets/Scripts/GameSetup/Maze/Crawler.cs" charset="ISO-8859-1" />
<file url="file://$PROJECT_DIR$/Assets/Scripts/GameSetup/Maze/Extensions.cs" charset="ISO-8859-1" />
<file url="file://$PROJECT_DIR$/Assets/Scripts/GameSetup/Maze/Wilsons.cs" charset="ISO-8859-1" />

View File

@@ -6,34 +6,12 @@
<component name="ChangeListManager">
<list default="true" id="f9183c68-daf0-43b8-be4c-fad79983f91b" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/UIManager.prefab" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/UIManager.prefab.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scove/UIScaleTest.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scove/UIScaleTest.unity" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Player Controller/InputReader.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Player Controller/InputReader.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/BaseUIController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/BaseUIController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/LobbyController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/LobbyController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/LoginController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/LoginController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/ProfileController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/ProfileController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/PrimeTweenInstaller.asset.meta" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/PrimeTweenInstaller.asset.meta" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/internal/PrimeTween.Installer.asmdef.meta" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/internal/PrimeTween.Installer.asmdef.meta" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/internal/PrimeTweenInstaller.cs.meta" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Third Parties/Plugins/PrimeTween/internal/PrimeTweenInstaller.cs.meta" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI Toolkit.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI Toolkit/UnityThemes.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI Toolkit/UnityThemes/UnityDefaultRuntimeTheme.tss" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI Toolkit/UnityThemes/UnityDefaultRuntimeTheme.tss.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Global.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Global.uss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Lobby.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Lobby.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/LoginPopup.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/LoginPopup.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/MainGameHUD.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainGameHUD.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/MainMenu.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainMenu.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Profile.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Profile.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/RoomItem.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/RoomItem.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Settings.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Settings.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Packages/manifest.json" beforeDir="false" afterPath="$PROJECT_DIR$/Packages/manifest.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Packages/packages-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/Packages/packages-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/MainPanelSettings.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainPanelSettings.asset" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -50,6 +28,8 @@
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/Assets/Third Parties/Photon/Fusion/Editor/Fusion.Unity.Editor.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Assets/UI/MainPanelSettings.asset" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Library/PackageCache/com.unity.render-pipelines.core@04ab0eefa0c3/Editor/Utilities/LocalizationHelper.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Library/PackageCache/com.unity.timeline@7f8b2fb101b6/Editor/Localization/Localization.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Library/PackageCache/com.wooshii.foldericons@201a18f355d3/FolderIcons/Editor/FolderIcons.cs" root0="SKIP_HIGHLIGHTING" />
</component>
<component name="McpProjectServerCommands">
@@ -169,7 +149,7 @@
<workItem from="1777269364664" duration="40284000" />
<workItem from="1777373072815" duration="1852000" />
<workItem from="1777376778745" duration="10727000" />
<workItem from="1777392719306" duration="6423000" />
<workItem from="1777392719306" duration="11529000" />
</task>
<servers />
</component>

View File

@@ -0,0 +1,97 @@
Elo Rating System
AI Handoff Specification — Hallucinate Game
1. B?i c?nh & M?c tiêu
Game Hallucinate là game PvP online 1v1 (real-time), dùng Photon Fusion / Unity Relay làm backend m?ng. H? th?ng Elo ???c yêu c?u ?? x?p h?ng ng??i ch?i sau m?i tr?n solo, hi?n th? trên màn hình Profile và ?i?u ph?i matchmaking.
Yêu c?u c?t lõi:
• Tính toán Elo sau m?i tr?n 1v1 hoàn thành
• K-factor ??ng theo s? tr?n và rating hi?n t?i
• Persist rating lên server (không ?? client t? tính)
• Tr? v? rating m?i cho c? 2 ng??i ch?i sau tr?n
• Hi?n th? lên ProfileController.cs qua data binding
2. Công th?c Elo
2.1. Expected Score
E(A) = 1 / (1 + 10 ^ ((RatingB - RatingA) / 400))
E(B) = 1 - E(A)
2.2. Rating m?i
NewRating(A) = OldRating(A) + K * (Result - E(A))
Trong ?ó Result: Th?ng = 1.0 | Thua = 0.0 | Hòa = 0.5
2.3. K-Factor ??ng
?i?u ki?n
K Value
Lý do
D??i 30 tr?n (Placement)
40
Rating ch?a ?n ??nh, c?n h?i t? nhanh
Rating < 1200
32
Tier th?p — thay ??i nhi?u h?n
1200 ? Rating < 2000
24
Tier trung bình — cân b?ng
Rating ? 2000
16
Tier cao — ?n ??nh, thay ??i ch?m
2.4. Ví d? tính toán
A (1500) vs B (1200), A th?ng:
E(A) = 1 / (1 + 10^((1200-1500)/400)) = 0.849
E(B) = 0.151
NewRating(A) = 1500 + 24*(1 - 0.849) = 1500 + 3.6 ? 1504
NewRating(B) = 1200 + 32*(0 - 0.151) = 1200 - 4.8 ? 1195
3. Rank Tiers
Rank
Rating Range
Màu g?i ý (UI)
Iron
< 800
#8A8A8A
Bronze
800 999
#CD7F32
Silver
1000 1199
#C0C0C0
Gold
1200 1499
#FFD700
Platinum
1500 1799
#4DC8A0
Diamond
1800 2099
#7B6EE8
Master
? 2100
#E84D8A
Rating kh?i ??u (m?c ??nh): 1000. Rating sàn (floor): 100 — không xu?ng d??i giá tr? này.
4. Placement Matches
• 30 tr?n ??u tiên là Placement Period (gamesPlayed < 30)
• K = 40 trong giai ?o?n này ?? rating h?i t? nhanh v? ?úng v? trí
• Tùy ch?n: không hi?n th? rank badge trong 30 tr?n ??u, ch? hi?n th? '?' ho?c 'Unranked'
• Sau tr?n 30, K-factor chuy?n sang b?ng ??ng ? m?c 2.3
5. Ki?n trúc & Lu?ng d? li?u
5.1. Nguyên t?c quan tr?ng
KHÔNG ?? client t? tính Elo r?i g?i lên. Ph?i tính trên Host/Server ?? tránh cheat.
5.2. Lu?ng x? lý
B??c
Th?c hi?n b?i
Mô t?
1
Client A & B
Tr?n k?t thúc, g?i k?t qu? lên Host qua RPC
2
Host (Photon Fusion)
Nh?n k?t qu?, xác minh h?p l?
3
Host
G?i EloSystem.Calculate() v?i rating c?a 2 ng??i

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 773d26e688e03024298a2a369cdcd1fe
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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;

View File

@@ -123,7 +123,10 @@
font-size: 16px;
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
transition-duration: 0.1s;
/* Transition mượt cho mọi thuộc tính */
transition-property: scale, background-color, translate;
transition-duration: 0.15s;
transition-timing-function: ease-out-back;
align-items: center;
justify-content: center;
}
@@ -133,6 +136,13 @@
background-color: rgba(255, 255, 255, 0.2);
}
/* Hiệu ứng nảy lò xo khi nhấn */
.button-spring:active {
scale: 0.92;
background-color: rgba(255, 255, 255, 0.3);
transition-duration: 0.05s;
}
.btn-settings { background-color: #7B6EE8; }
.btn-join { background-color: #4DC8A0; }
.btn-create { background-color: #E8834D; }

View File

@@ -21,7 +21,7 @@ MonoBehaviour:
m_ScaleMode: 1
m_ReferenceSpritePixelsPerUnit: 100
m_PixelsPerUnit: 100
m_Scale: 1
m_Scale: 1.3
m_ReferenceDpi: 96
m_FallbackDpi: 96
m_ReferenceResolution: {x: 1200, y: 800}