Appendix - Project so Far
Here's our full file directory:
Tutorial-Project/
├── Cargo.toml
├── build.rs
└── src/
├── lib.rs
│ ├── Components:
│ │ ├── Player
│ │ ├── Enemy
│ │ ├── Reticle
│ │ ├── Projectile
│ │ ├── LightSource
│ │ ├── GridDot
│ │ ├── GravitySource
│ │ ├── Velocity
│ │ └── Acceleration
│ ├── Resources:
│ │ └── PlayerInput
│ └── Systems:
│ ├── update_projectile_lifetime
│ ├── sync_player_movement_to_config_consts
│ ├── update_player_input
│ ├── spawn_everything
│ ├── apply_grid_lighting_system
│ ├── apply_grid_gravity_system
│ ├── update_player_acceleration_system
│ ├── update_enemy_acceleration_system
│ ├── apply_acceleration_system
│ ├── apply_velocity_system
│ ├── update_camera_position_system
│ ├── update_reticle_position_system
│ └── fire_projectile_system
├── config.rs
├── ffi.rs
├── grid_systems.rs
│ ├── apply_grid_lighting
│ └── apply_grid_gravity
├── movement_systems.rs
│ ├── update_player_acceleration
│ ├── update_enemy_acceleration
│ ├── apply_acceleration
│ └── apply_velocity
├── spawn_systems.rs
│ ├── spawn_grid_dots
│ ├── spawn_camera
│ ├── spawn_player
│ ├── spawn_enemy
│ └── spawn_reticle
└── util.rs
└── color_to_hsla
Individual files from src:
lib.rs
srclib.rs
use engine::prelude::*;
mod config;
mod ffi;
mod grid_systems;
mod movement_systems;
mod spawn_systems;
mod util;
use crate::config::*;
use crate::grid_systems::*;
use crate::movement_systems::*;
use crate::spawn_systems::*;
#[derive(Default)]
#[resource]
struct PlayerInput {
move_axes: Vec2,
aim_position: Vec2,
fire_projectile: bool,
}
#[component]
struct Player;
#[component]
struct Enemy;
#[component]
struct Reticle;
#[component]
struct Projectile {
lifetime: f32,
}
#[component]
struct LightSource {
falloff_distance: f32,
strength: f32,
falloff_power: f32,
}
#[derive(Default)]
#[component]
struct GridDot {
home_position: Vec2,
base_alpha: f32,
current_displacement: Vec2,
current_height: f32,
lit_base_alpha: f32,
color_buckets: [Option<ColorBucket>; MAX_BUCKETS],
}
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)]
struct ColorBucket {
h: f32,
s: f32,
l: f32,
current_strength: f32,
target_strength: f32,
}
#[derive(Default)]
#[component]
struct GravitySource {
strength: f32,
falloff_distance: f32,
falloff_power: f32,
}
#[derive(Default)]
#[component]
struct Velocity {
current_velocity: Vec2,
max_velocity: f32,
}
#[derive(Default)]
#[component]
struct Acceleration {
acceleration_rate: f32,
deceleration_rate: f32,
target_velocity: Vec2,
}
#[system]
fn update_projectile_lifetime(
mut projectiles: Query<(Mut<Projectile>, Ref<EntityId>)>,
frame_constants: Ref<FrameConstants>,
engine: Ref<Engine>,
) {
projectiles.for_each(|(mut projectile, entity_id)| {
projectile.lifetime -= frame_constants.delta_time;
if projectile.lifetime < 0.0 {
engine.despawn(*entity_id);
}
});
}
#[system]
fn sync_player_movement_to_config_consts(
mut player: Query<(Mut<Acceleration>, Mut<Velocity>, Ref<Player>)>,
) {
let Some((mut acceleration, mut velocity, _)) = player.get_mut(0) else {
return;
};
velocity.max_velocity = PLAYER_MAX_VELOCITY;
acceleration.acceleration_rate = PLAYER_ACCELERATION;
acceleration.deceleration_rate = PLAYER_DECELERATION;
}
#[system]
fn update_player_input(
mut player_input: Mut<PlayerInput>,
input: Ref<InputState>,
camera: Query<(Ref<Transform>, Ref<Camera>)>,
window: Ref<Window>,
) {
let key_to_f32 = |key_code| {
if input.keys[key_code].pressed() {
1.0
} else {
0.0
}
};
player_input.move_axes = Vec2::new(
key_to_f32(KeyCode::KeyD) - key_to_f32(KeyCode::KeyA),
key_to_f32(KeyCode::KeyW) - key_to_f32(KeyCode::KeyS),
)
.clamp_length_max(1.);
let Some((camera_transform, camera)) = camera.get(0) else {
return;
};
let normalized_cursor_position = input.mouse.cursor_position / window.size - 0.5;
let CameraAspect::FixedVirtualHeight { height } = camera.aspect else {
return;
};
let scale = Vec2::new(window.size.x * (height / window.size.y), -height);
let view_cursor_position = normalized_cursor_position * scale;
player_input.aim_position = view_cursor_position + camera_transform.position;
player_input.fire_projectile = input.mouse.buttons[MouseButton::Left].just_pressed();
}
#[system_once]
fn spawn_everything(engine: Ref<Engine>) {
spawn_grid_dots(&engine);
spawn_camera(&engine);
spawn_player(&engine);
spawn_enemy(&engine);
spawn_reticle(&engine);
}
#[system]
fn apply_grid_lighting_system(
grid_dots: Query<(Mut<Color>, Mut<GridDot>)>,
light_sources: Query<(Ref<Transform>, Ref<Color>, Ref<LightSource>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_grid_lighting(grid_dots, light_sources, frame_constants);
}
#[system]
fn apply_grid_gravity_system(
grid_dots: Query<(Mut<Transform>, Mut<GridDot>, Mut<Color>)>,
gravity_sources: Query<(Ref<Transform>, Ref<GravitySource>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_grid_gravity(grid_dots, gravity_sources, frame_constants);
}
#[system]
fn update_player_acceleration_system(
player_input: Ref<PlayerInput>,
player_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Player>)>,
) {
update_player_acceleration(player_input, player_query);
}
#[system]
fn update_enemy_acceleration_system(
enemy_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Transform>, Ref<Enemy>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_enemy_acceleration(enemy_query, player_query);
}
#[system]
fn apply_acceleration_system(
velocities_to_adjust: Query<(Ref<Acceleration>, Mut<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_acceleration(velocities_to_adjust, frame_constants);
}
#[system]
fn apply_velocity_system(
entities_to_move: Query<(Mut<Transform>, Ref<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_velocity(entities_to_move, frame_constants);
}
#[system]
fn update_camera_position_system(
camera_query: Query<(Mut<Transform>, Ref<Camera>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_camera_position(camera_query, player_query);
}
#[system]
fn update_reticle_position_system(
mut reticle_query: Query<(Mut<Transform>, Ref<Reticle>)>,
player_input: Ref<PlayerInput>,
) {
let Some((mut transform, _)) = reticle_query.get_mut(0) else {
return;
};
transform.position = player_input.aim_position;
}
#[system]
fn fire_projectile_system(
player_input: Ref<PlayerInput>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
engine: Ref<Engine>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
if player_input.fire_projectile {
fire_projectile(
player_input.aim_position,
player_transform.position,
&engine,
);
}
}
fn fire_projectile(aim_position: Vec2, player_position: Vec2, engine: &Engine) {
let direction = (aim_position - player_position).normalize();
let transform = &Transform {
position: player_position,
layer: 10.,
..Default::default()
};
let color = &Color::RED;
let circle_render = &CircleRender {
num_sides: 20,
size: Vec2::splat(15.0),
visible: true,
};
let light = &LightSource {
falloff_distance: 100.,
strength: 100.,
falloff_power: 1.5,
};
let grid_gravity = &GravitySource {
strength: 1000.,
falloff_distance: 100.,
falloff_power: 1.5,
};
let velocity = &Velocity {
current_velocity: direction * 2000.,
max_velocity: 2000.,
};
let projectile = &Projectile { lifetime: 2.0 };
engine.spawn(bundle!(
transform,
color,
circle_render,
light,
grid_gravity,
velocity,
projectile,
));
}
config.rs
srcconfig.rs
pub const PLAYER_MAX_VELOCITY: f32 = 750.;
pub const PLAYER_ACCELERATION: f32 = 9000.;
pub const PLAYER_DECELERATION: f32 = 12000.;
pub const ZERO_VELOCITY_THRESHOLD: f32 = 0.1;
pub const ENEMY_MAX_VELOCITY: f32 = 300.;
pub const ENEMY_ACCELERATION: f32 = 1000.;
pub const ENEMY_DECELERATION: f32 = 5000.;
pub const ENEMY_CHASE_DISTANCE: f32 = 1000.;
pub const ENEMY_STOP_DISTANCE: f32 = 200.;
pub const GRID_DOT_SIZE: f32 = 6.;
pub const GRID_DOT_SIDES: u32 = 5;
pub const GRID_DOT_SPACING: f32 = 25.;
pub const GRID_ROWS: u32 = 200;
pub const GRID_COLS: u32 = 200;
pub const GRID_DOT_ALPHA_1: f32 = 0.7;
pub const GRID_DOT_ALPHA_2: f32 = 0.8;
pub const GRID_DOT_COLOR_SPACING: u32 = 5;
pub const PLAYER_GRAVITY_STRENGTH: f32 = 10000.0;
pub const PLAYER_FALLOFF_POWER: f32 = 10.0;
pub const PLAYER_FALLOFF_DISTANCE: f32 = 1500.0;
pub const ENEMY_GRAVITY_STRENGTH: f32 = -10000.0;
pub const ENEMY_FALLOFF_POWER: f32 = 5.0;
pub const ENEMY_FALLOFF_DISTANCE: f32 = 250.0;
pub const MIN_GRAVITY_DISTANCE: f32 = 0.01;
pub const INFLUENCE_CUTOFF: f32 = 0.00001;
pub const MAX_DISPLACEMENT: f32 = 100.0;
pub const MAX_STRETCH_AMOUNT: f32 = 3.;
pub const STRETCH_SENSITIVITY: f32 = 0.3;
pub const MAX_HEIGHT_RANGE: f32 = 3.0;
pub const ALPHA_SENSITIVITY: f32 = 3.;
pub const MIN_ALPHA_PERCENTAGE: f32 = 0.11;
pub const SMOOTHING_TIME: f32 = 0.1;
pub const PLAYER_LIGHT_RANGE: f32 = 1000.0;
pub const PLAYER_LIGHT_INTENSITY: f32 = 6.0;
pub const PLAYER_LIGHT_FALLOFF_POWER: f32 = 3.5;
pub const ENEMY_LIGHT_RANGE: f32 = 400.0;
pub const ENEMY_LIGHT_INTENSITY: f32 = 10.;
pub const ENEMY_LIGHT_FALLOFF_POWER: f32 = 2.5;
pub const CHARGE_RATE: f32 = 2.;
pub const DISCHARGE_RATE: f32 = 0.5;
pub const LIGHT_ALPHA_BOOST: f32 = 0.75;
pub const MAX_LIT_ALPHA: f32 = 1.0;
pub const MAX_BUCKETS: usize = 8;
grid_systems.rs
srcgrid_systems.rs
use crate::config::*;
use crate::util::*;
use crate::{Color, ColorBucket, GravitySource, GridDot, LightSource, Transform};
use engine::prelude::*;
pub fn apply_grid_lighting(
mut grid_dots: Query<(Mut<Color>, Mut<GridDot>)>,
light_sources: Query<(Ref<Transform>, Ref<Color>, Ref<LightSource>)>,
frame_constants: Ref<FrameConstants>,
) {
let delta_time = frame_constants.delta_time;
// Pre-process light sources
let lights: Vec<_> = light_sources
.iter()
.map(|(transform, color, light)| {
let (h, s, l, _) = color_to_hsla(&color);
(
transform.position,
h,
s,
l,
light.falloff_distance,
light.strength,
light.falloff_power,
)
})
.collect();
grid_dots.for_each(|(mut color, mut grid_dot)| {
let dot_pos = grid_dot.home_position + grid_dot.current_displacement;
// === CALCULATE CURRENT LIGHTING ===
let mut current_colors = [(0.0, 0.0, 0.0, 0.0); MAX_BUCKETS];
let mut current_count = 0;
for (light_pos, h, s, l, range, intensity, falloff_power) in &lights {
let distance = (*light_pos - dot_pos).length();
let falloff = (1.0 - (distance / range).min(1.0)).powf(*falloff_power);
let strength = falloff * intensity;
if strength > 0.01 && current_count < MAX_BUCKETS {
current_colors[current_count] = (*h, *s, *l, strength);
current_count += 1;
}
}
// === MATCH EXISTING BUCKETS WITH CURRENT COLORS ===
// Track which current colors have been handled
let mut handled_colors = [false; MAX_BUCKETS];
// First, iterate over all existing buckets
for i in 0..MAX_BUCKETS {
if let Some(ref mut bucket) = grid_dot.color_buckets[i] {
let mut found_match = false;
// Check if this bucket matches any current color
for j in 0..current_count {
if !handled_colors[j] {
let (h, s, l, strength) = current_colors[j];
if (bucket.h - h).abs() < 1.0 && // 1 degrees hue tolerance
(bucket.s - s).abs() < 0.1 &&
(bucket.l - l).abs() < 0.1
{
bucket.target_strength = strength;
handled_colors[j] = true;
found_match = true;
break;
}
}
}
// If no match found, this bucket should fade out
if !found_match {
bucket.target_strength = 0.0;
}
}
}
// === FILL EMPTY BUCKETS WITH REMAINING CURRENT COLORS ===
for i in 0..current_count {
if !handled_colors[i] {
let (h, s, l, strength) = current_colors[i];
// Find empty slot for new bucket
for j in 0..MAX_BUCKETS {
if grid_dot.color_buckets[j].is_none() {
grid_dot.color_buckets[j] = Some(ColorBucket {
h,
s,
l,
current_strength: 0.0,
target_strength: strength,
});
break;
}
}
}
}
// === UPDATE BUCKETS ===
for i in 0..MAX_BUCKETS {
if let Some(ref mut bucket) = grid_dot.color_buckets[i] {
if bucket.current_strength < bucket.target_strength {
// Charge up towards target
let charge_amount = (bucket.target_strength - bucket.current_strength)
* CHARGE_RATE
* delta_time;
bucket.current_strength =
(bucket.current_strength + charge_amount).min(bucket.target_strength);
} else if bucket.current_strength > bucket.target_strength {
// Discharge down towards target
let discharge_amount = (bucket.current_strength - bucket.target_strength)
* DISCHARGE_RATE
* delta_time;
bucket.current_strength =
(bucket.current_strength - discharge_amount).max(bucket.target_strength);
}
// Clear if too weak
if bucket.current_strength < 0.00001 {
grid_dot.color_buckets[i] = None;
}
}
}
// === BLEND BUCKETS FOR DISPLAY ===
let mut total_strength = 0.0;
for i in 0..MAX_BUCKETS {
if let Some(bucket) = &grid_dot.color_buckets[i] {
total_strength += bucket.current_strength;
}
}
if total_strength > 0.01 {
// Weighted blend of all buckets
let mut blended_h = 0.0;
let mut blended_s = 0.0;
let mut blended_l = 0.0;
for i in 0..MAX_BUCKETS {
if let Some(bucket) = &grid_dot.color_buckets[i] {
let weight = bucket.current_strength / total_strength;
blended_h += bucket.h * weight;
blended_s += bucket.s * weight;
blended_l += bucket.l * weight;
}
}
// Mix with white based on total strength
let intensity = total_strength.min(1.0);
let final_s = blended_s * intensity;
let final_l = blended_l * intensity + (1.0 - intensity);
*color = Color::from_hsla(blended_h, final_s, final_l, color.a());
// Alpha boost
let alpha_boost = 1.0 + intensity * LIGHT_ALPHA_BOOST;
grid_dot.lit_base_alpha = (grid_dot.base_alpha * alpha_boost).min(MAX_LIT_ALPHA);
} else {
// No buckets - pure white
*color = Color::new(1.0, 1.0, 1.0, color.a());
grid_dot.lit_base_alpha = grid_dot.base_alpha;
}
});
}
pub fn apply_grid_gravity(
mut grid_dots: Query<(Mut<Transform>, Mut<GridDot>, Mut<Color>)>,
gravity_sources: Query<(Ref<Transform>, Ref<GravitySource>)>,
frame_constants: Ref<FrameConstants>,
) {
let sources: Vec<_> = gravity_sources
.iter()
.map(|(transform, gravity)| {
(
transform.position,
gravity.strength,
gravity.falloff_distance,
gravity.falloff_power,
)
})
.collect();
let dt = frame_constants.delta_time;
let smoothing_factor = (1.0 - (-dt / SMOOTHING_TIME).exp()).min(1.0);
grid_dots.for_each(|(mut transform, mut grid_dot, mut color)| {
// Calculate combined influence from all sources
let mut weighted_sum = Vec2::ZERO;
let mut max_possible = 0.0;
for (source_pos, strength, falloff_distance, falloff_power) in &sources {
let to_source = *source_pos - grid_dot.home_position;
let distance = to_source.length().max(MIN_GRAVITY_DISTANCE);
let direction = to_source / distance;
let falloff = (1.0 - (distance / falloff_distance).min(1.0)).powf(*falloff_power);
let influence = strength * falloff;
if influence.abs() > INFLUENCE_CUTOFF {
weighted_sum += direction * influence;
max_possible += strength.abs();
}
}
// Calculate influence direction
let influence_direction = weighted_sum.try_normalize().unwrap_or(Vec2::ZERO);
// For non-displacement effects, normalize by max possible
let influence_strength = if max_possible > 0.0 {
(weighted_sum.length() / max_possible).min(1.0)
} else {
0.0
};
// For displacement, don't normalize - just clamp the final result
// This preserves the actual push/pull strength
let displacement_vector = (weighted_sum).clamp_length_max(MAX_DISPLACEMENT);
let target_position = grid_dot.home_position + displacement_vector;
let target_height = influence_strength * MAX_HEIGHT_RANGE;
let stretch = 1. + (MAX_STRETCH_AMOUNT - 1.) * (influence_strength + STRETCH_SENSITIVITY);
let target_scale = Vec2::new(stretch, 1.0 / stretch.sqrt());
let target_rotation = influence_direction.y.atan2(influence_direction.x);
// Smooth position (stored as displacement for lighting system compatibility)
let new_position = transform.position.lerp(target_position, smoothing_factor);
grid_dot.current_displacement = new_position - grid_dot.home_position;
transform.position = new_position;
grid_dot.current_height = target_height;
// Smooth scale
transform.scale = transform.scale.lerp(target_scale, smoothing_factor);
// Smooth rotation with angle wrapping
let current_rot = transform.rotation;
let rotation_diff = {
let diff = target_rotation - current_rot;
if diff > std::f32::consts::PI {
diff - 2.0 * std::f32::consts::PI
} else if diff < -std::f32::consts::PI {
diff + 2.0 * std::f32::consts::PI
} else {
diff
}
};
transform.rotation = current_rot + rotation_diff * smoothing_factor;
// Apply alpha based on height
let height_factor = (grid_dot.current_height / MAX_HEIGHT_RANGE).clamp(0.0, 1.0);
let min_alpha = grid_dot.base_alpha * MIN_ALPHA_PERCENTAGE;
let alpha = grid_dot.base_alpha
- (grid_dot.base_alpha - min_alpha) * height_factor * ALPHA_SENSITIVITY;
*color = Color::new(color.r(), color.g(), color.b(), alpha);
});
}
movement_systems.rs
srcmovement_systems.rs
use crate::config::*;
use crate::{Acceleration, Enemy, Player, PlayerInput, Velocity};
use engine::prelude::*;
pub fn update_player_acceleration(
player_input: Ref<PlayerInput>,
mut player_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Player>)>,
) {
let Some((mut acceleration, velocity, _)) = player_query.get_mut(0) else {
return;
};
acceleration.target_velocity = player_input.move_axes * velocity.max_velocity;
}
pub fn update_enemy_acceleration(
mut enemy_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Transform>, Ref<Enemy>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
enemy_query.for_each(|(mut acceleration, velocity, transform, _)| {
let to_player = player_transform.position - transform.position;
let distance = to_player.length();
if distance > ENEMY_CHASE_DISTANCE {
acceleration.target_velocity = Vec2::ZERO;
} else {
let ideal_position =
player_transform.position - to_player.normalize() * ENEMY_STOP_DISTANCE;
let to_ideal = ideal_position - transform.position;
let distance_to_ideal = to_ideal.length();
if distance_to_ideal < 20.0 {
acceleration.target_velocity = Vec2::ZERO;
} else {
let speed_factor = if distance_to_ideal < 50.0 { 0.5 } else { 1.0 };
acceleration.target_velocity =
to_ideal.normalize_or_zero() * (velocity.max_velocity * speed_factor);
}
}
});
}
pub fn apply_acceleration(
mut velocities_to_adjust: Query<(Ref<Acceleration>, Mut<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
velocities_to_adjust.for_each(|(acceleration, mut velocity)| {
// Calculate the difference between current and target velocity
let velocity_delta = acceleration.target_velocity - velocity.current_velocity;
// Determine if we're accelerating or decelerating
let is_decelerating = acceleration.target_velocity == Vec2::ZERO
|| acceleration.target_velocity.dot(velocity.current_velocity) < 0.0;
let rate = if is_decelerating {
acceleration.deceleration_rate
} else {
acceleration.acceleration_rate
};
// Calculate the maximum change allowed this frame
let max_change = rate * frame_constants.delta_time;
// Apply the change, clamped to the maximum rate
let velocity_change = velocity_delta.clamp_length_max(max_change);
velocity.current_velocity += velocity_change;
// Check if we're trying to stop on each axis.
if acceleration.target_velocity.x.abs() < ZERO_VELOCITY_THRESHOLD
&& velocity.current_velocity.x.abs() < ZERO_VELOCITY_THRESHOLD
{
velocity.current_velocity.x = 0.;
}
if acceleration.target_velocity.y.abs() < ZERO_VELOCITY_THRESHOLD
&& velocity.current_velocity.y.abs() < ZERO_VELOCITY_THRESHOLD
{
velocity.current_velocity.y = 0.;
}
// Clamp to max velocity
velocity.current_velocity = velocity
.current_velocity
.clamp_length_max(velocity.max_velocity);
});
}
pub fn apply_velocity(
mut entities_to_move: Query<(Mut<Transform>, Ref<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
entities_to_move.for_each(|(mut transform, velocity)| {
transform.position += velocity.current_velocity * frame_constants.delta_time;
});
}
pub fn update_camera_position(
mut camera_query: Query<(Mut<Transform>, Ref<Camera>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
let Some((mut camera_transform, _)) = camera_query.get_mut(0) else {
return;
};
camera_transform.position = player_transform.position;
}
spawn_systems.rs
srcspawn_systems.rs
use crate::{Acceleration, Enemy, Engine, GravitySource, GridDot, LightSource, Player, Velocity};
use crate::{Reticle, config::*};
use engine::prelude::*;
pub fn spawn_grid_dots(engine: &Engine) {
// Calculate grid dimensions for centering at (0,0)
let grid_width = (GRID_COLS - 1) as f32 * GRID_DOT_SPACING;
let grid_height = (GRID_ROWS - 1) as f32 * GRID_DOT_SPACING;
let start_x = -grid_width / 2.0;
let start_y = -grid_height / 2.0;
for row in 0..=GRID_ROWS {
for col in 0..=GRID_COLS {
// Calculate position for this grid dot
let x = start_x + ((col) as f32 * GRID_DOT_SPACING);
let y = start_y + ((row) as f32 * GRID_DOT_SPACING);
let position = Vec2::new(x, y);
let transform = &Transform {
position,
scale: Vec2::splat(1.0),
..Default::default()
};
// Determine if this is a grid line dot
let is_grid_line =
row % GRID_DOT_COLOR_SPACING == 0 || col % GRID_DOT_COLOR_SPACING == 0;
let base_alpha = if is_grid_line {
GRID_DOT_ALPHA_2
} else {
GRID_DOT_ALPHA_1
};
// All dots start as white with their respective alpha
let color = &Color::new(1.0, 1.0, 1.0, base_alpha);
let circle_render = &CircleRender {
num_sides: GRID_DOT_SIDES,
size: Vec2::splat(GRID_DOT_SIZE),
visible: true,
};
let grid_dot = &GridDot {
home_position: position,
base_alpha,
..Default::default()
};
engine.spawn(bundle!(transform, color, circle_render, grid_dot));
}
}
}
pub fn spawn_player(engine: &Engine) {
let transform = &Transform {
position: Vec2::new(1000., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.96, 0.36, 0.9, 1.0);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
let velocity = &Velocity {
max_velocity: PLAYER_MAX_VELOCITY,
..Default::default()
};
let acceleration = &Acceleration {
acceleration_rate: PLAYER_ACCELERATION,
deceleration_rate: PLAYER_DECELERATION,
..Default::default()
};
let light = &LightSource {
falloff_distance: PLAYER_LIGHT_RANGE,
strength: PLAYER_LIGHT_INTENSITY,
falloff_power: PLAYER_LIGHT_FALLOFF_POWER,
};
let grid_gravity = &GravitySource {
strength: PLAYER_GRAVITY_STRENGTH,
falloff_distance: PLAYER_FALLOFF_DISTANCE,
falloff_power: PLAYER_FALLOFF_POWER,
};
engine.spawn(bundle!(
transform,
color,
circle_render,
&Player,
velocity,
acceleration,
light,
grid_gravity
));
}
pub fn spawn_camera(engine: &Engine) {
let camera = &Camera {
aspect: CameraAspect::FixedVirtualHeight { height: 1500. },
..Default::default()
};
engine.spawn(bundle!(&Transform::default(), camera));
}
pub fn spawn_enemy(engine: &Engine) {
let transform = &Transform {
position: Vec2::new(-110., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.2, 0.7, 0.7, 1.);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
let velocity = &Velocity {
max_velocity: ENEMY_MAX_VELOCITY,
..Default::default()
};
let acceleration = &Acceleration {
acceleration_rate: ENEMY_ACCELERATION,
deceleration_rate: ENEMY_DECELERATION,
..Default::default()
};
let light = &LightSource {
falloff_distance: ENEMY_LIGHT_RANGE,
strength: ENEMY_LIGHT_INTENSITY,
falloff_power: ENEMY_LIGHT_FALLOFF_POWER,
};
let grid_gravity = &GravitySource {
strength: ENEMY_GRAVITY_STRENGTH,
falloff_distance: ENEMY_FALLOFF_DISTANCE,
falloff_power: ENEMY_FALLOFF_POWER,
};
engine.spawn(bundle!(
transform,
color,
circle_render,
&Enemy,
velocity,
acceleration,
light,
grid_gravity
));
}
pub fn spawn_reticle(engine: &Engine) {
let transform = &Transform {
layer: 100.,
..Default::default()
};
let color = &Color::new(0.8, 0.6, 0.2, 1.0);
let circle_render = &CircleRender {
num_sides: 7,
size: Vec2::splat(15.0),
visible: true,
};
let light = &LightSource {
falloff_distance: 100.,
strength: 30.,
falloff_power: 1.5,
};
let grid_gravity = &GravitySource {
strength: -80.,
falloff_distance: 50.,
falloff_power: 0.5,
};
engine.spawn(bundle!(
transform,
color,
circle_render,
light,
grid_gravity,
&Reticle
));
}
util.rs
srcutil.rs
use engine::prelude::*;
// Helper function to convert Color to HSLA since it's not in the Color API
pub fn color_to_hsla(color: &Color) -> (f32, f32, f32, f32) {
let r = color.r();
let g = color.g();
let b = color.b();
let a = color.a();
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
// Calculate lightness
let l = (max + min) / 2.0;
if delta == 0.0 {
// Achromatic (gray)
return (0.0, 0.0, l, a);
}
// Calculate saturation
let s = if l < 0.5 {
delta / (max + min)
} else {
delta / (2.0 - max - min)
};
// Calculate hue
let h = if max == r {
((g - b) / delta + if g < b { 6.0 } else { 0.0 }) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
(h * 360.0, s, l, a)
}