Organize custom scripts and Shared under Assets/Scripts, and delete assembly definition files

This commit is contained in:
2026-07-01 20:36:56 +07:00
parent befc19bf37
commit 01048074ee
183 changed files with 180 additions and 3456 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 92ace9da10dc8bd49a47cbdb18f8d052
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using UnityEngine;
public enum NodeState
{
Success, Failure, Running
}
public abstract class Node
{
protected NodeState state;
public NodeState State => state;
public abstract NodeState Evaluate();
}
public class Selector : Node
{
protected List<Node> nodes = new List<Node>(); // children nodes
public Selector(List<Node> nodes)
{
this.nodes = nodes;
}
public override NodeState Evaluate()
{
foreach (var node in nodes)
{
switch (node.Evaluate())
{
case NodeState.Failure:
continue;
case NodeState.Success:
state = NodeState.Success;
return state;
case NodeState.Running:
state = NodeState.Running;
return state;
}
}
state = NodeState.Failure;
return state;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05bb68bbe2862134ab45f5267ec4b6bb

View File

@@ -0,0 +1,49 @@
using UnityEngine;
using TMPro;
using PrimeTween;
namespace Baba_yaga.UI
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class ChatBubble : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI textDisplay;
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private RectTransform bubbleRect;
private Transform mainCameraTransform;
private void Awake()
{
if (canvasGroup != null) canvasGroup.alpha = 0;
// gameObject.SetActive(false); // Bỏ dòng này để tránh tắt nhầm NPC gốc
}
private void LateUpdate()
{
// Tìm Camera nếu chưa có (Tránh lỗi Null nếu Camera chưa spawn hoặc bị xóa)
if (mainCameraTransform == null)
{
if (Camera.main != null) mainCameraTransform = Camera.main.transform;
else return;
}
// Billboard effect
transform.LookAt(transform.position + mainCameraTransform.rotation * Vector3.forward, mainCameraTransform.rotation * Vector3.up);
}
public void Show(string text, float duration = 4f)
{
gameObject.SetActive(true);
textDisplay.text = text;
// Animation using PrimeTween
/*PrimeTween.Sequence.Create()
.Group(Tween.Alpha(canvasGroup, 1f, 0.3f))
.Group(Tween.Scale(bubbleRect, Vector3.zero, Vector3.one, 0.4f, Ease.OutBack))
.Chain(Tween.Delay(duration))
.Chain(Tween.Alpha(canvasGroup, 0f, 0.5f))
.OnComplete(() => gameObject.SetActive(false));*/
}
}
}

View File

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

View File

@@ -0,0 +1,52 @@
using System.Collections;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
[Range(0, 360)]
public float viewAngle = 90f;
public float viewRadius = 20f;
public LayerMask obstacleLayerMask;
public LayerMask targetLayerMask;
[HideInInspector] public bool canSeePlayer = false;
[HideInInspector] public Vector3 lastKnownPlayerPosition;
void Start()
{
StartCoroutine(FindTargetWithDelay(0.1f));
}
IEnumerator FindTargetWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTargets();
}
}
private void FindVisibleTargets()
{
canSeePlayer = false;
var colliders = Physics.OverlapSphere(transform.position, viewRadius, targetLayerMask);
for (int i = 0; i < colliders.Length; i++)
{
var target = colliders[i].transform;
var direction = (target.position - transform.position).normalized;
var angle = Vector3.Angle(transform.forward, direction);
if (angle < viewAngle / 2)
{
float distanceToTarget = Vector3.Distance(transform.position, target.position);
if (!Physics.Raycast(transform.position, direction, distanceToTarget, obstacleLayerMask))
{
canSeePlayer = true;
lastKnownPlayerPosition = target.position;
Debug.DrawLine(transform.position, target.position, Color.blue, 1f);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 210b37cfe4a84a34a91d0a9e58856a60

View File

@@ -0,0 +1,135 @@
using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace Baba_yaga.AI
{
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class Part { public string text; }
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class Content { public Part[] parts; }
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class Candidate { public Content content; }
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class GeminiResponse { public Candidate[] candidates; }
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class GeminiService : MonoBehaviour
{
public static GeminiService Instance { get; private set; }
private int activeRequests = 0;
private const int MAX_CONCURRENT_REQUESTS = 5;
[SerializeField] private string[] apiKeys = { "YOUR_KEY_1", "YOUR_KEY_2" };
private int currentKeyIndex = 0;
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
private float nextRequestTime = 0f;
private string[] fallbackDialogues = {
"{ \"text\": \"Nice weather, isn't it?\", \"speedMod\": 0.0, \"suspicionMod\": -5.0, \"aggressionMod\": 0.0, \"braveryMod\": 0.0, \"healthMod\": 0.0 }",
"{ \"text\": \"Did you hear something? Probably just a rat.\", \"speedMod\": 0.0, \"suspicionMod\": 2.0, \"aggressionMod\": 0.1, \"braveryMod\": -5.0, \"healthMod\": 0.0 }",
"{ \"text\": \"I'm so tired of this shift.\", \"speedMod\": -0.2, \"suspicionMod\": 0.0, \"aggressionMod\": -0.1, \"braveryMod\": 5.0, \"healthMod\": 0.0 }",
"{ \"text\": \"You looks strong, I should be careful.\", \"speedMod\": 0.1, \"suspicionMod\": 5.0, \"aggressionMod\": -0.2, \"braveryMod\": 10.0, \"healthMod\": 0.0 }"
};
private void Awake()
{
if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); }
else { Destroy(gameObject); }
}
private string GetNextKey()
{
if (apiKeys == null || apiKeys.Length == 0) return "";
string key = apiKeys[currentKeyIndex];
currentKeyIndex = (currentKeyIndex + 1) % apiKeys.Length;
return key;
}
public void GetResponse(string persona, string prompt, Action<string> onComplete)
{
if (Time.time < nextRequestTime)
{
Debug.LogWarning("[Gemini] API is cooling down. Using fallback.");
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
return;
}
if (activeRequests >= MAX_CONCURRENT_REQUESTS)
{
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
return;
}
StartCoroutine(PostRequest(persona, prompt, onComplete));
}
private IEnumerator PostRequest(string persona, string prompt, Action<string> onComplete)
{
activeRequests++;
string jsonInstruction = " Respond ONLY with a JSON object: { " +
"'text': 'dialogue content', " +
"'speedMod': 0.0 (change movement speed), " +
"'suspicionMod': 0.0 (change suspicion level), " +
"'aggressionMod': 0.0 (0.1 to 0.5 makes NPC shoot faster, negative makes them slower), " +
"'braveryMod': 0.0 (positive makes them less likely to panic, negative makes them scared), " +
"'healthMod': 0.0 (positive heals NPC, negative damages them) " +
"}. Keep values realistic.";
string escapedPersona = persona.Replace("\"", "\\\"");
string escapedPrompt = prompt.Replace("\"", "\\\"");
var jsonBody = $@"{{
""systemInstruction"": {{""parts"": [{{ ""text"": ""{escapedPersona} {jsonInstruction}"" }}]}},
""contents"": [{{""parts"": [{{ ""text"": ""{escapedPrompt}"" }}]}}],
""generationConfig"": {{
""maxOutputTokens"": 150,
""temperature"": 0.8,
""responseMimeType"": ""application/json""
}}
}}";
var requestURL = $"{geminiURL}?key={GetNextKey()}";
using (var request = new UnityWebRequest(requestURL, "POST"))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson<GeminiResponse>(request.downloadHandler.text);
if (response?.candidates?.Length > 0 && response.candidates[0].content?.parts?.Length > 0)
{
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
}
}
else
{
Debug.LogError($"[Gemini] API Error: {request.error}");
if (request.responseCode == 429)
{
nextRequestTime = Time.time + 60f;
}
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
}
}
activeRequests--;
}
}
}

View File

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

View File

