use raylib::prelude::*; use crate::{ config::Config, effects::ActiveEffect, enemy::Enemy, level_gen::LevelGenerator, pickup::Pickup, platform::Platform, player::Player, }; pub struct World { pub player: Player, pub platforms: Vec, pub enemies: Vec, pub pickups: Vec, pub active_effects: Vec>, pub score: u64, pub elapsed: f32, score_f: f32, level_gen: LevelGenerator, } impl World { pub fn new(cfg: &Config) -> Self { let start_platform = Platform::new( 0.0, cfg.start_platform_y, cfg.screen_width as f32 * 0.65, cfg.platform_height, ); let mut platforms = vec![start_platform]; let mut enemies = Vec::new(); let mut pickups = Vec::new(); let mut level_gen = LevelGenerator::new(cfg); level_gen.generate_if_needed(cfg, &mut platforms, &mut enemies, &mut pickups); Self { player: Player::new(cfg), platforms, enemies, pickups, active_effects: Vec::new(), score: 0, score_f: 0.0, elapsed: 0.0, level_gen, } } pub fn update(&mut self, dt: f32, jump_pressed: bool, cfg: &Config) { self.elapsed += dt; let speed = cfg.scroll_speed(self.elapsed); // 1. Physics self.player.update(dt, cfg); // 2. Scroll world for p in &mut self.platforms { p.scroll(speed, dt); } for e in &mut self.enemies { e.scroll(speed, dt); } for pk in &mut self.pickups { pk.scroll(speed, dt); } self.level_gen.scroll(speed, dt); // 3. Resolve collisions (sets on_ground / alive) self.resolve_platform_collisions(); self.resolve_enemy_collisions(cfg); self.collect_pickups(cfg); // 4. Jump input — uses coyote_timer set during update, and effect multiplier if jump_pressed { let mult = self.jump_multiplier(); self.player.jump(cfg, mult); } // 5. Tick active effects; remove expired ones self.active_effects.retain_mut(|e| e.update(dt)); // 6. Recycle off-screen objects self.platforms.retain(|p| !p.is_off_screen()); self.enemies.retain(|e| !e.is_off_screen() && e.alive); self.pickups.retain(|pk| !pk.is_off_screen() && !pk.collected); // 7. Generate new content ahead self.level_gen.generate_if_needed( cfg, &mut self.platforms, &mut self.enemies, &mut self.pickups, ); // 8. Accumulate score with current multiplier let mult = self.score_multiplier(); self.score_f += cfg.score_per_second * mult * dt; self.score = self.score_f as u64; } // ── Effect queries ──────────────────────────────────────────────────────── pub fn is_invulnerable(&self) -> bool { self.active_effects.iter().any(|e| e.is_invulnerable()) } fn jump_multiplier(&self) -> f32 { self.active_effects.iter().fold(1.0_f32, |acc, e| acc * e.jump_multiplier()) } fn score_multiplier(&self) -> f32 { self.active_effects.iter().fold(1.0_f32, |acc, e| acc * e.score_multiplier()) } // ── Collision resolution ────────────────────────────────────────────────── fn resolve_platform_collisions(&mut self) { if !self.player.alive { return; } let ph = self.player.height; let px = self.player.x; let prev_bottom = self.player.prev_bottom(); let curr_bottom = self.player.bottom(); for platform in &self.platforms { let overlaps_x = self.player.right() > platform.x + 4.0 && px < platform.right() - 4.0; if !overlaps_x { continue; } // Top-landing: bottom crossed platform surface this frame while falling. if self.player.vy >= 0.0 && prev_bottom <= platform.top() + 1.0 && curr_bottom >= platform.top() { self.player.y = platform.top() - ph; self.player.vy = 0.0; self.player.on_ground = true; break; } } } fn resolve_enemy_collisions(&mut self, cfg: &Config) { if !self.player.alive { return; } let px = self.player.x; let prev_bottom = self.player.prev_bottom(); let curr_bottom = self.player.bottom(); let player_top = self.player.y; // Precompute effect queries before the mutable borrow of self.enemies. let invulnerable = self.is_invulnerable(); let score_mult = self.score_multiplier(); for enemy in &mut self.enemies { if !enemy.alive { continue; } let overlaps_x = self.player.right() > enemy.x && px < enemy.right(); if !overlaps_x { continue; } let overlaps_y = curr_bottom > enemy.top() && player_top < enemy.bottom(); if !overlaps_y { continue; } // Stomp: player was above enemy top and crosses it while falling. let stomp = self.player.vy > 0.0 && prev_bottom <= enemy.top() + 8.0 && curr_bottom >= enemy.top(); if stomp { enemy.alive = false; self.player.stomp_bounce(cfg); self.score_f += cfg.enemy_stomp_bonus * score_mult; self.score = self.score_f as u64; } else if !invulnerable { self.player.alive = false; return; } // invulnerable + non-stomp → pass through } } fn collect_pickups(&mut self, cfg: &Config) { if !self.player.alive { return; } let px = self.player.x; let curr_bottom = self.player.bottom(); let player_top = self.player.y; for pickup in &mut self.pickups { if pickup.collected { continue; } let overlaps_x = self.player.right() > pickup.x && px < pickup.right(); let overlaps_y = curr_bottom > pickup.y && player_top < pickup.bottom(); if overlaps_x && overlaps_y { pickup.collected = true; let effect = cfg.pickup_defs[pickup.def_index].create_effect(); self.active_effects.push(effect); } } } // ── Rendering ───────────────────────────────────────────────────────────── pub fn render(&self, d: &mut RaylibDrawHandle, cfg: &Config) { // Sky d.draw_rectangle(0, 0, cfg.screen_width, cfg.screen_height, Color::new(22, 24, 46, 255)); d.draw_rectangle(0, cfg.screen_height / 2, cfg.screen_width, cfg.screen_height / 2, Color::new(18, 20, 38, 255)); self.draw_platforms(d); self.draw_pickups(d, cfg); self.draw_enemies(d); self.draw_player(d); } /// Draw effect timer bars; called from game HUD so it layers above the world. pub fn draw_active_effects_hud(&self, d: &mut RaylibDrawHandle, cfg: &Config) { let bar_w = 180; let bar_h = 14; let x0 = 16; let mut y = 58; for effect in &self.active_effects { let tint = effect.tint(); let (r, g, b, _) = (tint.r, tint.g, tint.b, tint.a); // Background d.draw_rectangle(x0, y, bar_w, bar_h, Color::new(30, 30, 55, 200)); // Fill let fill = (effect.progress() * bar_w as f32) as i32; d.draw_rectangle(x0, y, fill, bar_h, Color::new(r, g, b, 200)); // Border d.draw_rectangle_lines(x0, y, bar_w, bar_h, Color::new(r, g, b, 160)); // Label let lbl = effect.label(); d.draw_text(lbl, x0 + 4, y + 1, 11, Color::new(cfg.screen_width as u8, 255, 255, 255)); // reuse white d.draw_text(lbl, x0 + 4, y + 1, 11, Color::WHITE); y += bar_h + 4; } } fn draw_platforms(&self, d: &mut RaylibDrawHandle) { for p in &self.platforms { d.draw_rectangle(p.x as i32, p.y as i32, p.width as i32, p.height as i32, Color::new(58, 160, 72, 255)); d.draw_rectangle(p.x as i32, p.y as i32, p.width as i32, 4, Color::new(100, 220, 110, 255)); d.draw_rectangle(p.x as i32, (p.y + p.height - 4.0) as i32, p.width as i32, 4, Color::new(38, 110, 50, 255)); } } fn draw_pickups(&self, d: &mut RaylibDrawHandle, cfg: &Config) { let s = cfg.pickup_size as i32; // Animate a gentle bob using elapsed time let bob = (self.elapsed * 3.0).sin() * 3.0; for pickup in &self.pickups { if pickup.collected { continue; } let def = &cfg.pickup_defs[pickup.def_index]; let (r, g, b) = def.color; let x = pickup.x as i32; let y = (pickup.y + bob) as i32; // Glow shadow d.draw_rectangle(x - 2, y - 2, s + 4, s + 4, Color::new(r, g, b, 60)); // Body d.draw_rectangle(x, y, s, s, Color::new(r, g, b, 220)); // Shine d.draw_rectangle(x + 3, y + 3, s / 3, s / 3, Color::new(255, 255, 255, 100)); // Border d.draw_rectangle_lines(x, y, s, s, Color::WHITE); // First letter of label let letter = &def.label[..1]; let lw = d.measure_text(letter, 16); d.draw_text(letter, x + (s - lw) / 2, y + (s - 16) / 2, 16, Color::WHITE); } } fn draw_enemies(&self, d: &mut RaylibDrawHandle) { for e in &self.enemies { if !e.alive { continue; } d.draw_rectangle(e.x as i32, e.y as i32, e.width as i32, e.height as i32, Color::new(210, 50, 50, 255)); let eye_y = (e.y + e.height * 0.28) as i32; d.draw_circle((e.x + e.width * 0.28) as i32, eye_y, 5.0, Color::WHITE); d.draw_circle((e.x + e.width * 0.72) as i32, eye_y, 5.0, Color::WHITE); d.draw_circle((e.x + e.width * 0.28) as i32 + 1, eye_y + 1, 2.5, Color::BLACK); d.draw_circle((e.x + e.width * 0.72) as i32 + 1, eye_y + 1, 2.5, Color::BLACK); let bx = e.x as i32; let bw = e.width as i32; let by = eye_y - 10; d.draw_line(bx + 4, by - 2, bx + bw / 2 - 2, by + 4, Color::new(140, 20, 20, 255)); d.draw_line(bx + bw / 2 + 2, by + 4, bx + bw - 4, by - 2, Color::new(140, 20, 20, 255)); } } fn draw_player(&self, d: &mut RaylibDrawHandle) { let p = &self.player; if !p.alive { return; } let x = p.x as i32; let y = p.y as i32; let w = p.width as i32; let h = p.height as i32; // Invulnerability pulsing outline if self.is_invulnerable() { let pulse = ((self.elapsed * 8.0).sin() * 0.5 + 0.5) as f32; let alpha = (150.0 + pulse * 105.0) as u8; d.draw_rectangle(x - 3, y - 3, w + 6, h + 6, Color::new(255, 200, 50, alpha)); } let body_color = if p.on_ground { Color::new(60, 120, 220, 255) } else { Color::new(80, 160, 255, 255) }; d.draw_rectangle(x, y + h / 4, w, h * 3 / 4, body_color); d.draw_rectangle(x + 4, y, w - 8, h / 2, Color::new(255, 210, 140, 255)); d.draw_rectangle(x + 8, y + 8, 6, 6, Color::WHITE); d.draw_rectangle(x + w - 14, y + 8, 6, 6, Color::WHITE); d.draw_rectangle(x + 10, y + 10, 3, 3, Color::BLACK); d.draw_rectangle(x + w - 12, y + 10, 3, 3, Color::BLACK); d.draw_rectangle(x, y + h / 4, w, 5, Color::new(220, 60, 60, 255)); } }