Update
This commit is contained in:
97
Assets/Scripts/Elo_System_Spec.txt
Normal file
97
Assets/Scripts/Elo_System_Spec.txt
Normal 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
|
||||
7
Assets/Scripts/Elo_System_Spec.txt.meta
Normal file
7
Assets/Scripts/Elo_System_Spec.txt.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 773d26e688e03024298a2a369cdcd1fe
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
|
||||
96
Assets/Scripts/UI/LocalizationManager.cs
Normal file
96
Assets/Scripts/UI/LocalizationManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/LocalizationManager.cs.meta
Normal file
2
Assets/Scripts/UI/LocalizationManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff7ac0ce8c8c98445b895ac53a4618f1
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user