td

Artifact [4e721cd1dd]
Login

Artifact [4e721cd1dd]

Artifact 4e721cd1dde6065181330caa113cbe72d2cfcf1b234a6df4a0aa3f6c87d82af7:


use crate::{
    animations::Animation,
    behavior::projectile::ProjectileBehavior,
    command::{Command, CommandBuffer},
    context::{BehaviorCtx, EventContext},
    enemy::{Corpse, Enemy},
    pool::GroundPool,
    registry::{DamageRegistry, EnemyRegistry, StatusRegistry, WeaponID, WeaponRegistry},
    rng::Rng,
    spatial::{EnemySnapshot, Grid, SpatialQuery},
    tower::Tower,
    types::{GameEvent, Source},
};
use macroquad::math::Vec2;

pub const ARENA_W: f32 = 800.0;
pub const ARENA_H: f32 = 800.0;
pub const ARENA_X: f32 = 50.0;
pub const ARENA_Y: f32 = 50.0;
pub const CENTER_X: f32 = ARENA_X + ARENA_W * 0.5;
pub const CENTER_Y: f32 = ARENA_Y + ARENA_H * 0.5;

pub const TOWER_RADIUS: f32 = 16.0;
pub const ENEMY_RADIUS: f32 = 10.0;
pub const PROJ_RADIUS: f32 = 5.0;
pub const ENEMY_SPEED: f32 = 60.0;
pub const PROJ_SPEED: f32 = 300.0;
pub const FIRE_RATE: f32 = 4.2;
pub const WAVE_INTERVAL: f32 = 2.5;
pub const MAX_ENEMIES: usize = 8;
pub const ENEMY_MAX_HP: f32 = 3.0;

pub type EntityId = u64;

pub enum UiAction {
    PickWeaponCrate(WeaponID),
    ClosePanel,
}

#[derive(PartialEq)]
pub struct WeaponCrateState {
    pub offered: [WeaponID; 3],
    pub cursor: usize,
}

#[derive(PartialEq)]
pub enum Screen {
    Playing,
    WeaponCrate(WeaponCrateState),
}

#[derive(Clone)]
pub struct Projectile {
    pub id: EntityId,
    pub pos: Vec2,
    pub vel: Vec2,
    pub born: f32,
    pub lifetime: f32,
    pub source: Source,
    pub radius: f32,
    pub behaviors: Vec<Box<dyn ProjectileBehavior>>,
}

impl Projectile {
    pub fn with_vel(&self, v: Vec2) -> Projectile {
        let mut p = self.clone();
        p.vel = v;
        p
    }
    pub fn expired(&self, now: f32) -> bool {
        now - self.born >= self.lifetime
    }
}

#[derive(PartialEq, Clone, Copy, Debug)]
pub enum PlayerStatus {
    None,
    ManuallyClosed,
    ToOpen,
}

pub struct Player {
    pub level_ups: u32,
    pub level: u32,
    pub xp: u32,
    next_level: u32,
    pub status: PlayerStatus,
}

impl Player {
    const BASE_XP: f32 = 10.0;
    const GROWTH: f32 = 1.6;

    fn xp_required(level: u32) -> u32 {
        (Self::BASE_XP * Self::GROWTH.powi(level as i32)).round() as u32
    }

    pub fn new() -> Self {
        Self {
            level_ups: 0,
            level: 0,
            xp: 0,
            next_level: Self::xp_required(0),
            status: PlayerStatus::None,
        }
    }

    pub fn add_xp(&mut self, xp: u32) {
        self.xp += xp;
        while self.xp >= self.next_level {
            self.xp -= self.next_level;
            self.level += 1;
            self.level_ups += 1;
            self.next_level = Self::xp_required(self.level);
        }
    }

    pub fn consume_level(&mut self) {
        self.level_ups = self.level_ups.saturating_sub(1);
    }

    pub fn has_level_ups(&self) -> bool {
        self.level_ups > 0
    }
}

pub struct World {
    pub state: Screen,
    pub tower: Tower,
    pub player: Player,
    pub enemies: Vec<Enemy>,
    pub pools: Vec<GroundPool>,
    pub animations: Vec<Animation>,
    pub projectiles: Vec<Projectile>,
    pub wave_timer: f32,
    pub weapon_registry: WeaponRegistry,
    pub enemy_registry: EnemyRegistry,
    pub damage_registry: DamageRegistry,
    pub status_registry: StatusRegistry,
    pub grid: Grid,
    pub rng: Rng,
    pub now: f32,
    pub arena: Arena,
    next_id: EntityId,
}

