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}");
}
}