@@ -0,0 +1,198 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class KamikazeAI : MonoBehaviour
{
[Header("References")]
public Transform player;
[Header("Detection")]
public float detectRange = 15f;
private bool canSeePlayer = false;
[Header("Movement & Random Patrol")]
public float patrolSpeed = 2.5f;
public float chaseSpeed = 7f;
public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên
public float patrolWaitTime = 2f; // Thời gian đứng nghỉ trước khi đổi sang điểm ngẫu nhiên mới
private Vector3 startPosition; // Tâm của khu vực tuần tra (Vị trí ban đầu)
private float currentWaitTime;
private NavMeshAgent agent;
private bool isExploding = false;
public Node behaviorTreeRoot;
public GameObject explosionEffectPrefab;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
// Lưu lại vị trí xuất phát để làm tâm, NPC sẽ chỉ đi loay hoay quanh khu vực này
startPosition = transform.position;
InitBehaviorTree();
}
private void Update()
{
if (isExploding) return;
if (player == null) FindPlayer();
else CheckVision();
behaviorTreeRoot?.Evaluate();
}
private void FindPlayer()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null) player = playerObj.transform;
}
private void CheckVision()
{
if (Vector3.Distance(transform.position, player.position) <= detectRange)
canSeePlayer = true;
else
canSeePlayer = false;
}
private void InitBehaviorTree()
{
var explodeSequence = new Sequence(new List<Node>
{
new TaskNode(CheckIsCloseEnoughToExplode),
new TaskNode(ActionTriggerExplosion)
});
var chaseSequence = new Sequence(new List<Node>
{
new TaskNode(CheckCanSeePlayer),
new TaskNode(ActionChase)
});
// Hành động tuần tra ngẫu nhiên
var patrolNode = new TaskNode(ActionRandomPatrol);
behaviorTreeRoot = new Selector(new List<Node>
{
explodeSequence,
chaseSequence,
patrolNode
});
}
#region CONDITIONS
private NodeState CheckCanSeePlayer()
{
return canSeePlayer ? NodeState.Success : NodeState.Failure;
}
private NodeState CheckIsCloseEnoughToExplode()
{
if (player == null) return NodeState.Failure;
float dist = Vector3.Distance(transform.position, player.position);
return dist <= 3f ? NodeState.Success : NodeState.Failure;
}
#endregion
#region ACTIONS
// HÀM TUẦN TRA NGẪU NHIÊN MỚI
private NodeState ActionRandomPatrol()
{
// Debug.Log("Wandering randomly...");
agent.isStopped = false;
agent.speed = patrolSpeed;
// Kiểm tra xem NPC đã đi đến điểm ngẫu nhiên hiện tại chưa
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
{
currentWaitTime += Time.deltaTime;
// Đứng đợi hết thời gian quy định rồi mới tìm đường mới
if (currentWaitTime >= patrolWaitTime)
{
// 1. Lấy một điểm ngẫu nhiên trong không gian hình cầu dựa trên bán kính
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
randomDirection += startPosition; // Cộng với tâm ban đầu để giới hạn khu vực
NavMeshHit hit;
// 2. Ép tọa độ ngẫu nhiên đó phải nằm TRÊN bề mặt xanh của NavMesh (tránh kẹt tường)
// Số 1 ở cuối là Area Mask (thường là Walkable)
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
{
agent.SetDestination(hit.position);
}
currentWaitTime = 0f; // Reset thời gian chờ
}
}
return NodeState.Running;
}
private NodeState ActionChase()
{
if (player == null) return NodeState.Failure;
Debug.Log("Kamikaze is rushing you!");
agent.isStopped = false;
agent.speed = chaseSpeed;
agent.SetDestination(player.position);
return NodeState.Running;
}
private NodeState ActionTriggerExplosion()
{
StartCoroutine(ExplosionRoutine());
return NodeState.Success;
}
#endregion
#region EXPLOSION LOGIC
private IEnumerator ExplosionRoutine()
{
isExploding = true;
agent.isStopped = true;
agent.velocity = Vector3.zero;
Debug.Log("BOMB ARMED!");
yield return new WaitForSeconds(1.5f);
if (player != null)
{
float distToPlayer = Vector3.Distance(transform.position, player.position);
if (distToPlayer <= 4f)
{
Debug.Log("BOOM! Player took damage!");
}
}
if (explosionEffectPrefab != null)
{
Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
}
Destroy(gameObject);
}
#endregion
// Vẽ vùng giới hạn tuần tra màu xanh lá cây trên Scene để bạn dễ căn chỉnh độ rộng
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
// Nếu game đang chạy thì vẽ quanh tâm startPosition, nếu chưa chạy thì vẽ quanh vị trí hiện tại
Vector3 center = Application.isPlaying ? startPosition : transform.position;
Gizmos.DrawWireSphere(center, patrolRadius);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6008ec58fb909034abd7293b55f0d558

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
public class Sequence : Node
{
private List<Node> nodes = new List<Node>();
public Sequence(List<Node> nodes)
{
this.nodes = nodes;
}
public override NodeState Evaluate()
{
var isAnyChildRunning = false;
foreach (var node in nodes)
{
switch (node.Evaluate())
{
case NodeState.Failure:
state = NodeState.Failure;
return state;
case NodeState.Success:
continue;
case NodeState.Running:
isAnyChildRunning = true;
continue;
}
}
state = isAnyChildRunning ? NodeState.Running : NodeState.Success;
return state;
}
}
public class TaskNode : Node
{
public delegate NodeState TaskDelegate();
private TaskDelegate action;
public TaskNode(TaskDelegate action)
{
this.action = action;
}
public override NodeState Evaluate()
{
return action();
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1f390a3d7b8c9eb49ac7d779c08ef5f5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 962e9e0d2b8d78d4fbb25fb03224f618
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 42e6f48f26d671a42a9b398d02557c1f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
#if false
using System.Collections.Generic;
using Fusion;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class _LobbyManager : MonoBehaviour
{
public GameObject lobbyPanel;
public _BasicSpawner spawner;
[Header("Character Selection")] public TMP_InputField playerNameInput;
[Header("Room List")] public GameObject roomListParent;
public GameObject roomListItemPrefab;
public TMP_InputField roomNameInput;
// Start is called once before the first execution of Update after the MonoBehaviour is created
async void Start()
{
lobbyPanel.SetActive(false);
spawner = FindFirstObjectByType<_BasicSpawner>();
await spawner.StartLobby();
}
public void OnNextButton()
{
var playerName = playerNameInput.text;
if (string.IsNullOrEmpty(playerName))
{
Debug.LogWarning("Player name cannot be empty!");
return;
}
// tạo 1 player _profile tạm thời, sau này sẽ gửi lên server để tạo player object
var _profile = new _PlayerProfile()
{
Name = playerName,
};
spawner.SetLocalPlayerProfile(_profile);
// đưa lên host để tạo player object, ở đây tạm thời chỉ log ra console
Debug.Log($"Player Name: {_profile.Name}, Class: {_profile.Role}");
// chuyển sang lobby panel
lobbyPanel.SetActive(true);
}
// hiển thị danh sách phòng
public void DisplayRoomList(List<SessionInfo> sessions)
{
Debug.Log($"Received {sessions.Count} sessions from lobby");
// clear danh sách cũ
foreach (Transform child in roomListParent.transform)
{
Destroy(child.gameObject);
}
if (sessions.Count == 0) return;
// tạo item mới cho mỗi phòng
foreach (var session in sessions)
{
var item = Instantiate(roomListItemPrefab, roomListParent.transform);
var text = item.GetComponentInChildren<TextMeshProUGUI>();
text.text = $"{session.Name} ({session.PlayerCount}/{session.MaxPlayers})";
var button = item.GetComponentInChildren<Button>();
button.onClick.AddListener(() => OnJoinRoom(session.Name));
item.SetActive(true);
}
}
async void OnJoinRoom(string sessionName)
{
await spawner.StartClient(sessionName);
}
public async void OnCreateRoomButton()
{
var roomName = roomNameInput.text;
if (string.IsNullOrEmpty(roomName))
{
Debug.LogWarning("Room name cannot be empty!");
return;
}
// tạo phòng mới với tên đã nhập
await spawner.StartHost(roomName, SceneRef.FromIndex(1));
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 258164a5e282e34489a3c62c443c22f0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1a38893b1d8574b45bce269c39824bd6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using Fusion;
namespace Baba_yaga.Game
{
[System.Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Game", sourceAssembly: "Opsive.UltimateCharacterController")]
public struct PlayerEloData
{
public int Rating;
public int GamesPlayed;
public PlayerEloData(int rating, int gamesPlayed)
{
Rating = rating;
GamesPlayed = gamesPlayed;
}
public static PlayerEloData Default => new PlayerEloData(1000, 0);
}
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Game", sourceAssembly: "Opsive.UltimateCharacterController")]
public struct EloResult : INetworkStruct
{
public int NewRatingA;
public int NewRatingB;
public int DeltaA;
public int DeltaB;
public EloResult(int nA, int nB, int dA, int dB)
{
NewRatingA = nA;
NewRatingB = nB;
DeltaA = dA;
DeltaB = dB;
}
}
}

View File

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

View File

@@ -0,0 +1,64 @@
using UnityEngine;
namespace Baba_yaga.Game
{
/// <summary>
/// Pure logic for Elo rating calculations.
/// Follows the 1v1 competitive formula with dynamic K-factor.
/// </summary>
public static class EloSystem
{
public const int RATING_FLOOR = 100;
public const int PLACEMENT_GAMES = 30;
public static EloResult Calculate(
int ratingA, int ratingB,
int gamesPlayedA, int gamesPlayedB,
float resultA) // 1=win, 0=lose, 0.5=draw
{
// 1. Expected Scores
float eA = 1f / (1f + Mathf.Pow(10f, (ratingB - ratingA) / 400f));
float eB = 1f - eA;
// 2. K-Factors
int kA = GetK(ratingA, gamesPlayedA);
int kB = GetK(ratingB, gamesPlayedB);
// 3. New Ratings
int nA = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingA + kA * (resultA - eA)));
int nB = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingB + kB * ((1f - resultA) - eB)));
return new EloResult(nA, nB, nA - ratingA, nB - ratingB);
}
private static int GetK(int rating, int gamesPlayed)
{
if (gamesPlayed < PLACEMENT_GAMES) return 40;
if (rating < 1200) return 32;
if (rating < 2000) return 24;
return 16;
}
public static string GetRank(int rating)
{
if (rating < 800) return "Iron";
if (rating < 1000) return "Bronze";
if (rating < 1200) return "Silver";
if (rating < 1500) return "Gold";
if (rating < 1800) return "Platinum";
if (rating < 2100) return "Diamond";
return "Master";
}
public static string GetRankColor(int rating)
{
if (rating < 800) return "#8A8A8A"; // Iron
if (rating < 1000) return "#CD7F32"; // Bronze
if (rating < 1200) return "#C0C0C0"; // Silver
if (rating < 1500) return "#FFD700"; // Gold
if (rating < 1800) return "#4DC8A0"; // Platinum
if (rating < 2100) return "#7B6EE8"; // Diamond
return "#E84D8A"; // Master
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 366843378ad652a41aeb5972186ebe18

View File

@@ -0,0 +1,114 @@
using Fusion;
using Baba_yaga.Game;
using Baba_yaga.UI;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace Baba_yaga.Network
{
/// <summary>
/// Orchestrates the Elo calculation and persistence on the Host.
/// Broadcasts results to all clients.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MatchEloManager : NetworkBehaviour
{
[Networked] public EloResult LastMatchResult { get; set; }
[Networked] public bool IsCalculating { get; set; }
private Dictionary<PlayerRef, string> _playerUsernames = new Dictionary<PlayerRef, string>();
// Simulating the database locally instead of Firebase
private static Dictionary<string, PlayerEloData> _localEloDatabase = new Dictionary<string, PlayerEloData>();
private Task<PlayerEloData> GetLocalPlayerData(string username)
{
if (!_localEloDatabase.TryGetValue(username, out var data))
{
data = PlayerEloData.Default;
_localEloDatabase[username] = data;
}
return Task.FromResult(data);
}
private Task SaveLocalPlayerData(string username, PlayerEloData data)
{
_localEloDatabase[username] = data;
return Task.CompletedTask;
}
public override void Spawned()
{
if (Object.HasStateAuthority)
{
// In a real scenario, you'd collect usernames as players join.
// For now, we assume they are provided or stored in PlayerRef custom data.
// Placeholder: Use a mock or collect from Session properties.
}
}
/// <summary>
/// Registers a player's username when they join.
/// </summary>
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_RegisterUsername(PlayerRef player, string username)
{
if (!_playerUsernames.ContainsKey(player))
{
_playerUsernames.Add(player, username);
Debug.Log($"[EloManager] Registered {username} for {player}");
}
}
/// <summary>
/// Called by GameManager on Host when match ends.
/// </summary>
public async void ProcessMatchResult(PlayerRef winner, PlayerRef loser, bool isDraw = false)
{
if (!Object.HasStateAuthority) return;
IsCalculating = true;
string nameA = _playerUsernames.GetValueOrDefault(winner, "Unknown_A");
string nameB = _playerUsernames.GetValueOrDefault(loser, "Unknown_B");
// 1. Fetch from local simulated database
var dataA = await GetLocalPlayerData(nameA);
var dataB = await GetLocalPlayerData(nameB);
// 2. Calculate
float resultA = isDraw ? 0.5f : 1.0f;
var result = EloSystem.Calculate(dataA.Rating, dataB.Rating, dataA.GamesPlayed, dataB.GamesPlayed, resultA);
// 3. Update Data Objects
dataA.Rating = result.NewRatingA;
dataA.GamesPlayed++;
dataB.Rating = result.NewRatingB;
dataB.GamesPlayed++;
// 4. Save to local simulated database
await Task.WhenAll(
SaveLocalPlayerData(nameA, dataA),
SaveLocalPlayerData(nameB, dataB)
);
// 5. Broadcast
LastMatchResult = result;
IsCalculating = false;
Debug.Log($"[EloManager] Match Processed. Winner: {nameA} (+{result.DeltaA}), Loser: {nameB} ({result.DeltaB})");
// Send RPC to show UI
RPC_NotifyClients(result);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
private void RPC_NotifyClients(EloResult result)
{
// This is where you'd trigger the Post-Match Rive/UI
Debug.Log($"[Client] Received Elo Update: {result.DeltaA} / {result.DeltaB}");
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b562adae77c550e4db1edcabf68b0530
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,171 @@
using UnityEngine;
using System.Text;
namespace Baba_yaga.GameSetup
{
[RequireComponent(typeof(CharacterController))]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts.GameSetup", sourceAssembly: "Opsive.UltimateCharacterController")]
public class CharacterAutoSetup : MonoBehaviour
{
[Header("Manual Overrides (If Detection Fails)")]
[SerializeField] private float defaultHeight = 1.8f;
[SerializeField] private float defaultShoulderWidth = 0.4f;
[Header("Settings")]
[SerializeField] private Transform modelRoot;
[SerializeField] private bool autoDetectOnStart = true;
[SerializeField] private float zCenterOffset = 0.05f;
private void Start()
{
if (autoDetectOnStart)
{
ApplyAutoSetup();
}
}
[ContextMenu("Apply Auto Setup")]
public void ApplyAutoSetup()
{
CharacterController controller = GetComponent<CharacterController>();
// PlayerStateMachine stateMachine = GetComponent<PlayerStateMachine>();
Animator animator = GetComponentInChildren<Animator>();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"<color=#4FC3F7><b>[AUTO-SETUP REPORT]</b></color> Character: <b>{gameObject.name}</b>");
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
// 1. HEIGHT DETECTION
float finalHeight = defaultHeight;
string heightMethod = "Default Fallback";
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Head) != null)
{
heightMethod = "Humanoid Bones (Feet to Head)";
Transform head = animator.GetBoneTransform(HumanBodyBones.Head);
// We measure from the local Y=0 (feet) to the head bone and add 10% for the skull/hair
float headHeight = transform.InverseTransformPoint(head.position).y;
finalHeight = headHeight * 1.12f; // 12% extra for the top of the skull
}
else
{
heightMethod = "Local Mesh Bounds";
Bounds localBounds = GetRelativeBounds();
if (localBounds.size.y > 0) finalHeight = localBounds.size.y;
}
sb.AppendLine(string.Format("<b>1. HEIGHT:</b> {0:F3}m ({1}) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, heightMethod, finalHeight));
// 2. RADIUS DETECTION
float shoulderWidth = defaultShoulderWidth;
string radiusMethod = "Default Fallback";
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Hips) != null)
{
radiusMethod = "Humanoid Bones (Shoulders)";
Transform leftArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
Transform rightArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
if (leftArm != null && rightArm != null)
{
float distance = Vector3.Distance(leftArm.position, rightArm.position);
shoulderWidth = distance * 1.2f; // Add 20% for arm/shoulder thickness
}
}
else
{
radiusMethod = "Bounds Width Fallback";
Bounds b = GetRelativeBounds();
shoulderWidth = b.size.x * 0.25f;
}
float finalRadius = shoulderWidth / 2f;
sb.AppendLine(string.Format("<b>2. RADIUS:</b> {0:F3}m ({1}) [/ 2] ➔ <color=#81C784>{2:F3}m</color>", shoulderWidth, radiusMethod, finalRadius));
// 3. COLLISION PRECISION
float skinWidth = finalRadius * 0.10f;
sb.AppendLine(string.Format("<b>3. SKIN WIDTH:</b> {0:F3}m (Radius) [x 0.10] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, skinWidth));
// 4. CAPSULE CENTER
// Center Y = (Height / 2) + SkinWidth
float centerY = (finalHeight / 2f) + skinWidth;
sb.AppendLine(string.Format("<b>4. CENTER Y:</b> ({0:F3}m / 2) + {1:F3}m (Skin) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, skinWidth, centerY));
sb.AppendLine(string.Format("<b>5. CENTER Z:</b> [Fixed Offset] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
// 5. MOVEMENT CONSTRAINTS
float stepOffset = finalHeight * 0.15f;
sb.AppendLine(string.Format("<b>6. STEP OFFSET:</b> {0:F3}m (Height) [x 0.15] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, stepOffset));
// 6. GROUND CHECK (STATE MACHINE)
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
// if (stateMachine != null)
// {
// float groundCheckRadius = finalRadius * 0.6f;
// float groundCheckOffsetY = finalHeight / 24f;
// Vector3 groundCheckOffset = new Vector3(0, groundCheckOffsetY, zCenterOffset);
//
// stateMachine.SetGroundCheck(groundCheckRadius, groundCheckOffset);
//
// sb.AppendLine("<b>7. GROUND CHECK SETUP:</b>");
// sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, groundCheckRadius));
// sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, groundCheckOffsetY));
// sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
// }
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
// Apply to Controller
controller.height = finalHeight;
controller.radius = finalRadius;
controller.skinWidth = skinWidth;
controller.center = new Vector3(0, centerY, zCenterOffset);
controller.slopeLimit = 45f;
controller.stepOffset = stepOffset;
controller.minMoveDistance = 0.001f;
Debug.Log(sb.ToString());
}
private Bounds GetRelativeBounds()
{
Transform targetRoot = modelRoot != null ? modelRoot : transform;
Renderer[] renderers = targetRoot.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return new Bounds(Vector3.zero, Vector3.zero);
// Using local bounds of SkinnedMeshRenderers for better accuracy on animated characters
Bounds combinedLocalBounds = new Bounds();
bool first = true;
foreach (Renderer renderer in renderers)
{
if (renderer is ParticleSystemRenderer) continue;
Bounds localB;
if (renderer is SkinnedMeshRenderer smr)
{
localB = smr.localBounds;
}
else
{
// For static meshes, convert world bounds back to local root space
Vector3 min = transform.InverseTransformPoint(renderer.bounds.min);
Vector3 max = transform.InverseTransformPoint(renderer.bounds.max);
localB = new Bounds((min + max) / 2f, max - min);
}
if (first)
{
combinedLocalBounds = localB;
first = false;
}
else
{
combinedLocalBounds.Encapsulate(localB);
}
}
return combinedLocalBounds;
}
}
}

View File

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

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace Baba_yaga.GameSetup
{
[CreateAssetMenu(fileName = "CharacterSetupSettings", menuName = "BABA_YAGA/Setup/Character Setup Settings")]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts.GameSetup", sourceAssembly: "Opsive.UltimateCharacterController")]
public class CharacterSetupSettings : ScriptableObject
{
[Header("Movement Constraints")]
public float slopeLimit = 45f;
[Range(0.01f, 0.5f)] public float stepHeightRatio = 0.15f; // Step offset as % of height
[Header("Precision & Collision")]
public float skinWidthRatio = 0.1f; // Skin width as % of radius
public float minMoveDistance = 0.001f;
[Header("Dimension Multipliers")]
[Tooltip("Multiplies the detected shoulder width to define Radius.")]
public float radiusMultiplier = 0.8f;
[Tooltip("Multiplies the detected bounding box height.")]
public float heightMultiplier = 1.0f;
[Header("Center Offset")]
[Tooltip("Y-axis offset for the center of the capsule (0.5 means exact middle).")]
public float centerYRatio = 0.5f;
}
}

View File

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

View File

@@ -0,0 +1,42 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga
{
[CreateAssetMenu(fileName = "GameSettings", menuName = "BABA_YAGA/Settings/GameSettings")]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
public class GameSettings : ScriptableObject
{
[BoxGroup("Camera")]
[PropertyRange(0.1f, 10f)]
public float sensitivity = 1.0f;
[BoxGroup("Camera")]
public bool invertX = false;
[BoxGroup("Camera")]
public bool invertY = false;
[BoxGroup("Camera")]
public bool sideBiasRight = true; // true for Right, false for Left
[BoxGroup("Camera")]
[PropertyRange(40f, 110f)]
public float fieldOfView = 60f;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Camera")]
private string SideBias => sideBiasRight ? "Right" : "Left";
[Button("Reset Defaults")]
private void ResetDefaults()
{
sensitivity = 1.0f;
invertX = false;
invertY = false;
sideBiasRight = true;
fieldOfView = 60f;
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e6a10948eca4f3f4eaeda0611c778875
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
using System.Collections;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class CrawlerAlgorithm : IMazeAlgorithm
{
private const int CrawlChance = 50;
private const int MinBoundary = 1;
private const int VerticalCrawlerCount = 3;
private const int HorizontalCrawlerCount = 3;
public void Generate(MazeGrid grid)
{
for (int i = 0; i < VerticalCrawlerCount; i++) CrawlV(grid, 0);
for (int i = 0; i < HorizontalCrawlerCount; i++) CrawlH(grid, 0);
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
for (int i = 0; i < VerticalCrawlerCount; i++) yield return CrawlV(grid, cellsPerFrame);
for (int i = 0; i < HorizontalCrawlerCount; i++) yield return CrawlH(grid, cellsPerFrame);
}
private IEnumerator CrawlV(MazeGrid grid, int cellsPerFrame)
{
bool done = false;
int x = Random.Range(MinBoundary, grid.Width - MinBoundary);
int z = MinBoundary;
while (!done)
{
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
if (Random.Range(0, 100) < CrawlChance)
{
x += Random.Range(-1, 2);
}
else
{
z += Random.Range(0, 2);
}
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
}
}
private IEnumerator CrawlH(MazeGrid grid, int cellsPerFrame)
{
bool done = false;
int x = MinBoundary;
int z = Random.Range(MinBoundary, grid.Depth - MinBoundary);
while (!done)
{
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
if (Random.Range(0, 100) < CrawlChance)
{
x += Random.Range(0, 2);
}
else
{
z += Random.Range(-1, 2);
}
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
}
}
}
}

View File

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

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Baba_yaga.GameSetup.Maze.Extensions
{
public static class ListExtensions
{
private static System.Random _rng = new System.Random();
/// <summary>
/// Shuffles a list using the Fisher-Yates algorithm.
/// </summary>
public static void Shuffle<T>(this IList<T> list)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = _rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7d9791b1b03c14f16a245b2d4577c5f9

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e1bb9cd9af7ffe40ad1a740c3c30dd6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Interface for all maze generation algorithms.
/// Supports both immediate and step-by-step (animated) generation.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public interface IMazeAlgorithm
{
/// <summary>
/// Generates the maze immediately in one frame.
/// </summary>
void Generate(MazeGrid grid);
/// <summary>
/// Generates the maze step-by-step for visualization.
/// </summary>
System.Collections.IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46b6a7796ba3c494581e4dcb884da064

View File

@@ -0,0 +1,32 @@
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Represents a 2D coordinate on the maze grid.
/// Used as a lightweight value type to avoid GC allocations.
/// </summary>
public readonly struct MapLocation
{
public readonly int x;
public readonly int z;
public MapLocation(int _x, int _z)
{
x = _x;
z = _z;
}
// Static predefined directions to eliminate magic numbers in algorithms
public static MapLocation Right => new MapLocation(1, 0);
public static MapLocation Left => new MapLocation(-1, 0);
public static MapLocation Up => new MapLocation(0, 1);
public static MapLocation Down => new MapLocation(0, -1);
/// <summary>
/// Returns a list of all 4 cardinal directions.
/// </summary>
public static System.Collections.Generic.List<MapLocation> Directions => new System.Collections.Generic.List<MapLocation>
{
Right, Up, Left, Down
};
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 987a7c46c96326a44b3a5f179fe61161

View File

@@ -0,0 +1,43 @@
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Defines the state of each cell in the maze.
/// Used to replace magic numbers and drive visual changes.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum MazeCellType
{
Wall, // Solid block
Corridor, // Finalized path
Processing, // Currently being evaluated by algorithm (Debug)
Path, // Temporary path (e.g., Wilson's crawler)
Start, // Entry point
End, // Exit point
StairsUp,
StairsDown,
Room
}
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum PieceType
{
None,
Wall,
Vertical_Straight, // Đường thẳng dọc
Horizontal_Straight, // Đường thẳng ngang
Corner, // Góc cua
T_Junction, // Ngã ba
Crossroads, // Ngã tư
Stairs,
StairsUp// Cầu thang (Điểm nối)
}
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public struct MazePieceData
{
public PieceType piece; // Hình dạng mảnh ghép
// Bạn có thể thêm rotation nếu cần xoay hướng model sau này
public int rotation;
public GameObject model;
}
}

View File

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

View File

@@ -0,0 +1,84 @@
using System;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Holds the logical state of the maze grid.
/// Notifies listeners whenever a cell changes to trigger visual updates.
/// </summary>
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeGrid
{
public int Width { get; set; }
public int Depth { get; set; }
public int Level { get; set; }
public MazePieceData[,] piecePlace;
public float scale = 1f;
private readonly MazeCellType[,] _cells;
/// <summary>
/// Event fired when a cell's type is changed.
/// Useful for the Renderer to trigger animations/FX.
/// </summary>
public event Action<int, int, MazeCellType> OnCellChanged;
public MazeGrid(int width, int depth)
{
Width = width;
Depth = depth;
piecePlace = new MazePieceData[width, depth];
_cells = new MazeCellType[width, depth];
// Initialize all as walls
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
_cells[x, z] = MazeCellType.Wall;
piecePlace[x, z].piece = PieceType.Wall;
}
}
}
public void SetCell(int x, int z, MazeCellType type)
{
if (IsInBounds(x, z))
{
if (_cells[x, z] != type)
{
_cells[x, z] = type;
OnCellChanged?.Invoke(x, z, type);
}
}
}
public MazeCellType GetCell(int x, int z)
{
if (IsInBounds(x, z))
return _cells[x, z];
return MazeCellType.Wall; // Treat out of bounds as walls
}
public bool IsInBounds(int x, int z)
{
return x >= 0 && x < Width && z >= 0 && z < Depth;
}
public int CountSquareNeighbours(int x, int z, MazeCellType targetType)
{
int count = 0;
if (x <= 0 || x >= Width - 1 || z <= 0 || z >= Depth - 1) return 5;
if (GetCell(x - 1, z) == targetType) count++;
if (GetCell(x + 1, z) == targetType) count++;
if (GetCell(x, z + 1) == targetType) count++;
if (GetCell(x, z - 1) == targetType) count++;
return count;
}
}
}

View File

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

View File

@@ -0,0 +1,361 @@
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Central controller for the Maze system.
/// Manages algorithm selection, debug speed, and regeneration.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeManager : MonoBehaviour
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum AlgorithmType { Recursive, Wilsons, Prims, Crawler, NoiseRecursive }
[BoxGroup("Generation")]
[InfoBox("Set the array size to control how many maze floors are generated. Runtime grid data is rebuilt when Regenerate runs.")]
[ValidateInput(nameof(HasAtLeastOneFloor), "Maze Manager needs at least one floor.")]
public MazeGrid[] mazes;
[BoxGroup("Generation")]
[MinValue(0.001f)]
public float floorHeight = 3.5f;
[BoxGroup("Generation")]
[MinValue(0)]
public int connectionsPerFloor = 2;
[BoxGroup("Generation")]
[SerializeField] private AlgorithmType selectedAlgorithm;
[BoxGroup("Generation/Grid Size")]
[PropertyRange(5, 200)]
[SerializeField] private int width = 30;
[BoxGroup("Generation/Grid Size")]
[PropertyRange(5, 200)]
[SerializeField] private int depth = 30;
[BoxGroup("Animation")]
[SerializeField] private bool animateGeneration = true;
[BoxGroup("Animation")]
[ShowIf(nameof(animateGeneration))]
[PropertyRange(1, 500)]
[LabelText("Generation Speed (Cells/Frame)")]
[SerializeField] private int generationSpeed = 50;
public static int cellsProcessedThisFrame;
[BoxGroup("Animation")]
[ShowIf(nameof(animateGeneration))]
public MazeRenderer.CellAnimationType cellAnimationType = MazeRenderer.CellAnimationType.ScaleUp;
[BoxGroup("Progress")]
[ProgressBar(0, 100)]
[ShowInInspector, ReadOnly]
private float completionPercentage;
[BoxGroup("References")]
[Required]
[SerializeField] private MazeRenderer mazeRenderer;
[BoxGroup("Rooms (Phase 2)")]
public bool generateRooms = true;
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public int numberOfRooms = 2;
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public Vector2Int minRoomSize = new Vector2Int(2, 2);
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public Vector2Int maxRoomSize = new Vector2Int(4, 4);
[BoxGroup("References")]
[Required]
[SerializeField] private Transform mazeContainer;
[FoldoutGroup("Manhole Prefabs")]
public GameObject straightManHoleLadder;
[FoldoutGroup("Manhole Prefabs")]
public GameObject straightManHoleUp;
[FoldoutGroup("Manhole Prefabs")]
public GameObject deadendManHoleLadder;
[FoldoutGroup("Manhole Prefabs")]
public GameObject deadendManHoleUp;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int FloorCount => mazes?.Length ?? 0;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private string CurrentGrid => _grid == null ? "None" : $"{_grid.Width}x{_grid.Depth}, Level {_grid.Level}";
private MazeGrid _grid;
private Coroutine _generationCoroutine;
private HashSet<Vector3Int> _modifiedCells = new HashSet<Vector3Int>();
private void Start()
{
Regenerate();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
Regenerate();
}
}
[ContextMenu("Clear Maze")]
[Button("Clear Maze", ButtonSizes.Large)]
public void ClearMaze()
{
if (_generationCoroutine != null)
{
StopCoroutine(_generationCoroutine);
_generationCoroutine = null;
}
if (mazeRenderer != null)
{
mazeRenderer.Clear();
}
completionPercentage = 0f;
_modifiedCells?.Clear();
}
[ContextMenu("Regenerate")]
[Button("Regenerate Maze", ButtonSizes.Large)]
public void Regenerate()
{
if (mazeRenderer == null)
{
Debug.LogError("MazeManager needs a MazeRenderer reference before regenerating.", this);
return;
}
if (mazeContainer == null)
{
Debug.LogError("MazeManager needs a maze container reference before regenerating.", this);
return;
}
if (mazes == null || mazes.Length == 0)
{
mazes = new MazeGrid[1];
}
ClearMaze();
mazeRenderer.currentAnimationType = cellAnimationType;
if (animateGeneration)
{
_generationCoroutine = StartCoroutine(GenerateMazeRoutine());
}
else
{
GenerateMazeInstant();
}
}
private void GenerateMazeInstant()
{
_modifiedCells.Clear();
completionPercentage = 0f;
for (int i = 0; i < mazes.Length; i++)
{
mazes[i] = new MazeGrid(width, depth);
mazes[i].Level = i;
CarveRooms(mazes[i]);
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
algorithmForFloor.Generate(mazes[i]);
}
GenerateConnections();
for (int i = 0; i < mazes.Length; i++)
{
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
}
if (mazes.Length > 0) _grid = mazes[0];
completionPercentage = 100f;
}
private IEnumerator GenerateMazeRoutine()
{
_modifiedCells.Clear();
completionPercentage = 0f;
for (int i = 0; i < mazes.Length; i++)
{
mazes[i] = new MazeGrid(width, depth);
mazes[i].Level = i;
int floorIndex = i;
mazes[i].OnCellChanged += (x, z, type) =>
{
if (type != MazeCellType.Wall)
{
_modifiedCells.Add(new Vector3Int(x, floorIndex, z));
int totalCells = width * depth * mazes.Length;
// Approximate the progress to reach roughly 100% since algorithms don't visit all cells
float fillRatio = 0.6f;
completionPercentage = Mathf.Clamp((_modifiedCells.Count / ((float)totalCells * fillRatio)) * 100f, 0, 99f);
}
};
CarveRooms(mazes[i]);
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
cellsProcessedThisFrame = 0;
yield return StartCoroutine(algorithmForFloor.GenerateStepByStep(mazes[i], generationSpeed));
}
GenerateConnections();
if (mazes.Length > 0) _grid = mazes[0];
completionPercentage = 100f;
_generationCoroutine = null;
}
private void GenerateConnections()
{
// Step 2: Create connections between adjacent floors
for (int i = 0; i < mazes.Length - 1; i++)
{
MazeGrid currentFloor = mazes[i];
MazeGrid nextFloor = mazes[i + 1];
List<Vector2Int> possibleConnections = new List<Vector2Int>();
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
// Check if both floors have a corridor at this position
bool isCurrentFloorPath = currentFloor.GetCell(x, z) == MazeCellType.Corridor;
bool isNextFloorPath = nextFloor.GetCell(x, z) == MazeCellType.Corridor;
if (isCurrentFloorPath && isNextFloorPath)
{
possibleConnections.Add(new Vector2Int(x, z));
}
}
}
ShuffleList(possibleConnections);
int connectionsMade = 0;
foreach (Vector2Int pos in possibleConnections)
{
if (connectionsMade >= connectionsPerFloor) break;
int x = pos.x;
int z = pos.y;
// Set stair cells
currentFloor.SetCell(x, z, MazeCellType.StairsUp);
nextFloor.SetCell(x, z, MazeCellType.StairsDown);
connectionsMade++;
}
}
}
private void CarveRooms(MazeGrid grid)
{
if (!generateRooms) return;
for (int i = 0; i < numberOfRooms; i++)
{
int w = Random.Range(minRoomSize.x, maxRoomSize.x + 1);
int d = Random.Range(minRoomSize.y, maxRoomSize.y + 1);
int startX = Random.Range(1, width - w - 1);
int startZ = Random.Range(1, depth - d - 1);
for (int x = startX; x < startX + w; x++)
{
for (int z = startZ; z < startZ + d; z++)
{
grid.SetCell(x, z, MazeCellType.Room);
}
}
// Carve guaranteed door to seed pathfinding
if (Random.value > 0.5f)
{
int doorX = Random.Range(startX, startX + w);
int doorZ = Random.value > 0.5f ? startZ + d : startZ - 1;
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
}
else
{
int doorX = Random.value > 0.5f ? startX + w : startX - 1;
int doorZ = Random.Range(startZ, startZ + d);
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
}
}
}
private void ShuffleList<T>(List<T> list)
{
for (int i = 0; i < list.Count; i++)
{
T temp = list[i];
int randomIndex = Random.Range(i, list.Count);
list[i] = list[randomIndex];
list[randomIndex] = temp;
}
}
private IMazeAlgorithm GetAlgorithm(AlgorithmType type)
{
return type switch
{
AlgorithmType.Recursive => new RecursiveAlgorithm(),
AlgorithmType.Wilsons => new WilsonsAlgorithm(),
AlgorithmType.Prims => new PrimsAlgorithm(),
AlgorithmType.Crawler => new CrawlerAlgorithm(),
AlgorithmType.NoiseRecursive => new NoiseRecursiveGenerator(),
_ => new RecursiveAlgorithm()
};
}
[Button("Find Scene References")]
private void FindSceneReferences()
{
if (mazeRenderer == null)
{
mazeRenderer = GetComponentInChildren<MazeRenderer>();
}
if (mazeContainer == null)
{
mazeContainer = transform;
}
}
private bool HasAtLeastOneFloor(MazeGrid[] floors)
{
return floors != null && floors.Length > 0;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3607adabe0c29c34591af73b414eb17a

View File

@@ -0,0 +1,489 @@
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Responsible for the visual representation of the maze.
/// Handles spawning, pooling, and animations with safety checks.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeRenderer : MonoBehaviour
{
[BoxGroup("Visuals")]
[Required]
[InlineEditor]
[SerializeField] private MazeVisualProfile visualProfile;
[BoxGroup("Visuals")]
[MinValue(0.001f)]
public float floorHeight = 3.5f;
public float Scale => visualProfile != null ? visualProfile.scale : 1f;
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum CellAnimationType { None, ScaleUp, DropDown, SpinIn }
[HideInInspector]
public CellAnimationType currentAnimationType = CellAnimationType.ScaleUp;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int SpawnedCellCount => _spawnedCells.Count;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int RenderedFloorCount => _grids.Count;
private readonly Dictionary<Vector3Int, GameObject> _spawnedCells = new Dictionary<Vector3Int, GameObject>();
private Transform _container;
private List<MazeGrid> _grids = new List<MazeGrid>();
public void Initialize(MazeGrid grid, Transform container, bool clearExisting = true)
{
if (visualProfile == null)
{
Debug.LogError("MazeRenderer needs a MazeVisualProfile before it can render.", this);
return;
}
if (grid == null)
{
Debug.LogError("MazeRenderer received a null MazeGrid.", this);
return;
}
if (clearExisting)
{
Clear();
}
_container = container;
if (!_grids.Contains(grid))
{
_grids.Add(grid);
grid.OnCellChanged += (x, z, type) => HandleCellChanged(grid, x, z, type);
}
// Initial render
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
UpdateCellVisual(grid, x, z, grid.GetCell(x, z), false);
}
}
}
public void Clear()
{
StopAllCoroutines();
foreach (var cell in _spawnedCells.Values)
{
if (cell == null) continue;
if (Application.isPlaying)
Destroy(cell);
else
DestroyImmediate(cell);
}
_spawnedCells.Clear();
foreach (var grid in _grids)
{
// Note: We can't easily unsubscribe because the lambda captures 'grid'.
// In a production environment, we should use a proper event handler method.
}
_grids.Clear();
}
[Button("Clear Spawned Maze")]
private void ClearFromInspector()
{
Clear();
}
private void HandleCellChanged(MazeGrid grid, int x, int z, MazeCellType type)
{
UpdateCellVisual(grid, x, z, type, true);
UpdateNeighborVisual(grid, x + 1, z);
UpdateNeighborVisual(grid, x - 1, z);
UpdateNeighborVisual(grid, x, z + 1);
UpdateNeighborVisual(grid, x, z - 1);
}
private void UpdateNeighborVisual(MazeGrid grid, int x, int z)
{
if (grid != null && grid.IsInBounds(x, z))
{
if (IsPath(grid, x, z))
{
MazeCellType type = grid.GetCell(x, z);
UpdateCellVisual(grid, x, z, type, false);
}
}
}
private void UpdateCellVisual(MazeGrid grid, int x, int z, MazeCellType type, bool animate)
{
Vector3Int posKey = new Vector3Int(x, grid.Level, z);
if (_spawnedCells.TryGetValue(posKey, out GameObject oldObj))
{
if (oldObj != null) DestroyImmediate(oldObj);
_spawnedCells.Remove(posKey);
}
if (type == MazeCellType.Wall) return;
float logicalSpacing = visualProfile.nodeSpacing; // Distances between Nodes
float halfSpacing = logicalSpacing / 2f;
float safeScale = Mathf.Max(0.001f, visualProfile.scale);
float spacingScale = logicalSpacing * safeScale;
float yOffset = grid.Level * floorHeight;
Vector3 localPos = new Vector3(x * spacingScale, yOffset, z * spacingScale);
GameObject cellParent = new GameObject($"Cell_{x}_{grid.Level}_{z}");
cellParent.transform.SetParent(_container);
cellParent.transform.localPosition = localPos;
bool spawnedAnything = false;
if (type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
{
// 1. Spawn Node (Intersection, Corner, T, DeadEnd, or Stairs)
GameObject nodePrefab;
Quaternion nodeRot;
Vector3 nodeOffset = Vector3.zero;
if (type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
{
nodePrefab = visualProfile.GetPrefab(type);
nodeRot = Quaternion.Euler(0, visualProfile.stairsOffset, 0);
}
else
{
(nodePrefab, nodeRot, nodeOffset) = GetNodePrefabAndRotation(grid, x, z);
}
if (nodePrefab != null)
{
GameObject node = Instantiate(nodePrefab, cellParent.transform);
node.transform.localPosition = nodeOffset * safeScale;
node.transform.localRotation = nodeRot;
node.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// 2. Spawn Edge X (Right path)
if (IsPath(grid, x + 1, z))
{
GameObject edgePrefab = visualProfile.corridorStraight;
if (edgePrefab != null)
{
GameObject edgeX = Instantiate(edgePrefab, cellParent.transform);
edgeX.transform.localPosition = new Vector3(halfSpacing * safeScale, 0, 0); // half units offset right
edgeX.transform.localRotation = Quaternion.Euler(0, 90f, 0); // pointing along X
edgeX.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
// 3. Spawn Edge Z (Top path)
if (IsPath(grid, x, z + 1))
{
GameObject edgePrefab = visualProfile.corridorStraight;
if (edgePrefab != null)
{
GameObject edgeZ = Instantiate(edgePrefab, cellParent.transform);
edgeZ.transform.localPosition = new Vector3(0, 0, halfSpacing * safeScale); // half units offset forward
edgeZ.transform.localRotation = Quaternion.identity; // pointing along Z
edgeZ.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
}
else if (type == MazeCellType.Room)
{
// Spawn Floor
if (visualProfile.roomFloorPrefab != null)
{
GameObject floor = Instantiate(visualProfile.roomFloorPrefab, cellParent.transform);
floor.transform.localPosition = Vector3.zero;
floor.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// Spawn Ceiling
if (visualProfile.roomCeilingPrefab != null)
{
GameObject ceiling = Instantiate(visualProfile.roomCeilingPrefab, cellParent.transform);
ceiling.transform.localPosition = new Vector3(0, floorHeight, 0);
ceiling.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// Spawn Room Edges (Walls or Doors)
MazeCellType top = grid.IsInBounds(x, z + 1) ? grid.GetCell(x, z + 1) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, top, new Vector3(0, 0, halfSpacing * safeScale), 0f, safeScale);
MazeCellType right = grid.IsInBounds(x + 1, z) ? grid.GetCell(x + 1, z) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, right, new Vector3(halfSpacing * safeScale, 0, 0), 90f, safeScale);
MazeCellType bottom = grid.IsInBounds(x, z - 1) ? grid.GetCell(x, z - 1) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, bottom, new Vector3(0, 0, -halfSpacing * safeScale), 180f, safeScale);
MazeCellType left = grid.IsInBounds(x - 1, z) ? grid.GetCell(x - 1, z) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, left, new Vector3(-halfSpacing * safeScale, 0, 0), 270f, safeScale);
spawnedAnything = true; // Always true if it reaches here
}
else
{
// Non-corridor logic (Start, End, etc)
GameObject prefab = visualProfile.GetPrefab(type);
if (prefab != null)
{
GameObject obj = Instantiate(prefab, cellParent.transform);
obj.transform.localPosition = Vector3.zero;
obj.transform.localRotation = Quaternion.identity;
obj.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
if (!spawnedAnything)
{
DestroyImmediate(cellParent);
return;
}
_spawnedCells[posKey] = cellParent;
if (animate && visualProfile.animationDuration > 0)
{
StartCoroutine(AnimateCell(cellParent.transform));
}
}
// =================================================================================
// THUẬT TOÁN BITMASK AUTO-TILING
// =================================================================================
private (GameObject, Quaternion, Vector3) GetNodePrefabAndRotation(MazeGrid grid, int x, int z)
{
bool top = IsPath(grid, x, z + 1);
bool right = IsPath(grid, x + 1, z);
bool bottom = IsPath(grid, x, z - 1);
bool left = IsPath(grid, x - 1, z);
int mask = 0;
if (top) mask += 1;
if (right) mask += 2;
if (bottom) mask += 4;
if (left) mask += 8;
GameObject prefabToSpawn = null;
float yRotation = 0f;
Vector3 offset = Vector3.zero;
// Push dead ends to the boundary where edges end
float endOffset = visualProfile.deadEndShift;
switch (mask)
{
case 1:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 0f;
offset = new Vector3(0, 0, endOffset);
break;
case 2:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 90f;
offset = new Vector3(endOffset, 0, 0);
break;
case 4:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 180f;
offset = new Vector3(0, 0, -endOffset);
break;
case 8:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 270f;
offset = new Vector3(-endOffset, 0, 0);
break;
case 5:
prefabToSpawn = visualProfile.corridorStraight;
yRotation = 0f;
break;
case 10:
prefabToSpawn = visualProfile.corridorStraight;
yRotation = 90f;
break;
case 3:
prefabToSpawn = visualProfile.corridorCorner;
yRotation = 0f;
break;
case 6:
prefabToSpawn = visualProfile.corridorCorner;
yRotation = 90f;
break;
case 12:
prefabToSpawn = visualProfile.corridorCorner;
yRotation = 180f;
break;
case 9:
prefabToSpawn = visualProfile.corridorCorner;
yRotation = 270f;
break;
case 11:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 0f;
break;
case 7:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 90f;
break;
case 14:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 180f;
break;
case 13:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 270f;
break;
case 15:
prefabToSpawn = visualProfile.corridorCross;
yRotation = 0f;
break;
default:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 0f;
break;
}
float finalRotation = yRotation;
if (prefabToSpawn == visualProfile.corridorTJunction) finalRotation += visualProfile.tJunctionOffset;
if (prefabToSpawn == visualProfile.corridorDeadEnd) finalRotation += visualProfile.deadEndOffset;
if (prefabToSpawn == visualProfile.corridorCorner) finalRotation += visualProfile.cornerOffset;
if (prefabToSpawn == null) prefabToSpawn = visualProfile.corridorPrefab;
return (prefabToSpawn, Quaternion.Euler(0, finalRotation, 0), offset);
}
private void SpawnRoomEdge(GameObject parent, MazeCellType neighborType, Vector3 offset, float yRot, float safeScale)
{
if (neighborType == MazeCellType.Room)
return; // Open space to another room cell
GameObject prefabToSpawn = null;
if (neighborType == MazeCellType.Corridor || neighborType == MazeCellType.Processing || neighborType == MazeCellType.Start || neighborType == MazeCellType.End)
{
prefabToSpawn = visualProfile.roomDoorwayPrefab;
}
else
{
prefabToSpawn = visualProfile.roomWallPrefab;
}
if (prefabToSpawn != null)
{
GameObject edge = Instantiate(prefabToSpawn, parent.transform);
edge.transform.localPosition = offset;
edge.transform.localRotation = Quaternion.Euler(0, yRot, 0);
edge.transform.localScale = Vector3.one * safeScale;
}
}
private bool IsPath(MazeGrid grid, int x, int z)
{
if (grid == null || !grid.IsInBounds(x, z)) return false;
MazeCellType type = grid.GetCell(x, z);
return type == MazeCellType.Corridor
|| type == MazeCellType.Processing
|| type == MazeCellType.Start
|| type == MazeCellType.End
|| type == MazeCellType.Path
|| type == MazeCellType.StairsUp
|| type == MazeCellType.StairsDown;
}
// =================================================================================
// ANIMATION
// =================================================================================
private IEnumerator AnimateCell(Transform target)
{
if (target == null) yield break;
if (currentAnimationType == CellAnimationType.None) yield break;
float duration = Mathf.Max(0.01f, visualProfile.animationDuration);
float elapsed = 0;
Vector3 finalScale = target.localScale;
Vector3 finalPosition = target.localPosition;
Quaternion finalRotation = target.localRotation;
if (currentAnimationType == CellAnimationType.ScaleUp)
{
target.localScale = Vector3.one * 0.001f;
}
else if (currentAnimationType == CellAnimationType.DropDown)
{
target.localPosition = finalPosition + Vector3.up * 5f;
}
else if (currentAnimationType == CellAnimationType.SpinIn)
{
target.localScale = Vector3.one * 0.001f;
target.localRotation = finalRotation * Quaternion.Euler(0, 180, 0);
}
while (elapsed < duration)
{
if (target == null) yield break;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
if (currentAnimationType == CellAnimationType.ScaleUp)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localScale = finalScale * Mathf.Max(0.001f, s);
}
else if (currentAnimationType == CellAnimationType.DropDown)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localPosition = Vector3.Lerp(finalPosition + Vector3.up * 5f, finalPosition, s);
}
else if (currentAnimationType == CellAnimationType.SpinIn)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localScale = finalScale * Mathf.Max(0.001f, s);
target.localRotation = Quaternion.Lerp(finalRotation * Quaternion.Euler(0, 180, 0), finalRotation, s);
}
yield return null;
}
if (target != null)
{
target.localScale = finalScale;
target.localPosition = finalPosition;
target.localRotation = finalRotation;
}
}
}
}

View File

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

View File

@@ -0,0 +1,147 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[CreateAssetMenu(fileName = "MazeVisualProfile", menuName = "BABA_YAGA/Maze/Visual Profile")]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeVisualProfile : ScriptableObject
{
[BoxGroup("Rotation Offsets")]
public float tJunctionOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float cornerOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float deadEndOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float stairsOffset = 0f;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject wallPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject corridorPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject processingPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject pathPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject startPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject endPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject stairUpPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject stairDownPrefab;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorStraight;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorCorner;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorTJunction;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorCross;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorDeadEnd;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomFloorPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomWallPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomCeilingPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomDoorwayPrefab;
[BoxGroup("Visualization")]
[MinValue(0.001f)]
public float scale = 0.167f;
[BoxGroup("Visualization")]
[MinValue(1f)]
[InfoBox("The physical distance between each grid cell. Default is 6 based on 3x3 intersections and 3x2 halls.")]
public float nodeSpacing = 6f;
[BoxGroup("Visualization")]
[InfoBox("How far to push dead-end caps from the center so they touch the hallway edge. Default is 1.0.")]
public float deadEndShift = 1.0f;
[BoxGroup("Visualization")]
[MinValue(0f)]
public float animationDuration = 0.25f;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Validation")]
private int MissingRequiredPrefabCount
{
get
{
var count = 0;
if (wallPrefab == null) count++;
if (corridorPrefab == null) count++;
if (stairUpPrefab == null) count++;
if (stairDownPrefab == null) count++;
if (corridorStraight == null) count++;
if (corridorCorner == null) count++;
if (corridorTJunction == null) count++;
if (corridorCross == null) count++;
if (corridorDeadEnd == null) count++;
return count;
}
}
public GameObject GetPrefab(MazeCellType type)
{
return type switch
{
MazeCellType.Wall => wallPrefab,
MazeCellType.Corridor => corridorPrefab,
MazeCellType.Processing => processingPrefab,
MazeCellType.Path => pathPrefab,
MazeCellType.Start => startPrefab,
MazeCellType.End => endPrefab,
MazeCellType.StairsUp => stairUpPrefab,
MazeCellType.StairsDown => stairDownPrefab,
_ => null
};
}
[Button("Log Missing Required Prefabs")]
private void LogMissingRequiredPrefabs()
{
if (MissingRequiredPrefabCount == 0)
{
Debug.Log($"{name}: all required maze prefabs are assigned.", this);
return;
}
Debug.LogWarning($"{name}: {MissingRequiredPrefabCount} required maze prefab reference(s) are missing.", this);
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 05fdc25279e7ac148a44fde646c93546
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
using System;
using System.Runtime.InteropServices;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze.Native
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze.Native", sourceAssembly: "Opsive.UltimateCharacterController")]
public class NativeNoiseProvider : IDisposable
{
private const string DLL_NAME = "BackroomsNoise";
[DllImport(DLL_NAME)]
private static extern IntPtr CreateNoiseGenerator(int seed, float frequency, int noiseType);
[DllImport(DLL_NAME)]
private static extern float GetNoiseValue(IntPtr handle, float x, float z);
[DllImport(DLL_NAME)]
private static extern void GetNoiseBuffer(IntPtr handle, float startX, float startZ, int width, int depth, float[] buffer);
[DllImport(DLL_NAME)]
private static extern void DestroyNoiseGenerator(IntPtr handle);
private IntPtr _handle;
public bool IsInitialized => _handle != IntPtr.Zero;
public NativeNoiseProvider(int seed, float frequency = 0.01f, int noiseType = 0)
{
try
{
_handle = CreateNoiseGenerator(seed, frequency, noiseType);
}
catch (DllNotFoundException)
{
Debug.LogWarning($"Native library '{DLL_NAME}' not found. Ensure it is compiled and placed in Plugins folder.");
}
}
public float GetNoise(float x, float z)
{
if (!IsInitialized) return 0f;
return GetNoiseValue(_handle, x, z);
}
public void FillBuffer(float startX, float startZ, int width, int depth, float[] buffer)
{
if (!IsInitialized || buffer == null) return;
GetNoiseBuffer(_handle, startX, startZ, width, depth, buffer);
}
public void Dispose()
{
if (IsInitialized)
{
DestroyNoiseGenerator(_handle);
_handle = IntPtr.Zero;
}
}
~NativeNoiseProvider()
{
Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fd030227a1b87a4f8826f9b317fbf87

View File

@@ -0,0 +1,159 @@
using System.Collections;
using System.Collections.Generic;
using Baba_yaga.GameSetup.Maze.Extensions;
using Baba_yaga.GameSetup.Maze.Native;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Advanced generator that combines C++ Native Noise with a Recursive Backtracking algorithm.
/// Creates a hybrid layout of large rooms and chaotic corridors.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class NoiseRecursiveGenerator : IMazeAlgorithm
{
private readonly List<MapLocation> _directions = MapLocation.Directions;
private float[] _noiseMap;
private int _seed = 1337;
// Thresholds
private const float RoomThreshold = 0.5f;
private const float CorridorThreshold = -0.3f;
private const int DeadEndNeighbourThreshold = 2;
public void Generate(MazeGrid grid)
{
InitializeNoise(grid);
// Step 1: Pre-place rooms based on noise peaks
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
float noise = GetNoiseAt(x, z, grid.Width);
if (noise > RoomThreshold)
{
grid.SetCell(x, z, MazeCellType.Corridor);
}
}
}
// Step 2: Run recursive carving in the "connectable" zones
// We start from a few points to ensure coverage
for (int i = 0; i < 5; i++)
{
int startX = Random.Range(1, grid.Width - 1);
int startZ = Random.Range(1, grid.Depth - 1);
GenerateRecursive(grid, startX, startZ);
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
InitializeNoise(grid);
// Visual feedback for noise pre-placement
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
float noise = GetNoiseAt(x, z, grid.Width);
if (noise > RoomThreshold)
{
grid.SetCell(x, z, MazeCellType.Processing);
}
}
if (z % 5 == 0) yield return null;
}
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
if (grid.GetCell(x, z) == MazeCellType.Processing)
grid.SetCell(x, z, MazeCellType.Corridor);
}
}
yield return GenerateRecursiveStepByStep(grid, 5, 5, cellsPerFrame);
}
private void InitializeNoise(MazeGrid grid)
{
_noiseMap = new float[grid.Width * grid.Depth];
using (var provider = new NativeNoiseProvider(_seed, 0.05f))
{
if (provider.IsInitialized)
{
provider.FillBuffer(0, 0, grid.Width, grid.Depth, _noiseMap);
}
else
{
// Fallback to Unity Perlin
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
_noiseMap[z * grid.Width + x] = Mathf.PerlinNoise(x * 0.1f, z * 0.1f) * 2f - 1f;
}
}
}
}
}
private float GetNoiseAt(int x, int z, int width)
{
if (_noiseMap == null) return 0f;
return _noiseMap[z * width + x];
}
private void GenerateRecursive(MazeGrid grid, int x, int z)
{
if (!grid.IsInBounds(x, z)) return;
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) return;
if (grid.GetCell(x, z) == MazeCellType.Corridor) return;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
GenerateRecursive(grid, x + dir.x, z + dir.z);
}
}
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
{
if (!grid.IsInBounds(x, z)) yield break;
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) yield break;
if (grid.GetCell(x, z) == MazeCellType.Corridor) yield break;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
yield return GenerateRecursiveStepByStep(grid, x + dir.x, z + dir.z, cellsPerFrame);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22e17049420e98f43a692fd3e7d7d261

View File

@@ -0,0 +1,112 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class PrimsAlgorithm : IMazeAlgorithm
{
private const int InitialX = 2;
private const int InitialZ = 2;
private const int MaxIterations = 10000;
private const int TargetCorridorNeighbours = 1;
public void Generate(MazeGrid grid)
{
int x = InitialX;
int z = InitialZ;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
int iterations = 0;
while (walls.Count > 0 && iterations < MaxIterations)
{
int rIndex = Random.Range(0, walls.Count);
MapLocation w = walls[rIndex];
walls.RemoveAt(rIndex);
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
{
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
{
if (!walls.Contains(nw)) walls.Add(nw);
}
}
iterations++;
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
int x = InitialX;
int z = InitialZ;
grid.SetCell(x, z, MazeCellType.Corridor);
yield return null;
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
foreach(var w in walls) grid.SetCell(w.x, w.z, MazeCellType.Processing);
int iterations = 0;
while (walls.Count > 0 && iterations < MaxIterations)
{
int rIndex = Random.Range(0, walls.Count);
MapLocation w = walls[rIndex];
walls.RemoveAt(rIndex);
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
{
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
{
if (grid.GetCell(nw.x, nw.z) == MazeCellType.Wall)
{
grid.SetCell(nw.x, nw.z, MazeCellType.Processing);
walls.Add(nw);
}
}
}
else
{
// If it's no longer a candidate, turn it back to Wall
grid.SetCell(w.x, w.z, MazeCellType.Wall);
}
iterations++;
}
}
private List<MapLocation> GetNeighbouringWalls(MazeGrid grid, int x, int z)
{
List<MapLocation> neighbours = new List<MapLocation>();
foreach (var dir in MapLocation.Directions)
{
int nx = x + dir.x;
int nz = z + dir.z;
// Correction
nx = x + dir.x;
nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
MazeCellType type = grid.GetCell(nx, nz);
if (type == MazeCellType.Wall || type == MazeCellType.Processing)
{
neighbours.Add(new MapLocation(nx, nz));
}
}
}
return neighbours;
}
}
}

View File

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

View File

@@ -0,0 +1,77 @@
using System.Collections;
using System.Collections.Generic;
using Baba_yaga.GameSetup.Maze.Extensions;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class RecursiveAlgorithm : IMazeAlgorithm
{
private const int StartX = 5;
private const int StartZ = 5;
private const int DeadEndNeighbourThreshold = 2;
private readonly List<MapLocation> _directions = MapLocation.Directions;
public void Generate(MazeGrid grid)
{
GenerateRecursive(grid, StartX, StartZ);
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
yield return GenerateRecursiveStepByStep(grid, StartX, StartZ, cellsPerFrame);
}
private void GenerateRecursive(MazeGrid grid, int x, int z)
{
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
int nx = x + dir.x;
int nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
GenerateRecursive(grid, nx, nz);
}
}
}
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
{
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
int nx = x + dir.x;
int nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
yield return GenerateRecursiveStepByStep(grid, nx, nz, cellsPerFrame);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2460c0e9379da9741b3d3387aa6c7a8e

View File

@@ -0,0 +1,182 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Wilson's Algorithm implementation based on the original provided logic.
/// Ensures paths are sparse and correctly finalized using specific neighbor constraints.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class WilsonsAlgorithm : IMazeAlgorithm
{
private const int MinBoundary = 2;
private const int MaxIterationSafety = 5000;
private const int MaxWalkSteps = 5000;
private readonly List<MapLocation> _directions = MapLocation.Directions;
private List<MapLocation> _notUsed = new List<MapLocation>();
public void Generate(MazeGrid grid)
{
// 1. Create a starting finalized cell (Type.Corridor represents state 2)
int x = Random.Range(MinBoundary, grid.Width - 1);
int z = Random.Range(MinBoundary, grid.Depth - 1);
grid.SetCell(x, z, MazeCellType.Corridor);
int safety = 0;
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
{
RandomWalkSync(grid);
safety++;
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
int x = Random.Range(MinBoundary, grid.Width - 1);
int z = Random.Range(MinBoundary, grid.Depth - 1);
grid.SetCell(x, z, MazeCellType.Corridor);
yield return null;
int safety = 0;
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
{
yield return RandomWalk(grid, cellsPerFrame);
safety++;
}
}
/// <summary>
/// Counts neighbors that are already part of the finalized maze (State 2 / Corridor).
/// </summary>
private int CountFinalizedNeighbours(MazeGrid grid, int x, int z)
{
int count = 0;
foreach (var d in _directions)
{
if (grid.GetCell(x + d.x, z + d.z) == MazeCellType.Corridor)
{
count++;
}
}
return count;
}
private int GetAvailableCells(MazeGrid grid)
{
_notUsed.Clear();
for (int z = 1; z < grid.Depth - 1; z++)
{
for (int x = 1; x < grid.Width - 1; x++)
{
if (CountFinalizedNeighbours(grid, x, z) == 0)
{
_notUsed.Add(new MapLocation(x, z));
}
}
}
return _notUsed.Count;
}
private void RandomWalkSync(MazeGrid grid)
{
if (_notUsed.Count == 0) return;
List<MapLocation> inWalk = new List<MapLocation>();
int rStartIndex = Random.Range(0, _notUsed.Count);
int cx = _notUsed[rStartIndex].x;
int cz = _notUsed[rStartIndex].z;
inWalk.Add(new MapLocation(cx, cz));
int loop = 0;
bool validPath = false;
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
{
// Mark as temporary walk (State 0 / Processing)
// Note: We don't set grid cell here in sync mode to avoid triggering events unnecessarily
// but we keep track of neighbors.
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
int nx = cx + rd.x;
int nz = cz + rd.z;
// User's original constraint: CountSquareNeighbours (nx, nz) < 2
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
{
cx = nx;
cz = nz;
inWalk.Add(new MapLocation(cx, cz));
}
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
loop++;
}
if (validPath)
{
foreach (MapLocation m in inWalk)
grid.SetCell(m.x, m.z, MazeCellType.Corridor);
}
}
private IEnumerator RandomWalk(MazeGrid grid, int cellsPerFrame)
{
if (_notUsed.Count == 0) yield break;
List<MapLocation> inWalk = new List<MapLocation>();
int rStartIndex = Random.Range(0, _notUsed.Count);
int cx = _notUsed[rStartIndex].x;
int cz = _notUsed[rStartIndex].z;
inWalk.Add(new MapLocation(cx, cz));
int loop = 0;
bool validPath = false;
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
{
grid.SetCell(cx, cz, MazeCellType.Processing); // State 0
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
int nx = cx + rd.x;
int nz = cz + rd.z;
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
{
cx = nx;
cz = nz;
inWalk.Add(new MapLocation(cx, cz));
}
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
loop++;
}
if (validPath)
{
foreach (MapLocation m in inWalk)
{
grid.SetCell(m.x, m.z, MazeCellType.Corridor); // State 2
}
}
else
{
foreach (MapLocation m in inWalk)
grid.SetCell(m.x, m.z, MazeCellType.Wall); // State 1
}
inWalk.Clear();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61156f8986612ca49a0672ad5542380f

View File

@@ -0,0 +1,15 @@
using UnityEngine;
[CreateAssetMenu(fileName = "ObjectInteraction", menuName = "BABA_YAGA/Scriptable Objects/ObjectInteraction")]
public class ObjectInteraction : ScriptableObject
{
[Header("UI Settings")]
public string promptText = "Interact";
[Header("Audio & Visuals")]
public AudioClip interactionSound;
public GameObject interactionVFX;
[Header("Settings")]
public float interactionCooldown = 0.5f;
}

View File

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

View File

@@ -0,0 +1,79 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
public class SettingsManager : MonoBehaviour
{
public static SettingsManager Instance { get; private set; }
[BoxGroup("Settings")]
[Required]
[InlineEditor]
[SerializeField] private GameSettings settings;
public GameSettings Settings => settings;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private bool IsActiveInstance => Instance == this;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
if (settings == null)
{
// Fallback or load from Resources if needed
settings = ScriptableObject.CreateInstance<GameSettings>();
}
}
else
{
Destroy(gameObject);
}
}
public void SetSensitivity(float value)
{
settings.sensitivity = value;
OnSettingsChanged?.Invoke();
}
public void SetInvertX(bool value)
{
settings.invertX = value;
OnSettingsChanged?.Invoke();
}
public void SetInvertY(bool value)
{
settings.invertY = value;
OnSettingsChanged?.Invoke();
}
public void SetSideBias(bool isRight)
{
settings.sideBiasRight = isRight;
OnSettingsChanged?.Invoke();
}
public void ToggleSideBias()
{
settings.sideBiasRight = !settings.sideBiasRight;
OnSettingsChanged?.Invoke();
}
[Button("Notify Settings Changed")]
private void NotifySettingsChanged()
{
OnSettingsChanged?.Invoke();
}
public event System.Action OnSettingsChanged;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86e70fc045fbf71469903c69f7f54e67

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f465518e0a6dc0e40982d7ffdfda36ac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a48b89f983fff0642987cca2cb779dd4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using Fusion;
using TMPro;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : NetworkBehaviour
{
public Text gameOverText; // Reference to the Game Over text UI element
private bool isGameOver = false; // Flag to check if the game is over
private void Start() {
if (gameOverText != null) {
// Ensure the Game Over text is hidden at the start of the game
gameOverText.gameObject.SetActive(false);
}
}
[SerializeField] private Baba_yaga.Network.MatchEloManager eloManager;
public void TriggerGameOver(PlayerRef winner, PlayerRef loser, bool isDraw = false) {
if (!isGameOver) {
isGameOver = true;
if (gameOverText != null) {
gameOverText.gameObject.SetActive(true);
}
// Only Host processes Elo
if (Runner.IsServer && eloManager != null) {
eloManager.ProcessMatchResult(winner, loser, isDraw);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1180f4b733c7599498f6eb4e77848e23

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f96029db35b52ba4182888a7f14d2ea7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,498 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fusion;
using Fusion.Sockets;
using UnityEngine;
using Baba_yaga;
namespace Baba_yaga.UI
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI", sourceAssembly: "Opsive.UltimateCharacterController")]
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
private static BasicSpawner _instance;
public static BasicSpawner Instance
{
get
{
if (_instance == null)
{
_instance = UnityEngine.Object.FindFirstObjectByType<BasicSpawner>();
}
return _instance;
}
}
private NetworkRunner _runner;
public NetworkRunner Runner => _runner;
private bool _isStarting = false;
private bool _isInternalShutdown = false;
public event Action<List<SessionInfo>> OnSessionListUpdatedEvent;
public event Action<string> OnShutdownEvent;
public event Action OnJoinStartedEvent;
public event Action OnJoinFailedEvent;
[Header("Prefabs")]
[SerializeField] private NetworkPrefabRef _playerPrefab;
[SerializeField] private NetworkPrefabRef _playerDataManagerPrefab;
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
// Ensure this is a root object so DontDestroyOnLoad works correctly
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
private async void Start()
{
// Auto-connect if we bypass the UI and start directly in the Main Scene
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "Main Scene")
{
Debug.Log("[BasicSpawner] Auto-starting Fusion in AutoHostOrClient mode for testing...");
if (_isStarting) return;
_isStarting = true;
try
{
await EnsureRunnerExists();
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
var result = await _runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Shared,
SessionName = "QuickTestRoom", // Hardcoded session for instant testing
SceneManager = sceneManager,
Scene = SceneRef.FromIndex(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex)
});
if (result.Ok)
{
Debug.Log("[BasicSpawner] Auto Connect SUCCESS!");
}
else
{
Debug.LogError($"[BasicSpawner] Auto Connect FAILED: {result.ShutdownReason}");
}
}
finally
{
_isStarting = false;
}
}
}
public PlayerProfile LocalPlayerProfile { get; private set; }
public void SetLocalPlayerProfile(PlayerProfile _profile)
{
LocalPlayerProfile = _profile;
}
private async Task EnsureRunnerExists()
{
if (_runner != null)
{
_isInternalShutdown = true;
try
{
if (_runner.IsRunning)
{
Debug.Log("[BasicSpawner] Shutting down existing runner before recreation.");
await _runner.Shutdown();
}
// Check if it still exists (Unity pseudo-null check)
if (_runner != null)
{
// Only log if it's actually a valid object to destroy
// If it's already marked for destruction, Unity == null will be true soon
Destroy(_runner);
}
_runner = null;
await Task.Yield();
}
finally
{
_isInternalShutdown = false;
}
}
if (this == null) return; // BasicSpawner itself might be destroyed
_runner = gameObject.GetComponent<NetworkRunner>();
if (_runner == null)
{
Debug.Log("[BasicSpawner] Creating new NetworkRunner component.");
_runner = gameObject.AddComponent<NetworkRunner>();
}
_runner.ProvideInput = true;
_runner.AddCallbacks(this);
}
public async Task StartLobby()
{
if (_isStarting) return;
// Nếu đã ở trong lobby rồi thì không cần làm gì
if (_runner != null && _runner.IsRunning && _runner.LobbyInfo.IsValid) return;
Debug.Log("[BasicSpawner] StartLobby called");
_isStarting = true;
try
{
await EnsureRunnerExists();
Debug.Log("[BasicSpawner] Joining Lobby...");
var result = await _runner.JoinSessionLobby(SessionLobby.ClientServer);
if (!result.Ok)
{
Debug.LogWarning($"Join lobby result: {result.ShutdownReason}");
}
}
finally
{
_isStarting = false;
}
}
public async Task<bool> StartHost(string sessionName, string displayName, string password = null)
{
// Wait for any existing startup process (like StartLobby) to finish
while (_isStarting)
{
await Task.Yield();
}
_isStarting = true;
try
{
Debug.Log($"[BasicSpawner] StartHost called: {sessionName} ({displayName})");
OnJoinStartedEvent?.Invoke();
bool sceneExists = false;
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCountInBuildSettings; i++)
{
if (UnityEngine.SceneManagement.SceneUtility.GetScenePathByBuildIndex(i).Contains("Main Scene"))
{
sceneExists = true;
break;
}
}
if (!sceneExists)
{
Debug.LogError("CRITICAL: 'Main Scene' is NOT in Build Settings!");
return false;
}
await EnsureRunnerExists();
var customProps = new Dictionary<string, SessionProperty>();
if (!string.IsNullOrEmpty(password))
{
customProps.Add("pw", password);
}
customProps.Add("rn", displayName);
// Re-create or find SceneManager to ensure it matches the new runner
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
var result = await _runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Host,
SessionName = sessionName,
SessionProperties = customProps,
PlayerCount = 2,
SceneManager = sceneManager
});
if (result.Ok)
{
Debug.Log("[BasicSpawner] StartHost SUCCESS");
if (_runner.IsServer && _playerDataManagerPrefab.IsValid)
{
if (FindFirstObjectByType<PlayerDataManager>() == null)
{
Debug.Log("[BasicSpawner] Spawning PlayerDataManager");
_runner.Spawn(_playerDataManagerPrefab, Vector3.zero, Quaternion.identity, null);
}
}
return true;
}
else
{
Debug.LogError($"[BasicSpawner] Fusion StartHost Failed: {result.ShutdownReason}.");
OnJoinFailedEvent?.Invoke();
return false;
}
}
finally
{
_isStarting = false;
}
}
public async Task<bool> StartClient(string sessionName, string password = null)
{
if (_isStarting) return false;
_isStarting = true;
try
{
OnJoinStartedEvent?.Invoke();
await EnsureRunnerExists();
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
var result = await _runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Client,
SessionName = sessionName,
SceneManager = sceneManager
});
if (result.Ok)
{
return true;
}
else
{
Debug.LogError($"[BasicSpawner] Fusion StartClient Failed: {result.ShutdownReason}");
OnJoinFailedEvent?.Invoke();
return false;
}
}
finally
{
_isStarting = false;
}
}
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
Debug.Log($"[BasicSpawner] PlayerJoined: {player.PlayerId}");
// In Shared Mode, there is no Server. Each client is responsible for spawning their own player.
if (player == runner.LocalPlayer)
{
SendLocalMetaData(player);
SpawnPlayer(runner, player);
}
}
private async void SendLocalMetaData(PlayerRef player)
{
PlayerDataManager pdm = null;
int retries = 0;
while (pdm == null && retries < 20)
{
pdm = FindFirstObjectByType<PlayerDataManager>();
if (pdm != null) break;
await Task.Delay(500);
retries++;
}
if (pdm != null)
{
string playerName = LocalPlayerProfile != null ? LocalPlayerProfile.Name : "Player " + player.PlayerId;
// Thêm hậu tố (HOST) nếu là server để dễ phân biệt
if (_runner.IsServer) playerName += " (HOST)";
_Role playerRole = _Role.Seeker;
var metaData = new _PlayerMetaData()
{
Name = playerName,
Role = playerRole,
IsReady = false
};
pdm.RPC_UpdatePlayerMetaData(player, metaData);
}
else
{
Debug.LogError("[BasicSpawner] Could not find PlayerDataManager after retries. Data will not sync.");
}
}
public void StartGame()
{
if (_runner != null && _runner.IsServer)
{
_runner.LoadScene("Main Scene");
}
}
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
{
runner.Despawn(networkObject);
_spawnedCharacters.Remove(player);
}
// Logic Reassign Leader (Logical)
if (runner.IsServer && PlayerDataManager.Instance != null && PlayerDataManager.Instance.Leader == player)
{
var nextLeader = runner.ActivePlayers.FirstOrDefault();
if (nextLeader != PlayerRef.None)
{
PlayerDataManager.Instance.Leader = nextLeader;
Debug.Log($"[BasicSpawner] Leader left. New logical leader: {nextLeader}");
}
}
if (runner.IsServer && player == runner.LocalPlayer)
{
runner.Shutdown();
}
}
public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason)
{
Debug.LogWarning($"[Fusion] Shutdown occurred. Reason: {shutdownReason}");
OnShutdownEvent?.Invoke(shutdownReason.ToString());
// Nếu shutdown là do hệ thống chủ động hủy để tạo runner mới, KHÔNG quay về Menu
if (_isInternalShutdown)
{
Debug.Log("[BasicSpawner] Internal shutdown detected, skipping Menu routing.");
return;
}
// Nếu đang trong quá trình Host Migration, đừng quay về menu
if (shutdownReason == ShutdownReason.HostMigration)
{
Debug.Log("[BasicSpawner] Shutdown due to Host Migration. Waiting for recovery...");
return;
}
/*if (UIManager.Instance != null)
{
UIManager.Instance.OnBackToMenu();
}*/
}
public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)
{
OnSessionListUpdatedEvent?.Invoke(sessionList);
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new PlayerInputData();
if (Baba_yaga.Network.FusionClientMovementBridge.Local != null)
{
data = Baba_yaga.Network.FusionClientMovementBridge.Local.GetLocalInputData();
}
input.Set(data);
}
public void OnConnectedToServer(NetworkRunner runner) { }
public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason) { }
public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data) { }
public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress) { }
public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
public async void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)
{
Debug.Log("[BasicSpawner] OnHostMigration triggered!");
// 1. Shutdown existing runner properly
await runner.Shutdown(false);
// 2. Create new runner
await EnsureRunnerExists();
// 3. Restart as new Host/Server using the migration token
var result = await _runner.StartGame(new StartGameArgs()
{
HostMigrationToken = hostMigrationToken,
SceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
});
/*if (result.Ok)
{
Debug.Log("[BasicSpawner] Host Migration SUCCESSFUL");
}
else
{
Debug.LogError($"[BasicSpawner] Host Migration FAILED: {result.ShutdownReason}");
UIManager.Instance?.OnBackToMenu();
}*/
}
public void OnSceneLoadDone(NetworkRunner runner)
{
string currentSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
if (runner.IsServer && currentSceneName == "Main Scene")
{
foreach (var player in runner.ActivePlayers)
{
if (!_spawnedCharacters.ContainsKey(player))
{
SpawnPlayer(runner, player);
}
}
}
/*if (currentSceneName == "Main Scene")
{
UIManager.Instance?.OnGameStarted();
/
BNM098TYU78I98IU7Y6T57U8I9I8U7Y6T57U8I7Y6T5Y67U8IU7Y6T57U8IU7Y6E4XDER45ESZXSDCER45EDSXZSDCEFR45TTRFGHJUIYTRW
}*/
}
private void SpawnPlayer(NetworkRunner runner, PlayerRef player)
{
Debug.Log($"[BasicSpawner] Spawning Player {player.PlayerId} at {Time.time}");
Vector3 spawnPosition = (player == runner.LocalPlayer) ? new Vector3(-8, 2, 0) : new Vector3(8, 2, 0);
var networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
// In Shared Mode, runner.Spawn automatically grants State Authority to the caller.
// We just need to assign Input Authority.
networkPlayerObject.AssignInputAuthority(player);
_spawnedCharacters[player] = networkPlayerObject;
}
private void OnGUI()
{
if (_runner != null && _runner.IsRunning)
{
GUI.color = Color.green;
GUI.Label(new Rect(10, 10, 300, 30), $"[Network] Session: {_runner.SessionInfo?.Name}");
GUI.Label(new Rect(10, 30, 300, 30), $"[Network] Players in Room: {_runner.ActivePlayers.Count()}");
GUI.Label(new Rect(10, 50, 300, 30), $"[Network] Am I Server?: {_runner.IsServer}");
}
}
public void OnSceneLoadStart(NetworkRunner runner) { }
}
}

