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>

//! Team management for the game.
//!
//! This module contains the [`Team`] struct which represents a team of fighters
//! in the game, including their color, name, gradient for pathfinding, and stats.

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

use crate::Color;

/// Represents a team in the game.
///
/// Each team has a unique color, name, and gradient for pathfinding.
/// The gradient is used to calculate distances from the team's cursor
/// to guide fighter movement.
///
/// # Example
///
/// ```
/// use liquidwar7core::{Team, RED};
/// use shortestpath::Gradient;
///
/// let gradient = Gradient::with_len(100);
/// let team = Team::new(gradient, RED, "Red Team".to_string());
///
/// assert_eq!(team.name(), "Red Team");
/// assert_eq!(team.color().r(), 1.0);
/// ```
/// Precision for floating-point comparisons.
const FLOAT_PRECISION: f32 = 1e-6;

/// Minimum gradient increment for spreading (base value).
pub const MIN_GRADIENT_INCREMENT: f32 = 1.0;

/// Computes a varying minimum gradient increment based on step count.
///
/// This adds semi-randomness to the gradient spreading to help break
/// fighter cycling patterns:
/// - Base value: [`MIN_GRADIENT_INCREMENT`] (most of the time)
/// - Once every 7 steps: 3.0 instead
/// - Once every 13 steps: 11.0 instead
/// - Once every 37 steps: 17.0 instead
///
/// These rare variations use prime numbers to create unpredictable spikes.
pub fn min_gradient_increment_from_steps_count(steps_count: u64) -> f32 {
    if steps_count.is_multiple_of(37) {
        17.0
    } else if steps_count.is_multiple_of(13) {
        11.0
    } else if steps_count.is_multiple_of(7) {
        3.0
    } else {
        MIN_GRADIENT_INCREMENT
    }
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Team {
    gradient: Gradient,
    color: Color,
    name: String,
    fighter_count: usize,
    average_health: f32,
    gradient_increment: f32,
}

impl PartialEq for Team {
    fn eq(&self, other: &Self) -> bool {
        self.gradient == other.gradient
            && self.color == other.color
            && self.name == other.name
            && self.fighter_count == other.fighter_count
            && (self.average_health - other.average_health).abs() < FLOAT_PRECISION
            && (self.gradient_increment - other.gradient_increment).abs() < FLOAT_PRECISION
    }
}

impl Eq for Team {}

impl Team {
    /// Creates a new team with the given gradient, color, and name.
    ///
    /// The fighter count and average health are initialized to 0.
    /// The gradient increment is initialized to [`MIN_GRADIENT_INCREMENT`].
    pub fn new(gradient: Gradient, color: Color, name: String) -> Self {
        Self {
            gradient,
            color,
            name,
            fighter_count: 0,
            average_health: 0.0,
            gradient_increment: MIN_GRADIENT_INCREMENT,
        }
    }

    /// Returns a reference to the team's gradient.
    ///
    /// The gradient stores distances from the cursor for pathfinding.
    pub fn gradient(&self) -> &Gradient {
        &self.gradient
    }

    /// Returns a mutable reference to the team's gradient.
    pub fn gradient_mut(&mut self) -> &mut Gradient {
        &mut self.gradient
    }

    /// Returns the team's color.
    pub fn color(&self) -> &Color {
        &self.color
    }

    /// Sets the team's color.
    pub fn set_color(&mut self, color: Color) {
        self.color = color;
    }

    /// Returns the team's name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Sets the team's name.
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }

    /// Returns the number of fighters in this team.
    pub fn fighter_count(&self) -> usize {
        self.fighter_count
    }

    /// Sets the number of fighters in this team.
    pub fn set_fighter_count(&mut self, fighter_count: usize) {
        self.fighter_count = fighter_count;
    }

    /// Returns the average health of fighters in this team (0.0 to 1.0).
    pub fn average_health(&self) -> f32 {
        self.average_health
    }

    /// Sets the average health of fighters in this team.
    pub fn set_average_health(&mut self, average_health: f32) {
        self.average_health = average_health;
    }

    /// Returns the gradient increment for this team.
    ///
    /// This value is used for gradient spreading and represents the distance
    /// at the cursor position before it was set to 0.
    pub fn gradient_increment(&self) -> f32 {
        self.gradient_increment
    }

    /// Sets the gradient increment for this team.
    pub fn set_gradient_increment(&mut self, gradient_increment: f32) {
        self.gradient_increment = gradient_increment;
    }
}

impl std::fmt::Display for Team {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Team \"{}\" ({}, {} fighters)",
            self.name, self.color, self.fighter_count
        )
    }
}