//! 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,
}],
}
}