use bevy::prelude::*;
mod common;
use common::body::{self, Blow, BodyMaterials, Chunk, GRANULARITIES, ORIGIN, SOFTENINGS};
use common::light_and_floor;
#[derive(Resource)]
struct Aim(Vec3);
#[derive(Component)]
struct AimMarker;
#[derive(Resource)]
struct Granularity(usize);
#[derive(Component)]
struct HudStatus;
#[derive(Resource)]
struct Status(String);
impl Default for Status {
fn default() -> Self {
Status("hit it: 1 projectile 2 slash 3 blade 4 blast 5 pull".into())
}
}
#[derive(Resource)]
struct Soften(usize);
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy_carnage — sever (1-5 to hit, arrows to aim, G granularity, R reset)"
.into(),
resolution: (960u32, 680u32).into(),
..default()
}),
..default()
}))
.insert_resource(Aim(Vec3::new(0.0, 0.25, 0.0)))
.insert_resource(Granularity(GRANULARITIES.len() - 1))
.insert_resource(Soften(2))
.init_resource::<Status>()
.add_systems(Startup, setup)
.add_systems(Update, (aim_marker, strike, integrate, hud))
.run();
}
fn setup(world: &mut World) {
let camera = Transform::from_xyz(2.25, 1.35, 2.95).looking_at(Vec3::new(0.0, 0.76, 0.0), Vec3::Y);
world.spawn((Camera3d::default(), camera));
light_and_floor(world);
let soften = SOFTENINGS[world.resource::<Soften>().0];
let baked = body::Baked::bake(world, soften, &[]);
let materials = BodyMaterials::new(world);
let granularity = world.resource::<Granularity>().0;
let damage = body::Damage::fresh(&baked, granularity);
let marker = world.resource_mut::<Assets<Mesh>>().add(Mesh::from(Sphere::new(0.05)));
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);
spawn_hud(world);
}
fn spawn_hud(world: &mut World) {
world.spawn((
Text::new(
"arrows / WASD aim\n 1 projectile 2 slash 3 blade 4 blast 5 pull\n G granularity T soften 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(
status: Res<Status>,
granularity: Res<Granularity>,
soften: Res<Soften>,
standing: Query<(), With<body::Attached>>,
mut line: Query<&mut Text, With<HudStatus>>,
) {
let text = format!(
"{}\n{} of {} standing | soften {:.2} | granularity {}",
status.0,
standing.iter().count(),
GRANULARITIES[granularity.0],
SOFTENINGS[soften.0],
GRANULARITIES[granularity.0],
);
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.8, -0.7, -0.6), Vec3::new(0.8, 1.2, 0.6));
for mut t in &mut marker {
t.translation = ORIGIN + aim.0;
}
}
fn strike(world: &mut World) {
let pressed = |world: &World, key: KeyCode| world.resource::<ButtonInput<KeyCode>>().just_pressed(key);
let (reset, coarser, rounder) = (
pressed(world, KeyCode::KeyR),
pressed(world, KeyCode::KeyG),
pressed(world, KeyCode::KeyT),
);
if reset || coarser || rounder {
if coarser {
let mut g = world.resource_mut::<Granularity>();
g.0 = (g.0 + 1) % GRANULARITIES.len();
let now = g.0;
info!("granularity: standing at {} pieces — same bake, different frontier", GRANULARITIES[now]);
}
if rounder {
let mut t = world.resource_mut::<Soften>();
t.0 = (t.0 + 1) % SOFTENINGS.len();
let now = t.0;
info!(
"soften: {:.2} — re-baking, because the rounding is built into the drawn mesh rather \
than applied by a shader. The colliders come out identical either way.",
SOFTENINGS[now]
);
}
if reset {
info!("reset");
}
let granularity = world.resource::<Granularity>().0;
body::clear(world);
if rounder {
let soften = SOFTENINGS[world.resource::<Soften>().0];
let baked = body::Baked::bake(world, soften, &[]);
world.insert_resource(baked);
}
let damage = {
let baked = world.resource::<body::Baked>();
body::Damage::fresh(baked, granularity)
};
world.insert_resource(damage);
body::stand(world, granularity);
let (g, t) = (GRANULARITIES[granularity], SOFTENINGS[world.resource::<Soften>().0]);
world.resource_mut::<Status>().0 = if rounder {
format!("soften {t:.2} - re-baked; the colliders are identical either way")
} else if coarser {
format!("granularity {g} - same bake, read at a different frontier")
} else {
"reset".into()
};
return;
}
let blow = [
(KeyCode::Digit1, Blow::Projectile),
(KeyCode::Digit2, Blow::Slash),
(KeyCode::Digit3, Blow::SweptBlade),
(KeyCode::Digit4, Blow::Blast),
(KeyCode::Digit5, Blow::Pull),
]
.into_iter()
.find(|(key, _)| pressed(world, *key))
.map(|(_, blow)| blow);
if let Some(blow) = blow {
let at = world.resource::<Aim>().0;
let out = body::strike(world, blow, at);
let said = match (out.newly, out.off) {
(0, _) if out.reached == 0 => {
format!("{}: landed on nothing - the aim is off the body", blow.label())
}
(0, _) => format!(
"{}: nothing left to break here. Move the aim (arrows) or reset (R)",
blow.label()
),
(n, 0) => format!(
"{}: severed {n} bond(s), but nothing came loose yet. Hit it again",
blow.label()
),
(n, off) => format!("{}: severed {n} bond(s), {off} fragment(s) came off", blow.label()),
};
world.resource_mut::<Status>().0 = said;
}
}
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);
}
}