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>

//! RGB color representation.
//!
//! This module contains the [`Color`] struct for representing colors
//! using floating-point RGB values, along with predefined color constants.

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

// =============================================================================
// 12-color game palette
// =============================================================================
// Designed for maximum distinguishability when displayed together.
// Colors are organized by hue, alternating between saturated and lighter variants.
//
// Primary saturated colors (high contrast, bold):
//   RED, ORANGE, YELLOW, GREEN, CYAN, BLUE
//
// Secondary/variant colors (distinct but harmonious):
//   PINK, BEIGE, LIME, SKY, PURPLE, MAGENTA
// =============================================================================

// --- Reds/Warm ---
/// Pure red - bold and unmistakable (hue 0°).
pub const RED: Color = Color { r: 1.0, g: 0.0, b: 0.0 };
/// Warm pink - lighter red variant, clearly pink not red (hue ~350°).
pub const PINK: Color = Color { r: 1.0, g: 0.4, b: 0.6 };

// --- Oranges/Yellows ---
/// Vibrant orange - warm and energetic (hue ~30°).
pub const ORANGE: Color = Color { r: 1.0, g: 0.5, b: 0.0 };
/// Warm beige/tan - earthy neutral, distinct from yellow (hue ~40°).
pub const BEIGE: Color = Color { r: 0.9, g: 0.75, b: 0.5 };

// --- Yellows/Greens ---
/// Bright yellow - maximum visibility (hue 60°).
pub const YELLOW: Color = Color { r: 1.0, g: 1.0, b: 0.0 };
/// Electric lime - yellow-green, clearly not pure green (hue ~80°).
pub const LIME: Color = Color { r: 0.6, g: 1.0, b: 0.2 };

// --- Greens ---
/// Pure green - classic green (hue 120°).
pub const GREEN: Color = Color { r: 0.0, g: 0.85, b: 0.0 };

// --- Cyans/Blues ---
/// Aqua cyan - blue-green, bright and fresh (hue 180°).
pub const CYAN: Color = Color { r: 0.0, g: 0.9, b: 0.9 };
/// Sky blue - lighter blue variant, airy (hue ~200°).
pub const SKY: Color = Color { r: 0.3, g: 0.6, b: 1.0 };
/// Deep blue - rich and distinct from sky/cyan (hue 240°).
pub const BLUE: Color = Color { r: 0.2, g: 0.2, b: 1.0 };

// --- Purples/Magentas ---
/// Vivid purple - Valérie's favorite! (hue ~270°).
pub const PURPLE: Color = Color { r: 0.6, g: 0.2, b: 1.0 };
/// Hot magenta - red-purple, distinct from both red and purple (hue ~320°).
pub const MAGENTA: Color = Color { r: 1.0, g: 0.2, b: 0.6 };

/// Represents an RGB color with floating-point components.
///
/// Each component (red, green, blue) is a float typically in the range [0.0, 1.0],
/// where 0.0 is no intensity and 1.0 is full intensity.
///
/// # Example
///
/// ```
/// use liquidwar7core::Color;
///
/// let red = Color::new(1.0, 0.0, 0.0);
/// let white = Color::new(1.0, 1.0, 1.0);
/// let gray = Color::new(0.5, 0.5, 0.5);
///
/// assert_eq!(red.r(), 1.0);
/// assert_eq!(red.g(), 0.0);
/// assert_eq!(red.b(), 0.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Color {
    r: f32,
    g: f32,
    b: f32,
}

impl Eq for Color {}

