first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a998cf95b69f374a3371ea2215e78c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using OTG.Lab06.Characters;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Abilities
|
||||
{
|
||||
public readonly struct AbilityCastContext
|
||||
{
|
||||
public AbilityCastContext(AbilityData ability, Transform caster, PlayerStats playerStats, Vector3 origin, Vector3 forward)
|
||||
{
|
||||
Ability = ability;
|
||||
Caster = caster;
|
||||
PlayerStats = playerStats;
|
||||
Origin = origin;
|
||||
Forward = forward.sqrMagnitude <= 0.0001f ? Vector3.forward : forward.normalized;
|
||||
}
|
||||
|
||||
public AbilityData Ability { get; }
|
||||
public Transform Caster { get; }
|
||||
public PlayerStats PlayerStats { get; }
|
||||
public Vector3 Origin { get; }
|
||||
public Vector3 Forward { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b1c5987e5ece80c8bb4717d9f7eadde
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Abilities
|
||||
{
|
||||
[CreateAssetMenu(menuName = "OTG/Lab06/Ability Data", fileName = "AbilityData")]
|
||||
public sealed class AbilityData : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string abilityId;
|
||||
public string abilityName;
|
||||
[TextArea(2, 5)] public string description;
|
||||
public Sprite icon;
|
||||
|
||||
[Header("Numbers")]
|
||||
public float manaCost;
|
||||
public float cooldown;
|
||||
public float damage;
|
||||
public float castRange;
|
||||
|
||||
[Header("Behaviour")]
|
||||
public GameObject effectPrefab;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 583149b3796dedd8f900ff0e616d02ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Abilities
|
||||
{
|
||||
public abstract class AbilityEffect : MonoBehaviour
|
||||
{
|
||||
public abstract void Activate(AbilityCastContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ceef3b14322f2aa3094f34228660f0ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OTG.Lab06.Characters;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Abilities
|
||||
{
|
||||
public sealed class AbilityManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PlayerStats playerStats;
|
||||
[SerializeField] private Transform castOrigin;
|
||||
[SerializeField] private List<AbilityData> learnedAbilities = new List<AbilityData>();
|
||||
|
||||
private readonly List<AbilityRuntime> runtimes = new List<AbilityRuntime>();
|
||||
|
||||
public event Action<IReadOnlyList<AbilityRuntime>> AbilitiesChanged;
|
||||
public event Action ResourcesChanged;
|
||||
|
||||
public IReadOnlyList<AbilityRuntime> Abilities => runtimes;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (playerStats == null)
|
||||
{
|
||||
playerStats = GetComponent<PlayerStats>();
|
||||
}
|
||||
|
||||
if (castOrigin == null)
|
||||
{
|
||||
castOrigin = transform;
|
||||
}
|
||||
|
||||
RebuildRuntime();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = 0; i < runtimes.Count; i++)
|
||||
{
|
||||
runtimes[i].Tick(Time.deltaTime);
|
||||
}
|
||||
|
||||
HandleHotkeys();
|
||||
AbilitiesChanged?.Invoke(runtimes);
|
||||
}
|
||||
|
||||
public void SetLearnedAbilities(IEnumerable<AbilityData> abilities)
|
||||
{
|
||||
learnedAbilities.Clear();
|
||||
learnedAbilities.AddRange(abilities);
|
||||
RebuildRuntime();
|
||||
}
|
||||
|
||||
public bool TryCast(int abilityIndex)
|
||||
{
|
||||
if (abilityIndex < 0 || abilityIndex >= runtimes.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AbilityRuntime runtime = runtimes[abilityIndex];
|
||||
AbilityData ability = runtime.Data;
|
||||
|
||||
if (ability == null || !runtime.IsReady || playerStats == null || !playerStats.SpendMana(ability.manaCost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ability.effectPrefab == null)
|
||||
{
|
||||
Debug.LogWarning($"Ability '{ability.abilityName}' has no effect prefab.", ability);
|
||||
playerStats.RestoreMana(ability.manaCost);
|
||||
return false;
|
||||
}
|
||||
|
||||
Quaternion rotation = Quaternion.LookRotation(GetCastForward(), Vector3.up);
|
||||
GameObject effectObject = Instantiate(ability.effectPrefab, GetCastOrigin(), rotation);
|
||||
AbilityEffect effect = effectObject.GetComponent<AbilityEffect>();
|
||||
if (effect == null)
|
||||
{
|
||||
Debug.LogWarning($"Ability prefab '{ability.effectPrefab.name}' has no AbilityEffect component.", ability.effectPrefab);
|
||||
Destroy(effectObject);
|
||||
playerStats.RestoreMana(ability.manaCost);
|
||||
return false;
|
||||
}
|
||||
|
||||
var context = new AbilityCastContext(ability, transform, playerStats, GetCastOrigin(), GetCastForward());
|
||||
effect.Activate(context);
|
||||
runtime.StartCooldown();
|
||||
ResourcesChanged?.Invoke();
|
||||
AbilitiesChanged?.Invoke(runtimes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RebuildRuntime()
|
||||
{
|
||||
runtimes.Clear();
|
||||
foreach (AbilityData ability in learnedAbilities)
|
||||
{
|
||||
if (ability != null)
|
||||
{
|
||||
runtimes.Add(new AbilityRuntime(ability));
|
||||
}
|
||||
}
|
||||
|
||||
AbilitiesChanged?.Invoke(runtimes);
|
||||
}
|
||||
|
||||
private void HandleHotkeys()
|
||||
{
|
||||
for (int i = 0; i < runtimes.Count && i < 9; i++)
|
||||
{
|
||||
if (Input.GetKeyDown((KeyCode)((int)KeyCode.Alpha1 + i)))
|
||||
{
|
||||
TryCast(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetCastOrigin()
|
||||
{
|
||||
return castOrigin == null ? transform.position + Vector3.up : castOrigin.position;
|
||||
}
|
||||
|
||||
private Vector3 GetCastForward()
|
||||
{
|
||||
if (Camera.main != null)
|
||||
{
|
||||
Vector3 cameraForward = Camera.main.transform.forward;
|
||||
cameraForward.y = 0f;
|
||||
if (cameraForward.sqrMagnitude > 0.01f)
|
||||
{
|
||||
return cameraForward.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 forward = transform.forward;
|
||||
forward.y = 0f;
|
||||
return forward.sqrMagnitude <= 0.01f ? Vector3.forward : forward.normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd3c590200e1b8d059a875031b6fde6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Abilities
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class AbilityRuntime
|
||||
{
|
||||
public AbilityRuntime(AbilityData data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public AbilityData Data { get; }
|
||||
public float CooldownRemaining { get; private set; }
|
||||
public bool IsReady => CooldownRemaining <= 0f;
|
||||
public float CooldownNormalized => Data == null || Data.cooldown <= 0f ? 0f : Mathf.Clamp01(CooldownRemaining / Data.cooldown);
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (CooldownRemaining <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CooldownRemaining = Mathf.Max(0f, CooldownRemaining - deltaTime);
|
||||
}
|
||||
|
||||
public void StartCooldown()
|
||||
{
|
||||
CooldownRemaining = Data == null ? 0f : Mathf.Max(0f, Data.cooldown);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca078e83fa216cd60ae3941e64408c83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ee7ed34d5f40de199ec4c6b3b6065ec
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Characters
|
||||
{
|
||||
public sealed class ManaCrystal : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float restoreAmount = 35f;
|
||||
[SerializeField] private float respawnTime = 7f;
|
||||
[SerializeField] private GameObject visualRoot;
|
||||
|
||||
private Collider triggerCollider;
|
||||
private float cooldown;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
triggerCollider = GetComponent<Collider>();
|
||||
if (visualRoot == null && transform.childCount > 0)
|
||||
{
|
||||
visualRoot = transform.GetChild(0).gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
transform.Rotate(Vector3.up, 45f * Time.deltaTime, Space.World);
|
||||
|
||||
if (cooldown <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cooldown -= Time.deltaTime;
|
||||
if (cooldown <= 0f)
|
||||
{
|
||||
SetAvailable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
PlayerStats stats = other.GetComponentInParent<PlayerStats>();
|
||||
if (stats == null || cooldown > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stats.RestoreMana(restoreAmount);
|
||||
cooldown = respawnTime;
|
||||
SetAvailable(false);
|
||||
}
|
||||
|
||||
private void SetAvailable(bool available)
|
||||
{
|
||||
if (triggerCollider != null)
|
||||
{
|
||||
triggerCollider.enabled = available;
|
||||
}
|
||||
|
||||
if (visualRoot != null)
|
||||
{
|
||||
visualRoot.SetActive(available);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf05fce7fd259f2ef9cfdd279ef00cc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Characters
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public sealed class PlayerController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 6f;
|
||||
[SerializeField] private float rotationSpeed = 12f;
|
||||
[SerializeField] private float gravity = -24f;
|
||||
[SerializeField] private Transform cameraTransform;
|
||||
|
||||
private CharacterController characterController;
|
||||
private float verticalVelocity;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
characterController = GetComponent<CharacterController>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (cameraTransform == null && Camera.main != null)
|
||||
{
|
||||
cameraTransform = Camera.main.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
|
||||
input = Vector2.ClampMagnitude(input, 1f);
|
||||
|
||||
Vector3 forward = cameraTransform == null ? Vector3.forward : cameraTransform.forward;
|
||||
Vector3 right = cameraTransform == null ? Vector3.right : cameraTransform.right;
|
||||
forward.y = 0f;
|
||||
right.y = 0f;
|
||||
forward.Normalize();
|
||||
right.Normalize();
|
||||
|
||||
Vector3 move = forward * input.y + right * input.x;
|
||||
if (move.sqrMagnitude > 0.01f)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(move, Vector3.up);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
if (characterController.isGrounded && verticalVelocity < 0f)
|
||||
{
|
||||
verticalVelocity = -2f;
|
||||
}
|
||||
|
||||
verticalVelocity += gravity * Time.deltaTime;
|
||||
Vector3 velocity = move * moveSpeed + Vector3.up * verticalVelocity;
|
||||
characterController.Move(velocity * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d36fc8809523bc02b9b0f6db8d4ecd1b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using OTG.Lab06.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Characters
|
||||
{
|
||||
public sealed class PlayerStats : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float health = 100f;
|
||||
[SerializeField] private float mana = 100f;
|
||||
|
||||
private ResourceStat healthStat;
|
||||
private ResourceStat manaStat;
|
||||
|
||||
public event Action<float, float> HealthChanged;
|
||||
public event Action<float, float> ManaChanged;
|
||||
|
||||
public float Health { get { EnsureInitialized(); return healthStat.Current; } }
|
||||
public float MaxHealth { get { EnsureInitialized(); return healthStat.Max; } }
|
||||
public float Mana { get { EnsureInitialized(); return manaStat.Current; } }
|
||||
public float MaxMana { get { EnsureInitialized(); return manaStat.Max; } }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureInitialized();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
HealthChanged?.Invoke(Health, MaxHealth);
|
||||
ManaChanged?.Invoke(Mana, MaxMana);
|
||||
}
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
EnsureInitialized();
|
||||
healthStat.Subtract(amount);
|
||||
}
|
||||
|
||||
public void Heal(float amount)
|
||||
{
|
||||
EnsureInitialized();
|
||||
healthStat.Add(amount);
|
||||
}
|
||||
|
||||
public bool SpendMana(float amount)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return manaStat.TrySpend(amount);
|
||||
}
|
||||
|
||||
public void RestoreMana(float amount)
|
||||
{
|
||||
EnsureInitialized();
|
||||
manaStat.Add(amount);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (healthStat != null && manaStat != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
healthStat = new ResourceStat(health);
|
||||
manaStat = new ResourceStat(mana);
|
||||
healthStat.Changed += (current, max) => HealthChanged?.Invoke(current, max);
|
||||
manaStat.Changed += (current, max) => ManaChanged?.Invoke(current, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c1de8ed2b96ea7af8f9a5268fd46f43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Characters
|
||||
{
|
||||
public sealed class ThirdPersonCamera : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform target;
|
||||
[SerializeField] private float mouseSensitivity = 2.5f;
|
||||
[SerializeField] private float smoothTime = 0.12f;
|
||||
[SerializeField] private float minPitch = 45f;
|
||||
[SerializeField] private float maxPitch = 60f;
|
||||
[SerializeField] private float initialPitch = 52f;
|
||||
[SerializeField] private float distance = 14f;
|
||||
[SerializeField] private float lookAhead = 3.5f;
|
||||
[SerializeField] private float targetHeight = 1.25f;
|
||||
|
||||
private Vector3 velocity;
|
||||
private float yaw;
|
||||
private float pitch;
|
||||
|
||||
public void SetTarget(Transform newTarget)
|
||||
{
|
||||
target = newTarget;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
yaw = transform.eulerAngles.y;
|
||||
pitch = Mathf.Clamp(initialPitch, minPitch, maxPitch);
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(1))
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
else if (Input.GetMouseButtonUp(1))
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButton(1))
|
||||
{
|
||||
yaw += Input.GetAxis("Mouse X") * mouseSensitivity;
|
||||
pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity;
|
||||
pitch = Mathf.Clamp(pitch, minPitch, maxPitch);
|
||||
}
|
||||
|
||||
Vector3 forward = Quaternion.Euler(0f, yaw, 0f) * Vector3.forward;
|
||||
float pitchRadians = pitch * Mathf.Deg2Rad;
|
||||
float horizontalDistance = Mathf.Cos(pitchRadians) * distance;
|
||||
float height = Mathf.Sin(pitchRadians) * distance;
|
||||
|
||||
Vector3 desiredPosition = target.position - forward * horizontalDistance + Vector3.up * height;
|
||||
Vector3 lookTarget = target.position + forward * lookAhead + Vector3.up * targetHeight;
|
||||
|
||||
transform.position = Vector3.SmoothDamp(transform.position, desiredPosition, ref velocity, smoothTime);
|
||||
transform.rotation = Quaternion.LookRotation(lookTarget - transform.position, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77a9e5c97b39968429ddf7ef8a6d0687
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f675801d7ba5741ca372e41e02394a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
using OTG.Lab06.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Combat
|
||||
{
|
||||
public sealed class Enemy : MonoBehaviour, IDamageable
|
||||
{
|
||||
[SerializeField] private float health = 80f;
|
||||
[SerializeField] private GameObject deathEffectPrefab;
|
||||
|
||||
private bool isAlive = true;
|
||||
|
||||
public Transform Transform => transform;
|
||||
public bool IsAlive => isAlive;
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
if (!isAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
health -= Mathf.Max(0f, amount);
|
||||
FloatingDamageManager.Show(amount, transform.position + Vector3.up * 2.1f);
|
||||
|
||||
if (health <= 0f)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
isAlive = false;
|
||||
if (deathEffectPrefab != null)
|
||||
{
|
||||
Instantiate(deathEffectPrefab, transform.position + Vector3.up * 0.6f, Quaternion.identity);
|
||||
}
|
||||
|
||||
Destroy(gameObject, 0.05f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 843f0415e5497d8ebb5efafe243655cc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace OTG.Lab06.Combat
|
||||
{
|
||||
public sealed class FloatingDamage : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Text label;
|
||||
[SerializeField] private float lifetime = 1.1f;
|
||||
[SerializeField] private float riseSpeed = 1.2f;
|
||||
|
||||
private float remaining;
|
||||
|
||||
public void Initialize(float amount)
|
||||
{
|
||||
if (label == null)
|
||||
{
|
||||
label = GetComponentInChildren<Text>();
|
||||
}
|
||||
|
||||
label.text = Mathf.RoundToInt(amount).ToString();
|
||||
remaining = lifetime;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
transform.position += Vector3.up * (riseSpeed * Time.deltaTime);
|
||||
if (Camera.main != null)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(transform.position - Camera.main.transform.position, Vector3.up);
|
||||
}
|
||||
|
||||
remaining -= Time.deltaTime;
|
||||
float alpha = Mathf.Clamp01(remaining / lifetime);
|
||||
if (label != null)
|
||||
{
|
||||
Color color = label.color;
|
||||
color.a = alpha;
|
||||
label.color = color;
|
||||
}
|
||||
|
||||
if (remaining <= 0f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33319da925118fecb85b8ee57a280c93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Combat
|
||||
{
|
||||
public sealed class FloatingDamageManager : MonoBehaviour
|
||||
{
|
||||
private static FloatingDamageManager instance;
|
||||
|
||||
[SerializeField] private FloatingDamage floatingDamagePrefab;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static void Show(float amount, Vector3 position)
|
||||
{
|
||||
if (instance == null || instance.floatingDamagePrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FloatingDamage damage = Instantiate(instance.floatingDamagePrefab, position, Quaternion.identity);
|
||||
damage.Initialize(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72e42db6e80bea56d9317fe7e537f134
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6ea54aba635f7aa386d2dc640ddfe4d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Core
|
||||
{
|
||||
public interface IDamageable
|
||||
{
|
||||
Transform Transform { get; }
|
||||
bool IsAlive { get; }
|
||||
void TakeDamage(float amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3f3e434b30daf3198c7069ec1b9dc34
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Core
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class ResourceStat
|
||||
{
|
||||
[SerializeField] private float maxValue = 100f;
|
||||
[SerializeField] private float currentValue = 100f;
|
||||
|
||||
public event Action<float, float> Changed;
|
||||
|
||||
public float Current => currentValue;
|
||||
public float Max => maxValue;
|
||||
public float Normalized => maxValue <= 0f ? 0f : currentValue / maxValue;
|
||||
|
||||
public ResourceStat(float maxValue)
|
||||
{
|
||||
this.maxValue = Mathf.Max(1f, maxValue);
|
||||
currentValue = this.maxValue;
|
||||
}
|
||||
|
||||
public void Initialize(float value)
|
||||
{
|
||||
maxValue = Mathf.Max(1f, value);
|
||||
currentValue = maxValue;
|
||||
Changed?.Invoke(currentValue, maxValue);
|
||||
}
|
||||
|
||||
public bool TrySpend(float amount)
|
||||
{
|
||||
if (amount <= 0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentValue < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetCurrent(currentValue - amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Add(float amount)
|
||||
{
|
||||
if (amount <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetCurrent(currentValue + amount);
|
||||
}
|
||||
|
||||
public void Subtract(float amount)
|
||||
{
|
||||
if (amount <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetCurrent(currentValue - amount);
|
||||
}
|
||||
|
||||
private void SetCurrent(float value)
|
||||
{
|
||||
currentValue = Mathf.Clamp(value, 0f, maxValue);
|
||||
Changed?.Invoke(currentValue, maxValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b3c84173aeb6f4b08a1a9e547f70a17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ff22c64f2280c74d8cd209c6814b2bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
using OTG.Lab06.Abilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Effects
|
||||
{
|
||||
public sealed class BlinkEffect : AbilityEffect
|
||||
{
|
||||
[SerializeField] private float obstaclePadding = 0.8f;
|
||||
[SerializeField] private LayerMask obstacleMask = ~0;
|
||||
[SerializeField] private float visualLifetime = 0.8f;
|
||||
|
||||
public override void Activate(AbilityCastContext context)
|
||||
{
|
||||
Vector3 start = context.Caster.position;
|
||||
Vector3 direction = context.Forward;
|
||||
float distance = Mathf.Max(0.1f, context.Ability.castRange);
|
||||
Vector3 target = start + direction * distance;
|
||||
CharacterController controller = context.Caster.GetComponent<CharacterController>();
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.enabled = false;
|
||||
}
|
||||
|
||||
if (Physics.SphereCast(start + Vector3.up, 0.45f, direction, out RaycastHit hit, distance, obstacleMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
target = hit.point - direction * obstaclePadding;
|
||||
target.y = start.y;
|
||||
}
|
||||
|
||||
context.Caster.position = target;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.enabled = true;
|
||||
}
|
||||
|
||||
transform.position = target + Vector3.up * 0.05f;
|
||||
Destroy(gameObject, visualLifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea451cce3e2635fa0b12e607a4978a19
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
using OTG.Lab06.Abilities;
|
||||
using OTG.Lab06.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Effects
|
||||
{
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public sealed class FireballEffect : AbilityEffect
|
||||
{
|
||||
[SerializeField] private float speed = 18f;
|
||||
[SerializeField] private float lifeTime = 4f;
|
||||
[SerializeField] private LayerMask hitMask = ~0;
|
||||
|
||||
private AbilityCastContext context;
|
||||
private Vector3 direction;
|
||||
private float remainingLife;
|
||||
private bool activated;
|
||||
|
||||
public override void Activate(AbilityCastContext context)
|
||||
{
|
||||
this.context = context;
|
||||
direction = context.Forward;
|
||||
remainingLife = Mathf.Min(lifeTime, Mathf.Max(0.5f, context.Ability.castRange / Mathf.Max(1f, speed)));
|
||||
activated = true;
|
||||
transform.position = context.Origin + direction * 0.9f;
|
||||
transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Collider ownCollider = GetComponent<Collider>();
|
||||
ownCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!activated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float step = speed * Time.deltaTime;
|
||||
if (Physics.Raycast(transform.position, direction, out RaycastHit hit, step, hitMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
ApplyHit(hit.collider);
|
||||
return;
|
||||
}
|
||||
|
||||
transform.position += direction * step;
|
||||
remainingLife -= Time.deltaTime;
|
||||
if (remainingLife <= 0f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!activated || other.transform == context.Caster)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyHit(other);
|
||||
}
|
||||
|
||||
private void ApplyHit(Collider hitCollider)
|
||||
{
|
||||
if (hitCollider.transform == context.Caster || hitCollider.GetComponentInParent<AbilityEffect>() != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IDamageable damageable = hitCollider.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null && damageable.IsAlive)
|
||||
{
|
||||
damageable.TakeDamage(context.Ability.damage);
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fb2a27798b1a5343b1d83247d298809
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using OTG.Lab06.Abilities;
|
||||
using OTG.Lab06.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Effects
|
||||
{
|
||||
public sealed class IceNovaEffect : AbilityEffect
|
||||
{
|
||||
[SerializeField] private float visualLifetime = 1.2f;
|
||||
[SerializeField] private LayerMask hitMask = ~0;
|
||||
|
||||
public override void Activate(AbilityCastContext context)
|
||||
{
|
||||
transform.position = context.Caster.position + Vector3.up * 0.05f;
|
||||
|
||||
float radius = Mathf.Max(0.1f, context.Ability.castRange);
|
||||
Collider[] hits = Physics.OverlapSphere(context.Caster.position, radius, hitMask, QueryTriggerInteraction.Ignore);
|
||||
var damaged = new HashSet<IDamageable>();
|
||||
|
||||
foreach (Collider hit in hits)
|
||||
{
|
||||
IDamageable damageable = hit.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null && damageable.IsAlive && damageable.Transform != context.Caster && damaged.Add(damageable))
|
||||
{
|
||||
damageable.TakeDamage(context.Ability.damage);
|
||||
}
|
||||
}
|
||||
|
||||
Destroy(gameObject, visualLifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b67bd6b1b5bec522bf4df1a07f67357
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.Effects
|
||||
{
|
||||
public sealed class SelfDestruct : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float lifetime = 1f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Destroy(gameObject, lifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 099f4381a14a70847b2fcc301915e4dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d9c9027ef49b5708a180636c069d1cd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using OTG.Lab06.Abilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OTG.Lab06.UI
|
||||
{
|
||||
public sealed class AbilityBarUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private AbilityManager abilityManager;
|
||||
[SerializeField] private AbilitySlotUI slotPrefab;
|
||||
[SerializeField] private Transform slotRoot;
|
||||
[SerializeField] private TooltipUI tooltip;
|
||||
|
||||
private readonly List<AbilitySlotUI> slots = new List<AbilitySlotUI>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (abilityManager != null)
|
||||
{
|
||||
abilityManager.AbilitiesChanged += RebuildOrRefresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (abilityManager != null)
|
||||
{
|
||||
abilityManager.AbilitiesChanged -= RebuildOrRefresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (abilityManager != null)
|
||||
{
|
||||
RebuildOrRefresh(abilityManager.Abilities);
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind(AbilityManager manager, AbilitySlotUI prefab, Transform root, TooltipUI tooltipUI)
|
||||
{
|
||||
if (abilityManager != null)
|
||||
{
|
||||
abilityManager.AbilitiesChanged -= RebuildOrRefresh;
|
||||
}
|
||||
|
||||
abilityManager = manager;
|
||||
slotPrefab = prefab;
|
||||
slotRoot = root;
|
||||
tooltip = tooltipUI;
|
||||
|
||||
if (isActiveAndEnabled && abilityManager != null)
|
||||
{
|
||||
abilityManager.AbilitiesChanged += RebuildOrRefresh;
|
||||
RebuildOrRefresh(abilityManager.Abilities);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildOrRefresh(IReadOnlyList<AbilityRuntime> abilities)
|
||||
{
|
||||
if (slotPrefab == null || slotRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (slots.Count < abilities.Count)
|
||||
{
|
||||
AbilitySlotUI slot = Instantiate(slotPrefab, slotRoot);
|
||||
slots.Add(slot);
|
||||
}
|
||||
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
bool active = i < abilities.Count;
|
||||
slots[i].gameObject.SetActive(active);
|
||||
if (active)
|
||||
{
|
||||
slots[i].Bind(abilities[i], i, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1b8660e98f014b3880af4c5b70f2dc6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
using OTG.Lab06.Abilities;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace OTG.Lab06.UI
|
||||
{
|
||||
public sealed class AbilitySlotUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
[SerializeField] private Image icon;
|
||||
[SerializeField] private Image cooldownOverlay;
|
||||
[SerializeField] private Text hotkeyLabel;
|
||||
[SerializeField] private Text nameLabel;
|
||||
[SerializeField] private Text manaLabel;
|
||||
[SerializeField] private Text cooldownLabel;
|
||||
|
||||
private AbilityRuntime runtime;
|
||||
private TooltipUI tooltip;
|
||||
|
||||
public void Bind(AbilityRuntime abilityRuntime, int index, TooltipUI tooltipUI)
|
||||
{
|
||||
runtime = abilityRuntime;
|
||||
tooltip = tooltipUI;
|
||||
|
||||
if (icon != null)
|
||||
{
|
||||
icon.sprite = runtime.Data.icon;
|
||||
icon.color = runtime.Data.icon == null ? new Color(0.35f, 0.35f, 0.35f, 1f) : Color.white;
|
||||
}
|
||||
|
||||
if (hotkeyLabel != null)
|
||||
{
|
||||
hotkeyLabel.text = (index + 1).ToString();
|
||||
}
|
||||
|
||||
if (manaLabel != null)
|
||||
{
|
||||
manaLabel.text = Mathf.RoundToInt(runtime.Data.manaCost).ToString();
|
||||
}
|
||||
|
||||
if (nameLabel != null)
|
||||
{
|
||||
nameLabel.text = GetShortName(runtime.Data.abilityName);
|
||||
}
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static string GetShortName(string abilityName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(abilityName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return abilityName.Length <= 9 ? abilityName : abilityName.Substring(0, 9);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (runtime == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cooldownOverlay != null)
|
||||
{
|
||||
cooldownOverlay.fillAmount = runtime.CooldownNormalized;
|
||||
}
|
||||
|
||||
if (cooldownLabel != null)
|
||||
{
|
||||
bool show = runtime.CooldownRemaining > 0.05f;
|
||||
cooldownLabel.gameObject.SetActive(show);
|
||||
if (show)
|
||||
{
|
||||
cooldownLabel.text = runtime.CooldownRemaining.ToString("0.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (tooltip != null && runtime != null)
|
||||
{
|
||||
tooltip.Show(runtime.Data, transform as RectTransform);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
if (tooltip != null)
|
||||
{
|
||||
tooltip.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d80b838b44dd63b8a83685aa2df02f15
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
using OTG.Lab06.Characters;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace OTG.Lab06.UI
|
||||
{
|
||||
public sealed class ResourceBarUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PlayerStats playerStats;
|
||||
[SerializeField] private Image fill;
|
||||
[SerializeField] private Text valueLabel;
|
||||
[SerializeField] private bool useMana;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Unsubscribe();
|
||||
}
|
||||
|
||||
public void Bind(PlayerStats stats, bool mana)
|
||||
{
|
||||
Unsubscribe();
|
||||
playerStats = stats;
|
||||
useMana = mana;
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (playerStats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMana)
|
||||
{
|
||||
playerStats.ManaChanged += OnChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
playerStats.HealthChanged += OnChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
if (playerStats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerStats.ManaChanged -= OnChanged;
|
||||
playerStats.HealthChanged -= OnChanged;
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (playerStats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnChanged(useMana ? playerStats.Mana : playerStats.Health, useMana ? playerStats.MaxMana : playerStats.MaxHealth);
|
||||
}
|
||||
|
||||
private void OnChanged(float current, float max)
|
||||
{
|
||||
float normalized = max <= 0f ? 0f : Mathf.Clamp01(current / max);
|
||||
if (fill != null)
|
||||
{
|
||||
fill.type = Image.Type.Filled;
|
||||
fill.fillMethod = Image.FillMethod.Horizontal;
|
||||
fill.fillOrigin = (int)Image.OriginHorizontal.Left;
|
||||
fill.fillAmount = normalized;
|
||||
}
|
||||
|
||||
if (valueLabel != null)
|
||||
{
|
||||
valueLabel.text = $"{Mathf.RoundToInt(current)} / {Mathf.RoundToInt(max)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1633c268542d115823df8771e20b9b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using OTG.Lab06.Abilities;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace OTG.Lab06.UI
|
||||
{
|
||||
public sealed class TooltipUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Text titleLabel;
|
||||
[SerializeField] private Text descriptionLabel;
|
||||
[SerializeField] private Text statsLabel;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = transform as RectTransform;
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void Show(AbilityData data, RectTransform anchor)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rectTransform == null)
|
||||
{
|
||||
rectTransform = transform as RectTransform;
|
||||
}
|
||||
|
||||
titleLabel.text = data.abilityName;
|
||||
descriptionLabel.text = data.description;
|
||||
statsLabel.text = $"Damage: {Mathf.RoundToInt(data.damage)}\nMana: {Mathf.RoundToInt(data.manaCost)}\nCooldown: {data.cooldown:0.#}s";
|
||||
gameObject.SetActive(true);
|
||||
|
||||
if (rectTransform != null && anchor != null)
|
||||
{
|
||||
rectTransform.position = anchor.position + new Vector3(0f, 142f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13ea93e0504244d82b04abc14e512442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user