td

Artifact [2683da99b9]
Login

Artifact [2683da99b9]

Artifact 2683da99b9beff89cb9db5e63f4c1aedf8e69059b947c142a38b7cb5478ed5f8:


use std::collections::HashMap;

use macroquad::prelude::*;

use crate::{
    animations::AnimationFrame,
    behavior::{
        enemy::{dispatch_death, dispatch_enemy_tick},
        pool::dispatch_pool_tick,
        projectile::{
            ProjectileBehavior, dispatch_enemy_hit, dispatch_expire, dispatch_projectile_tick,
            dispatch_wall_hit,
        },
        status::dispatch_status_tick,
    },
    command::CommandBuffer,
    context::{DeathContext, ExpireContext, HitContext, TickContext},
    enemy::*,
    geometry::normal_off_enemy,
    rng::Rng,
    spatial::{EnemySnapshot, SpatialQuery},
    types::{DamageDelivery, GameEvent},
    world::*,
};

fn run_system(world: &mut World, f: impl FnOnce(&mut World, &mut CommandBuffer, &mut Rng)) {
    let mut cmd = CommandBuffer::default();
    let mut rng = world.rng.fork();
    f(world, &mut cmd, &mut rng);
    let mut events = Vec::new();
    apply_commands(world, cmd, &mut events);
    deliver_events(world, events);
}

fn pool_pass(world: &mut World, dt: f32) {
    run_system(world, |world, cmd, rng| {
        let snapshot = EnemySnapshot::collect(&world.enemies);
        world.grid.rebuild(&snapshot);
        let query = SpatialQuery::new(&snapshot, &world.grid); // borrows world.grid
        for pool in world.pools.iter_mut() {
            // disjoint field — coexists
            let mut tc = TickContext {
                core: ctx(cmd, &query, rng, pool.source, world.now),
                dt,
            };
            dispatch_pool_tick(pool, &mut tc); // mem::take, like dispatch_tick
        }
    });
}
pub fn step_world(world: &mut World, dt: f32) {
    world.now += dt;
    tick_wave(world, dt);
    tick_enemies(world, dt);
    tick_tower(world, dt);
    tick_projectiles(world, dt);
    death_sweep(world);
    animation_system(world, dt);
}

pub fn tick_wave(world: &mut World, dt: f32) {
    world.wave_timer -= dt;
    if world.wave_timer <= 0.0 && world.enemies.len() < MAX_ENEMIES {
        if let Some(blob) = world.enemy_registry.id_of("blob") {
            let tier = (10.0 + world.now) / 10.0;
            let mut enemy = world.enemy_registry.make_kind(blob, tier.round() as u32);

            let ch = [
                Vec2::new(CENTER_X, ARENA_Y),
                Vec2::new(ARENA_X + ARENA_W, CENTER_Y),
                Vec2::new(CENTER_X, ARENA_Y + ARENA_H),
                Vec2::new(ARENA_X, CENTER_Y),
            ];

            enemy.pos = ch[world.now as usize % 4];
            world.spawn_enemy(enemy);
        }
        world.wave_timer = WAVE_INTERVAL;
    }
}

pub fn animation_system(world: &mut World, dt: f32) {
    for animation in &mut world.animations {
        animation.elapsed += dt;
    }
    world.animations.retain(|a| a.elapsed < a.duration);
}

pub fn tick_enemies(world: &mut World, dt: f32) {
    run_system(world, |world, cmd, rng| {
        let snapshot = EnemySnapshot::collect(&world.enemies);
        world.grid.rebuild(&snapshot);
        let query = SpatialQuery::new(&snapshot, &world.grid);
        for e in world.enemies.iter_mut() {
            let mut c = TickContext {
                core: ctx(cmd, &query, rng, crate::types::Source::None, world.now),
                dt,
            };
            dispatch_enemy_tick(e, &mut c);
            dispatch_status_tick(e, &mut c);
            e.pos += e.vel * dt;
        }
    })
}