View File

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

View File

@@ -0,0 +1,263 @@
using UnityEngine;
using Fusion;
using Opsive.Shared.Events;
using Opsive.UltimateCharacterController.Networking;
using Opsive.UltimateCharacterController.Networking.Character;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Character.Abilities;
using Opsive.UltimateCharacterController.Character.Abilities.Items;
using Opsive.UltimateCharacterController.Camera;
using Opsive.UltimateCharacterController.Input;
using Baba_yaga;
using Opsive.UltimateCharacterController.Game;
namespace Baba_yaga.Network
{
// Ensure Opsive components load before this
[DefaultExecutionOrder(100)]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
public class FusionClientMovementBridge : NetworkBehaviour, INetworkInfo, INetworkCharacter, ILookSource
{
public static FusionClientMovementBridge Local { get; private set; }
[Networked]
public PlayerInputData SyncInput { get; set; }
private UltimateCharacterLocomotion m_CharacterLocomotion;
private UltimateCharacterLocomotionHandler m_LocoHandler;
private Opsive.UltimateCharacterController.Input.PlayerInput m_PlayerInput;
private void Awake()
{
m_CharacterLocomotion = GetComponent<UltimateCharacterLocomotion>();
m_LocoHandler = GetComponent<UltimateCharacterLocomotionHandler>();
m_PlayerInput = GetComponent<Opsive.UltimateCharacterController.Input.PlayerInput>();
}
public override void Spawned()
{
// Because we passed State Authority to the client in BasicSpawner,
// HasStateAuthority is true ONLY for the local player.
// Check HasInputAuthority as well so the client's character correctly activates input!
bool isLocal = Object.HasStateAuthority || Object.HasInputAuthority;
if (isLocal)
{
Local = this;
}
// 1. Isolate Input: Only the local player should read keyboard/mouse inputs.
if (m_LocoHandler != null) m_LocoHandler.enabled = isLocal;
var activeInput = GetComponent<UnityInput>(); // Corrected class reference
if (activeInput != null) activeInput.enabled = isLocal;
// 2. Isolate Camera: Only attach the camera if this is the local player.
if (isLocal)
{
var cameraController = UnityEngine.Object.FindFirstObjectByType<CameraController>();
if (cameraController != null)
{
cameraController.Character = gameObject;
}
}
else
{
// For remote players, register this bridge as the LookSource
EventHandler.ExecuteEvent<ILookSource>(gameObject, "OnCharacterAttachLookSource", this);
}
}
public override void Despawned(NetworkRunner runner, bool hasState)
{
if (Local == this)
{
Local = null;
}
}
public override void FixedUpdateNetwork()
{
// ONLY attempt to get input if we are the Host (StateAuthority) or the owning Client (InputAuthority).
// Calling GetInput on a Proxy (someone else's character) is what causes the GetTypeKey exception!
if (Object.HasStateAuthority || Object.HasInputAuthority)
{
if (GetInput<PlayerInputData>(out var input))
{
// Apply input locally so the Client's character actually moves predicted!
if (m_CharacterLocomotion != null)
{
m_CharacterLocomotion.InputVector = input.InputVector;
m_CharacterLocomotion.RawInputVector = input.RawInputVector;
m_CharacterLocomotion.DeltaRotation = input.DeltaRotation;
}
if (Object.HasStateAuthority)
{
SyncInput = input;
}
}
}
// If we do NOT have StateAuthority AND we do NOT have InputAuthority, it's a Proxy (remote player).
if (Object.IsProxy)
{
// Ensure look source is attached for remote players
if (m_CharacterLocomotion != null && m_CharacterLocomotion.LookSource != (ILookSource)this)
{
EventHandler.ExecuteEvent<ILookSource>(gameObject, "OnCharacterAttachLookSource", this);
}
// Sync the movement inputs to KinematicObjectManager so it moves the remote player character
if (m_CharacterLocomotion != null && m_CharacterLocomotion.KinematicObjectIndex != -1)
{
KinematicObjectManager.SetCharacterMovementInput(
m_CharacterLocomotion.KinematicObjectIndex,
SyncInput.Direction.x,
SyncInput.Direction.y
);
}
if (m_CharacterLocomotion != null)
{
m_CharacterLocomotion.InputVector = SyncInput.InputVector;
m_CharacterLocomotion.RawInputVector = SyncInput.RawInputVector;
m_CharacterLocomotion.DeltaRotation = SyncInput.DeltaRotation;
// Sync remote abilities based on state
UpdateRemoteAbility<SpeedChange>(SyncInput.sprint);
UpdateRemoteAbility<Jump>(SyncInput.jump);
UpdateRemoteAbility<HeightChange>(SyncInput.crouch);
UpdateRemoteAbility<Aim>(SyncInput.aim);
UpdateRemoteAbility<Use>(SyncInput.use);
UpdateRemoteAbility<Reload>(SyncInput.reload);
}
}
}
private void UpdateRemoteAbility<T>(bool shouldBeActive) where T : Ability
{
if (m_CharacterLocomotion == null) return;
var ability = m_CharacterLocomotion.GetAbility<T>();
if (ability != null)
{
if (shouldBeActive && !ability.IsActive)
{
m_CharacterLocomotion.TryStartAbility(ability, true);
}
else if (!shouldBeActive && ability.IsActive)
{
m_CharacterLocomotion.TryStopAbility(ability, true);
}
}
}
public PlayerInputData GetLocalInputData()
{
var data = new PlayerInputData();
if (m_CharacterLocomotion == null) return data;
if (m_PlayerInput != null)
{
data.Direction = new Vector2(m_PlayerInput.GetAxisRaw("Horizontal"), m_PlayerInput.GetAxisRaw("Vertical"));
// Sync abilities active states or buttons
data.sprint = IsAbilityActive<SpeedChange>() || m_PlayerInput.GetButton("Change Speeds");
data.jump = IsAbilityActive<Jump>() || m_PlayerInput.GetButton("Jump");
data.crouch = IsAbilityActive<HeightChange>() || m_PlayerInput.GetButton("Crouch");
data.aim = IsAbilityActive<Aim>() || m_PlayerInput.GetButton("Aim");
data.use = IsAbilityActive<Use>() || m_PlayerInput.GetButton("Fire1");
data.reload = IsAbilityActive<Reload>() || m_PlayerInput.GetButton("Reload");
}
// Sync locomotion internal state parameters
data.InputVector = m_CharacterLocomotion.InputVector;
data.RawInputVector = m_CharacterLocomotion.RawInputVector;
data.DeltaRotation = m_CharacterLocomotion.DeltaRotation;
data.rot = transform.rotation;
// Sync Look direction and look pitch
var lookSource = m_CharacterLocomotion.LookSource;
if (lookSource != null)
{
data.LookPitch = lookSource.Pitch;
data.LookDirection = lookSource.LookDirection(true);
}
return data;
}
private bool IsAbilityActive<T>() where T : Ability
{
if (m_CharacterLocomotion == null) return false;
var ability = m_CharacterLocomotion.GetAbility<T>();
return ability != null && ability.IsActive;
}
// --- ILookSource Implementation ---
public GameObject GameObject => gameObject;
public Transform Transform => transform;
public float LookDirectionDistance => 1f;
public float Pitch => SyncInput.LookPitch;
public Vector3 LookPosition()
{
var animator = GetComponent<Animator>();
if (animator != null)
{
var head = animator.GetBoneTransform(HumanBodyBones.Head);
if (head != null) return head.position;
}
return transform.position + Vector3.up * 1.5f;
}
public Vector3 LookDirection(bool characterLookDirection)
{
return SyncInput.LookDirection == Vector3.zero ? transform.forward : SyncInput.LookDirection;
}
public Vector3 LookDirection(Vector3 lookPosition, bool characterLookDirection, int layerMask, bool useRecoil)
{
return LookDirection(characterLookDirection);
}
// --- INetworkInfo Implementation ---
public bool IsLocalPlayer() => Object.HasStateAuthority || Object.HasInputAuthority;
public bool IsServer() => Runner.IsServer;
// Return FALSE because we are doing Client-Authoritative movement!
public bool IsServerAuthoritative() => false;
// --- INetworkCharacter Implementation ---
// Since we are using Fusion's NetworkTransform to sync position,
// we can leave these methods empty. Opsive will handle the local
// movement, and Fusion's NetworkTransform will drag the remote players.
public void SetPosition(Vector3 position, bool snapAnimator) { }
public void SetRotation(Quaternion rotation, bool snapAnimator) { }
public void SetPositionAndRotation(Vector3 position, Quaternion rotation, bool snapAnimator) { }
public void ResetRotationPosition() { }
public void SetActive(bool active, bool uiEvent) { }
public void LoadDefaultLoadout() { }
public void EquipUnequipItem(uint itemID, int slotID, bool equip) { }
public void ItemIdentifierPickup(uint id, int amount, int slot, bool immediate, bool force) { }
public void RemoveAllItems() { }
public void Fire(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, float strength) { }
public void StartItemReload(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction) { }
public void ReloadItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool fullClip) { }
public void ItemReloadComplete(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool success, bool immediateReload) { }
public void MeleeHitCollider(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, int hitboxIndex, RaycastHit raycastHit, GameObject hitGameObject, UltimateCharacterLocomotion hitCharacterLocomotion) { }
public void ThrowItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { }
public void EnableThrowableObjectMeshRenderers(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { }
public void StartStopBeginEndMagicActions(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool begin, bool start) { }
public void MagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID, Vector3 dir, Vector3 target) { }
public void MagicImpact(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, uint castID, GameObject source, GameObject target, Vector3 pos, Vector3 norm) { }
public void StopMagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID) { }
public void ToggleFlashlight(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool active) { }
public void PushRigidbody(Rigidbody rb, Vector3 force, Vector3 point) { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39343fbd597ed5947b34fa2777fa1b7c

View File

@@ -0,0 +1,74 @@
using Fusion;
using UnityEngine;
using Baba_yaga.Game;
using Baba_yaga.UI;
using System.Linq;
namespace Baba_yaga.Network
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MatchResultManager : NetworkBehaviour
{
public static MatchResultManager Instance { get; private set; }
public override void Spawned()
{
if (Object.HasStateAuthority) Instance = this;
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_BroadcastResult(PlayerRef winner, EloResult eloResult)
{
Debug.Log($"Game Over! Winner: {winner}. Elo updated.");
// Update local Elo display and show Result UI
if (Runner.LocalPlayer == winner)
{
ShowResultUI(true, eloResult.DeltaA, eloResult.NewRatingA);
}
else
{
ShowResultUI(false, eloResult.DeltaB, eloResult.NewRatingB);
}
}
private void ShowResultUI(bool isWin, int delta, int newRating)
{
// In a real scenario, we might push a new Result screen
// For now, let's assume HUD has a result panel
Debug.Log($"RESULT: {(isWin ? "WIN" : "LOSS")} | Delta: {delta} | New Rating: {newRating}");
// Save to PlayerPrefs as a dummy "Server" persistence
PlayerPrefs.SetInt("EloRating", newRating);
int gamesPlayed = PlayerPrefs.GetInt("GamesPlayed", 0);
PlayerPrefs.SetInt("GamesPlayed", gamesPlayed + 1);
PlayerPrefs.Save();
}
public void ProcessMatchEnd(PlayerRef winner)
{
if (!Object.HasStateAuthority) return;
// Get ratings for both players
// In a real game, these would come from the server/metadata
int ratingA = PlayerPrefs.GetInt("EloRating", 1000);
int ratingB = 1000; // Placeholder for opponent
int gamesA = PlayerPrefs.GetInt("GamesPlayed", 0);
int gamesB = 0; // Placeholder
float resultA = (Runner.LocalPlayer == winner) ? 1.0f : 0.0f;
EloResult elo = EloSystem.Calculate(ratingA, ratingB, gamesA, gamesB, resultA);
RPC_BroadcastResult(winner, elo);
// Shut down runner after some delay
Invoke(nameof(ShutdownRunner), 5.0f);
}
private void ShutdownRunner()
{
Runner.Shutdown();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9eac0255d30a2bb40a43ff12cdcdf960

View File

@@ -0,0 +1,30 @@
using Fusion;
using UnityEngine;
public class PlayerData : NetworkBehaviour
{
[Networked]
public _Role PlayerRole { get; set; }
public override void Spawned()
{
if (Object.HasInputAuthority)
{
SetupByRole(PlayerRole);
}
}
void SetupByRole(_Role role)
{
if (role == _Role.Seeker)
{
Debug.Log("I am Seeker");
// bật flashlight
}
else
{
Debug.Log("I am Trapper");
// bật trap UI
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96ce77b74a34e7440a0b54af32c6d402

View File

@@ -0,0 +1,80 @@
using System;
using Fusion;
using UnityEngine;
// struct quản lý thông tin
public struct _PlayerMetaData : INetworkStruct
{
public NetworkString<_16> Name;
public _Role Role;
public NetworkBool IsReady;
}
public class PlayerDataManager : NetworkBehaviour
{
public static PlayerDataManager Instance { get; private set; }
[Networked]
public NetworkDictionary<PlayerRef, _PlayerMetaData> Players => default;
[Networked]
public PlayerRef Leader { get; set; }
public event Action<PlayerRef, string> OnChatMessageReceived;
public override void Spawned()
{
Instance = this;
if (Object.HasStateAuthority)
{
Leader = Runner.LocalPlayer;
}
}
public override void Despawned(NetworkRunner runner, bool hasState)
{
if (Instance == this) Instance = null;
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_TransferLeader(PlayerRef newLeader)
{
if (Players.ContainsKey(newLeader))
{
Leader = newLeader;
}
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_UpdatePlayerMetaData(PlayerRef playerRef, _PlayerMetaData metaData)
{
if (Object == null || !Object.IsValid) return;
Players.Set(playerRef, metaData);
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_SetReady(PlayerRef playerRef, bool ready)
{
if (Object == null || !Object.IsValid) return;
if (Players.TryGet(playerRef, out var data))
{
data.IsReady = ready;
Players.Set(playerRef, data);
}
}
[Rpc(RpcSources.All, RpcTargets.All)]
public void RPC_SendChatMessage(PlayerRef sender, string message)
{
OnChatMessageReceived?.Invoke(sender, message);
}
public bool TryGetPlayerMetaData(PlayerRef playerRef, out _PlayerMetaData metaData)
{
metaData = default;
// Kiểm tra xem object đã được Spawned chưa trước khi truy cập networked property
if (Object == null || !Object.IsValid) return false;
return Players.TryGet(playerRef, out metaData);
}
}

View File

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

View File

@@ -0,0 +1,56 @@
using Fusion;
using TMPro;
using UnityEngine;
public enum _Role
{
Seeker,
Trapper
}
[System.Serializable]
public class PlayerProfile
{
public string Name = "Player";
public _Role Role = _Role.Seeker;
}
public class PlayerInfo : NetworkBehaviour
{
[Networked] public string playerName { get; set; }
public PlayerDataManager playerDataManager;
public TextMeshProUGUI nameText;
public GameObject[] characterIcons; // mảng chứa icon tương ứng với từng class, có thể gán trong inspector
// sau khi game object được tạo ra trên mạng,
// sẽ gọi phương thức này để khởi tạo thông tin player
public override void Spawned()
{
playerDataManager = FindFirstObjectByType<PlayerDataManager>(); // tìm PlayerDataManager trong scene
}
// phương thức này sẽ được gọi mỗi frame để cập nhật thông tin hiển thị của player
public override void Render()
{
if (playerDataManager == null) return;
if (playerDataManager.TryGetPlayerMetaData(Object.InputAuthority, out var metadata))
{
var name = metadata.Name;
var charClass = metadata.Role;
if (nameText != null)
nameText.text = $"{name} ({charClass})";
if (characterIcons != null)
{
for (var i = 0; i < characterIcons.Length; i++)
{
if (characterIcons[i] != null)
characterIcons[i].SetActive(i == (int)charClass); // hiển thị icon tương ứng với class của player
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70abb536cf50f2948882e913634daedf

View File

@@ -0,0 +1,49 @@
using Fusion;
using UnityEngine;
namespace Baba_yaga
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
public struct PlayerInputData : INetworkInput, INetworkStruct
{
// Di chuyển (thường là Vector2 cho X/Y hoặc WASD)
public Vector2 Direction;
// Trạng thái chạy nhanh
public NetworkBool sprint;
// Trạng thái nhảy
public NetworkBool jump;
// Góc quay của Camera/Nhân vật (Dùng Quaternion hoặc Vector3 tùy thuộc vào PlanarRotation của bạn)
public Quaternion rot;
// Vector di chuyển đã qua xử lý (UCC CharacterLocomotion.InputVector)
public Vector2 InputVector;
// Vector di chuyển thô (UCC UltimateCharacterLocomotion.RawInputVector)
public Vector2 RawInputVector;
// Độ lệch xoay của nhân vật (UCC CharacterLocomotion.DeltaRotation)
public Vector3 DeltaRotation;
// Trạng thái ngồi (UCC HeightChange ability)
public NetworkBool crouch;
// Trạng thái ngắm bắn (UCC Aim ability)
public NetworkBool aim;
// Trạng thái sử dụng item/tấn công (UCC Use ability)
public NetworkBool use;
// Trạng thái thay đạn (UCC Reload ability)
public NetworkBool reload;
// Góc Pitch của nguồn nhìn (UCC ILookSource.Pitch)
public float LookPitch;
// Hướng nhìn của nhân vật (UCC ILookSource.LookDirection)
public Vector3 LookDirection;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 731ed4a4b6e0ae64c8194463a76646c7

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9ddac8c6596e46dfaec8a37379dbfeec
timeCreated: 1780542076

View File

@@ -0,0 +1,8 @@
using UnityEngine;
public class StickyNote : MonoBehaviour
{
[TextArea] public string noteText = "Enter note here...";
public Color noteColor = Color.yellow;
public bool showAlways = true; // Show even when not selected
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 20733ff6cdef89e408acddf8ce51503d

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e0fd09e39d1b90458b7097e555a9f3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82bfa2ea8f45df942b2e9d2855a5ff6e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
using UnityEngine;
public class TeleportTrap : MonoBehaviour {
private Vector3 GetRandomPosition() {
Vector3 randomPosition;
int attempts = 0;
do {
// Generate random X and Z positions rounded to nearest 10, then offset by 5
float x = Mathf.Round(Random.Range(-45f, 45f) / 10f) * 10f + 5f;
float z = Mathf.Round(Random.Range(-45f, 45f) / 10f) * 10f + 5f;
// Fixed Y position for traps
randomPosition = new Vector3(x, 3.5f, z);
attempts++;
if (attempts > 100) {
Debug.LogWarning("No valid position found for the death trap.");
// Exit if too many attempts are made
break;
}
}
while (
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Walls")) || // Check for walls
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Collectible")) || // Check for collectibles
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Player")) // Check for player
);
// Return a valid position for the death trap
return randomPosition;
}
private Vector3 GetPositionTeleport() {
Vector3 spawnPosition = GetRandomPosition();
return spawnPosition;
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.transform.position = GetPositionTeleport();
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5ce3d3ef991b9c34baf5cdc7e782f909
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
1. Hệ thống Core (Dễ)
* Mục tiêu: Xây dựng nền tảng cho mọi vật thể tương tác.
* Các bước:
* Hoàn thiện Interface IInteractable và lớp trừu tượng BaseInteractable.
* Tạo script PlayerInteraction sử dụng Raycast từ Camera để phát hiện vật thể và nhận lệnh nhấn phím (ví dụ: phím E).
2. UI Prompt & Âm thanh (Dễ)
* Mục tiêu: Phản hồi trực quan và âm thanh cho người chơi.
* Các bước:
* Thêm thành phần hiển thị Text vào HUD (sử dụng UIToolkit như các UI hiện tại của bạn).
* Tích hợp âm thanh từ dữ liệu ObjectInteraction (ScriptableObject) vào hàm tương tác cơ bản.
3. Highlight & Tương tác vật lý (Trung bình)
* Mục tiêu: Làm nổi bật vật thể và thực hiện các tương tác đóng/mở.
* Các bước:
* Object Highlight: Sử dụng Outline shader hoặc thay đổi Material khi người chơi nhìn vào vật thể.
* Door Interaction: Hoàn thiện logic mở/đóng cửa (kết nối với Animator hoặc script có sẵn).
* Lever/Switch: Tạo hệ thống cần gạt sử dụng UnityEvent để kích hoạt các sự kiện khác trong màn chơi.
4. Tương tác đặc biệt & Logic (Khó)
* Mục tiêu: Các cơ chế gameplay phức tạp hơn.
* Các bước:
* Fake Wall: Tường giả biến mất hoặc cho phép đi xuyên qua.
* Teleport: Dịch chuyển người chơi đến vị trí chỉ định.
* Exit: Xử lý chuyển Scene hoặc kết thúc màn chơi.
* Mirror: Hiệu ứng gương phản chiếu và logic tương tác riêng.
5. Đồng bộ Online & Kiểm thử (Cực khó)
* Mục tiêu: Đảm bảo hệ thống hoạt động trong môi trường Multiplayer.
* Các bước:
* Sử dụng Photon (RPC) để đồng bộ trạng thái vật thể (ví dụ: khi một người mở cửa, tất cả mọi người đều thấy cửa mở).
Cách thức triển khai: Chúng ta sẽ đi theo mô hình Modular. Tôi sẽ xây dựng hệ thống Core trước, sau đó mỗi loại vật thể (Cửa, Cần gạt, Tường giả...) sẽ là một Module riêng kế thừa từ Core. Điều này giúp code của bạn sạch, dễ
quản lý và dễ mở rộng sau này.

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91b95b0bf23143b68483f912b558e6f0
timeCreated: 1773383929

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 50f4c7bcd8223714fa716961ca9e3688
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,135 @@
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections.Generic;
namespace Baba_yaga.UI.Components
{
/// <summary>
/// An adjustable Vector Element that you can "sketch" and tweak directly in UI Builder (UXML).
/// Supports Parametric shapes (Pill, Polygon, Star) and Custom Paths.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI.Components", sourceAssembly: "Opsive.UltimateCharacterController")]
public class VectorShapeElement : VisualElement
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI.Components", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum ShapeType { Pill, Polygon, Star, CustomPath }
public new class UxmlFactory : UxmlFactory<VectorShapeElement, UxmlTraits> { }
public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlEnumAttributeDescription<ShapeType> m_ShapeType = new UxmlEnumAttributeDescription<ShapeType> { name = "shape-type", defaultValue = ShapeType.Pill };
UxmlColorAttributeDescription m_FillColor = new UxmlColorAttributeDescription { name = "fill-color", defaultValue = Color.white };
UxmlColorAttributeDescription m_StrokeColor = new UxmlColorAttributeDescription { name = "stroke-color", defaultValue = Color.black };
UxmlFloatAttributeDescription m_StrokeWidth = new UxmlFloatAttributeDescription { name = "stroke-width", defaultValue = 2f };
UxmlFloatAttributeDescription m_CornerRadius = new UxmlFloatAttributeDescription { name = "corner-radius", defaultValue = 10f };
UxmlIntAttributeDescription m_Sides = new UxmlIntAttributeDescription { name = "sides", defaultValue = 5 };
UxmlFloatAttributeDescription m_Inwardness = new UxmlFloatAttributeDescription { name = "star-inwardness", defaultValue = 0.5f };
UxmlStringAttributeDescription m_PathData = new UxmlStringAttributeDescription { name = "path-data", defaultValue = "" };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var ate = ve as VectorShapeElement;
ate.shapeType = m_ShapeType.GetValueFromBag(bag, cc);
ate.fillColor = m_FillColor.GetValueFromBag(bag, cc);
ate.strokeColor = m_StrokeColor.GetValueFromBag(bag, cc);
ate.strokeWidth = m_StrokeWidth.GetValueFromBag(bag, cc);
ate.cornerRadius = m_CornerRadius.GetValueFromBag(bag, cc);
ate.sides = Mathf.Max(3, m_Sides.GetValueFromBag(bag, cc));
ate.inwardness = Mathf.Clamp01(m_Inwardness.GetValueFromBag(bag, cc));
ate.pathData = m_PathData.GetValueFromBag(bag, cc);
ate.MarkDirtyRepaint();
}
}
public ShapeType shapeType { get; set; }
public Color fillColor { get; set; }
public Color strokeColor { get; set; }
public float strokeWidth { get; set; }
public float cornerRadius { get; set; }
public int sides { get; set; }
public float inwardness { get; set; }
public string pathData { get; set; }
public VectorShapeElement()
{
generateVisualContent += OnGenerateVisualContent;
}
private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
var paint = mgc.painter2D;
var rect = contentRect;
if (rect.width <= 0 || rect.height <= 0) return;
paint.BeginPath();
paint.fillColor = fillColor;
paint.strokeColor = strokeColor;
paint.lineWidth = strokeWidth;
switch (shapeType)
{
case ShapeType.Pill: DrawPill(paint, rect); break;
case ShapeType.Polygon: DrawPolygon(paint, rect, false); break;
case ShapeType.Star: DrawPolygon(paint, rect, true); break;
case ShapeType.CustomPath: DrawCustomPath(paint); break;
}
paint.ClosePath();
paint.Fill();
if (strokeWidth > 0) paint.Stroke();
}
private void DrawPill(Painter2D paint, Rect rect)
{
float r = Mathf.Min(cornerRadius, rect.width / 2f, rect.height / 2f);
paint.MoveTo(new Vector2(r, 0));
paint.LineTo(new Vector2(rect.width - r, 0));
paint.ArcTo(new Vector2(rect.width, 0), new Vector2(rect.width, r), r);
paint.LineTo(new Vector2(rect.width, rect.height - r));
paint.ArcTo(new Vector2(rect.width, rect.height), new Vector2(rect.width - r, rect.height), r);
paint.LineTo(new Vector2(r, rect.height));
paint.ArcTo(new Vector2(0, rect.height), new Vector2(0, rect.height - r), r);
paint.LineTo(new Vector2(0, r));
paint.ArcTo(new Vector2(0, 0), new Vector2(r, 0), r);
}
private void DrawPolygon(Painter2D paint, Rect rect, bool isStar)
{
Vector2 center = rect.center;
float radius = Mathf.Min(rect.width, rect.height) / 2f;
int totalPoints = isStar ? sides * 2 : sides;
float angleStep = 360f / totalPoints;
for (int i = 0; i < totalPoints; i++)
{
float angle = (i * angleStep - 90f) * Mathf.Deg2Rad;
float r = radius;
if (isStar && i % 2 != 0) r *= inwardness;
Vector2 pos = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * r;
if (i == 0) paint.MoveTo(pos);
else paint.LineTo(pos);
}
}
private void DrawCustomPath(Painter2D paint)
{
if (string.IsNullOrEmpty(pathData)) return;
string[] pairs = pathData.Split(' ');
for (int i = 0; i < pairs.Length; i++)
{
string[] coords = pairs[i].Split(',');
if (coords.Length == 2 && float.TryParse(coords[0], out float x) && float.TryParse(coords[1], out float y))
{
if (i == 0) paint.MoveTo(new Vector2(x, y));
else paint.LineTo(new Vector2(x, y));
}
}
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More