kirby can suck bubbles
All checks were successful
Build Bevy Game (Linux) / build (push) Successful in 19m20s

This commit is contained in:
Crizomb 2025-12-29 23:20:32 +01:00
parent 0eb55dbe58
commit 3dc47f958d
9 changed files with 182 additions and 76 deletions

View file

@ -2,4 +2,5 @@ max_width = 120
chain_width = 120
fn_call_width = 120
use_small_heuristics = "Max"
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View file

@ -1,13 +1,17 @@
use crate::density_grid::DensityObject;
use crate::kirby::Kirby;
use crate::physics_body::PhysicsBody;
use std::{f32::consts::TAU, fmt::Debug};
use bevy::prelude::*;
use rand::Rng;
use std::f32::consts::TAU;
use std::fmt::Debug;
use crate::{
density_grid::DensityObject,
kirby::Kirby,
life::{DamageDealer, Life},
physics_body::PhysicsBody,
};
#[derive(Component)]
#[require(Sprite, PhysicsBody, DensityObject)]
#[require(Sprite, PhysicsBody, DensityObject, Life, DamageDealer)]
pub struct Bubble {
move_force: f32,
}
@ -16,7 +20,7 @@ pub fn bubble_spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture: Handle<Image> = asset_server.load("sprites/bubble.png");
let mut rng = rand::rng();
for _ in 0..1000 {
for _ in 0..500 {
let angle = rng.random_range(0.0..TAU);
let pos = Vec2::from_angle(angle) * 500.0;
@ -25,27 +29,29 @@ pub fn bubble_spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
Sprite { image: texture.clone(), ..default() },
Transform::from_translation(pos.extend(0.0)).with_scale(Vec3::splat(1.0)),
PhysicsBody { mass: 10.0, force: Vec2::ZERO, velocity: Vec2::ZERO, drag: 0.05 },
Life::new(10.0),
DamageDealer::new(0.5),
));
}
}
pub fn bubble_move(
mut lana_query: Query<(&mut PhysicsBody, &Transform, &Bubble), With<Bubble>>,
mut bubble_query: Query<(&mut PhysicsBody, &Transform, &Bubble), With<Bubble>>,
kirby_query: Query<(&Transform, &Kirby), With<Kirby>>,
) {
for (mut lana_body, lana_transform, lana) in &mut lana_query {
for (mut bubble_body, bubble_transform, bubble) in &mut bubble_query {
let mut nearest_kirby_pos = Vec3::ZERO;
let mut nearest_squared = f32::MAX;
for (kirby_transform, _kirby) in kirby_query {
let dist_squared = kirby_transform.translation.distance_squared(lana_transform.translation);
let dist_squared = kirby_transform.translation.distance_squared(bubble_transform.translation);
if (dist_squared) < nearest_squared {
nearest_squared = dist_squared;
nearest_kirby_pos = kirby_transform.translation;
}
}
let dir = (nearest_kirby_pos - lana_transform.translation).normalize().xy();
lana_body.force += dir * lana.move_force;
let dir = (nearest_kirby_pos - bubble_transform.translation).normalize().xy();
bubble_body.force += dir * bubble.move_force;
}
}

View file

@ -1,10 +1,10 @@
use std::f32::consts::TAU;
use crate::globals::MAX_MAP_WIDTH;
use crate::physics_body::PhysicsBody;
use bevy::prelude::*;
use rand::Rng;
use crate::{globals::MAX_MAP_WIDTH, physics_body::PhysicsBody};
const CELL_SIZE: usize = 8;
const MAX_MAP_SIZE: usize = MAX_MAP_WIDTH * 2;
const ARRAY_WIDTH: usize = MAX_MAP_SIZE / CELL_SIZE;

View file

@ -1,13 +1,23 @@
use crate::animation::{AnimationIndices, AnimationTimer};
use crate::physics_body::PhysicsBody;
use crate::sphere_collider::SphereCollider;
use bevy::prelude::*;
const KIRBY_SCALE: f32 = 3.0;
use crate::{
animation::{AnimationIndices, AnimationTimer},
life::{DamageDealer, Life},
physics_body::PhysicsBody,
sphere_collider::SphereCollider,
};
#[derive(Component)]
#[require(Sprite, PhysicsBody, SphereCollider, AnimationIndices)]
#[require(Transform, SphereCollider, DamageDealer)]
pub struct SuckArea;
#[derive(Component)]
#[require(Sprite, PhysicsBody, SphereCollider, AnimationIndices, Life)]
pub struct Kirby {
speed_force: f32,
sucking: bool,
pub is_sucking: bool,
}
pub fn kirby_spawn(
@ -21,17 +31,29 @@ pub fn kirby_spawn(
let texture_atlas_layout = texture_atlas_layouts.add(layout);
let animation_indices = AnimationIndices { first: 3, last: 5 }; // idle
commands.spawn((
Kirby { speed_force: 10000.0, sucking: false },
let kirby_entity = commands
.spawn((
Kirby { speed_force: 10000.0, is_sucking: false },
Sprite::from_atlas_image(
texture,
TextureAtlas { layout: texture_atlas_layout, index: animation_indices.first },
),
Transform::from_scale(Vec3::splat(3.0)),
Transform::from_scale(Vec3::splat(KIRBY_SCALE)),
animation_indices,
AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
body,
SphereCollider::new(50.0),
))
.id();
commands.entity(kirby_entity).with_children(|parent| {
parent.spawn((
Transform::from_xyz(40.0, 0.0, 0.0),
SuckArea,
SphereCollider::new(80.0),
DamageDealer::new(10000.0),
));
});
}
pub fn get_dir(keys: Res<ButtonInput<KeyCode>>) -> Vec2 {
@ -55,26 +77,26 @@ pub fn get_dir(keys: Res<ButtonInput<KeyCode>>) -> Vec2 {
pub fn kirby_actions(
keys: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut PhysicsBody, &mut Kirby, &mut AnimationIndices, &mut Transform, &mut Sprite), With<Kirby>>,
mut query: Query<(&mut PhysicsBody, &mut Kirby, &mut AnimationIndices, &mut Transform), With<Kirby>>,
) {
let space_just_pressed = keys.just_pressed(KeyCode::Space);
let space_just_released = keys.just_released(KeyCode::Space);
let dir = get_dir(keys);
for (mut body, mut kirby, mut anim_indices, mut transform, mut sprite) in &mut query {
for (mut body, mut kirby, mut anim_indices, mut transform) in &mut query {
if space_just_released {
kirby.sucking = false
kirby.is_sucking = false
};
if space_just_pressed {
kirby.sucking = true;
kirby.is_sucking = true;
anim_indices.change(0, 2);
};
if kirby.sucking {
if kirby.is_sucking {
continue;
};
if dir.x != 0.0 {
sprite.flip_x = dir.x < 0.0;
transform.scale.x = if dir.x < 0.0 { -KIRBY_SCALE } else { KIRBY_SCALE };
}
body.force += dir * kirby.speed_force;
if dir == Vec2::ZERO {
@ -84,9 +106,3 @@ pub fn kirby_actions(
}
}
}
#[derive(Component)]
#[require(Sprite, Transform)]
pub struct KirbySuction {
box_size: Vec2,
}

96
src/life.rs Normal file
View file

@ -0,0 +1,96 @@
use bevy::prelude::*;
use crate::{
bubble::Bubble,
kirby::{Kirby, SuckArea},
sphere_collider::SphereCollider,
};
#[derive(Component)]
pub struct Life {
max_life: f32,
current_life: f32,
}
impl Life {
pub fn new(max_life: f32) -> Self {
Self { max_life, current_life: max_life }
}
pub fn damage(&mut self, amount: f32) {
self.current_life -= amount;
}
pub fn heal(&mut self, amount: f32) {
self.current_life = (self.current_life + amount).min(self.max_life);
}
}
impl Default for Life {
fn default() -> Self {
Life::new(100.0)
}
}
pub fn despawn_if_dead(mut commands: Commands, query: Query<(Entity, &Life)>) {
for (entity, life) in query.iter() {
if life.current_life < 0.0 {
commands.entity(entity).despawn();
}
}
}
#[derive(Component, Default)]
#[require(SphereCollider)]
pub struct DamageDealer {
damage_strength: f32,
}
impl DamageDealer {
pub fn new(damage: f32) -> Self {
DamageDealer { damage_strength: damage }
}
}
// Considerating ennemies are ponctual
pub fn bubble_receive_damage(
time: Res<Time<Fixed>>,
mut enemy_query: Query<(&GlobalTransform, &mut Life), With<Bubble>>,
kirby_query: Query<(&Kirby, &Children)>,
suck_area_query: Query<(&GlobalTransform, &SphereCollider, &DamageDealer), With<SuckArea>>,
) {
for (kirby, children) in &kirby_query {
if !kirby.is_sucking {
continue;
}
for child in children.iter() {
let Ok((suck_transform, suck_collider, suck_damage)) = suck_area_query.get(child) else {
continue;
};
for (enemy_transform, mut enemy_life) in &mut enemy_query {
if suck_collider.point_inside(suck_transform.translation(), enemy_transform.translation()) {
enemy_life.damage(suck_damage.damage_strength * time.delta_secs());
}
}
}
}
}
// Considerating allies are ponctual
pub fn kirby_receive_damage(
mut ally_query: Query<(&GlobalTransform, &mut Life), With<Kirby>>,
mut enemy_query: Query<(&GlobalTransform, &SphereCollider, &DamageDealer, &mut Life), With<Bubble>>,
) {
for (ally_transform, mut ally_life) in &mut ally_query {
for (enemy_transform, enemy_sphere_collider, enemy_damage_dealer, mut bubble_life) in &mut enemy_query {
if enemy_sphere_collider.point_inside(ally_transform.translation(), enemy_transform.translation()) {
ally_life.damage(enemy_damage_dealer.damage_strength);
bubble_life.damage(1000000.0); // Bubble explode
}
}
}
}

View file

@ -1,23 +1,22 @@
use animation::animate_sprite;
use bevy::prelude::*;
use bubble::bubble_move;
use bubble::bubble_spawn;
use bubble::{bubble_move, bubble_spawn};
use camera::spawn_camera;
use density_grid::DensityGrid;
use density_grid::density_grid_force;
use density_grid::density_grid_update;
use density_grid::init_density_object;
use kirby::kirby_actions;
use kirby::kirby_spawn;
use density_grid::{DensityGrid, density_grid_force, density_grid_update, init_density_object};
use kirby::{kirby_actions, kirby_spawn};
use life::despawn_if_dead;
use map::MapBounds;
use physics_body::integrate;
use crate::life::bubble_receive_damage;
mod animation;
mod bubble;
mod camera;
mod density_grid;
mod globals;
mod kirby;
mod life;
mod map;
mod physics_body;
mod sphere_collider;
@ -25,6 +24,7 @@ mod sphere_collider;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.insert_resource(Time::<Fixed>::from_seconds(1.0 / 60.0))
.insert_resource(MapBounds { min: Vec2::ONE * -600.0, max: Vec2::ONE * 600.0 })
.insert_resource(DensityGrid::new())
.add_systems(Startup, spawn_camera)
@ -35,6 +35,8 @@ fn main() {
.add_systems(FixedUpdate, (density_grid_update, density_grid_force).chain())
.add_systems(FixedUpdate, kirby_actions)
.add_systems(FixedUpdate, bubble_move)
.add_systems(FixedUpdate, bubble_receive_damage)
.add_systems(FixedPostUpdate, despawn_if_dead)
.add_systems(FixedPostUpdate, integrate)
.run();
}

View file

@ -1,17 +1,7 @@
use bevy::{prelude::*, render::renderer};
#[derive(Component)]
pub struct MapRoot;
use bevy::prelude::*;
#[derive(Resource)]
pub struct MapBounds {
pub min: Vec2,
pub max: Vec2,
}
pub fn setup_map_bounds(mut commands: Commands) {
commands.insert_resource(MapBounds {
min: Vec2::new(-320.0, -240.0),
max: Vec2::new(320.0, 240.0),
});
}

View file

@ -1,6 +1,7 @@
use crate::map::MapBounds;
use bevy::prelude::*;
use crate::map::MapBounds;
#[derive(Component)]
#[require(Transform)]
pub struct PhysicsBody {
@ -12,20 +13,11 @@ pub struct PhysicsBody {
impl Default for PhysicsBody {
fn default() -> Self {
Self {
mass: 1.0,
force: Vec2::ZERO,
velocity: Vec2::ZERO,
drag: 0.0,
}
Self { mass: 1.0, force: Vec2::ZERO, velocity: Vec2::ZERO, drag: 0.0 }
}
}
pub fn integrate(
bounds: Res<MapBounds>,
mut query: Query<(&mut Transform, &mut PhysicsBody)>,
time: Res<Time<Fixed>>,
) {
pub fn integrate(bounds: Res<MapBounds>, mut query: Query<(&mut Transform, &mut PhysicsBody)>, time: Res<Time<Fixed>>) {
for (mut transform, mut physicsbody) in &mut query {
let force = physicsbody.force;
let mass = physicsbody.mass;

View file

@ -7,10 +7,13 @@ pub struct SphereCollider {
}
impl SphereCollider {
fn point_inside(&self, self_pos: Vec3, point: Vec3) -> bool {
pub fn new(radius: f32) -> Self {
SphereCollider { radius: radius }
}
pub fn point_inside(&self, self_pos: Vec3, point: Vec3) -> bool {
(self_pos - point).length_squared() < self.radius * self.radius
}
fn collides(&self, self_pos: Vec3, other: &SphereCollider, other_pos: Vec3) -> bool {
pub fn collides(&self, self_pos: Vec3, other: &SphereCollider, other_pos: Vec3) -> bool {
let radius_sum = self.radius + other.radius;
(self_pos - other_pos).length_squared() < radius_sum * radius_sum
}