pub fn tick_tower(world: &mut World, dt: f32) {
    let center = Vec2::new(CENTER_X, CENTER_Y);

    if let Some(nearest) = world
        .enemies
        .iter()
        .min_by(|a, b| a.pos.distance(center).total_cmp(&b.pos.distance(center)))
    {
        let dir = nearest.pos - center;
        world.tower.angle = dir.y.atan2(dir.x);
    }

    world.tower.cooldown -= dt;
    if world.tower.cooldown <= 0.0 && !world.enemies.is_empty() {
        world.tower.cooldown = 1.0 / FIRE_RATE;
        let vel = Vec2::new(world.tower.angle.cos(), world.tower.angle.sin()) * PROJ_SPEED;
        let id = world.alloc_id();
        // Materialized fresh from the tower's (id, tier) loadout every shot — `Tower`
        // holds no `Box<dyn ProjectileBehavior>` state of its own, so a tier bump from a
        // weapon-crate pick is picked up on the very next shot with no separate rebuild step.
        let mut behaviors: Vec<Box<dyn ProjectileBehavior>> = world
            .tower
            .loadout()
            .map(|(wid, tier)| world.weapon_registry.make(wid, tier))
            .collect();
        behaviors.sort_by_key(|b| b.priority());
        if let Some(visuals) = world
            .animation_set_registry
            .id_of(crate::weapons::PROJECTILE_VISUALS.name())
        {
            let base_visuals = world.animation_set_registry.make(visuals, ());
            let mut visuals = (*base_visuals).clone();
            for behavior in &behaviors {
                behavior.contribute_animations(&mut visuals);
            }
            world.projectiles.push(Projectile {
                id,
                pos: world.tower.pos,
                vel,
                born: world.now,
                lifetime: 1000.0,
                source: crate::types::Source::Tower,
                radius: PROJ_RADIUS,
                visuals: std::sync::Arc::new(visuals),
                behaviors,
            });
        }
    }
}

fn death_sweep(world: &mut World) {
    run_system(world, |world, cmd, rng| {
        let snapshot = EnemySnapshot::collect(&world.enemies);
        world.grid.rebuild(&snapshot);
        let query = SpatialQuery::new(&snapshot, &world.grid); // borrows world.grid
        for e in world.enemies.iter_mut() {
            if !e.alive() {
                let mut dc = DeathContext {
                    core: ctx(cmd, &query, rng, e.killer, world.now),
                    pos: e.pos,
                    killer: e.killer,
                };
                dispatch_death(e, &mut dc); // ← on_death runs HERE, on the live enemy
                cmd.emit(GameEvent::EnemyKilled(Corpse::of(e))); // ← lightweight notify for OTHERS
            }
        }
    });
    world.enemies.retain(|e| e.alive());
    // Deferred until after the loop/borrow above ends: opening a crate needs `&mut World`
    // as a whole (rng + registry + state), which can't be split from the `enemies`
    // borrow the way a single-field access like `world.player.add_xp` can.
    if world.player.has_level_ups()
        && world.player.status != PlayerStatus::ManuallyClosed
        && matches!(world.state, Screen::Playing)
    {
        world.open_weapon_crate();
    }
}

