td

Artifact [00f9db3138]
Login

Artifact [00f9db3138]

Artifact 00f9db313864ab3f49fb2ce47f017abdcf7a07f2c62d36a113880bc3b10b561e:


//! Blob — the basic enemy. Gameplay construction stays independent of visual registries.

use std::{collections::HashMap, sync::Arc};

use macroquad::{
    color::{Color, WHITE},
    math::Vec2,
    shapes::draw_circle,
};

use crate::{
    animations::{
        AnimationClip, AnimationFrame, AnimationSet, DrawItem, FrameOutput, Shape, visual_slots,
    },
    enemies::{EnemyBlueprint, behaviors::SplitOnDeath, movement::SeekCenter},
    enemy::Enemy,
    registry::{
        AnimationRef, AnimationSetID, AnimationSetRef, ContentDefinition, ContentPack, EnemyID,
    },
    types::Source,
};

pub struct Blob;

impl Blob {
    pub const IDLE_ANIMATION: AnimationRef = AnimationRef::new("blob_idle");
    pub const MOVE_ANIMATION: AnimationRef = AnimationRef::new("blob_move");
    pub const DEATH_ANIMATION: AnimationRef = AnimationRef::new("blob_death");
    pub const VISUAL_SET: AnimationSetRef = AnimationSetRef::new("blob");

    const BASE_XP: f32 = 1.0;
    const BASE_HP: f32 = 5.0;
    const HP_GROWTH: f32 = 1.3;
    const XP_GROWTH: f32 = 1.3;

    pub fn draw_idle(frame: &AnimationFrame) -> FrameOutput {
        draw_blob(frame, 1.0)
    }

    pub fn draw_move(frame: &AnimationFrame) -> FrameOutput {
        let pulse = 1.0 + (frame.elapsed * 10.0).sin() * 0.06;
        draw_blob(frame, pulse)
    }

    pub fn draw_death(frame: &AnimationFrame) -> FrameOutput {
        let remaining = 1.0 - frame.progress();
        let color = Color::new(remaining, 0.47, 0.95, remaining);
        FrameOutput {
            items: vec![DrawItem {
                shape: Shape::Circle {
                    pos: frame.pos,
                    radius: frame.radius,
                    color,
                },
                tint_color: WHITE,
                tint_amount: 0.0,
                alpha: 1.0,
                scale: 1.0,
            }],
        }
    }

    pub fn add_content(pack: &mut ContentPack) {
        pack.add_animation(ContentDefinition::direct(
            Self::IDLE_ANIMATION.name(),
            "Blob Idle",
            0,
            |_| AnimationClip {
                duration: 1.0,
                draw: Self::draw_idle,
            },
        ));
        pack.add_animation(ContentDefinition::direct(
            Self::MOVE_ANIMATION.name(),
            "Blob Move",
            0,
            |_| AnimationClip {
                duration: 0.6,
                draw: Self::draw_move,
            },
        ));
        pack.add_animation(ContentDefinition::direct(
            Self::DEATH_ANIMATION.name(),
            "Blob Death",
            0,
            |_| AnimationClip {
                duration: 2.5,
                draw: Self::draw_death,
            },
        ));
        pack.add_animation_set(ContentDefinition::linked(
            Self::VISUAL_SET.name(),
            "Blob",
            0,
            |content| {
                let idle = content.require(Self::IDLE_ANIMATION)?;
                let movement = content.require(Self::MOVE_ANIMATION)?;
                let death = content.require(Self::DEATH_ANIMATION)?;
                let set = Arc::new(AnimationSet::new([
                    (visual_slots::IDLE, idle),
                    (visual_slots::MOVEMENT, movement),
                    (visual_slots::END, death),
                ]));
                Ok(Box::new(move |_| Arc::clone(&set)))
            },
        ));
        pack.add_enemy(ContentDefinition::linked("blob", "Blob", 0, |content| {
            let visuals = content.require(Self::VISUAL_SET)?;
            Ok(Box::new(move |tier| {
                let mut enemy = Self::create(tier);
                enemy.visuals = visuals;
                enemy
            }))
        }));
    }
}

impl EnemyBlueprint for Blob {
    fn create(tier: u32) -> Enemy {
        let hp = (Self::BASE_HP * Self::HP_GROWTH.powf(tier as f32)).round();
        let xp = (Self::BASE_XP * Self::XP_GROWTH.powf(tier as f32)).round();
        let mut template = Enemy {
            id: 0,
            pos: Vec2::ZERO,
            hp,
            tier,
            radius: 10.0,
            kind: EnemyID::PLACEHOLDER,
            visuals: AnimationSetID::PLACEHOLDER,
            statuses: Vec::new(),
            killer: Source::None,
            resists: HashMap::new(),
            xp: xp as u32,
            behaviors: Vec::new(),
            max_hp: hp,
            vel: Vec2::ZERO,
        };
        template.behaviors.push(SplitOnDeath::create(3));
        template.behaviors.push(SeekCenter::create(0));
        template
    }
}

fn draw_blob(frame: &AnimationFrame, scale: f32) -> FrameOutput {
    let hp = frame.health_fraction.clamp(0.0, 1.0);
    let color = Color::new(0.3, hp * 0.2, hp * 0.2, 1.0);
    FrameOutput {
        items: vec![DrawItem {
            shape: Shape::Circle {
                pos: frame.pos,
                radius: frame.radius * scale,
                color,
            },
            tint_color: WHITE,
            tint_amount: 0.0,
            alpha: 1.0,
            scale: 1.0,
        }],
    }
}