use bevy::prelude::*;
use bevy_carnage::{CarnageSettings, CarnageVfxPlugin, PoolDecal};
mod common;
use common::body::{self, Chunk, ORIGIN};
use common::light_and_floor;
const GRANULARITY: usize = 0;
const SOFTEN: f32 = 0.5;
const CALIBRE: f32 = 0.05;
const SHATTER: u32 = 6;
#[derive(Resource)]
struct Aim(Vec3);
#[derive(Component)]
struct AimMarker;
#[derive(Resource, Default)]
struct Bores(Vec<bevy_carnage::Bore>);
#[derive(Component)]
struct HudStatus;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy_carnage — pooling (space to shoot, arrows to aim, R reset)".into(),
resolution: (960u32, 680u32).into(),
..default()
}),
..default()
}))
.add_plugins(CarnageVfxPlugin)
.insert_resource(Aim(Vec3::new(0.0, 0.10, 0.0)))
.init_resource::<Bores>()
.init_resource::<body::Thrown>()
.init_resource::<body::Pools>()
.add_systems(Startup, setup)
.add_systems(Update, (aim_marker, shoot, integrate, body::bleed, hud).chain())
.run();
}
fn setup(world: &mut World) {
let camera = Transform::from_xyz(1.55, 1.55, 2.10).looking_at(ORIGIN - Vec3::Y * 0.70, Vec3::Y);
world.spawn((Camera3d::default(), bevy::core_pipeline::prepass::DepthPrepass, camera));
light_and_floor(world);
let baked = body::Baked::bake(world, SOFTEN, &[]);
let materials = body::BodyMaterials::new(world);
let damage = body::Damage::fresh(&baked, GRANULARITY);
let marker = world.resource_mut::<Assets<Mesh>>().add(Mesh::from(Sphere::new(0.04)));
world.spawn((
AimMarker,
Mesh3d(marker),
MeshMaterial3d(materials.aim.clone()),
Transform::from_translation(ORIGIN),
));
world.insert_resource(baked);
world.insert_resource(materials);
world.insert_resource(damage);
body::stand(world, GRANULARITY);
world.spawn((
Text::new("arrows / WASD aim\n space shoot R reset"),
TextFont { font_size: FontSize::Px(15.0), ..default() },
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.85)),
Node { position_type: PositionType::Absolute, top: px(12), left: px(14), ..default() },
));
world.spawn((
HudStatus,
Text::new(""),
TextFont { font_size: FontSize::Px(15.0), ..default() },
TextColor(Color::srgba(1.0, 0.92, 0.55, 0.95)),
Node { position_type: PositionType::Absolute, bottom: px(14), left: px(14), ..default() },
));
}
fn hud(
pools: Res<body::Pools>,
settings: Res<CarnageSettings>,
thrown: Res<body::Thrown>,
decals: Query<(), With<PoolDecal>>,
mut line: Query<&mut Text, With<HudStatus>>,
) {
let widest = pools.0.iter().map(|p| p.radius).fold(0.0f32, f32::max);
let text = format!(
"{} plug(s) thrown -> {} slick(s) of {} max, widest {widest:.3} m | merge radius {:.2} m",
thrown.0,
decals.iter().count(),
settings.blood.max_pools,
settings.blood.pool_merge_radius,
);
for mut t in &mut line {
if t.0 != text {
t.0 = text.clone();
}
}
}
fn aim_marker(
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
mut aim: ResMut<Aim>,
mut marker: Query<&mut Transform, With<AimMarker>>,
) {
let step = 1.1 * time.delta_secs();
let mut d = Vec3::ZERO;
for (key, delta) in [
(KeyCode::ArrowUp, Vec3::Y),
(KeyCode::KeyW, Vec3::Y),
(KeyCode::ArrowDown, -Vec3::Y),
(KeyCode::KeyS, -Vec3::Y),
(KeyCode::ArrowLeft, -Vec3::X),
(KeyCode::KeyA, -Vec3::X),
(KeyCode::ArrowRight, Vec3::X),
(KeyCode::KeyD, Vec3::X),
] {
if keys.pressed(key) {
d += delta;
}
}
aim.0 += d * step;
aim.0 = aim.0.clamp(Vec3::new(-0.4, -0.5, -0.4), Vec3::new(0.4, 0.6, 0.4));
for mut t in &mut marker {
t.translation = ORIGIN + aim.0;
}
}
fn shoot(world: &mut World) {
let pressed =
|world: &World, key: KeyCode| world.resource::<ButtonInput<KeyCode>>().just_pressed(key);
if pressed(world, KeyCode::KeyR) {
world.resource_mut::<Bores>().0.clear();
world.resource_mut::<body::Thrown>().0 = 0;
body::wipe(world);
rebake(world);
return;
}
if !pressed(world, KeyCode::Space) {
return;
}
let at = world.resource::<Aim>().0;
let bore = body::bore_at(at, CALIBRE, SHATTER);
world.resource_mut::<Bores>().0.push(bore);
rebake(world);
}
fn rebake(world: &mut World) {
let bores = world.resource::<Bores>().0.clone();
body::clear(world);
let baked = body::Baked::bake(world, SOFTEN, &bores);
let damage = body::Damage::fresh(&baked, GRANULARITY);
world.insert_resource(baked);
world.insert_resource(damage);
body::stand(world, GRANULARITY);
body::spawn_gore(world);
}
fn integrate(time: Res<Time>, mut chunks: Query<(&mut Chunk, &mut Transform)>) {
let dt = time.delta_secs() * 0.55;
if dt <= 0.0 {
return;
}
for (mut chunk, mut transform) in &mut chunks {
body::integrate(&mut chunk, &mut transform, dt);
}
}