pub fn tick_projectiles(world: &mut World, dt: f32) {
    run_system(world, |world, cmd, rng| {
        let snapshot = EnemySnapshot::collect(&world.enemies);
        world.grid.rebuild(&snapshot);
        let query = SpatialQuery::new(&snapshot, &world.grid);
        let mut expired: Vec<EntityId> = Vec::new();
        for p in world.projectiles.iter_mut() {
            {
                let mut tc = TickContext {
                    core: ctx(cmd, &query, rng, p.source, world.now),
                    dt,
                };
                dispatch_projectile_tick(p, &mut tc);
            }
            p.pos += p.vel * dt;
            let core = ctx(cmd, &query, rng, p.source, world.now);
            let mut hc = HitContext::new(core, p.pos, Vec2::ZERO);
            if let Some(eid) = query.first_hit(p.pos, p.radius)
                && let Some(e) = world.enemies.iter_mut().find(|e| e.id == eid)
            {
                hc.normal = normal_off_enemy(p, e);
                let consumed = dispatch_enemy_hit(p, e, &mut hc);
                if consumed {
                    expired.push(p.id);
                }
                hc.core
                    .cmd
                    .damage(eid, hc.damage.clone(), hc.source, DamageDelivery::Hit);
                for status in hc.status.drain(..) {
                    hc.core.cmd.apply_status(eid, status);
                }
            } else if let Some(n) = world.arena.wall_hit(p) {
                hc.normal = n;
                if dispatch_wall_hit(p, &mut hc) {
                    expired.push(p.id)
                }
            }
            if p.expired(world.now) {
                let mut ec = ExpireContext {
                    core: ctx(cmd, &query, rng, p.source, world.now),
                    reason: crate::context::ExpireReason::Lifetime,
                };
                dispatch_expire(p, &mut ec);
                expired.push(p.id);
            }
        }
        for projectile in world
            .projectiles
            .iter()
            .filter(|projectile| expired.contains(&projectile.id))
        {
            if let Some(end) = projectile.visuals.get(crate::animations::visual_slots::END) {
                cmd.spawn_animation(world.animation_registry.spawn(
                    end,
                    crate::animations::AnimationSpawn {
                        pos: projectile.pos,
                        radius: projectile.radius,
                    },
                ));
            }
        }
        world.projectiles.retain(|p| !expired.contains(&p.id));
    });
}

struct CrateLayout {
    cards: Vec<Rect>,
}

fn crate_layout(sw: f32, sh: f32, cards: usize) -> CrateLayout {
    let (bw, bh, gap) = (200.0, 260.0, 24.0);
    let total = bw * 3.0 + gap * 2.0;
    let (x0, y0) = ((sw - total) / 2.0, (sh - bh) / 2.0);
    let cards = (0..cards)
        .map(|i| Rect::new(x0 + i as f32 * (bw + gap), y0, bw, bh))
        .collect();
    CrateLayout { cards }
}

fn draw_box(r: Rect, selected: bool) {
    let bg = if selected {
        Color::new(0.20, 0.30, 0.50, 1.0)
    } else {
        Color::new(0.10, 0.10, 0.12, 1.0)
    };
    draw_rectangle(r.x, r.y, r.w, r.h, bg);
    draw_rectangle_lines(
        r.x,
        r.y,
        r.w,
        r.h,
        2.0,
        if selected { YELLOW } else { GRAY },
    );
}

fn draw_centered(text: String, cx: f32, y: f32, size: u16, color: Color) {
    let d = measure_text(&text, None, size, 1.0);
    draw_text(text, cx - d.width / 2.0, y, size as f32, color);
}

pub fn weapons_crate(world: &mut World, _t: f64, _dt: f32) -> Option<UiAction> {
    let Screen::WeaponCrate(s) = &mut world.state else {
        return None;
    };
    let offers = s.offered.clone();

    let lay = crate_layout(screen_width(), screen_height(), offers.len());

    if is_key_pressed(KeyCode::Left) && s.cursor > 0 {
        s.cursor -= 1
    }
    if is_key_pressed(KeyCode::Tab) && !offers.is_empty() {
        s.cursor = (s.cursor + 1) % offers.len();
    }
    if is_key_pressed(KeyCode::Right) && s.cursor + 1 < offers.len() {
        s.cursor += 1
    }
    if is_key_pressed(KeyCode::Space) {
        return Some(UiAction::ClosePanel);
    }
    let mut chosen = None;
    if is_key_pressed(KeyCode::Enter) {
        chosen = Some(s.cursor as u32)
    }
    // draw — read-only over s + lay
    for (i, r) in lay.cards.iter().enumerate() {
        draw_box(*r, i == s.cursor);
        let id = offers[i];
        let name = world.weapon_registry.display_name(id);

        draw_centered(name.to_owned(), r.x + r.w / 2.0, r.y + 40.0, 24, WHITE);
    }

    chosen.map(|e| UiAction::PickWeaponCrate(offers[e as usize]))
}