impl World {
    pub fn new() -> Self {
        let arena = Arena {
            width: ARENA_W,
            height: ARENA_H,
        };
        let cell_size = ENEMY_RADIUS;
        let damage_registry = DamageRegistry::new();
        let weapon_registry = WeaponRegistry::new(&damage_registry);
        let enemy_registry = EnemyRegistry::new();
        let status_registry = StatusRegistry::new(&damage_registry);
        Self {
            state: Screen::Playing,
            tower: Tower::new(&weapon_registry),
            player: Player::new(),
            enemies: Vec::new(),
            projectiles: Vec::new(),
            animations: Vec::new(),
            pools: Vec::new(),
            weapon_registry,
            enemy_registry,
            damage_registry,
            rng: Rng::seeded(1),
            grid: Grid::new(arena.width, arena.height, cell_size),
            arena,
            wave_timer: 0.0,
            next_id: 0,
            now: 0.0,
            status_registry,
        }
    }

    pub fn alloc_id(&mut self) -> EntityId {
        let id = self.next_id;
        self.next_id += 1;
        id
    }

    pub fn enemy_mut(&mut self, id: EntityId) -> Option<&mut Enemy> {
        self.enemies.iter_mut().find(|e| e.id == id)
    }

    pub fn spawn_projectile(&mut self, mut p: Projectile) {
        p.id = self.alloc_id();
        self.projectiles.push(p);
    }
    pub fn spawn_pool(&mut self, p: GroundPool) {
        self.pools.push(p);
    }
    pub fn spawn_enemy(&mut self, mut e: Enemy) {
        e.id = self.alloc_id();
        self.enemies.push(e);
    }
    fn despawn(&mut self, id: EntityId) {
        self.enemies.retain(|e| e.id != id);
        self.projectiles.retain(|p| p.id != id);
    }

    /// Picks 3 offers, with repeats if the catalog has fewer than 3 entries — the catalog
    /// is expected to grow, but this should never panic in the meantime.
    fn roll_weapon_offers(&mut self) -> [WeaponID; 3] {
        let all = self.weapon_registry.all();
        std::array::from_fn(|_| all[self.rng.next_u32() as usize % all.len()])
    }

    /// The one place a weapon crate gets opened, so a fresh level-up and a re-roll after
    /// dismissing one crate (when more level-ups are still queued) can't drift apart.
    pub fn open_weapon_crate(&mut self) {
        if !self.player.has_level_ups() {
            return;
        }
        let offered = self.roll_weapon_offers();
        self.state = Screen::WeaponCrate(WeaponCrateState { offered, cursor: 0 });
    }

    /// Called when the player picks or closes a weapon crate. If leveling up granted more
    /// than one pick (a big xp reward crossed several thresholds at once), immediately
    /// rolls the next crate instead of dropping back to `Playing` with picks still owed.
    pub fn dismiss_weapon_crate(&mut self) {
        self.player.consume_level();
        if self.player.level_ups > 0 {
            self.open_weapon_crate();
        } else {
            self.state = Screen::Playing;
        }
    }
    /// Just hides the panel — any pending level-ups survive, since closing without
    /// picking shouldn't spend a credit you haven't used. `Space` (see `main.rs`) is what
    /// brings the panel back later while `level_ups > 0`.
    pub fn close_weapon_crate_without_choice(&mut self) {
        self.state = Screen::Playing;
        self.player.status = PlayerStatus::ManuallyClosed;
    }
}

// convenience constructor for the shared core
pub fn ctx<'a>(
    cmd: &'a mut CommandBuffer,
    query: &'a SpatialQuery<'a>,
    rng: &'a mut Rng,
    source: Source,
    now: f32,
) -> BehaviorCtx<'a> {
    BehaviorCtx {
        cmd,
        query,
        rng,
        source,
        now,
    }
}

pub struct Arena {
    pub width: f32,
    pub height: f32,
}

impl Arena {
    pub fn wall_hit(&self, p: &Projectile) -> Option<Vec2> {
        wall_hit(p, self)
    }
}

pub fn wall_hit(p: &Projectile, a: &Arena) -> Option<Vec2> {
    let mut n = Vec2 { x: 0.0, y: 0.0 };
    if p.pos.x - p.radius < 0.0 {
        n.x += 1.0;
    } else if p.pos.x + p.radius > a.width {
        n.x -= 1.0;
    }
    if p.pos.y - p.radius < 0.0 {
        n.y += 1.0;
    } else if p.pos.y + p.radius > a.height {
        n.y -= 1.0;
    }
    if n.x == 0.0 && n.y == 0.0 {
        None
    } else {
        Some(n.normalize())
    }
}

