using UnityEngine; using System.Collections; using System.Collections.Generic; /// /// A monster that lives in a room. /// Behaviour: /// - Idles / wanders aimlessly within its spawn room when no agents are present. /// - When an agent enters the room, closes in to melee range and attacks each tick. /// - Each monster has its own health pool and 1d4 fist weapon. /// - When killed it fires MonsterKilled and destroys itself. /// - Represented as a red sphere identical in size to a solo agent. /// public class Monster : MonoBehaviour { // ------------------------------------------------------------------ // // Inspector // // ------------------------------------------------------------------ // [Header("Stats")] [SerializeField] private int maxHealth = 10; [SerializeField] private float movementSpeed = 1.5f; [Header("Combat")] [SerializeField] private float attackCooldown = 1.2f; // seconds between attacks [SerializeField] private float aggroRange = 12f; // range at which monsters notice agents entering [Header("Wander")] [SerializeField] private float wanderRadius = 3f; [SerializeField] private float wanderInterval = 2.5f; // ------------------------------------------------------------------ // // Runtime state // // ------------------------------------------------------------------ // private int currentHealth; private Weapon weapon; private MazeRoom homeRoom; // The room this monster belongs to private MazeData maze; private AIAgent target; // Current attack target private float lastAttackTime; private float lastWanderTime; private Vector3 wanderTarget; private bool isDead; // ------------------------------------------------------------------ // // Public interface // // ------------------------------------------------------------------ // /// /// Difficulty modifier applied during spawn (see MonsterSpawner). /// Higher = more health and slightly faster. /// public float DifficultyMultiplier { get; private set; } = 1f; public int MaxHealth => maxHealth; public int CurrentHealth => currentHealth; public bool IsDead => isDead; public MazeRoom HomeRoom => homeRoom; /// /// Convenience: threat value used by agents to estimate room danger. /// Sum of all alive monster health in a room. /// public int ThreatValue => isDead ? 0 : currentHealth; /// Fires when this monster is killed. Passes itself as argument. public System.Action OnMonsterKilled; // ------------------------------------------------------------------ // // Initialisation // // ------------------------------------------------------------------ // /// /// Called by MonsterSpawner immediately after instantiation. /// public void Init(MazeRoom room, MazeData mazeData, float difficultyMultiplier = 1f) { homeRoom = room; maze = mazeData; DifficultyMultiplier = difficultyMultiplier; maxHealth = Mathf.RoundToInt(maxHealth * difficultyMultiplier); currentHealth = maxHealth; movementSpeed = movementSpeed * Mathf.Lerp(1f, 1.3f, difficultyMultiplier - 1f); weapon = Weapon.Fists(); wanderTarget = transform.position; lastWanderTime = Time.time; } // ------------------------------------------------------------------ // // Unity loop // // ------------------------------------------------------------------ // void Update() { if (isDead || maze == null) return; AcquireTarget(); if (target != null) CombatUpdate(); else WanderUpdate(); } // ------------------------------------------------------------------ // // Target acquisition // // ------------------------------------------------------------------ // private void AcquireTarget() { // Drop dead targets if (target != null && (target == null || target.IsDead || target.HasReachedGoal)) { target = null; } if (target != null) return; // Find the nearest live agent within aggro range float bestDist = aggroRange * aggroRange; AIAgent best = null; foreach (var agent in FindObjectsByType(FindObjectsSortMode.None)) { if (agent.IsDead) continue; // Only aggro agents that are in or entering this room Vector2Int agentTile = WorldToTile(agent.transform.position); MazeRoom agentRoom = maze.GetRoomAtTile(agentTile.x, agentTile.y); if (agentRoom == null || agentRoom.Id != homeRoom.Id) continue; float sqDist = (agent.transform.position - transform.position).sqrMagnitude; if (sqDist < bestDist) { bestDist = sqDist; best = agent; } } target = best; } // ------------------------------------------------------------------ // // Combat // // ------------------------------------------------------------------ // private void CombatUpdate() { float dist = Vector3.Distance(transform.position, target.transform.position); if (dist > weapon.MeleeRange) { // Close in Vector3 dir = (target.transform.position - transform.position).normalized; transform.position += dir * movementSpeed * Time.deltaTime; } else { // Attack if (Time.time - lastAttackTime >= attackCooldown) { lastAttackTime = Time.time; PerformAttack(); } } } private void PerformAttack() { if (target == null || target.IsDead) return; // Monsters use base hit chance — no advantage modifier if (weapon.TryHit(1f)) { int dmg = weapon.RollDamage(); target.TakeDamage(dmg, this); Debug.Log($"[Monster] hit {target.AgentName} for {dmg} (HP left: {target.CurrentHealth})"); } else { Debug.Log($"[Monster] missed {target.AgentName}"); } } // ------------------------------------------------------------------ // // Wander (no target) // // ------------------------------------------------------------------ // private void WanderUpdate() { // Move toward wander target if (Vector3.Distance(transform.position, wanderTarget) > 0.2f) { Vector3 dir = (wanderTarget - transform.position).normalized; transform.position += dir * (movementSpeed * 0.5f) * Time.deltaTime; } // Pick new wander target periodically if (Time.time - lastWanderTime > wanderInterval) { lastWanderTime = Time.time; PickNewWanderTarget(); } } private void PickNewWanderTarget() { if (homeRoom == null) return; // Random tile inside home room int x = Random.Range(homeRoom.MinX + 1, homeRoom.MaxX); int y = Random.Range(homeRoom.MinY + 1, homeRoom.MaxY); wanderTarget = new Vector3(x + 0.5f, 1f, y + 0.5f); } // ------------------------------------------------------------------ // // Damage / death // // ------------------------------------------------------------------ // /// /// Deal damage to this monster. Called by agents during their attack. /// Returns true if this hit killed the monster. /// public bool TakeDamage(int amount) { if (isDead) return false; currentHealth -= amount; if (currentHealth <= 0) { Die(); return true; } return false; } private void Die() { if (isDead) return; isDead = true; currentHealth = 0; Debug.Log($"[Monster] in room {homeRoom?.Id} died."); OnMonsterKilled?.Invoke(this); Destroy(gameObject); } // ------------------------------------------------------------------ // // Helpers // // ------------------------------------------------------------------ // private Vector2Int WorldToTile(Vector3 worldPos) => new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.z)); // ------------------------------------------------------------------ // // Mouse interaction – click to inspect (future) // // ------------------------------------------------------------------ // void OnMouseDown() { Debug.Log($"[Monster] Room {homeRoom?.Id} | HP {currentHealth}/{maxHealth} | Difficulty ×{DifficultyMultiplier:F1}"); } }