td

systems.rs at trunk
Login

systems.rs at trunk

File src/systems.rs artifact 9d34c52af1 on branch trunk


use macroquad::prelude::*;

use crate::{
    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 {
        let blob = world.enemy_registry.id_of("blob").unwrap();
        let tier = (10.0 + world.now) / 10.0;
        println!("Tier {} at {}", tier, world.now);
        let mut enemy = world.enemy_registry.make_kind(blob, tier.round() as u32);
        enemy.pos = Vec2 {
            x: 0.0,
            y: world.arena.height * 0.5,
        };
        world.spawn_enemy(enemy);
        world.wave_timer = WAVE_INTERVAL;
    }
}

pub fn animation_system(world: &mut World, dt: f32) {
    let mut removed: Vec<EntityId> = Vec::new();
    for animation in &mut world.animations {
        animation.elapsed += dt;
        if animation.duration - animation.elapsed <= 0.0 {
            removed.push(animation.id);
        }
        animation.animate();
    }
    world.animations.retain(|a| !removed.contains(&a.id));
}

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());
        world.projectiles.push(Projectile {
            id,
            pos: world.tower.pos,
            vel,
            born: world.now,
            lifetime: 1000.0,
            source: crate::types::Source::Tower,
            radius: PROJ_RADIUS,
            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);
            }
        }
        world.projectiles.retain(|p| !expired.contains(&p.id));
    });
}

struct CrateLayout {
    cards: [Rect; 3],
}

fn crate_layout(sw: f32, sh: f32) -> 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);
    CrateLayout {
        cards: std::array::from_fn(|i| Rect::new(x0 + i as f32 * (bw + gap), y0, bw, bh)),
    }
}

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;

    let lay = crate_layout(screen_width(), screen_height());

    if is_key_pressed(KeyCode::Left) && s.cursor > 0 {
        s.cursor -= 1
    }
    if is_key_pressed(KeyCode::Right) && s.cursor < 2 {
        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 {
        enemy.animate();
    }

    // Projectiles
    for proj in &world.projectiles {
        draw_circle(proj.pos.x, proj.pos.y, PROJ_RADIUS, YELLOW);
    }

    for animation in &world.animations {
        animation.animate();
    }

    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,
    );
}