pub fn apply_action(world: &mut World, action: UiAction) {
    match action {
        UiAction::PickWeaponCrate(id) => {
            world.tower.equip(id); // adds it fresh at tier 1, or bumps an existing tier
            world.dismiss_weapon_crate();
            world.player.status = PlayerStatus::None
        }
        UiAction::ClosePanel => world.close_weapon_crate_without_choice(),
    }
}

pub fn draw_simulation(world: &World) {
    clear_background(Color::from_rgba(18, 18, 28, 255));

    // Arena
    draw_rectangle_lines(ARENA_X, ARENA_Y, ARENA_W, ARENA_H, 2.0, DARKGRAY);

    // Spawn point markers
    for sp in &[
        Vec2::new(CENTER_X, ARENA_Y),
        Vec2::new(ARENA_X + ARENA_W, CENTER_Y),
        Vec2::new(CENTER_X, ARENA_Y + ARENA_H),
        Vec2::new(ARENA_X, CENTER_Y),
    ] {
        draw_circle_lines(sp.x, sp.y, 8.0, 1.5, DARKGRAY);
    }

    // Tower
    let t = &world.tower;
    draw_circle(t.pos.x, t.pos.y, TOWER_RADIUS, BLUE);
    let tip = t.pos + Vec2::new(t.angle.cos(), t.angle.sin()) * (TOWER_RADIUS + 14.0);
    draw_line(t.pos.x, t.pos.y, tip.x, tip.y, 4.0, SKYBLUE);

    // Enemies
    for enemy in &world.enemies {
        let visuals = world.animation_set_registry.make(enemy.visuals, ());
        let slot = if enemy.vel.length_squared() > f32::EPSILON {
            crate::animations::visual_slots::MOVEMENT
        } else {
            crate::animations::visual_slots::IDLE
        };
        if let Some(kind) = visuals.get(slot) {
            world.animation_registry.draw(
                kind,
                AnimationFrame {
                    pos: enemy.pos,
                    radius: enemy.radius,
                    elapsed: world.now,
                    duration: 1.0,
                    health_fraction: (enemy.hp / enemy.max_hp).clamp(0.0, 1.0),
                },
            );
        }
    }

    // Projectiles
    for proj in &world.projectiles {
        if let Some(movement) = proj.visuals.get(crate::animations::visual_slots::MOVEMENT) {
            world.animation_registry.draw(
                movement,
                AnimationFrame {
                    pos: proj.pos,
                    radius: proj.radius,
                    elapsed: world.now - proj.born,
                    duration: 1.0,
                    health_fraction: 1.0,
                },
            );
        }
    }

    for animation in &world.animations {
        world
            .animation_registry
            .draw(animation.kind, animation.frame());
    }

    draw_text(format!("FPS: {}", get_fps()), 10.0, 20.0, 18.0, GRAY);
    draw_text(
        format!("Hit DPS: {:.1}", world.tower.meters.hit.dps()),
        10.0,
        20.0 + 18.0,
        18.0,
        GRAY,
    );
    draw_text(
        format!("DoT DPS: {:.1}", world.tower.meters.dot.dps()),
        10.0,
        20.0 + 18.0 * 2.0,
        18.0,
        GRAY,
    );
    draw_text(
        format!("Total DPS: {:.1}", world.tower.meters.combined_dps()),
        10.0,
        20.0 + 18.0 * 3.0,
        18.0,
        GRAY,
    );
}