1use bevy::{
2 app::{App, Plugin, Update},
3 camera::visibility::Visibility,
4 ecs::{
5 component::Component,
6 entity::Entity,
7 query::{With, Without},
8 resource::Resource,
9 schedule::IntoScheduleConfigs,
10 system::{Commands, If, Query, Res, ResMut, Single},
11 },
12 input::{
13 ButtonInput, common_conditions::input_just_pressed, keyboard::KeyCode, mouse::MouseButton,
14 },
15 log::tracing,
16 math::{Quat, Vec2},
17 state::{
18 app::AppExtStates,
19 condition::in_state,
20 state::{NextState, OnEnter, OnExit, State},
21 },
22 transform::components::Transform,
23 ui::widget::Text,
24};
25use shapes::{Circle, Intersect, lerp::Lerp};
26
27use crate::{
28 editor::{
29 StateText,
30 train::{
31 TrainEditorState,
32 placing::states::{
33 color_virtual_carriages_green, color_virtual_carriages_red, on_enter_placing_train,
34 on_exit_placing_train,
35 placed_state::{PlacedTrainState, place_train},
36 },
37 },
38 },
39 sim::{
40 track::{
41 node::{ConnectionLocation, TrackNode},
42 pos::TrackPos,
43 segment::TrackSegment,
44 },
45 train::{COUPLER_LENGTH, CarriageBogies, DOUBLE_BOGIE_IDX, SINGLE_BOGIE_IDX},
46 },
47 utils::Dir,
48};
49
50use self::{
51 states::hovering::{HoveringTrack, IsHoveringTrack, on_enter_hovering, on_exit_hovering},
52 templates::CarriageTemplate,
53};
54
55mod observers;
56mod states;
57pub mod templates;
58
59#[derive(Resource)]
60struct CurrentPlacingDir(Dir);
61
62#[derive(Clone, Resource)]
64pub struct CurrentPlacingTrain {
65 pub carriages: Vec<CarriageTemplate>,
66}
67
68impl Default for CurrentPlacingTrain {
70 fn default() -> Self {
71 Self {
73 carriages: vec![CarriageTemplate::default(); 2],
74 }
75 }
76}
77
78#[derive(Component, Default)]
81struct Virtual;
82
83#[derive(Component)]
84#[require(Virtual)]
85struct VirtualTrain {
86 carriages: Vec<Entity>,
87}
88
89#[derive(Component)]
90#[require(Virtual)]
91struct VirtualCarriage {
92 length: f32,
93 bogies: CarriageBogies,
94}
95
96#[derive(Component)]
97#[require(Virtual)]
98struct VirtualBogie;
99
100fn input_handler(input: Res<ButtonInput<KeyCode>>, mut placing_dir: ResMut<CurrentPlacingDir>) {
102 if input.just_pressed(KeyCode::KeyR) {
103 match placing_dir.0 {
104 Dir::Positive => placing_dir.0 = Dir::Negative,
105 Dir::Negative => placing_dir.0 = Dir::Positive,
106 }
107 }
108}
109
110fn get_intersection(ints: impl Iterator<Item = (f32, Vec2)>, dir: Dir) -> Option<(f32, Vec2)> {
111 match dir {
112 Dir::Positive => ints.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap()),
113 Dir::Negative => ints.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap()),
114 }
115}
116
117fn get_filtered_intersection(
118 ints: impl Iterator<Item = (f32, Vec2)>,
119 dir: Dir,
120 curr_idx: f32,
121) -> Option<(f32, Vec2)> {
122 get_intersection(
123 ints.filter(|(int_idx, _)| match dir {
124 Dir::Positive => *int_idx > curr_idx,
125 Dir::Negative => *int_idx < curr_idx,
126 }),
127 dir,
128 )
129}
130
131fn move_n_meters_along_active(
133 n_meters: f32,
134 curr_idx: f32,
135 curr_segment_entity: Entity,
136 curr_dir: Dir,
137 q_segments: &Query<(Entity, &TrackSegment, &Transform)>,
138 q_nodes: &Query<(Entity, &TrackNode)>,
139) -> Option<(Entity, f32, Dir)> {
140 let curr_segment = q_segments.get(curr_segment_entity).unwrap();
141 let curr_curve_point = curr_segment.1.shape.lerp(curr_idx);
142 let offset_circle = Circle::new(curr_curve_point, n_meters);
143
144 tracing::debug!(
145 "moving from segment {} and idx {}",
146 curr_segment.0,
147 curr_idx
148 );
149 let mut active_segment = Some(curr_segment);
151 let mut active_dir = Some(curr_dir);
152
153 while let Some(r#as) = active_segment
154 && let Some(ad) = active_dir
155 {
156 let curr_offset = r#as.2.translation.truncate() - curr_segment.2.translation.truncate();
158 let mut active_shape = r#as.1.shape.clone();
160 active_shape.translate_by(curr_offset);
161
162 let intersections = active_shape.intersections(&offset_circle);
164 tracing::debug!(
165 "intersections (with segment {} {:?}, circle {:?}): {:?}",
166 r#as.0,
167 active_shape,
168 offset_circle,
169 intersections
170 );
171
172 let intersection = intersections.and_then(|ints| {
173 if r#as.0 == curr_segment.0 {
174 return get_filtered_intersection(ints.into_iter(), curr_dir, curr_idx);
175 }
176
177 get_intersection(ints.into_iter(), curr_dir)
178 });
179
180 match intersection {
181 Some((int_idx, _)) => return Some((r#as.0, int_idx, ad)),
183 None => {
185 tracing::debug!("no intersection found, trying again");
186 let ac = q_nodes
187 .get(match curr_dir {
188 Dir::Positive => r#as.1.to_node,
189 Dir::Negative => r#as.1.from_node,
190 })
191 .unwrap()
192 .1
193 .connections
194 .get_active_for(r#as.0);
195
196 active_segment = ac.map(|ac| q_segments.get(ac.segment).unwrap());
197 active_dir = ac.map(|ac| match ac.location {
198 ConnectionLocation::Start => Dir::Positive,
199 ConnectionLocation::End => Dir::Negative,
200 });
201 }
202 }
203 }
204
205 tracing::debug!("no active connection found, cannot continue");
206 None
207}
208
209fn get_n_points_along_active_track(
211 spacings: &[f32],
212 segment: Entity,
213 idx: f32,
214 dir: Dir,
215 q_segments: &Query<(Entity, &TrackSegment, &Transform)>,
216 q_nodes: &Query<(Entity, &TrackNode)>,
217) -> Option<Vec<(Entity, f32, Dir, Vec2)>> {
218 let mut curr_segment = q_segments.get(segment).unwrap();
219 let mut curr_dir = dir;
220 let mut curr_idx = idx;
221
222 let mut points = Vec::with_capacity(spacings.len());
223 points.push((segment, idx, dir, curr_segment.1.shape.lerp(idx)));
224
225 for spacing in spacings {
226 match move_n_meters_along_active(
227 *spacing,
228 curr_idx,
229 curr_segment.0,
230 curr_dir,
231 q_segments,
232 q_nodes,
233 ) {
234 Some((final_segment_ent, final_idx, final_dir)) => {
235 curr_segment = q_segments.get(final_segment_ent).unwrap();
236 curr_idx = final_idx;
237 curr_dir = final_dir;
238
239 points.push((
240 final_segment_ent,
241 curr_idx,
242 curr_dir,
243 curr_segment.1.shape.lerp(curr_idx),
244 ));
245 }
246 None => return None,
248 }
249 }
250
251 Some(points)
252}
253
254fn update_placing_train(
256 mut cmd: Commands,
257 q_train: Single<(&VirtualTrain, &TrackPos), Without<VirtualBogie>>,
258 q_carriages: Query<&VirtualCarriage>,
259 mut q_bogies: Query<(Entity, &mut TrackPos), With<VirtualBogie>>,
260 q_segments: Query<(Entity, &TrackSegment, &Transform)>,
261 q_nodes: Query<(Entity, &TrackNode)>,
262 mut next_state: ResMut<NextState<PlacedTrainState>>,
263) {
264 let curr_segment = q_segments.get(q_train.1.segment).unwrap();
266 let curr_dir = match q_train.1.dir.as_ref().unwrap() {
267 Dir::Positive => Dir::Negative,
268 Dir::Negative => Dir::Positive,
269 };
270 let curr_idx = q_train.1.idx;
271
272 let mut spacings = Vec::new();
273 for (i, carriage) in q_train.0.carriages.iter().enumerate() {
274 let carriage = q_carriages.get(*carriage).unwrap();
275
276 match carriage.bogies {
277 CarriageBogies::Single(_) => {
278 spacings.push(carriage.length * SINGLE_BOGIE_IDX);
279 spacings.push(carriage.length * SINGLE_BOGIE_IDX);
280 }
281 CarriageBogies::Double { front: _, back: _ } => {
282 spacings.push(carriage.length * DOUBLE_BOGIE_IDX.0);
283 spacings.push(carriage.length * (DOUBLE_BOGIE_IDX.1 - DOUBLE_BOGIE_IDX.0));
284 spacings.push(carriage.length * (1.0 - DOUBLE_BOGIE_IDX.1));
285 }
286 }
287
288 if i != q_train.0.carriages.len() - 1 {
289 spacings.push(COUPLER_LENGTH);
290 spacings.push(COUPLER_LENGTH);
291 }
292 }
293
294 match get_n_points_along_active_track(
295 &spacings,
296 curr_segment.0,
297 curr_idx,
298 curr_dir,
299 &q_segments,
300 &q_nodes,
301 ) {
302 Some(points) => {
303 let mut points = points.into_iter();
304 points.next();
306
307 for carriage_ent in q_train.0.carriages.iter() {
308 let carriage = q_carriages.get(*carriage_ent).unwrap();
309
310 match carriage.bogies {
311 CarriageBogies::Single(entity) => {
312 let (_, mut bogie_pos) = q_bogies.get_mut(entity).unwrap();
313
314 let target = points.next().unwrap();
315 bogie_pos.segment = target.0;
316 bogie_pos.idx = target.1;
317 bogie_pos.dir = Some(target.2.reverse());
318
319 points.next();
321 points.next();
322 points.next();
323 }
324 CarriageBogies::Double { front, back } => {
325 let bogies = q_bogies.get_many_mut([front, back]).unwrap();
326
327 for mut bogie in bogies {
328 let target = points.next().unwrap();
329 bogie.1.segment = target.0;
330 bogie.1.idx = target.1;
331 bogie.1.dir = Some(target.2.reverse());
334 }
335
336 points.next();
338 points.next();
339 points.next();
340 }
341 }
342
343 cmd.entity(*carriage_ent).insert(Visibility::Visible);
344 }
345
346 next_state.set(PlacedTrainState::Valid);
347 }
348 None => {
349 for carriage in q_train.0.carriages.iter() {
350 cmd.entity(*carriage).insert(Visibility::Hidden);
351 }
352
353 next_state.set(PlacedTrainState::Invalid);
354 }
355 }
356}
357
358fn move_virtual_carriages_to_bogies(
359 q_carriages: Query<(&VirtualCarriage, &mut Transform), Without<VirtualBogie>>,
360 q_bogies: Query<&Transform, With<VirtualBogie>>,
361) {
362 for (carriage, mut transform) in q_carriages {
363 match &carriage.bogies {
364 CarriageBogies::Single(bogie) => {
365 *transform = *q_bogies.get(*bogie).unwrap();
366 }
367 CarriageBogies::Double { front, back } => {
368 let front_trans = *q_bogies.get(*front).unwrap();
369 let back_trans = *q_bogies.get(*back).unwrap();
370
371 transform.translation = (front_trans.translation + back_trans.translation) / 2.0;
372 let dir = front_trans.translation.truncate() - back_trans.translation.truncate();
373 transform.rotation = Quat::from_rotation_z(f32::atan2(dir.y, dir.x));
374 }
375 }
376 }
377}
378
379fn update_state_text(
380 mut text: Single<&mut Text, With<StateText>>,
381 current_dir: If<Res<CurrentPlacingDir>>,
382 valid: Option<Res<State<PlacedTrainState>>>,
383) {
384 text.0 = format!(
385 "Placing train\n[Esc] to exit\nCurrent direction: {}\nCurrent state: {}",
386 match current_dir.0.0 {
387 Dir::Positive => "Positive",
388 Dir::Negative => "Negative",
389 },
390 valid
391 .map(|v| v.get().to_string())
392 .unwrap_or(String::from("-"))
393 );
394}
395
396fn register_state_handlers(app: &mut App) {
397 app.add_sub_state::<HoveringTrack>()
398 .add_computed_state::<IsHoveringTrack>()
399 .add_sub_state::<PlacedTrainState>()
400 .add_systems(
401 OnEnter(TrainEditorState::PlacingTrain),
402 on_enter_placing_train,
403 )
404 .add_systems(
405 OnExit(TrainEditorState::PlacingTrain),
406 on_exit_placing_train,
407 )
408 .add_systems(OnEnter(IsHoveringTrack), on_enter_hovering)
409 .add_systems(OnExit(IsHoveringTrack), on_exit_hovering)
410 .add_systems(
411 OnEnter(PlacedTrainState::Valid),
412 color_virtual_carriages_green,
413 )
414 .add_systems(
415 OnEnter(PlacedTrainState::Invalid),
416 color_virtual_carriages_red,
417 );
418}
419
420pub struct PlacingTrainPlugin;
421
422impl Plugin for PlacingTrainPlugin {
423 fn build(&self, app: &mut App) {
424 register_state_handlers(app);
425
426 app.add_systems(
427 Update,
428 (
429 input_handler,
430 update_state_text,
431 update_placing_train,
432 move_virtual_carriages_to_bogies,
433 )
434 .run_if(in_state(TrainEditorState::PlacingTrain)),
435 )
436 .add_systems(
437 Update,
438 place_train
439 .after(update_placing_train)
440 .run_if(in_state(PlacedTrainState::Valid))
441 .run_if(input_just_pressed(MouseButton::Left)),
442 );
443 }
444}