/// The ONE function that mutates the world from commands. Intentionally dumb: it never
/// calls back into behaviors, so there's no re-entrancy. Deaths caused here have their
/// consequences (on_death, drops, emits) handled in later phases, not recursively.
pub fn apply_commands(world: &mut World, buf: CommandBuffer, events: &mut Vec<GameEvent>) {
    for cmd in buf.cmds {
        match cmd {
            Command::SpawnProjectile(p) => world.spawn_projectile(p),
            Command::SpawnPool(p) => world.spawn_pool(p),
            Command::SpawnEnemy(e) => world.spawn_enemy(e),
            Command::Damage {
                target,
                amount,
                source,
                delivery,
            } => {
                if let Some(e) = world.enemy_mut(target) {
                    let d: f32;
                    match e.apply_damage(amount, source) {
                        crate::enemy::EnemyState::Alive(dmg) => d = dmg,
                        crate::enemy::EnemyState::Killed(dmg) => {
                            d = dmg;
                            events.push(GameEvent::EnemyKilled(Corpse::of(e)));
                        }
                    }
                    if matches!(source, Source::Tower) {
                        world.tower.meters.record(delivery, world.now, d);
                    }
                }
            }
            Command::ApplyStatus { target, status } => {
                if let Some(e) = world.enemy_mut(target) {
                    e.push_status(status);
                }
            }
            Command::Despawn(id) => world.despawn(id),
            Command::GrantResource { kind, amount } => match kind {
                crate::types::ResourceKind::Wp => world.player.add_xp(amount),
                crate::types::ResourceKind::Sp => {}
                crate::types::ResourceKind::Tp => {}
            },
            Command::Emit(ev) => events.push(ev),
            Command::SpawnAnimation(mut animation) => {
                animation.id = world.alloc_id();
                world.animations.push(animation);
            }
            Command::Replicate(transform) => match transform.entity {
                crate::command::TransformEntity::Enemy => {
                    let mut id = None;
                    let mut behaviors = None;
                    let mut tier = 1;
                    if let Some(e) = world.enemy_mut(transform.id) {
                        id = Some(e.kind);
                        behaviors = Some(e.behaviors.clone());
                        tier = e.tier;
                    }
                    if let Some(id) = id
                        && let Some(behaviors) = behaviors
                    {
                        let mut template = world.enemy_registry.make_kind(id, tier);
                        template.behaviors = behaviors;
                        template.pos = transform.location;
                        template.id = world.alloc_id(); // else every split child is id 0
                        world.enemies.push(template);
                    }
                }
            },
        }
    }
}

pub fn deliver_events(world: &mut World, mut events: Vec<GameEvent>) {
    let mut guard = 0;
    while !events.is_empty() && guard < 8 {
        guard += 1;

        let mut cmd = CommandBuffer::default();
        let mut rng = world.rng.fork();

        // read side for this wave (scoped so the &world.grid borrow ends before apply_commands)
        {
            let snapshot = EnemySnapshot::collect(&world.enemies);
            world.grid.rebuild(&snapshot);
            let query = SpatialQuery::new(&snapshot, &world.grid);

            for ev in &events {
                let mut ec = EventContext {
                    core: ctx(&mut cmd, &query, &mut rng, Source::None, world.now),
                };
                handle_event(ev, &mut ec); // ← THE missing reaction: pushes commands
            }
        }

        let mut next = Vec::new();
        apply_commands(world, cmd, &mut next); // Command::Emit → next  → next wave
        events = next;
    }
    // (optional) if guard hit 8 with events remaining, a cascade ran away — log/debug_assert
}

fn handle_event(ev: &GameEvent, c: &mut EventContext) {
    match ev {
        GameEvent::EnemyKilled(corpse) => {
            // built-in death VFX (kind → animation), spawned at the corpse position
            c.cmd.spawn_animation(Animation {
                kind: corpse.death_anim,
                pos: corpse.pos,
                duration: 2.5,
                elapsed: 0.0,
                id: 0,
            });

            c.cmd
                .grant_resource(crate::types::ResourceKind::Wp, corpse.xp);
        }
        GameEvent::Custom(_) => {}
        GameEvent::DespawnEnemy(e) => {
            c.cmd.despawn(*e);
        }
    }
}