1
use bevy::{
2
math::ops::{atan2, hypot},
3
prelude::*,
4
};
5
use bevy_rapier2d::prelude::*;
6
use std::f32::consts::PI;
7
8
use crate::{
9
EdgeBehaviour, FireCooldown, FireRate, GAMEPAD_STICK_DEADZONE, GAMEPAD_TRIGGER_DEADZONE,
10
Health, ScreenEdge,
11
sound::{SoundEvent, SoundKind},
12
};
13
14
const PLAYER0_STARTING_POSITION: Vec3 = Vec3::new(-600.0, -350.0, 1.0);
15
const PLAYER0_STARTING_ANGLE_R: f32 = 0.6;
16
const PLAYER0_STARTING_SPEED: f32 = 800.;
17
const PLAYER1_STARTING_POSITION: Vec3 = Vec3::new(700.0, -450.0, 1.0);
18
const PLAYER1_STARTING_ANGLE_R: f32 = PI - 0.7;
19
const PLAYER1_STARTING_SPEED: f32 = 700.;
20
21
const PLAYER_STARTING_HEALTH: f32 = 1000.;
22
const PLAYER_STARTING_FIRE_RATE_PS: f32 = 5.;
23
pub(crate) const PLAYER_LENGTH: f32 = 40.;
24
pub(crate) const PLAYER_WIDTH: f32 = 30.;
25
const PLAYER_ACCELERATION: f32 = 3000.;
26
const PLAYER_MAX_ROTATION_SPEED_RPS: f32 = PI * 1.5;
27
const PLAYER_DAMPING: f32 = 1.5;
28
29
const PLAYER_BULLET_SIZE: Vec2 = Vec2::new(4.5, 18.);
30
const PLAYER_BULLET_SPEED: f32 = 700.;
31
const PLAYER_BULLET_SPREAD: f32 = PI / 16.;
32
33
const PLAYER0_COLOR: Color = Color::srgb(0.5, 0.35, 0.8);
34
const PLAYER1_COLOR: Color = Color::srgb(0.5, 0.8, 0.35);
35
36
#[derive(PartialEq, Copy, Clone)]
37
pub(crate) struct PlayerId(pub(crate) usize); // XXX: we index directly into a Vec<&Gamepad> with this.
38
39
#[derive(Component)]
40
pub(crate) struct Player(pub(crate) PlayerId);
41
42
#[derive(Component, Copy, Clone)]
43
pub(crate) struct PlayerBullet {
44
pub(crate) shooter: PlayerId,
45
pub(crate) damage: f32,
46
}
47
48
#[derive(Resource)]
49
pub(crate) struct PlayerBulletAssets {
50
mesh: Mesh2d,
51
player0_material: MeshMaterial2d<ColorMaterial>,
52
player1_material: MeshMaterial2d<ColorMaterial>,
53
}
54
55
impl FromWorld for PlayerBulletAssets {
56
fn from_world(world: &mut World) -> Self {
57
return Self {
58
mesh: Mesh2d(world.add_asset(Rectangle::from_size(PLAYER_BULLET_SIZE))),
59
player0_material: MeshMaterial2d(
60
world.add_asset(PLAYER0_COLOR.mix(&Color::WHITE, 0.3)),
61
),
62
player1_material: MeshMaterial2d(
63
world.add_asset(PLAYER1_COLOR.mix(&Color::WHITE, 0.3)),
64
),
65
};
66
}
67
}
68
69
pub(crate) fn setup(
70
mut commands: Commands,
71
mut meshes: ResMut<Assets<Mesh>>,
72
mut materials: ResMut<Assets<ColorMaterial>>,
73
) {
74
for player_id in [0, 1] {
75
commands
76
.spawn(Player(PlayerId(player_id)))
77
.insert(Mesh2d(meshes.add(Triangle2d::default())))
78
.insert(MeshMaterial2d(materials.add(if player_id == 0 {
79
PLAYER0_COLOR
80
} else {
81
PLAYER1_COLOR
82
})))
83
.insert(
84
Transform::from_translation(if player_id == 0 {
85
PLAYER0_STARTING_POSITION
86
} else {
87
PLAYER1_STARTING_POSITION
88
})
89
.with_scale(Vec2::new(PLAYER_WIDTH, PLAYER_LENGTH).extend(1.))
90
.with_rotation(Quat::from_rotation_z(
91
if player_id == 0 {
92
PLAYER0_STARTING_ANGLE_R
93
} else {
94
PLAYER1_STARTING_ANGLE_R
95
} - PI / 2.,
96
)),
97
)
98
.insert(Velocity {
99
linvel: Vec2::from_angle(if player_id == 0 {
100
PLAYER0_STARTING_ANGLE_R
101
} else {
102
PLAYER1_STARTING_ANGLE_R
103
}) * if player_id == 0 {
104
PLAYER0_STARTING_SPEED
105
} else {
106
PLAYER1_STARTING_SPEED
107
},
108
angvel: 0.0,
109
})
110
.insert(RigidBody::Dynamic)
111
.insert(Sleeping::disabled())
112
.insert(Ccd::enabled())
113
.insert(Collider::triangle(
114
Vec2::Y * 0.5,
115
Vec2::new(-0.5, -0.5),
116
Vec2::new(0.5, -0.5),
117
))
118
.insert(ColliderMassProperties::Mass(500.0))
119
.insert(CollisionGroups::new(
120
if player_id == 0 {
121
Group::GROUP_10
122
} else {
123
Group::GROUP_11
124
},
125
if player_id == 0 {
126
Group::GROUP_11
127
} else {
128
Group::GROUP_10
129
} | Group::GROUP_1,
130
))
131
.insert(ActiveEvents::COLLISION_EVENTS)
132
.insert(LockedAxes::ROTATION_LOCKED)
133
.insert(Damping {
134
linear_damping: PLAYER_DAMPING,
135
angular_damping: 0.0,
136
})
137
.insert(ExternalImpulse::default())
138
.insert(ScreenEdge(EdgeBehaviour::Wraps))
139
.insert(FireRate(PLAYER_STARTING_FIRE_RATE_PS))
140
.insert(FireCooldown(Timer::from_seconds(0., TimerMode::Once)))
141
.insert(Health::new(PLAYER_STARTING_HEALTH));
142
}
143
}
144
145
pub(crate) fn handle_player_movement(
146
gamepads: Query<&Gamepad>,
147
players: Query<(&Player, &mut Transform, &mut ExternalImpulse)>,
148
time: Res<Time>,
149
) {
150
let gamepads = gamepads.iter().collect::<Vec<_>>();
151
152
for (player, mut transform, mut ext_impulse) in players {
153
let gamepad = &gamepads[player.0.0];
154
155
let lsx = gamepad.get(GamepadAxis::LeftStickX).unwrap();
156
let lsy = gamepad.get(GamepadAxis::LeftStickY).unwrap();
157
if lsx.abs() > GAMEPAD_STICK_DEADZONE || lsy.abs() > GAMEPAD_STICK_DEADZONE {
158
let at = atan2(lsy, lsx);
159
let dist = hypot(lsy, lsx).min(1.0);
160
161
let new_rotation = Quat::rotate_towards(
162
&transform.rotation,
163
Quat::from_rotation_z(at - PI / 2.0),
164
time.delta_secs() * PLAYER_MAX_ROTATION_SPEED_RPS * dist,
165
);
166
transform.rotation = new_rotation;
167
}
168
169
let lt2 = gamepad.get(GamepadButton::LeftTrigger2).unwrap();
170
if lt2.abs() > GAMEPAD_TRIGGER_DEADZONE {
171
ext_impulse.impulse = (transform.rotation * Vec3::Y * PLAYER_ACCELERATION * lt2).xy();
172
} else {
173
ext_impulse.impulse = Vec2::ZERO;
174
}
175
}
176
}
177
178
pub(crate) fn handle_player_fire(
179
mut commands: Commands,
180
bullet_assets: Res<PlayerBulletAssets>,
181
gamepads: Query<&Gamepad>,
182
players: Query<(&Player, &Transform, &Velocity, &mut FireCooldown, &FireRate)>,
183
mut sound_events: EventWriter<SoundEvent>,
184
time: Res<Time>,
185
) {
186
let gamepads = gamepads.iter().collect::<Vec<_>>();
187
188
for (player, transform, velocity, mut fire_cooldown, fire_rate) in players {
189
fire_cooldown.0.tick(time.delta());
190
191
let gamepad = &gamepads[player.0.0];
192
let rt2 = gamepad.get(GamepadButton::RightTrigger2).unwrap();
193
if rt2.abs() > 0. {
194
if fire_cooldown.0.finished() {
195
let base_trans = Transform::from_translation(transform.translation.with_z(0.))
196
.with_rotation(transform.rotation);
197
let mut rl = base_trans.clone();
198
rl.rotate_z(PLAYER_BULLET_SPREAD);
199
let mut rr = base_trans.clone();
200
rr.rotate_z(-PLAYER_BULLET_SPREAD);
201
let material = if player.0.0 == 0 {
202
&bullet_assets.player0_material
203
} else {
204
&bullet_assets.player1_material
205
};
206
let bullet = PlayerBullet {
207
shooter: player.0,
208
damage: 100.,
209
};
210
for rot in [base_trans.rotation, rl.rotation, rr.rotation] {
211
commands
212
.spawn(bullet)
213
.insert(bullet_assets.mesh.clone())
214
.insert(material.clone())
215
.insert(base_trans)
216
.insert(Velocity {
217
linvel: velocity.linvel + (rot * Vec3::Y).xy() * PLAYER_BULLET_SPEED,
218
angvel: 0.0,
219
})
220
.insert(ScreenEdge(EdgeBehaviour::Despawns))
221
.insert(RigidBody::KinematicVelocityBased)
222
.insert(Sleeping::disabled())
223
.insert(Ccd::enabled())
224
.insert(Collider::cuboid(PLAYER_BULLET_SIZE.x, PLAYER_BULLET_SIZE.y))
225
.insert(CollisionGroups::new(
226
if player.0.0 == 0 {
227
Group::GROUP_10
228
} else {
229
Group::GROUP_11
230
},
231
Group::GROUP_1
232
| if player.0.0 == 0 {
233
Group::GROUP_11
234
} else {
235
Group::GROUP_10
236
},
237
))
238
.insert(Sensor);
239
}
240
fire_cooldown.0 = Timer::from_seconds(1. / fire_rate.0, TimerMode::Repeating);
241
sound_events.write(SoundEvent(SoundKind::PlayerFire));
242
}
243
}
244
}
245
}
246