use bevy::{
prelude::*,
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
window::WindowResolution,
};
use bevy_tnua::{
builtins::{TnuaBuiltinJump, TnuaBuiltinWalk},
control_helpers::TnuaSimpleAirActionsCounter,
controller::{TnuaController, TnuaControllerBundle, TnuaControllerPlugin},
TnuaAction,
};
use bevy_tnua_xpbd2d::{TnuaXpbd2dPlugin, TnuaXpbd2dSensorShape};
use bevy_xpbd_2d::prelude::*;
use leafwing_input_manager::prelude::*;
mod behavior_script;
mod camera;
mod simple_bt;
mod undertaker;
pub const LOGICAL_WIDTH: u32 = 1280;
pub const LOGICAL_HEIGHT: u32 = 720;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "Bulle".to_string(),
resolution: WindowResolution::new(
LOGICAL_WIDTH as f32,
LOGICAL_HEIGHT as f32,
),
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
bevy_egui::EguiPlugin,
camera::CameraPlugin,
PhysicsPlugins::default(),
TnuaControllerPlugin::default(),
TnuaXpbd2dPlugin::default(),
PhysicsDebugPlugin::default(),
InputManagerPlugin::<PlayerMovement>::default(),
))
.add_plugins((undertaker::UndertakerPlugin, layered::LayeredPlugin))
.insert_resource(Gravity(Vec2::NEG_Y * 1000.0))
.add_systems(Startup, create_test_stuff)
.add_systems(Update, update_tnua)
.add_systems(
Update,
action_state_swap_layers.in_set(layered::LayeredPluginSet::PreUpdate),
)
.run()
}
mod layered {
use bevy::prelude::*;
use bevy_xpbd_2d::{components::CollisionLayers, prelude::*};
pub struct LayeredPlugin;
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LayeredPluginSet {
PreUpdate,
Update,
}
impl Plugin for LayeredPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
update_collision_layers.in_set(LayeredPluginSet::Update),
)
.configure_sets(
Update,
(LayeredPluginSet::PreUpdate, LayeredPluginSet::Update).chain(),
);
}
}
#[derive(PhysicsLayer, Clone, Copy)]
pub enum GameLayer {
Ground,
LayerA,
LayerB,
ProjectileA,
ProjectileB,
}
impl GameLayer {
pub fn into_projectile(self) -> Self {
match self {
Self::Ground => Self::Ground,
Self::LayerA | Self::ProjectileA => Self::ProjectileA,
Self::LayerB | Self::ProjectileB => Self::ProjectileB,
}
}
}
#[derive(Component, Clone, Copy, Debug)]
pub enum Layered {
Ground,
A,
B,
}
impl Layered {
pub fn swapped(self) -> Self {
match self {
Self::Ground => Self::Ground,
Self::A => Self::B,
Self::B => Self::A,
}
}
pub fn layer(self) -> GameLayer {
match self {
Self::Ground => GameLayer::Ground,
Self::A => GameLayer::LayerA,
Self::B => GameLayer::LayerB,
}
}
}
fn layer_a_collision_layers() -> CollisionLayers {
CollisionLayers::new(
[GameLayer::LayerA],
[GameLayer::LayerA, GameLayer::ProjectileA, GameLayer::Ground],
)
}
fn layer_a_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
CollisionLayers::new(
GameLayer::ProjectileA,
if !self_interact {
Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerA])
} else {
[GameLayer::Ground, GameLayer::LayerA, GameLayer::ProjectileA].into()
},
)
}
fn layer_b_collision_layers() -> CollisionLayers {
CollisionLayers::new(
[GameLayer::LayerB],
[GameLayer::LayerB, GameLayer::ProjectileB, GameLayer::Ground],
)
}
fn layer_b_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
CollisionLayers::new(
GameLayer::ProjectileB,
if !self_interact {
Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerB])
} else {
[GameLayer::Ground, GameLayer::LayerB, GameLayer::ProjectileB].into()
},
)
}
fn ground_collision_layers() -> CollisionLayers {
CollisionLayers::new(GameLayer::Ground, LayerMask::ALL)
}
fn wide_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
CollisionLayers::new(
[GameLayer::ProjectileA, GameLayer::ProjectileB],
if !self_interact {
Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerA, GameLayer::LayerB])
} else {
[
GameLayer::Ground,
GameLayer::LayerA,
GameLayer::LayerB,
GameLayer::ProjectileA,
GameLayer::ProjectileB,
]
.into()
},
)
}
fn update_collision_layers(query: Query<(Entity, Ref<Layered>)>, mut commands: Commands) {
for (entity, layered) in query.iter() {
match &*layered {
Layered::A => {
commands
.entity(entity)
.try_insert(layer_a_collision_layers());
}
Layered::B => {
commands
.entity(entity)
.try_insert(layer_b_collision_layers());
}
Layered::Ground => {
commands
.entity(entity)
.try_insert(ground_collision_layers());
}
}
}
}
}
#[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
enum PlayerMovement {
Walk,
Jump,
Swap,
Q,
W,
E,
A,
S,
D,
}
#[derive(Component)]
struct Player;
fn default_player_map() -> InputMap<PlayerMovement> {
InputMap::new([
(
PlayerMovement::Jump,
Into::<UserInput>::into(KeyCode::ArrowUp),
),
(
PlayerMovement::Walk,
VirtualAxis::horizontal_arrow_keys().into(),
),
(PlayerMovement::Swap, KeyCode::Space.into()),
])
}
#[derive(Component)]
struct TnuaConfig {
max_speed: f32,
air_actions: usize,
walk: TnuaBuiltinWalk,
jump: TnuaBuiltinJump,
}
fn create_test_stuff(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.insert_resource(undertaker::UndertakerConfig {
under_y: -1000.,
..default()
});
commands.spawn((
Player,
camera::CameraTarget(0),
RigidBody::Dynamic,
Collider::capsule(32.0, 16.0),
layered::Layered::A,
InputManagerBundle::with_map(default_player_map()),
TnuaConfig {
max_speed: 5000.,
air_actions: 2,
walk: TnuaBuiltinWalk {
acceleration: 2000.,
air_acceleration: 1500.,
float_height: 64.0,
..default()
},
jump: TnuaBuiltinJump {
height: 192.,
reschedule_cooldown: Some(0.1),
upslope_extra_gravity: 300.0,
takeoff_extra_gravity: 300.0,
takeoff_above_velocity: 64.0,
fall_extra_gravity: 200.0,
shorten_extra_gravity: 2400.0,
peak_prevention_at_upward_velocity: 32.0,
peak_prevention_extra_gravity: 2000.0,
..default()
},
},
TnuaControllerBundle::default(),
TnuaXpbd2dSensorShape(Collider::rectangle(30.0, 2.0)),
TnuaSimpleAirActionsCounter::default(),
undertaker::Mortal::respawnable(Vec3::new(320.0, 320.0, 0.0)),
LayerTextureSwap(materials.add(Color::hsl(270., 0.85, 0.4))),
MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Capsule2d::new(32.0, 32.0))),
material: materials.add(Color::hsl(0., 0.95, 0.7)),
transform: Transform::from_xyz(320.0, 320.0, 0.0),
..default()
},
));
commands.spawn((
camera::CameraTarget(1),
RigidBody::Dynamic,
Collider::capsule(64.0, 48.0),
layered::Layered::A,
undertaker::Mortal::respawnable(Vec3::new(320.0, 640.0, 0.0)),
MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Capsule2d::new(48.0, 64.0))),
material: materials.add(Color::hsl(10., 0.95, 0.5)),
transform: Transform::from_xyz(320.0, 640.0, 0.0),
..default()
},
));
commands.spawn((
camera::CameraTarget(2),
layered::Layered::Ground,
RigidBody::Static,
Collider::rectangle(1000.0, 100.0),
TransformBundle::from(Transform::from_xyz(0.0, -100.0, 0.0)),
));
commands.spawn((
camera::CameraTarget(3),
MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Circle { radius: 64.0 })),
material: materials.add(Color::hsl(180., 0.95, 0.7)),
transform: Transform::from_xyz(-320.0, -640.0, 0.0),
..default()
},
));
commands.spawn((
camera::CameraTarget(1),
layered::Layered::B,
RigidBody::Dynamic,
Collider::capsule(32.0, 32.0),
undertaker::Mortal::default(),
MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Capsule2d::new(32.0, 32.0))),
material: materials.add(Color::hsl(5., 0.6, 0.75)),
transform: Transform::from_xyz(320.0, 960.0, 0.0),
..default()
},
));
}
#[derive(Component)]
struct LayerTextureSwap(Handle<ColorMaterial>);
#[allow(clippy::type_complexity)]
fn action_state_swap_layers(
mut query: Query<(
Entity,
&ActionState<PlayerMovement>,
&GlobalTransform,
&mut layered::Layered,
Option<&mut LayerTextureSwap>,
Option<&mut Handle<ColorMaterial>>,
)>,
spatial_query: SpatialQuery,
) {
for (ent, action_state, gtransform, mut layer, mut maybe_lts, mut maybe_mat) in query.iter_mut()
{
if action_state.just_pressed(&PlayerMovement::Swap) {
let blocker_collider = Collider::circle(32. * 2.);
let blockers = spatial_query.shape_intersections(
&blocker_collider,
gtransform.translation().truncate(),
0.,
SpatialQueryFilter::from_mask(layer.swapped().layer()),
);
if blockers.is_empty() {
*layer = layer.swapped();
if let Some(mut lts) = maybe_lts {
let new_mat = lts.0.clone();
if let Some(mat) = maybe_mat.as_mut() {
lts.0 = mat.clone();
*mat.as_mut() = new_mat;
}
}
}
}
}
}
fn update_tnua(
mut query: Query<(
&ActionState<PlayerMovement>,
&mut TnuaController,
&TnuaConfig,
&mut TnuaSimpleAirActionsCounter,
)>,
) {
for (action_state, mut tnua_controller, config, mut air_counter) in query.iter_mut() {
let mov_dir = action_state.clamped_value(&PlayerMovement::Walk);
let direction = mov_dir * Vec3::X;
air_counter.update(tnua_controller.as_mut());
let speed_factor = 1.0;
tnua_controller.basis(TnuaBuiltinWalk {
desired_velocity: direction * speed_factor * config.max_speed,
desired_forward: direction.normalize_or_zero(),
..config.walk
});
if action_state.pressed(&PlayerMovement::Jump) {
tnua_controller.action(TnuaBuiltinJump {
allow_in_air: air_counter.air_count_for(TnuaBuiltinJump::NAME)
<= config.air_actions,
..config.jump
});
}
}
}