impl Color {
    /// Creates a new color with the given RGB components.
    pub fn new(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b }
    }

    /// Returns the red component.
    pub fn r(&self) -> f32 {
        self.r
    }

    /// Returns the green component.
    pub fn g(&self) -> f32 {
        self.g
    }

    /// Returns the blue component.
    pub fn b(&self) -> f32 {
        self.b
    }

    /// Sets the red component.
    pub fn set_r(&mut self, r: f32) {
        self.r = r;
    }

    /// Sets the green component.
    pub fn set_g(&mut self, g: f32) {
        self.g = g;
    }

    /// Sets the blue component.
    pub fn set_b(&mut self, b: f32) {
        self.b = b;
    }

    /// Returns a new color scaled by the given health value.
    ///
    /// Health of 0.0 returns black, health of 1.0 returns the original color.
    /// Values are linearly interpolated between.
    ///
    /// # Example
    ///
    /// ```
    /// use liquidwar7core::{Color, RED};
    ///
    /// let full_health = RED.apply_health(1.0);
    /// assert_eq!(full_health, RED);
    ///
    /// let no_health = RED.apply_health(0.0);
    /// assert_eq!(no_health, Color::new(0.0, 0.0, 0.0));
    ///
    /// let half_health = RED.apply_health(0.5);
    /// assert_eq!(half_health, Color::new(0.5, 0.0, 0.0));
    /// ```
    pub fn apply_health(&self, health: f32) -> Color {
        Color {
            r: self.r * health,
            g: self.g * health,
            b: self.b * health,
        }
    }

    /// Returns the name of this color if it matches a predefined palette color.
    ///
    /// Returns `None` for custom colors that don't match any predefined constant.
    ///
    /// # Example
    ///
    /// ```
    /// use liquidwar7core::{Color, RED, PURPLE};
    ///
    /// assert_eq!(RED.color_name(), Some("red"));
    /// assert_eq!(PURPLE.color_name(), Some("purple"));
    ///
    /// let custom = Color::new(0.5, 0.5, 0.5);
    /// assert_eq!(custom.color_name(), None);
    /// ```
    pub fn color_name(&self) -> Option<&'static str> {
        match *self {
            RED => Some("red"),
            PINK => Some("pink"),
            ORANGE => Some("orange"),
            BEIGE => Some("beige"),
            YELLOW => Some("yellow"),
            LIME => Some("lime"),
            GREEN => Some("green"),
            CYAN => Some("cyan"),
            SKY => Some("sky"),
            BLUE => Some("blue"),
            PURPLE => Some("purple"),
            MAGENTA => Some("magenta"),
            _ => None,
        }
    }
}

/// Returns an iterator over all 12 palette colors.
///
/// Primary colors (RGB + CMY) are returned first, followed by secondary colors
/// ordered for maximum contrast between consecutive colors.
///
/// # Example
///
/// ```
/// use liquidwar7core::{all_colors, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, ORANGE};
///
/// let colors: Vec<_> = all_colors().collect();
/// assert_eq!(colors.len(), 12);
/// // Primary: RGB + CMY
/// assert_eq!(colors[0], RED);
/// assert_eq!(colors[1], GREEN);
/// assert_eq!(colors[2], BLUE);
/// assert_eq!(colors[3], YELLOW);
/// assert_eq!(colors[4], CYAN);
/// assert_eq!(colors[5], MAGENTA);
/// // Secondary starts at index 6
/// assert_eq!(colors[6], ORANGE);
/// ```
pub fn all_colors() -> impl Iterator<Item = Color> {
    [
        // Primary colors: RGB + CMY (the classic 6)
        RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA,
        // Secondary colors: ordered for max contrast between N and N+1
        // orange(warm) -> sky(cool) -> lime(yellow-green) -> purple(blue-red) -> beige(neutral) -> pink(light-warm)
        ORANGE, SKY, LIME, PURPLE, BEIGE, PINK,
    ]
    .into_iter()
}

impl Default for Color {
    /// Returns black (0.0, 0.0, 0.0).
    fn default() -> Self {
        Self::new(0.0, 0.0, 0.0)
    }
}

impl std::fmt::Display for Color {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.color_name() {
            Some(name) => write!(f, "{}", name),
            None => write!(f, "Color(r={:.2}, g={:.2}, b={:.2})", self.r, self.g, self.b),
        }
    }
}