liquidwar7core 0.2.0

Liquidwar7 core logic library, low-level things which are game-engine agnostic.
Documentation
// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Fighter entity for the game.
//!
//! This module contains the [`Fighter`] struct which represents an individual
//! unit on the battlefield that belongs to a team.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// The current status of a fighter on the battlefield.
///
/// This determines both the fighter's behavior and rendering:
/// - `Moving`: Fighter is actively moving toward cursor (rendered as sphere)
/// - `Stalled`: Fighter wants to move but is blocked by ally (rendered as cube)
/// - `Attacking`: Fighter is blocked by enemy and attacking (rendered as cone)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum FighterStatus {
    /// Fighter is actively moving toward the cursor.
    #[default]
    Moving,
    /// Fighter wants to move but is blocked by an ally.
    Stalled,
    /// Fighter is blocked by an enemy and attacking.
    Attacking,
}

/// Represents an individual fighter unit on the battlefield.
///
/// A fighter belongs to a team and has a position and health. Fighters
/// move towards their team's cursor and can fight enemy fighters.
///
/// # Fields
///
/// - `x`, `y`, `z` - The fighter's position on the battlefield (cell = floor of position)
/// - `health` - The fighter's health (0.0 to 1.0)
/// - `team_id` - The ID of the team this fighter belongs to
/// - `gradient_index` - Index in the gradient_mesh (QuadMesh), for gradient lookups
///
/// # Example
///
/// ```
/// use liquidwar7core::Fighter;
///
/// let team_id = 0x1234567890abcdef_u64;
/// let fighter = Fighter::new(team_id, 5.5, 5.5, 0.0, 42, 1.0);
///
/// assert!(fighter.is_alive());
/// assert_eq!(fighter.position(), (5.5, 5.5, 0.0));
/// assert_eq!(fighter.cell(10, 10, 1), (5, 5, 0));
/// ```
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fighter {
    /// The x coordinate of the fighter's position.
    pub x: f32,
    /// The y coordinate of the fighter's position.
    pub y: f32,
    /// The z coordinate of the fighter's position.
    pub z: f32,
    /// Index in the gradient_mesh (QuadMesh), for gradient lookups.
    pub gradient_index: usize,
    /// The fighter's health, ranging from 0.0 (dead) to 1.0 (full health).
    pub health: f32,
    /// The ID of the team this fighter belongs to.
    pub team_id: u64,
    /// Action points accumulated for movement and actions.
    pub action_points: f32,
    /// Current status of the fighter (Moving, Stalled, etc.).
    pub status: FighterStatus,
}

impl Fighter {
    /// Creates a new fighter with the given parameters.
    ///
    /// The health value is clamped to the range [0.0, 1.0].
    ///
    /// # Arguments
    ///
    /// * `team_id` - The team this fighter belongs to
    /// * `x`, `y`, `z` - Position coordinates (cell center, e.g., 5.5, 5.5, 0.0)
    /// * `gradient_index` - Index in the gradient_mesh (QuadMesh)
    /// * `health` - Initial health (clamped to 0.0-1.0)
    pub fn new(
        team_id: u64,
        x: f32,
        y: f32,
        z: f32,
        gradient_index: usize,
        health: f32,
    ) -> Self {
        Self {
            x,
            y,
            z,
            gradient_index,
            health: health.clamp(0.0, 1.0),
            team_id,
            action_points: 0.0,
            status: FighterStatus::Moving,
        }
    }

    /// Returns the fighter's position as a tuple (x, y, z).
    pub fn position(&self) -> (f32, f32, f32) {
        (self.x, self.y, self.z)
    }

    /// Returns the cell coordinates (floor of position), clamped to grid bounds.
    ///
    /// The cell is derived from the position: cell_x = floor(x), etc.
    /// Values are clamped to [0, dimension-1] for each axis.
    #[inline]
    pub fn cell(&self, width: usize, height: usize, depth: usize) -> (usize, usize, usize) {
        let max_x = width.saturating_sub(1);
        let max_y = height.saturating_sub(1);
        let max_z = depth.saturating_sub(1);
        (
            (self.x.max(0.0) as usize).min(max_x),
            (self.y.max(0.0) as usize).min(max_y),
            (self.z.max(0.0) as usize).min(max_z),
        )
    }

    /// Returns true if the fighter is still alive (health > 0).
    pub fn is_alive(&self) -> bool {
        self.health > 0.0
    }
}

impl Eq for Fighter {}

impl std::fmt::Display for Fighter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Fighter(pos=({:.1}, {:.1}, {:.1}), health={:.0}%)",
            self.x, self.y, self.z, self.health * 100.0
        )
    }
}