use freecs::{Entity, Schedule, ecs};
use macroquad::prelude::*;
use std::collections::HashMap;
ecs! {
GameWorld {
position: Position => POSITION,
velocity: Velocity => VELOCITY,
tower: Tower => TOWER,
enemy: Enemy => ENEMY,
projectile: Projectile => PROJECTILE,
grid_cell: GridCell => GRID_CELL,
grid_position: GridPosition => GRID_POSITION,
health_bar: HealthBar => HEALTH_BAR,
visual_effect: VisualEffect => VISUAL_EFFECT,
range_indicator: RangeIndicator => RANGE_INDICATOR,
money_popup: MoneyPopup => MONEY_POPUP,
}
Tags {
basic_enemy => BASIC_ENEMY,
tank_enemy => TANK_ENEMY,
fast_enemy => FAST_ENEMY,
flying_enemy => FLYING_ENEMY,
healer_enemy => HEALER_ENEMY,
basic_tower => BASIC_TOWER,
frost_tower => FROST_TOWER,
cannon_tower => CANNON_TOWER,
sniper_tower => SNIPER_TOWER,
poison_tower => POISON_TOWER,
path_cell => PATH_CELL,
}
Events {
enemy_spawned: EnemySpawnedEvent,
enemy_died: EnemyDiedEvent,
enemy_reached_end: EnemyReachedEndEvent,
projectile_hit: ProjectileHitEvent,
tower_placed: TowerPlacedEvent,
tower_sold: TowerSoldEvent,
tower_upgraded: TowerUpgradedEvent,
wave_completed: WaveCompletedEvent,
wave_started: WaveStartedEvent,
}
GameResources {
money: u32,
lives: u32,
wave: u32,
game_state: GameState,
selected_tower_type: TowerType,
spawn_timer: f32,
enemies_to_spawn: Vec<EnemySpawnInfo>,
mouse_grid_pos: Option<(i32, i32)>,
path: Vec<Vec2>,
wave_announce_timer: f32,
game_speed: f32,
current_hp: u32,
max_hp: u32,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum TowerType {
#[default]
Basic,
Frost,
Cannon,
Sniper,
Poison,
}
impl TowerType {
fn cost(&self) -> u32 {
match self {
TowerType::Basic => 60,
TowerType::Frost => 120,
TowerType::Cannon => 200,
TowerType::Sniper => 180,
TowerType::Poison => 150,
}
}
fn upgrade_cost(&self, current_level: u32) -> u32 {
(self.cost() as f32 * 0.5 * current_level as f32) as u32
}
fn damage(&self, level: u32) -> f32 {
let base = match self {
TowerType::Basic => 15.0,
TowerType::Frost => 8.0,
TowerType::Cannon => 50.0,
TowerType::Sniper => 80.0,
TowerType::Poison => 5.0,
};
base * (1.0 + 0.25 * (level - 1) as f32)
}
fn range(&self, level: u32) -> f32 {
let base = match self {
TowerType::Basic => 100.0,
TowerType::Frost => 80.0,
TowerType::Cannon => 120.0,
TowerType::Sniper => 180.0,
TowerType::Poison => 90.0,
};
base * (1.0 + 0.15 * (level - 1) as f32)
}
fn fire_rate(&self, level: u32) -> f32 {
let base = match self {
TowerType::Basic => 0.5,
TowerType::Frost => 1.0,
TowerType::Cannon => 2.0,
TowerType::Sniper => 3.0,
TowerType::Poison => 0.8,
};
base * (1.0 - 0.1 * (level - 1) as f32).max(0.2)
}
fn color(&self) -> Color {
match self {
TowerType::Basic => GREEN,
TowerType::Frost => Color::new(0.2, 0.6, 1.0, 1.0),
TowerType::Cannon => RED,
TowerType::Sniper => DARKGRAY,
TowerType::Poison => Color::new(0.6, 0.2, 0.8, 1.0),
}
}
fn projectile_speed(&self) -> f32 {
match self {
TowerType::Basic => 300.0,
TowerType::Frost => 200.0,
TowerType::Cannon => 250.0,
TowerType::Sniper => 500.0,
TowerType::Poison => 250.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum GameState {
#[default]
WaitingForWave,
WaveInProgress,
GameOver,
Victory,
Paused,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum EnemyType {
#[default]
Normal,
Fast,
Tank,
Flying,
Shielded,
Healer,
Boss,
}
impl EnemyType {
fn base_health(&self) -> f32 {
match self {
EnemyType::Normal => 50.0,
EnemyType::Fast => 30.0,
EnemyType::Tank => 150.0,
EnemyType::Flying => 40.0,
EnemyType::Shielded => 80.0,
EnemyType::Healer => 60.0,
EnemyType::Boss => 500.0,
}
}
fn health(&self, wave: u32) -> f32 {
let health_multiplier = 1.0 + (wave as f32 - 1.0) * 0.5;
self.base_health() * health_multiplier
}
fn speed(&self) -> f32 {
match self {
EnemyType::Normal => 40.0,
EnemyType::Fast => 80.0,
EnemyType::Tank => 20.0,
EnemyType::Flying => 60.0,
EnemyType::Shielded => 30.0,
EnemyType::Healer => 35.0,
EnemyType::Boss => 15.0,
}
}
fn value(&self, wave: u32) -> u32 {
let base = match self {
EnemyType::Normal => 10,
EnemyType::Fast => 15,
EnemyType::Tank => 30,
EnemyType::Flying => 20,
EnemyType::Shielded => 25,
EnemyType::Healer => 40,
EnemyType::Boss => 100,
};
base + wave * 2
}
fn shield(&self) -> f32 {
match self {
EnemyType::Shielded => 50.0,
EnemyType::Boss => 100.0,
_ => 0.0,
}
}
fn color(&self) -> Color {
match self {
EnemyType::Normal => RED,
EnemyType::Fast => ORANGE,
EnemyType::Tank => DARKGRAY,
EnemyType::Flying => SKYBLUE,
EnemyType::Shielded => Color::new(0.5, 0.0, 0.8, 1.0),
EnemyType::Healer => Color::new(0.2, 0.8, 0.3, 1.0),
EnemyType::Boss => Color::new(0.6, 0.0, 0.6, 1.0),
}
}
fn size(&self) -> f32 {
match self {
EnemyType::Normal => 15.0,
EnemyType::Fast => 12.0,
EnemyType::Tank => 20.0,
EnemyType::Flying => 15.0,
EnemyType::Shielded => 18.0,
EnemyType::Healer => 16.0,
EnemyType::Boss => 30.0,
}
}
}
#[derive(Debug, Clone)]
pub struct EnemySpawnInfo {
enemy_type: EnemyType,
spawn_time: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Position(pub Vec2);
#[derive(Debug, Clone, Copy, Default)]
pub struct Velocity(pub Vec2);
#[derive(Debug, Clone, Copy, Default)]
pub struct Tower {
pub tower_type: TowerType,
pub level: u32,
pub cooldown: f32,
pub target: Option<freecs::Entity>,
pub fire_animation: f32,
pub tracking_time: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Enemy {
pub health: f32,
pub max_health: f32,
pub shield_health: f32,
pub max_shield: f32,
pub speed: f32,
pub path_index: usize,
pub path_progress: f32,
pub value: u32,
pub enemy_type: EnemyType,
pub slow_duration: f32,
pub poison_duration: f32,
pub poison_damage: f32,
pub is_flying: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Projectile {
pub damage: f32,
pub target: freecs::Entity,
pub speed: f32,
pub tower_type: TowerType,
pub start_position: Vec2,
pub arc_height: f32,
pub flight_progress: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GridCell {
pub x: i32,
pub y: i32,
pub occupied: bool,
pub is_path: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GridPosition {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HealthBar {
pub enemy_entity: freecs::Entity,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum EffectType {
#[default]
Explosion,
PoisonBubble,
DeathParticle,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct VisualEffect {
pub effect_type: EffectType,
pub lifetime: f32,
pub age: f32,
pub velocity: Vec2,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RangeIndicator {
pub tower_entity: freecs::Entity,
pub visible: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MoneyPopup {
pub lifetime: f32,
pub amount: i32,
}
#[derive(Debug, Clone)]
pub struct EnemySpawnedEvent {
pub entity: Entity,
pub enemy_type: EnemyType,
}
#[derive(Debug, Clone)]
pub struct EnemyDiedEvent {
pub entity: Entity,
pub position: Vec2,
pub reward: u32,
pub enemy_type: EnemyType,
}
#[derive(Debug, Clone)]
pub struct EnemyReachedEndEvent {
pub entity: Entity,
pub damage: u32,
}
#[derive(Debug, Clone)]
pub struct ProjectileHitEvent {
pub projectile: Entity,
pub target: Entity,
pub position: Vec2,
pub damage: f32,
pub tower_type: TowerType,
}
#[derive(Debug, Clone)]
pub struct TowerPlacedEvent {
pub entity: Entity,
pub tower_type: TowerType,
pub grid_x: i32,
pub grid_y: i32,
pub cost: u32,
}
#[derive(Debug, Clone)]
pub struct TowerSoldEvent {
pub entity: Entity,
pub tower_type: TowerType,
pub grid_x: i32,
pub grid_y: i32,
pub refund: u32,
}
#[derive(Debug, Clone)]
pub struct TowerUpgradedEvent {
pub entity: Entity,
pub tower_type: TowerType,
pub old_level: u32,
pub new_level: u32,
pub cost: u32,
}
#[derive(Debug, Clone)]
pub struct WaveCompletedEvent {
pub wave: u32,
}
#[derive(Debug, Clone)]
pub struct WaveStartedEvent {
pub wave: u32,
pub enemy_count: usize,
}
const GRID_SIZE: i32 = 12;
const TILE_SIZE: f32 = 40.0;
const BASE_WIDTH: f32 = 1024.0;
const BASE_HEIGHT: f32 = 768.0;
fn get_scale() -> f32 {
(screen_width() / BASE_WIDTH).min(screen_height() / BASE_HEIGHT)
}
fn get_offset() -> Vec2 {
let scale = get_scale();
let scaled_width = BASE_WIDTH * scale;
let scaled_height = BASE_HEIGHT * scale;
Vec2::new(
(screen_width() - scaled_width) / 2.0,
(screen_height() - scaled_height) / 2.0,
)
}
fn grid_to_base(grid_x: i32, grid_y: i32) -> Vec2 {
let num_cells = (GRID_SIZE + 1) as f32;
let grid_width = num_cells * TILE_SIZE;
let grid_height = num_cells * TILE_SIZE;
let grid_offset_x = (BASE_WIDTH - grid_width) / 2.0;
let grid_offset_y = (BASE_HEIGHT - grid_height) / 2.0;
let tile_x = (grid_x + GRID_SIZE / 2) as f32;
let tile_y = (grid_y + GRID_SIZE / 2) as f32;
Vec2::new(
grid_offset_x + (tile_x + 0.5) * TILE_SIZE,
grid_offset_y + (tile_y + 0.5) * TILE_SIZE,
)
}
fn grid_to_screen(grid_x: i32, grid_y: i32) -> Vec2 {
let base_pos = grid_to_base(grid_x, grid_y);
let scale = get_scale();
let offset = get_offset();
Vec2::new(offset.x + base_pos.x * scale, offset.y + base_pos.y * scale)
}
fn screen_to_grid(screen_pos: Vec2) -> Option<(i32, i32)> {
let scale = get_scale();
let offset = get_offset();
let num_cells = (GRID_SIZE + 1) as f32;
let grid_width = num_cells * TILE_SIZE;
let grid_height = num_cells * TILE_SIZE;
let grid_offset_x = (BASE_WIDTH - grid_width) / 2.0;
let grid_offset_y = (BASE_HEIGHT - grid_height) / 2.0;
let local_x = (screen_pos.x - offset.x) / scale;
let local_y = (screen_pos.y - offset.y) / scale;
let rel_x = local_x - grid_offset_x;
let rel_y = local_y - grid_offset_y;
if rel_x < 0.0 || rel_y < 0.0 || rel_x >= grid_width || rel_y >= grid_height {
return None;
}
let tile_x = (rel_x / TILE_SIZE).floor() as i32;
let tile_y = (rel_y / TILE_SIZE).floor() as i32;
let grid_x = tile_x - GRID_SIZE / 2;
let grid_y = tile_y - GRID_SIZE / 2;
Some((grid_x, grid_y))
}
fn initialize_grid(world: &mut GameWorld) {
for x in -GRID_SIZE / 2..=GRID_SIZE / 2 {
for y in -GRID_SIZE / 2..=GRID_SIZE / 2 {
let entity = world.spawn_entities(GRID_CELL, 1)[0];
world.set_grid_cell(
entity,
GridCell {
x,
y,
occupied: false,
is_path: false,
},
);
}
}
}
fn create_path(world: &mut GameWorld) {
let path = [
Vec2::new(-6.0, 0.0),
Vec2::new(-3.0, 0.0),
Vec2::new(-3.0, -4.0),
Vec2::new(3.0, -4.0),
Vec2::new(3.0, 2.0),
Vec2::new(-1.0, 2.0),
Vec2::new(-1.0, 5.0),
Vec2::new(6.0, 5.0),
];
let num_cells = (GRID_SIZE + 1) as f32;
let grid_width = num_cells * TILE_SIZE;
let grid_height = num_cells * TILE_SIZE;
let grid_offset_x = (BASE_WIDTH - grid_width) / 2.0;
let grid_offset_y = (BASE_HEIGHT - grid_height) / 2.0;
let screen_path: Vec<Vec2> = path
.iter()
.map(|&p| {
Vec2::new(
grid_offset_x + (p.x + GRID_SIZE as f32 / 2.0 + 0.5) * TILE_SIZE,
grid_offset_y + (p.y + GRID_SIZE as f32 / 2.0 + 0.5) * TILE_SIZE,
)
})
.collect();
world.resources.path = screen_path;
let mut cells_to_mark = Vec::new();
for index in 0..path.len() - 1 {
let start = path[index];
let end = path[index + 1];
let steps = 20;
for step in 0..=steps {
let t = step as f32 / steps as f32;
let pos = start + (end - start) * t;
let grid_x = pos.x.round() as i32;
let grid_y = pos.y.round() as i32;
cells_to_mark.push((grid_x, grid_y));
}
}
let grid_entities: Vec<_> = world.query_entities(GRID_CELL).collect();
for entity in grid_entities {
if let Some(cell) = world.get_grid_cell_mut(entity) {
for &(grid_x, grid_y) in &cells_to_mark {
if cell.x == grid_x && cell.y == grid_y {
cell.is_path = true;
cell.occupied = true;
world.add_path_cell(entity);
break;
}
}
}
}
}
fn spawn_tower(
world: &mut GameWorld,
grid_x: i32,
grid_y: i32,
tower_type: TowerType,
) -> freecs::Entity {
let position = grid_to_base(grid_x, grid_y);
let entities = EntityBuilder::new()
.with_position(Position(position))
.with_grid_position(GridPosition {
x: grid_x,
y: grid_y,
})
.with_tower(Tower {
tower_type,
level: 1,
cooldown: 0.0,
target: None,
fire_animation: 0.0,
tracking_time: 0.0,
})
.spawn(world, 1);
let entity = entities[0];
match tower_type {
TowerType::Basic => world.add_basic_tower(entity),
TowerType::Frost => world.add_frost_tower(entity),
TowerType::Cannon => world.add_cannon_tower(entity),
TowerType::Sniper => world.add_sniper_tower(entity),
TowerType::Poison => world.add_poison_tower(entity),
}
let cost = tower_type.cost();
world.resources.money -= cost;
world.send_tower_placed(TowerPlacedEvent {
entity,
tower_type,
grid_x,
grid_y,
cost,
});
spawn_range_indicator(world, entity);
entity
}
fn spawn_range_indicator(world: &mut GameWorld, tower_entity: freecs::Entity) {
let entity = world.spawn_entities(RANGE_INDICATOR, 1)[0];
world.set_range_indicator(
entity,
RangeIndicator {
tower_entity,
visible: false,
},
);
}
fn spawn_enemy(world: &mut GameWorld, enemy_type: EnemyType) -> freecs::Entity {
let start_pos = world.resources.path[0];
let health = enemy_type.health(world.resources.wave);
let shield = enemy_type.shield();
let entities = EntityBuilder::new()
.with_position(Position(start_pos))
.with_velocity(Velocity(Vec2::ZERO))
.with_enemy(Enemy {
health,
max_health: health,
shield_health: shield,
max_shield: shield,
speed: enemy_type.speed(),
path_index: 0,
path_progress: 0.0,
value: enemy_type.value(world.resources.wave),
enemy_type,
slow_duration: 0.0,
poison_duration: 0.0,
poison_damage: 0.0,
is_flying: enemy_type == EnemyType::Flying,
})
.spawn(world, 1);
let entity = entities[0];
world.set_health_bar(
entity,
HealthBar {
enemy_entity: entity,
},
);
match enemy_type {
EnemyType::Normal => world.add_basic_enemy(entity),
EnemyType::Tank => world.add_tank_enemy(entity),
EnemyType::Fast => world.add_fast_enemy(entity),
EnemyType::Flying => world.add_flying_enemy(entity),
EnemyType::Healer => world.add_healer_enemy(entity),
_ => world.add_basic_enemy(entity),
}
world.send_enemy_spawned(EnemySpawnedEvent { entity, enemy_type });
entity
}
fn spawn_projectile(
world: &mut GameWorld,
from: Vec2,
target: freecs::Entity,
tower_type: TowerType,
level: u32,
) -> freecs::Entity {
let arc_height = if tower_type == TowerType::Cannon {
50.0
} else {
0.0
};
EntityBuilder::new()
.with_position(Position(from))
.with_velocity(Velocity(Vec2::ZERO))
.with_projectile(Projectile {
damage: tower_type.damage(level),
target,
speed: tower_type.projectile_speed(),
tower_type,
start_position: from,
arc_height,
flight_progress: 0.0,
})
.spawn(world, 1)[0]
}
fn spawn_visual_effect(
world: &mut GameWorld,
position: Vec2,
effect_type: EffectType,
velocity: Vec2,
lifetime: f32,
) {
EntityBuilder::new()
.with_position(Position(position))
.with_visual_effect(VisualEffect {
effect_type,
lifetime,
age: 0.0,
velocity,
})
.spawn(world, 1);
}
fn spawn_money_popup(world: &mut GameWorld, position: Vec2, amount: i32) {
EntityBuilder::new()
.with_position(Position(position))
.with_money_popup(MoneyPopup {
lifetime: 0.0,
amount,
})
.spawn(world, 1);
}
fn can_place_tower_at(world: &GameWorld, x: i32, y: i32) -> bool {
let mut has_tower = false;
world
.query()
.with(TOWER | GRID_POSITION)
.iter(|_entity, table, index| {
if table.grid_position[index].x == x && table.grid_position[index].y == y {
has_tower = true;
}
});
if has_tower {
return false;
}
let mut can_place = false;
world.query().with(GRID_CELL).iter(|_entity, table, index| {
if table.grid_cell[index].x == x
&& table.grid_cell[index].y == y
&& !table.grid_cell[index].occupied
{
can_place = true;
}
});
can_place
}
fn mark_cell_occupied(world: &mut GameWorld, x: i32, y: i32) {
world
.query_mut()
.with(GRID_CELL)
.iter(|_entity, table, index| {
if table.grid_cell[index].x == x && table.grid_cell[index].y == y {
table.grid_cell[index].occupied = true;
}
});
}
fn plan_wave(world: &mut GameWorld) {
world.resources.wave += 1;
let wave = world.resources.wave;
let mut spawns = Vec::new();
let enemy_count = 5 + wave * 2;
let enemy_types = match wave {
1..=2 => vec![(EnemyType::Normal, 1.0)],
3..=4 => vec![(EnemyType::Normal, 0.7), (EnemyType::Fast, 0.3)],
5..=6 => vec![
(EnemyType::Normal, 0.5),
(EnemyType::Fast, 0.3),
(EnemyType::Tank, 0.2),
],
7..=8 => vec![
(EnemyType::Normal, 0.3),
(EnemyType::Fast, 0.3),
(EnemyType::Tank, 0.2),
(EnemyType::Flying, 0.2),
],
9..=10 => vec![
(EnemyType::Normal, 0.2),
(EnemyType::Fast, 0.2),
(EnemyType::Tank, 0.2),
(EnemyType::Flying, 0.2),
(EnemyType::Shielded, 0.2),
],
11..=12 => vec![
(EnemyType::Fast, 0.2),
(EnemyType::Tank, 0.2),
(EnemyType::Flying, 0.2),
(EnemyType::Shielded, 0.2),
(EnemyType::Healer, 0.2),
],
13..=14 => vec![
(EnemyType::Tank, 0.2),
(EnemyType::Flying, 0.2),
(EnemyType::Shielded, 0.2),
(EnemyType::Healer, 0.2),
(EnemyType::Boss, 0.2),
],
_ => vec![
(EnemyType::Tank, 0.15),
(EnemyType::Flying, 0.2),
(EnemyType::Shielded, 0.2),
(EnemyType::Healer, 0.2),
(EnemyType::Boss, 0.25),
],
};
let spawn_interval = match wave {
1..=3 => 1.0,
4..=6 => 0.8,
7..=9 => 0.6,
_ => 0.5,
};
let mut spawn_time = 0.0;
for _ in 0..enemy_count {
let roll: f32 = rand::gen_range(0.0, 1.0);
let mut cumulative = 0.0;
let mut selected_type = EnemyType::Normal;
for (enemy_type, probability) in &enemy_types {
cumulative += probability;
if roll < cumulative {
selected_type = *enemy_type;
break;
}
}
spawns.push(EnemySpawnInfo {
enemy_type: selected_type,
spawn_time,
});
spawn_time += spawn_interval;
}
world.resources.enemies_to_spawn = spawns.clone();
world.resources.spawn_timer = 0.0;
world.resources.game_state = GameState::WaveInProgress;
world.resources.wave_announce_timer = 3.0;
world.send_wave_started(WaveStartedEvent {
wave,
enemy_count: spawns.len(),
});
}
fn input_system(world: &mut GameWorld) {
let mouse_pos = Vec2::new(mouse_position().0, mouse_position().1);
world.resources.mouse_grid_pos = screen_to_grid(mouse_pos);
let left_clicked = is_mouse_button_pressed(MouseButton::Left);
let right_clicked = is_mouse_button_pressed(MouseButton::Right);
if left_clicked
&& let Some((grid_x, grid_y)) = world.resources.mouse_grid_pos
&& can_place_tower_at(world, grid_x, grid_y)
{
let tower_type = world.resources.selected_tower_type;
if world.resources.money >= tower_type.cost() {
let cost = tower_type.cost();
spawn_tower(world, grid_x, grid_y, tower_type);
mark_cell_occupied(world, grid_x, grid_y);
let pos = grid_to_base(grid_x, grid_y);
spawn_money_popup(world, pos, -(cost as i32));
}
}
if right_clicked && let Some((grid_x, grid_y)) = world.resources.mouse_grid_pos {
let mut tower_entity = None;
world
.query()
.with(TOWER | GRID_POSITION)
.iter(|entity, table, index| {
if table.grid_position[index].x == grid_x && table.grid_position[index].y == grid_y
{
tower_entity = Some(entity);
}
});
if let Some(tower_entity) = tower_entity {
sell_tower(world, tower_entity, grid_x, grid_y);
}
}
if (is_key_pressed(KeyCode::U) || is_mouse_button_pressed(MouseButton::Middle))
&& let Some((grid_x, grid_y)) = world.resources.mouse_grid_pos
{
let mut tower_entity = None;
world
.query()
.with(TOWER | GRID_POSITION)
.iter(|entity, table, index| {
if table.grid_position[index].x == grid_x && table.grid_position[index].y == grid_y
{
tower_entity = Some(entity);
}
});
if let Some(tower_entity) = tower_entity {
upgrade_tower(world, tower_entity, grid_x, grid_y);
}
}
if is_key_pressed(KeyCode::Key1) {
world.resources.selected_tower_type = TowerType::Basic;
} else if is_key_pressed(KeyCode::Key2) {
world.resources.selected_tower_type = TowerType::Frost;
} else if is_key_pressed(KeyCode::Key3) {
world.resources.selected_tower_type = TowerType::Cannon;
} else if is_key_pressed(KeyCode::Key4) {
world.resources.selected_tower_type = TowerType::Sniper;
} else if is_key_pressed(KeyCode::Key5) {
world.resources.selected_tower_type = TowerType::Poison;
}
if is_key_pressed(KeyCode::LeftBracket) {
world.resources.game_speed = (world.resources.game_speed - 0.5).max(0.5);
} else if is_key_pressed(KeyCode::RightBracket) {
world.resources.game_speed = (world.resources.game_speed + 0.5).min(3.0);
} else if is_key_pressed(KeyCode::Backslash) {
world.resources.game_speed = 1.0;
}
if is_key_pressed(KeyCode::P) {
match world.resources.game_state {
GameState::WaveInProgress => world.resources.game_state = GameState::Paused,
GameState::Paused => world.resources.game_state = GameState::WaveInProgress,
_ => {}
}
}
if is_key_pressed(KeyCode::R)
&& matches!(
world.resources.game_state,
GameState::GameOver | GameState::Victory
)
{
restart_game(world);
}
}
fn wave_spawning_system(world: &mut GameWorld, delta_time: f32) {
if world.resources.game_state != GameState::WaveInProgress {
return;
}
world.resources.spawn_timer += delta_time;
let current_time = world.resources.spawn_timer;
let mut spawns_to_process = Vec::new();
for (index, spawn_info) in world.resources.enemies_to_spawn.iter().enumerate() {
if spawn_info.spawn_time <= current_time {
spawns_to_process.push((index, spawn_info.enemy_type));
}
}
for (_index, enemy_type) in spawns_to_process.iter() {
spawn_enemy(world, *enemy_type);
}
for &(index, _) in spawns_to_process.iter().rev() {
world.resources.enemies_to_spawn.remove(index);
}
let enemy_count = world.query_entities(ENEMY).count();
if world.resources.enemies_to_spawn.is_empty() && enemy_count == 0 {
world.send_wave_completed(WaveCompletedEvent {
wave: world.resources.wave,
});
if world.resources.wave >= 20 {
world.resources.game_state = GameState::Victory;
} else {
plan_wave(world);
}
}
}
fn enemy_movement_system(world: &mut GameWorld, delta_time: f32) {
let path = world.resources.path.clone();
let mut enemies_to_remove = Vec::new();
let mut hp_damage = 0;
let mut enemy_positions = Vec::new();
world
.query()
.with(ENEMY | POSITION)
.iter(|entity, table, index| {
enemy_positions.push((
entity,
table.position[index].0,
table.enemy[index].enemy_type,
));
});
for (healer_entity, healer_pos, enemy_type) in &enemy_positions {
if *enemy_type == EnemyType::Healer {
for (other_entity, other_pos, _) in &enemy_positions {
if healer_entity != other_entity {
let distance = (*other_pos - *healer_pos).length();
if distance < 60.0
&& let Some(enemy) = world.get_enemy_mut(*other_entity)
{
enemy.health = (enemy.health + 10.0 * delta_time).min(enemy.max_health);
}
}
}
}
}
let enemy_entities: Vec<_> = world.query_entities(ENEMY | POSITION).collect();
for entity in enemy_entities {
let enemy = world.get_enemy(entity).unwrap();
let mut path_index = enemy.path_index;
let mut path_progress = enemy.path_progress;
let speed_multiplier = if enemy.slow_duration > 0.0 { 0.5 } else { 1.0 };
let speed = enemy.speed * speed_multiplier;
path_progress += speed * delta_time;
if path_index < path.len() - 1 {
let current = path[path_index];
let next = path[path_index + 1];
let segment_length = (next - current).length();
if path_progress >= segment_length {
path_progress -= segment_length;
path_index += 1;
if path_index >= path.len() - 1 {
enemies_to_remove.push(entity);
hp_damage += 1;
world.send_enemy_reached_end(EnemyReachedEndEvent { entity, damage: 1 });
continue;
}
}
let current = path[path_index];
let next = path[path_index + 1];
let direction = (next - current).normalize();
let base_position = current + direction * path_progress;
let mut poison_death = false;
if let Some(enemy) = world.get_enemy_mut(entity) {
enemy.path_index = path_index;
enemy.path_progress = path_progress;
if enemy.slow_duration > 0.0 {
enemy.slow_duration -= delta_time;
}
if enemy.poison_duration > 0.0 {
enemy.poison_duration -= delta_time;
enemy.health -= enemy.poison_damage * delta_time;
if enemy.health <= 0.0 {
poison_death = true;
}
}
}
if poison_death {
enemies_to_remove.push(entity);
} else if let Some(pos) = world.get_position_mut(entity) {
pos.0 = base_position;
}
}
}
if hp_damage > 0 {
if world.resources.current_hp >= hp_damage {
world.resources.current_hp -= hp_damage;
} else {
world.resources.current_hp = 0;
}
if world.resources.current_hp == 0 {
world.resources.current_hp = world.resources.max_hp;
world.resources.lives = world.resources.lives.saturating_sub(1);
if world.resources.lives == 0 {
world.resources.game_state = GameState::GameOver;
}
}
}
for entity in enemies_to_remove {
if let Some(enemy) = world.get_enemy(entity) {
world.resources.money += enemy.value;
}
world.queue_despawn_entity(entity);
}
world.apply_commands();
}
fn tower_targeting_system(world: &mut GameWorld) {
let mut enemy_data = Vec::new();
world
.query()
.with(ENEMY | POSITION)
.iter(|entity, table, index| {
enemy_data.push((
entity,
table.position[index].0,
table.enemy[index].is_flying,
));
});
let tower_entities: Vec<_> = world.query_entities(TOWER | POSITION).collect();
for tower_entity in tower_entities {
let tower_data = world.get_tower(tower_entity).unwrap();
let tower_pos = world.get_position(tower_entity).unwrap().0;
let range = tower_data.tower_type.range(tower_data.level);
let range_squared = range * range;
let mut closest_enemy = None;
let mut closest_distance = f32::MAX;
for &(enemy_entity, enemy_pos, _is_flying) in &enemy_data {
let distance_squared = (enemy_pos - tower_pos).length_squared();
if distance_squared <= range_squared && distance_squared < closest_distance {
closest_distance = distance_squared;
closest_enemy = Some(enemy_entity);
}
}
if let Some(tower) = world.get_tower_mut(tower_entity) {
tower.target = closest_enemy;
if tower.target.is_some() {
tower.tracking_time += get_frame_time();
} else {
tower.tracking_time = 0.0;
}
}
}
}
fn tower_shooting_system(world: &mut GameWorld, delta_time: f32) {
let mut projectiles_to_spawn = Vec::new();
let tower_entities: Vec<_> = world.query_entities(TOWER | POSITION).collect();
for entity in tower_entities {
let tower_pos = world.get_position(entity).unwrap().0;
if let Some(tower) = world.get_tower_mut(entity) {
tower.cooldown -= delta_time;
if tower.fire_animation > 0.0 {
tower.fire_animation -= delta_time * 3.0;
}
if tower.cooldown <= 0.0 && tower.target.is_some() {
let can_fire = if tower.tower_type == TowerType::Sniper {
tower.tracking_time >= 2.0
} else {
true
};
if can_fire {
projectiles_to_spawn.push((
tower_pos,
tower.target.unwrap(),
tower.tower_type,
tower.level,
));
tower.cooldown = tower.tower_type.fire_rate(tower.level);
tower.fire_animation = 1.0;
tower.tracking_time = 0.0;
}
}
}
}
for (from, target, tower_type, level) in projectiles_to_spawn {
spawn_projectile(world, from, target, tower_type, level);
if tower_type == TowerType::Cannon {
for _ in 0..6 {
let offset = Vec2::new(rand::gen_range(-5.0, 5.0), rand::gen_range(-5.0, 5.0));
spawn_visual_effect(world, from + offset, EffectType::Explosion, Vec2::ZERO, 0.3);
}
}
}
}
fn projectile_movement_system(world: &mut GameWorld, delta_time: f32) {
let mut projectiles_to_remove = Vec::new();
let mut hits = Vec::new();
let mut enemy_positions = HashMap::new();
world
.query()
.with(ENEMY | POSITION)
.iter(|entity, table, index| {
enemy_positions.insert(entity, table.position[index].0);
});
let projectile_entities: Vec<_> = world.query_entities(PROJECTILE | POSITION).collect();
for projectile_entity in projectile_entities {
let mut projectile_data = *world.get_projectile(projectile_entity).unwrap();
let old_pos = world.get_position(projectile_entity).unwrap().0;
if let Some(&target_pos) = enemy_positions.get(&projectile_data.target) {
let total_distance = (target_pos - projectile_data.start_position).length();
let distance_to_target = (target_pos - old_pos).length();
let new_pos = if projectile_data.arc_height > 0.0 {
projectile_data.flight_progress +=
(projectile_data.speed * delta_time) / total_distance;
projectile_data.flight_progress = projectile_data.flight_progress.min(1.0);
projectile_data.start_position
+ (target_pos - projectile_data.start_position)
* projectile_data.flight_progress
} else {
let direction = (target_pos - old_pos).normalize();
old_pos + direction * projectile_data.speed * delta_time
};
if distance_to_target < 10.0 || projectile_data.flight_progress >= 1.0 {
hits.push((
projectile_data.target,
projectile_data.damage,
projectile_data.tower_type,
target_pos,
));
projectiles_to_remove.push(projectile_entity);
world.send_projectile_hit(ProjectileHitEvent {
projectile: projectile_entity,
target: projectile_data.target,
position: target_pos,
damage: projectile_data.damage,
tower_type: projectile_data.tower_type,
});
} else {
if let Some(projectile) = world.get_projectile_mut(projectile_entity) {
projectile.flight_progress = projectile_data.flight_progress;
}
if let Some(pos) = world.get_position_mut(projectile_entity) {
pos.0 = new_pos;
}
}
} else {
projectiles_to_remove.push(projectile_entity);
}
}
for (enemy_entity, damage, tower_type, hit_pos) in hits {
match tower_type {
TowerType::Frost => {
if let Some(enemy) = world.get_enemy_mut(enemy_entity) {
enemy.slow_duration = 2.0;
}
apply_damage_to_enemy(world, enemy_entity, damage);
}
TowerType::Poison => {
if let Some(enemy) = world.get_enemy_mut(enemy_entity) {
enemy.poison_duration = 3.0;
enemy.poison_damage = 5.0;
}
apply_damage_to_enemy(world, enemy_entity, damage);
for _ in 0..3 {
let velocity =
Vec2::new(rand::gen_range(-20.0, 20.0), rand::gen_range(-20.0, 20.0));
spawn_visual_effect(world, hit_pos, EffectType::PoisonBubble, velocity, 2.0);
}
}
TowerType::Cannon => {
for _ in 0..8 {
let velocity =
Vec2::new(rand::gen_range(-30.0, 30.0), rand::gen_range(-30.0, 30.0));
spawn_visual_effect(world, hit_pos, EffectType::Explosion, velocity, 0.5);
}
for (&enemy_entity, &enemy_pos) in &enemy_positions {
let distance = (enemy_pos - hit_pos).length();
if distance < 60.0 {
let damage_falloff = 1.0 - (distance / 60.0);
apply_damage_to_enemy(world, enemy_entity, damage * damage_falloff);
}
}
}
_ => {
apply_damage_to_enemy(world, enemy_entity, damage);
}
}
}
for entity in projectiles_to_remove {
world.queue_despawn_entity(entity);
}
world.apply_commands();
}
fn apply_damage_to_enemy(world: &mut GameWorld, enemy_entity: freecs::Entity, damage: f32) {
let mut should_remove = false;
let mut death_pos = Vec2::ZERO;
let mut money_earned = 0;
let mut enemy_type = EnemyType::Normal;
if let Some(enemy) = world.get_enemy_mut(enemy_entity) {
let was_alive = enemy.health > 0.0;
if enemy.shield_health > 0.0 {
let shield_damage = damage.min(enemy.shield_health);
enemy.shield_health -= shield_damage;
let remaining_damage = damage - shield_damage;
if remaining_damage > 0.0 {
enemy.health -= remaining_damage;
}
} else {
enemy.health -= damage;
}
if was_alive && enemy.health <= 0.0 {
money_earned = enemy.value;
enemy_type = enemy.enemy_type;
should_remove = true;
}
}
if should_remove {
if let Some(pos) = world.get_position(enemy_entity) {
death_pos = pos.0;
}
world.send_enemy_died(EnemyDiedEvent {
entity: enemy_entity,
position: death_pos,
reward: money_earned,
enemy_type,
});
world.queue_despawn_entity(enemy_entity);
}
}
fn visual_effects_system(world: &mut GameWorld, delta_time: f32) {
let mut effects_to_remove = Vec::new();
world
.query_mut()
.with(VISUAL_EFFECT | POSITION)
.iter(|entity, table, index| {
table.visual_effect[index].age += delta_time;
if table.visual_effect[index].age >= table.visual_effect[index].lifetime {
effects_to_remove.push(entity);
} else {
let velocity = table.visual_effect[index].velocity;
table.position[index].0 += velocity * delta_time;
}
});
for entity in effects_to_remove {
world.queue_despawn_entity(entity);
}
world.apply_commands();
}
fn update_money_popups(world: &mut GameWorld, delta_time: f32) {
let mut popups_to_remove = Vec::new();
world
.query_mut()
.with(MONEY_POPUP | POSITION)
.iter(|entity, table, index| {
table.money_popup[index].lifetime += delta_time;
if table.money_popup[index].lifetime > 2.0 {
popups_to_remove.push(entity);
} else {
table.position[index].0.y -= delta_time * 30.0;
}
});
for entity in popups_to_remove {
world.queue_despawn_entity(entity);
}
world.apply_commands();
}
fn upgrade_tower(
world: &mut GameWorld,
tower_entity: freecs::Entity,
grid_x: i32,
grid_y: i32,
) -> bool {
if let Some(tower) = world.get_tower(tower_entity) {
let current_level = tower.level;
if current_level >= 4 {
return false;
}
let upgrade_cost = tower.tower_type.upgrade_cost(current_level);
if world.resources.money < upgrade_cost {
return false;
}
let tower_type = tower.tower_type;
world.resources.money -= upgrade_cost;
if let Some(tower) = world.get_tower_mut(tower_entity) {
tower.level += 1;
let new_level = tower.level;
world.send_tower_upgraded(TowerUpgradedEvent {
entity: tower_entity,
tower_type,
old_level: current_level,
new_level,
cost: upgrade_cost,
});
let position = grid_to_base(grid_x, grid_y);
spawn_money_popup(world, position, -(upgrade_cost as i32));
return true;
}
}
false
}
fn sell_tower(world: &mut GameWorld, tower_entity: freecs::Entity, grid_x: i32, grid_y: i32) {
if let Some(tower) = world.get_tower(tower_entity) {
let tower_type = tower.tower_type;
let refund = (tower.tower_type.cost() as f32 * 0.7) as u32;
world.resources.money += refund;
let position = grid_to_base(grid_x, grid_y);
spawn_money_popup(world, position, refund as i32);
world.send_tower_sold(TowerSoldEvent {
entity: tower_entity,
tower_type,
grid_x,
grid_y,
refund,
});
world
.query_mut()
.with(GRID_CELL)
.iter(|_entity, table, index| {
if table.grid_cell[index].x == grid_x && table.grid_cell[index].y == grid_y {
table.grid_cell[index].occupied = false;
}
});
let range_indicators_to_remove: Vec<_> = world
.query_entities(RANGE_INDICATOR)
.filter_map(|range_entity| {
world
.get_range_indicator(range_entity)
.filter(|indicator| indicator.tower_entity == tower_entity)
.map(|_| range_entity)
})
.collect();
for range_entity in range_indicators_to_remove {
world.queue_despawn_entity(range_entity);
}
world.queue_despawn_entity(tower_entity);
world.apply_commands();
}
}
fn restart_game(world: &mut GameWorld) {
let towers_to_remove: Vec<_> = world.query_entities(TOWER).collect();
for entity in towers_to_remove {
world.queue_despawn_entity(entity);
}
let enemies_to_remove: Vec<_> = world.query_entities(ENEMY).collect();
for entity in enemies_to_remove {
world.queue_despawn_entity(entity);
}
let projectiles_to_remove: Vec<_> = world.query_entities(PROJECTILE).collect();
for entity in projectiles_to_remove {
world.queue_despawn_entity(entity);
}
let effects_to_remove: Vec<_> = world.query_entities(VISUAL_EFFECT).collect();
for entity in effects_to_remove {
world.queue_despawn_entity(entity);
}
let money_popups_to_remove: Vec<_> = world.query_entities(MONEY_POPUP).collect();
for entity in money_popups_to_remove {
world.queue_despawn_entity(entity);
}
let range_indicators_to_remove: Vec<_> = world.query_entities(RANGE_INDICATOR).collect();
for entity in range_indicators_to_remove {
world.queue_despawn_entity(entity);
}
world.apply_commands();
world.resources.money = 200;
world.resources.lives = 1;
world.resources.wave = 0;
world.resources.current_hp = 20;
world.resources.max_hp = 20;
world.resources.game_state = GameState::WaitingForWave;
world.resources.game_speed = 1.0;
world.resources.spawn_timer = 0.0;
world.resources.enemies_to_spawn.clear();
world.resources.wave_announce_timer = 0.0;
}
fn render_grid(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
for entity in world.query_entities(GRID_CELL) {
if let Some(cell) = world.get_grid_cell(entity) {
let base_pos = grid_to_base(cell.x, cell.y);
let pos = Vec2::new(offset.x + base_pos.x * scale, offset.y + base_pos.y * scale);
let path_start = Vec2::new(
offset.x + world.resources.path[0].x * scale,
offset.y + world.resources.path[0].y * scale,
);
let path_end = Vec2::new(
offset.x + world.resources.path.last().unwrap().x * scale,
offset.y + world.resources.path.last().unwrap().y * scale,
);
let is_start = (pos - path_start).length() < TILE_SIZE * scale / 2.0;
let is_end = (pos - path_end).length() < TILE_SIZE * scale / 2.0;
let color = if is_start {
ORANGE
} else if is_end {
BLUE
} else if cell.is_path {
Color::new(0.5, 0.3, 0.1, 1.0)
} else {
Color::new(0.1, 0.3, 0.1, 1.0)
};
draw_rectangle(
pos.x - TILE_SIZE * scale / 2.0 + scale,
pos.y - TILE_SIZE * scale / 2.0 + scale,
(TILE_SIZE - 2.0) * scale,
(TILE_SIZE - 2.0) * scale,
color,
);
}
}
if let Some((grid_x, grid_y)) = world.resources.mouse_grid_pos
&& can_place_tower_at(world, grid_x, grid_y)
{
let tower_type = world.resources.selected_tower_type;
if world.resources.money >= tower_type.cost() {
let pos = grid_to_screen(grid_x, grid_y);
draw_rectangle(
pos.x - TILE_SIZE * scale / 2.0 + scale,
pos.y - TILE_SIZE * scale / 2.0 + scale,
(TILE_SIZE - 2.0) * scale,
(TILE_SIZE - 2.0) * scale,
Color::new(
tower_type.color().r,
tower_type.color().g,
tower_type.color().b,
0.3,
),
);
draw_circle_lines(
pos.x,
pos.y,
tower_type.range(1) * scale,
2.0,
Color::new(
tower_type.color().r,
tower_type.color().g,
tower_type.color().b,
0.5,
),
);
}
}
}
fn render_towers(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
world
.query()
.with(TOWER | POSITION)
.iter(|_entity, table, index| {
let tower = &table.tower[index];
let pos = &table.position[index];
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let base_size = 20.0 + tower.fire_animation * 4.0;
let size = base_size * (1.0 + 0.15 * (tower.level - 1) as f32) * scale;
let color = tower.tower_type.color();
let level_brightness = 1.0 + 0.2 * (tower.level - 1) as f32;
let upgraded_color = Color::new(
(color.r * level_brightness).min(1.0),
(color.g * level_brightness).min(1.0),
(color.b * level_brightness).min(1.0),
color.a,
);
draw_circle(screen_pos.x, screen_pos.y, size / 2.0, upgraded_color);
draw_circle_lines(screen_pos.x, screen_pos.y, size / 2.0, 2.0, BLACK);
for ring in 1..tower.level {
let ring_radius = size / 2.0 + ring as f32 * 3.0 * scale;
draw_circle_lines(screen_pos.x, screen_pos.y, ring_radius, 1.5, upgraded_color);
}
if tower.tower_type == TowerType::Sniper
&& let Some(target_entity) = tower.target
&& let Some(target_pos) = world.get_position(target_entity)
{
let target_screen_pos = Vec2::new(
offset.x + target_pos.0.x * scale,
offset.y + target_pos.0.y * scale,
);
draw_line(
screen_pos.x,
screen_pos.y,
target_screen_pos.x,
target_screen_pos.y,
2.0,
RED,
);
}
});
if let Some((grid_x, grid_y)) = world.resources.mouse_grid_pos {
let mut tower_data = None;
world
.query()
.with(TOWER | GRID_POSITION | POSITION)
.iter(|_entity, table, index| {
if table.grid_position[index].x == grid_x && table.grid_position[index].y == grid_y
{
tower_data = Some((table.tower[index], table.position[index]));
}
});
if let Some((tower, pos)) = tower_data {
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let range = tower.tower_type.range(tower.level);
draw_circle_lines(
screen_pos.x,
screen_pos.y,
range * scale,
2.0,
tower.tower_type.color(),
);
if tower.level < 4 {
let upgrade_cost = tower.tower_type.upgrade_cost(tower.level);
let text = format!("U: Upgrade (${}) Lv{}", upgrade_cost, tower.level);
let can_afford = world.resources.money >= upgrade_cost;
let text_color = if can_afford { GREEN } else { RED };
draw_text(
&text,
screen_pos.x - 60.0 * scale,
screen_pos.y - 35.0 * scale,
20.0 * scale,
text_color,
);
} else {
draw_text(
"MAX LEVEL",
screen_pos.x - 40.0 * scale,
screen_pos.y - 35.0 * scale,
20.0 * scale,
GOLD,
);
}
if let Some(target_entity) = tower.target
&& let Some(target_pos) = world.get_position(target_entity)
{
let target_screen_pos = Vec2::new(
offset.x + target_pos.0.x * scale,
offset.y + target_pos.0.y * scale,
);
draw_line(
screen_pos.x,
screen_pos.y,
target_screen_pos.x,
target_screen_pos.y,
2.0,
RED,
);
}
}
}
}
fn render_enemies(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
world
.query()
.with(ENEMY | POSITION)
.iter(|_entity, table, index| {
let enemy = &table.enemy[index];
let pos = &table.position[index];
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let size = enemy.enemy_type.size() * scale;
draw_circle(screen_pos.x, screen_pos.y, size, enemy.enemy_type.color());
draw_circle_lines(screen_pos.x, screen_pos.y, size, 2.0, BLACK);
if enemy.shield_health > 0.0 {
let shield_alpha = enemy.shield_health / enemy.max_shield;
draw_circle_lines(
screen_pos.x,
screen_pos.y,
size + 3.0 * scale,
2.0,
Color::new(0.5, 0.5, 1.0, shield_alpha),
);
}
let health_percent = enemy.health / enemy.max_health;
let bar_width = size * 2.0;
let bar_height = 4.0 * scale;
let bar_y = screen_pos.y - size - 10.0 * scale;
draw_rectangle(
screen_pos.x - bar_width / 2.0,
bar_y,
bar_width,
bar_height,
BLACK,
);
let health_color = if health_percent > 0.5 {
GREEN
} else if health_percent > 0.25 {
YELLOW
} else {
RED
};
draw_rectangle(
screen_pos.x - bar_width / 2.0,
bar_y,
bar_width * health_percent,
bar_height,
health_color,
);
});
}
fn render_projectiles(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
world
.query()
.with(PROJECTILE | POSITION)
.iter(|_entity, table, index| {
let projectile = &table.projectile[index];
let pos = &table.position[index];
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let color = match projectile.tower_type {
TowerType::Basic => YELLOW,
TowerType::Frost => SKYBLUE,
TowerType::Cannon => ORANGE,
TowerType::Sniper => LIGHTGRAY,
TowerType::Poison => Color::new(0.5, 0.0, 0.8, 1.0),
};
let size = match projectile.tower_type {
TowerType::Cannon => 8.0,
TowerType::Sniper => 10.0,
_ => 5.0,
} * scale;
draw_circle(screen_pos.x, screen_pos.y, size, color);
});
}
fn render_visual_effects(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
world
.query()
.with(VISUAL_EFFECT | POSITION)
.iter(|_entity, table, index| {
let effect = &table.visual_effect[index];
let pos = &table.position[index];
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let progress = effect.age / effect.lifetime;
let alpha = 1.0 - progress;
match effect.effect_type {
EffectType::Explosion => {
let size = (1.0 - progress) * 10.0 * scale;
draw_circle(
screen_pos.x,
screen_pos.y,
size,
Color::new(1.0, 0.5, 0.0, alpha),
);
}
EffectType::PoisonBubble => {
let size = 5.0 * (1.0 + progress * 0.5) * scale;
draw_circle(
screen_pos.x,
screen_pos.y,
size,
Color::new(0.5, 0.0, 0.8, alpha * 0.6),
);
}
EffectType::DeathParticle => {
let size = (1.0 - progress) * 5.0 * scale;
draw_circle(
screen_pos.x,
screen_pos.y,
size,
Color::new(1.0, 0.0, 0.0, alpha),
);
}
}
});
}
fn render_money_popups(world: &GameWorld) {
let scale = get_scale();
let offset = get_offset();
world
.query()
.with(MONEY_POPUP | POSITION)
.iter(|_entity, table, index| {
let popup = &table.money_popup[index];
let pos = &table.position[index];
let screen_pos = Vec2::new(offset.x + pos.0.x * scale, offset.y + pos.0.y * scale);
let progress = popup.lifetime / 2.0;
let alpha = 1.0 - progress.min(1.0);
let text_scale = 1.0 + progress * 0.5;
let text = if popup.amount > 0 {
format!("+${}", popup.amount)
} else {
format!("-${}", -popup.amount)
};
let color = if popup.amount > 0 {
Color::new(0.0, 1.0, 0.0, alpha)
} else {
Color::new(1.0, 0.0, 0.0, alpha)
};
draw_text(
&text,
screen_pos.x - 20.0 * scale,
screen_pos.y,
20.0 * scale * text_scale,
color,
);
});
}
fn enemy_died_event_handler(world: &mut GameWorld) {
for event in world.collect_enemy_died() {
world.resources.money += event.reward;
for _ in 0..6 {
let velocity = Vec2::new(rand::gen_range(-40.0, 40.0), rand::gen_range(-40.0, 40.0));
spawn_visual_effect(
world,
event.position,
EffectType::DeathParticle,
velocity,
0.8,
);
}
if event.reward > 0 {
spawn_money_popup(world, event.position, event.reward as i32);
}
}
}
fn enemy_spawned_event_handler(world: &mut GameWorld) {
for event in world.collect_enemy_spawned() {
let position = world.get_position(event.entity).map(|p| p.0);
if let Some(pos) = position {
for _ in 0..4 {
let velocity =
Vec2::new(rand::gen_range(-30.0, 30.0), rand::gen_range(-30.0, 30.0));
spawn_visual_effect(world, pos, EffectType::DeathParticle, velocity, 0.5);
}
}
}
}
fn enemy_reached_end_event_handler(world: &mut GameWorld) {
for event in world.collect_enemy_reached_end() {
let position = world.get_position(event.entity).map(|p| p.0);
if let Some(pos) = position {
for _ in 0..8 {
let velocity =
Vec2::new(rand::gen_range(-50.0, 50.0), rand::gen_range(-50.0, 50.0));
spawn_visual_effect(world, pos, EffectType::Explosion, velocity, 0.6);
}
}
}
}
fn projectile_hit_event_handler(world: &mut GameWorld) {
for event in world.collect_projectile_hit() {
for _ in 0..3 {
let velocity = Vec2::new(rand::gen_range(-25.0, 25.0), rand::gen_range(-25.0, 25.0));
spawn_visual_effect(world, event.position, EffectType::Explosion, velocity, 0.3);
}
}
}
fn tower_placed_event_handler(world: &mut GameWorld) {
for event in world.collect_tower_placed() {
let pos = grid_to_base(event.grid_x, event.grid_y);
for _ in 0..5 {
let offset = Vec2::new(rand::gen_range(-15.0, 15.0), rand::gen_range(-15.0, 15.0));
spawn_visual_effect(world, pos + offset, EffectType::Explosion, Vec2::ZERO, 0.4);
}
}
}
fn tower_sold_event_handler(world: &mut GameWorld) {
for event in world.collect_tower_sold() {
let pos = grid_to_base(event.grid_x, event.grid_y);
for _ in 0..8 {
let velocity = Vec2::new(rand::gen_range(-40.0, 40.0), rand::gen_range(-40.0, 40.0));
spawn_visual_effect(world, pos, EffectType::DeathParticle, velocity, 0.7);
}
}
}
fn tower_upgraded_event_handler(world: &mut GameWorld) {
for event in world.collect_tower_upgraded() {
let position = world.get_position(event.entity).map(|p| p.0);
if let Some(pos) = position {
for _ in 0..12 {
let angle = rand::gen_range(0.0, std::f32::consts::TAU);
let speed = rand::gen_range(20.0, 60.0);
let velocity = Vec2::new(angle.cos() * speed, angle.sin() * speed);
spawn_visual_effect(world, pos, EffectType::Explosion, velocity, 0.8);
}
}
}
}
fn wave_started_event_handler(world: &mut GameWorld) {
for event in world.collect_wave_started() {
world.resources.wave_announce_timer = 2.0;
world.resources.wave = event.wave;
}
}
fn wave_completed_event_handler(world: &mut GameWorld) {
for event in world.collect_wave_completed() {
let bonus = 20 + event.wave * 5;
world.resources.money += bonus;
}
}
fn tag_query_example_system(world: &GameWorld) {
let flying_enemy_count = world.query_entities(FLYING_ENEMY).count();
let healer_enemy_count = world.query_entities(HEALER_ENEMY).count();
let tank_enemy_count = world.query_entities(TANK_ENEMY).count();
let sniper_tower_count = world.query_entities(SNIPER_TOWER).count();
let frost_tower_count = world.query_entities(FROST_TOWER).count();
let _ = flying_enemy_count > 0 && sniper_tower_count == 0;
let _ = healer_enemy_count > 0 && tank_enemy_count > 2;
if frost_tower_count > 0 {
world
.query()
.with(FROST_TOWER | TOWER)
.iter(|_entity, table, index| {
let _tower = &table.tower[index];
});
}
}
fn render_ui(world: &GameWorld) {
let money_text = format!("Money: ${}", world.resources.money);
draw_text(&money_text, 10.0, 30.0, 30.0, GREEN);
let lives_text = format!("Lives: {}", world.resources.lives);
draw_text(&lives_text, 10.0, 60.0, 25.0, RED);
let hp_text = format!(
"HP: {}/{}",
world.resources.current_hp, world.resources.max_hp
);
draw_text(&hp_text, 10.0, 90.0, 25.0, YELLOW);
let wave_text = format!("Wave: {}", world.resources.wave);
draw_text(&wave_text, screen_width() - 150.0, 30.0, 30.0, SKYBLUE);
let speed_text = format!("Speed: {}x", world.resources.game_speed);
draw_text(&speed_text, screen_width() - 150.0, 60.0, 20.0, WHITE);
let total_hp =
(world.resources.lives - 1) * world.resources.max_hp + world.resources.current_hp;
let max_total_hp = world.resources.lives * world.resources.max_hp;
let health_percentage = total_hp as f32 / max_total_hp as f32;
let bar_width = 200.0;
let bar_height = 20.0;
let bar_x = 10.0;
let bar_y = 100.0;
draw_rectangle(bar_x, bar_y, bar_width, bar_height, BLACK);
let health_color = if health_percentage > 0.5 {
GREEN
} else if health_percentage > 0.25 {
YELLOW
} else {
RED
};
draw_rectangle(
bar_x,
bar_y,
bar_width * health_percentage,
bar_height,
health_color,
);
let tower_ui_y = 140.0;
let tower_types = [
(TowerType::Basic, "1"),
(TowerType::Frost, "2"),
(TowerType::Cannon, "3"),
(TowerType::Sniper, "4"),
(TowerType::Poison, "5"),
];
for (index, (tower_type, key)) in tower_types.iter().enumerate() {
let x = 10.0 + index as f32 * 60.0;
let is_selected = world.resources.selected_tower_type == *tower_type;
let can_afford = world.resources.money >= tower_type.cost();
let color = if is_selected {
tower_type.color()
} else if can_afford {
Color::new(
tower_type.color().r * 0.7,
tower_type.color().g * 0.7,
tower_type.color().b * 0.7,
1.0,
)
} else {
DARKGRAY
};
draw_rectangle(x, tower_ui_y, 50.0, 50.0, color);
draw_rectangle_lines(x, tower_ui_y, 50.0, 50.0, 2.0, BLACK);
draw_text(key, x + 5.0, tower_ui_y + 20.0, 20.0, BLACK);
draw_text(
&format!("${}", tower_type.cost()),
x + 5.0,
tower_ui_y + 45.0,
15.0,
BLACK,
);
}
if world.resources.wave_announce_timer > 0.0 {
let alpha = if world.resources.wave_announce_timer < 1.0 {
world.resources.wave_announce_timer
} else {
1.0
};
let text = format!("WAVE {}", world.resources.wave);
let text_size = 60.0;
let text_dims = measure_text(&text, None, text_size as u16, 1.0);
draw_text(
&text,
screen_width() / 2.0 - text_dims.width / 2.0,
screen_height() / 2.0 - 100.0,
text_size,
Color::new(1.0, 0.8, 0.0, alpha),
);
}
match world.resources.game_state {
GameState::WaitingForWave => {
let text = "Press SPACE to start wave";
let text_size = 40.0;
let text_dims = measure_text(text, None, text_size as u16, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dims.width / 2.0,
screen_height() / 2.0,
text_size,
WHITE,
);
}
GameState::Paused => {
let text = "PAUSED - Press P to resume";
let text_size = 50.0;
let text_dims = measure_text(text, None, text_size as u16, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dims.width / 2.0,
screen_height() / 2.0,
text_size,
YELLOW,
);
}
GameState::GameOver => {
let text = "GAME OVER - Press R to restart";
let text_size = 50.0;
let text_dims = measure_text(text, None, text_size as u16, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dims.width / 2.0,
screen_height() / 2.0,
text_size,
RED,
);
}
GameState::Victory => {
let text = "VICTORY! Press R to restart";
let text_size = 50.0;
let text_dims = measure_text(text, None, text_size as u16, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dims.width / 2.0,
screen_height() / 2.0,
text_size,
GREEN,
);
}
_ => {}
}
let controls_text = "Controls: 1-5: Tower Type | Left Click: Place | Right Click: Sell | U/Middle Click: Upgrade | [/]: Speed | P: Pause | R: Restart";
draw_text(controls_text, 10.0, screen_height() - 10.0, 15.0, LIGHTGRAY);
}
fn wave_spawning_system_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
wave_spawning_system(world, delta_time);
}
fn enemy_movement_system_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
enemy_movement_system(world, delta_time);
}
fn tower_shooting_system_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
tower_shooting_system(world, delta_time);
}
fn projectile_movement_system_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
projectile_movement_system(world, delta_time);
}
fn visual_effects_system_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
visual_effects_system(world, delta_time);
}
fn update_money_popups_wrapper(world: &mut GameWorld) {
let delta_time = get_frame_time() * world.resources.game_speed;
update_money_popups(world, delta_time);
}
#[macroquad::main("Tower Defense")]
async fn main() {
let mut world = GameWorld::default();
world.resources.money = 200;
world.resources.lives = 1;
world.resources.wave = 0;
world.resources.current_hp = 20;
world.resources.max_hp = 20;
world.resources.game_state = GameState::WaitingForWave;
world.resources.game_speed = 1.0;
world.resources.selected_tower_type = TowerType::Basic;
initialize_grid(&mut world);
create_path(&mut world);
let mut game_schedule = Schedule::new();
game_schedule
.push("wave_spawning", wave_spawning_system_wrapper)
.push("enemy_movement", enemy_movement_system_wrapper)
.push("tower_targeting", tower_targeting_system)
.push("tower_shooting", tower_shooting_system_wrapper)
.push("projectile_movement", projectile_movement_system_wrapper)
.push("visual_effects", visual_effects_system_wrapper)
.push("update_money_popups", update_money_popups_wrapper)
.push("enemy_died", enemy_died_event_handler)
.push("enemy_spawned", enemy_spawned_event_handler)
.push("enemy_reached_end", enemy_reached_end_event_handler)
.push("projectile_hit", projectile_hit_event_handler)
.push("tower_placed", tower_placed_event_handler)
.push("tower_sold", tower_sold_event_handler)
.push("tower_upgraded", tower_upgraded_event_handler)
.push("wave_started", wave_started_event_handler)
.push("wave_completed", wave_completed_event_handler)
.push_readonly("tag_query_example", tag_query_example_system);
let mut render_schedule = Schedule::new();
render_schedule
.push_readonly("render_grid", render_grid)
.push_readonly("render_towers", render_towers)
.push_readonly("render_enemies", render_enemies)
.push_readonly("render_projectiles", render_projectiles)
.push_readonly("render_visual_effects", render_visual_effects)
.push_readonly("render_money_popups", render_money_popups)
.push_readonly("render_ui", render_ui);
loop {
clear_background(Color::new(0.05, 0.05, 0.05, 1.0));
input_system(&mut world);
if world.resources.game_state != GameState::Paused {
game_schedule.run(&mut world);
}
if world.resources.wave_announce_timer > 0.0 {
world.resources.wave_announce_timer -= get_frame_time();
}
if is_key_pressed(KeyCode::Space) && world.resources.game_state == GameState::WaitingForWave
{
plan_wave(&mut world);
}
render_schedule.run(&mut world);
world.step();
next_frame().await;
}
}