1 use crate::app::{App, ShowEverything};
2 use crate::common::{CityPicker, CommonState};
3 use crate::edit::EditMode;
4 use crate::game::{ChooseSomething, PopupMsg, PromptInput, State, Transition};
5 use crate::helpers::{nice_map_name, ID};
6 use crate::sandbox::gameplay::{GameplayMode, GameplayState};
7 use crate::sandbox::SandboxControls;
8 use crate::sandbox::SandboxMode;
9 use abstutil::Timer;
10 use geom::{Distance, Polygon};
11 use map_model::{BuildingID, IntersectionID, Position, NORMAL_LANE_THICKNESS};
12 use rand::seq::SliceRandom;
13 use rand::Rng;
14 use sim::{
15     DontDrawAgents, DrivingGoal, IndividTrip, PersonID, PersonSpec, Scenario, SidewalkSpot,
16     SpawnTrip, TripEndpoint, TripMode, TripSpec,
17 };
18 use widgetry::{
19     hotkey, lctrl, Btn, Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
20     Panel, ScreenRectangle, Spinner, Text, TextExt, VerticalAlignment, Widget,
21 };
22 
23 // TODO Maybe remember what things were spawned, offer to replay this later
24 pub struct Freeform {
25     top_center: Panel,
26 }
27 
28 impl Freeform {
new(ctx: &mut EventCtx, app: &App) -> Box<dyn GameplayState>29     pub fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn GameplayState> {
30         Box::new(Freeform {
31             top_center: make_top_center(ctx, app),
32         })
33     }
34 }
35 
36 impl GameplayState for Freeform {
event( &mut self, ctx: &mut EventCtx, app: &mut App, _: &mut SandboxControls, ) -> Option<Transition>37     fn event(
38         &mut self,
39         ctx: &mut EventCtx,
40         app: &mut App,
41         _: &mut SandboxControls,
42     ) -> Option<Transition> {
43         match self.top_center.event(ctx) {
44             Outcome::Clicked(x) => match x.as_ref() {
45                 "change map" => {
46                     Some(Transition::Push(CityPicker::new(
47                         ctx,
48                         app,
49                         Box::new(|ctx, app| {
50                             // The map will be switched before this callback happens.
51                             let path = abstutil::path_map(app.primary.map.get_name());
52                             Transition::Multi(vec![
53                                 Transition::Pop,
54                                 Transition::Replace(SandboxMode::new(
55                                     ctx,
56                                     app,
57                                     GameplayMode::Freeform(path),
58                                 )),
59                             ])
60                         }),
61                     )))
62                 }
63                 "change traffic" => Some(Transition::Push(make_change_traffic(
64                     ctx,
65                     app,
66                     self.top_center.rect_of("change traffic").clone(),
67                     "none".to_string(),
68                 ))),
69                 "edit map" => Some(Transition::Push(EditMode::new(
70                     ctx,
71                     app,
72                     GameplayMode::Freeform(abstutil::path_map(app.primary.map.get_name())),
73                 ))),
74                 "Start a new trip" => Some(Transition::Push(AgentSpawner::new(ctx, None))),
75                 "Record trips as a scenario" => Some(Transition::Push(PromptInput::new(
76                     ctx,
77                     "Name this scenario",
78                     Box::new(|name, ctx, app| {
79                         if abstutil::file_exists(abstutil::path_scenario(
80                             app.primary.map.get_name(),
81                             &name,
82                         )) {
83                             Transition::Push(PopupMsg::new(
84                                 ctx,
85                                 "Error",
86                                 vec![format!(
87                                     "A scenario called \"{}\" already exists, please pick another \
88                                      name",
89                                     name
90                                 )],
91                             ))
92                         } else {
93                             app.primary
94                                 .sim
95                                 .generate_scenario(&app.primary.map, name)
96                                 .save();
97                             Transition::Pop
98                         }
99                     }),
100                 ))),
101                 _ => unreachable!(),
102             },
103             _ => None,
104         }
105     }
106 
draw(&self, g: &mut GfxCtx, _: &App)107     fn draw(&self, g: &mut GfxCtx, _: &App) {
108         self.top_center.draw(g);
109     }
110 }
111 
make_top_center(ctx: &mut EventCtx, app: &App) -> Panel112 fn make_top_center(ctx: &mut EventCtx, app: &App) -> Panel {
113     let rows = vec![
114         Widget::row(vec![
115             Line("Sandbox").small_heading().draw(ctx),
116             Widget::vert_separator(ctx, 50.0),
117             "Map:".draw_text(ctx),
118             Btn::text_fg(format!("{} ↓", nice_map_name(app.primary.map.get_name()))).build(
119                 ctx,
120                 "change map",
121                 lctrl(Key::L),
122             ),
123             "Traffic:".draw_text(ctx),
124             Btn::text_fg("none ↓").build(ctx, "change traffic", hotkey(Key::S)),
125             Btn::svg_def("system/assets/tools/edit_map.svg").build(ctx, "edit map", lctrl(Key::E)),
126         ])
127         .centered(),
128         Widget::row(vec![
129             Btn::text_fg("Start a new trip").build_def(ctx, None),
130             Btn::text_fg("Record trips as a scenario").build_def(ctx, None),
131         ])
132         .centered(),
133         Text::from_all(vec![
134             Line("Select an intersection and press "),
135             Line(Key::Z.describe()).fg(ctx.style().hotkey_color),
136             Line(" to start traffic nearby"),
137         ])
138         .draw(ctx),
139     ];
140 
141     Panel::new(Widget::col(rows))
142         .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
143         .build(ctx)
144 }
145 
make_change_traffic( ctx: &mut EventCtx, app: &App, btn: ScreenRectangle, current: String, ) -> Box<dyn State>146 pub fn make_change_traffic(
147     ctx: &mut EventCtx,
148     app: &App,
149     btn: ScreenRectangle,
150     current: String,
151 ) -> Box<dyn State> {
152     let mut choices = Vec::new();
153     for name in abstutil::list_all_objects(abstutil::path_all_scenarios(app.primary.map.get_name()))
154     {
155         if name == "weekday" {
156             choices.push(Choice::new("realistic weekday traffic", name).tooltip(
157                 "Trips will begin throughout the entire day. Midnight is usually quiet, so you \
158                  may need to fast-forward to morning rush hour. Data comes from Puget Sound \
159                  Regional Council's Soundcast model.",
160             ));
161         } else {
162             choices.push(Choice::new(name.clone(), name));
163         }
164     }
165     choices.push(
166         Choice::new("trips between home and work", "home_to_work".to_string()).tooltip(
167             "Randomized people will leave homes in the morning, go to work, then return in the \
168              afternoon. It'll be very quiet before 7am and between 10am to 5pm.",
169         ),
170     );
171     choices.push(
172         Choice::new("random unrealistic trips", "random".to_string()).tooltip(
173             "Lots of trips will start at midnight, but not constantly appear through the day.",
174         ),
175     );
176     choices.push(Choice::new(
177         "none, except for buses -- you manually spawn traffic",
178         "none".to_string(),
179     ));
180     let choices = choices
181         .into_iter()
182         .map(|c| {
183             if c.data == current {
184                 c.active(false)
185             } else {
186                 c
187             }
188         })
189         .collect();
190 
191     ChooseSomething::new_below(
192         ctx,
193         &btn,
194         choices,
195         Box::new(|scenario_name, ctx, app| {
196             let map_path = abstutil::path_map(app.primary.map.get_name());
197             Transition::Multi(vec![
198                 Transition::Pop,
199                 Transition::Replace(SandboxMode::new(
200                     ctx,
201                     app,
202                     if scenario_name == "none" {
203                         GameplayMode::Freeform(map_path)
204                     } else {
205                         GameplayMode::PlayScenario(map_path, scenario_name, Vec::new())
206                     },
207                 )),
208             ])
209         }),
210     )
211 }
212 
213 struct AgentSpawner {
214     panel: Panel,
215     source: Option<TripEndpoint>,
216     goal: Option<(TripEndpoint, Option<Polygon>)>,
217     confirmed: bool,
218 }
219 
220 impl AgentSpawner {
new(ctx: &mut EventCtx, start: Option<BuildingID>) -> Box<dyn State>221     fn new(ctx: &mut EventCtx, start: Option<BuildingID>) -> Box<dyn State> {
222         let mut spawner = AgentSpawner {
223             source: None,
224             goal: None,
225             confirmed: false,
226             panel: Panel::new(Widget::col(vec![
227                 Widget::row(vec![
228                     Line("New trip").small_heading().draw(ctx),
229                     Btn::plaintext("X")
230                         .build(ctx, "close", hotkey(Key::Escape))
231                         .align_right(),
232                 ]),
233                 "Click a building or border to specify start"
234                     .draw_text(ctx)
235                     .named("instructions"),
236                 Widget::row(vec![
237                     "Type of trip:".draw_text(ctx),
238                     Widget::dropdown(
239                         ctx,
240                         "mode",
241                         TripMode::Drive,
242                         TripMode::all()
243                             .into_iter()
244                             .map(|m| Choice::new(m.ongoing_verb(), m))
245                             .collect(),
246                     ),
247                 ]),
248                 Widget::row(vec![
249                     "Number of trips:".draw_text(ctx),
250                     Spinner::new(ctx, (1, 1000), 1).named("number"),
251                 ]),
252                 Btn::text_fg("Confirm").inactive(ctx).named("Confirm"),
253             ]))
254             .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
255             .build(ctx),
256         };
257         if let Some(b) = start {
258             spawner.source = Some(TripEndpoint::Bldg(b));
259             spawner.panel.replace(
260                 ctx,
261                 "instructions",
262                 "Click a building or border to specify end"
263                     .draw_text(ctx)
264                     .named("instructions"),
265             );
266         }
267         Box::new(spawner)
268     }
269 }
270 
271 impl State for AgentSpawner {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition272     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
273         match self.panel.event(ctx) {
274             Outcome::Clicked(x) => match x.as_ref() {
275                 "close" => {
276                     return Transition::Pop;
277                 }
278                 "Confirm" => {
279                     let map = &app.primary.map;
280                     let mut scenario = Scenario::empty(map, "one-shot");
281                     let from = self.source.take().unwrap();
282                     let to = self.goal.take().unwrap().0;
283                     for i in 0..self.panel.spinner("number") as usize {
284                         if let Some(trip) = SpawnTrip::new(
285                             from.clone(),
286                             to.clone(),
287                             self.panel.dropdown_value("mode"),
288                             map,
289                         ) {
290                             scenario.people.push(PersonSpec {
291                                 id: PersonID(app.primary.sim.get_all_people().len() + i),
292                                 orig_id: None,
293                                 trips: vec![IndividTrip::new(app.primary.sim.time(), trip)],
294                             });
295                         }
296                     }
297                     let mut rng = app.primary.current_flags.sim_flags.make_rng();
298                     scenario.instantiate(
299                         &mut app.primary.sim,
300                         map,
301                         &mut rng,
302                         &mut Timer::new("spawn trip"),
303                     );
304                     app.primary.sim.tiny_step(map, &mut app.primary.sim_cb);
305                     app.recalculate_current_selection(ctx);
306                     return Transition::Pop;
307                 }
308                 _ => unreachable!(),
309             },
310             Outcome::Changed => {
311                 // We need to recalculate the path to see if this is sane. Otherwise we could trick
312                 // a pedestrian into wandering on/off a highway border.
313                 if self.goal.is_some() {
314                     let to = self.goal.as_ref().unwrap().0.clone();
315                     if let Some(path) = TripEndpoint::path_req(
316                         self.source.clone().unwrap(),
317                         to.clone(),
318                         self.panel.dropdown_value("mode"),
319                         &app.primary.map,
320                     )
321                     .and_then(|req| app.primary.map.pathfind(req))
322                     {
323                         self.goal = Some((
324                             to,
325                             path.trace(&app.primary.map, Distance::ZERO, None)
326                                 .map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS)),
327                         ));
328                     } else {
329                         self.goal = None;
330                         self.confirmed = false;
331                         self.panel.replace(
332                             ctx,
333                             "instructions",
334                             "Click a building or border to specify end"
335                                 .draw_text(ctx)
336                                 .named("instructions"),
337                         );
338                         self.panel.replace(
339                             ctx,
340                             "Confirm",
341                             Btn::text_fg("Confirm").inactive(ctx).named("Confirm"),
342                         );
343                     }
344                 }
345             }
346             _ => {}
347         }
348 
349         ctx.canvas_movement();
350 
351         if self.confirmed {
352             return Transition::Keep;
353         }
354 
355         if ctx.redo_mouseover() {
356             app.primary.current_selection = app.calculate_current_selection(
357                 ctx,
358                 &DontDrawAgents {},
359                 &ShowEverything::new(),
360                 false,
361                 true,
362                 true,
363             );
364             if let Some(ID::Intersection(i)) = app.primary.current_selection {
365                 if !app.primary.map.get_i(i).is_border() {
366                     app.primary.current_selection = None;
367                 }
368             } else if let Some(ID::Building(_)) = app.primary.current_selection {
369             } else {
370                 app.primary.current_selection = None;
371             }
372         }
373         if let Some(hovering) = match app.primary.current_selection {
374             Some(ID::Intersection(i)) => Some(TripEndpoint::Border(i, None)),
375             Some(ID::Building(b)) => Some(TripEndpoint::Bldg(b)),
376             None => None,
377             _ => unreachable!(),
378         } {
379             if self.source.is_none() && app.per_obj.left_click(ctx, "start here") {
380                 self.source = Some(hovering);
381                 self.panel.replace(
382                     ctx,
383                     "instructions",
384                     "Click a building or border to specify end"
385                         .draw_text(ctx)
386                         .named("instructions"),
387                 );
388             } else if self.source.is_some() && self.source != Some(hovering.clone()) {
389                 if self
390                     .goal
391                     .as_ref()
392                     .map(|(to, _)| to != &hovering)
393                     .unwrap_or(true)
394                 {
395                     if let Some(path) = TripEndpoint::path_req(
396                         self.source.clone().unwrap(),
397                         hovering.clone(),
398                         self.panel.dropdown_value("mode"),
399                         &app.primary.map,
400                     )
401                     .and_then(|req| app.primary.map.pathfind(req))
402                     {
403                         self.goal = Some((
404                             hovering,
405                             path.trace(&app.primary.map, Distance::ZERO, None)
406                                 .map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS)),
407                         ));
408                     } else {
409                         self.goal = None;
410                     }
411                 }
412 
413                 if self.goal.is_some() && app.per_obj.left_click(ctx, "end here") {
414                     app.primary.current_selection = None;
415                     self.confirmed = true;
416                     self.panel.replace(
417                         ctx,
418                         "instructions",
419                         "Confirm the trip settings"
420                             .draw_text(ctx)
421                             .named("instructions"),
422                     );
423                     self.panel.replace(
424                         ctx,
425                         "Confirm",
426                         Btn::text_fg("Confirm").build_def(ctx, hotkey(Key::Enter)),
427                     );
428                 }
429             }
430         } else {
431             self.goal = None;
432         }
433 
434         Transition::Keep
435     }
436 
draw(&self, g: &mut GfxCtx, app: &App)437     fn draw(&self, g: &mut GfxCtx, app: &App) {
438         self.panel.draw(g);
439         CommonState::draw_osd(g, app);
440 
441         if let Some(ref endpt) = self.source {
442             g.draw_polygon(
443                 Color::BLUE.alpha(0.8),
444                 match endpt {
445                     TripEndpoint::Border(i, _) => app.primary.map.get_i(*i).polygon.clone(),
446                     TripEndpoint::Bldg(b) => app.primary.map.get_b(*b).polygon.clone(),
447                 },
448             );
449         }
450         if let Some((ref endpt, ref poly)) = self.goal {
451             g.draw_polygon(
452                 Color::GREEN.alpha(0.8),
453                 match endpt {
454                     TripEndpoint::Border(i, _) => app.primary.map.get_i(*i).polygon.clone(),
455                     TripEndpoint::Bldg(b) => app.primary.map.get_b(*b).polygon.clone(),
456                 },
457             );
458             if let Some(p) = poly {
459                 g.draw_polygon(Color::PURPLE, p.clone());
460             }
461         }
462     }
463 }
464 
spawn_agents_around(i: IntersectionID, app: &mut App)465 pub fn spawn_agents_around(i: IntersectionID, app: &mut App) {
466     let map = &app.primary.map;
467     let sim = &mut app.primary.sim;
468     let mut rng = app.primary.current_flags.sim_flags.make_rng();
469     let mut spawner = sim.make_spawner();
470 
471     if map.all_buildings().is_empty() {
472         println!("No buildings, can't pick destinations");
473         return;
474     }
475 
476     let mut timer = Timer::new(format!(
477         "spawning agents around {} (rng seed {:?})",
478         i, app.primary.current_flags.sim_flags.rng_seed
479     ));
480 
481     let now = sim.time();
482     for l in &map.get_i(i).incoming_lanes {
483         let lane = map.get_l(*l);
484         if lane.is_driving() || lane.is_biking() {
485             for _ in 0..10 {
486                 let vehicle_spec = if rng.gen_bool(0.7) && lane.is_driving() {
487                     Scenario::rand_car(&mut rng)
488                 } else {
489                     Scenario::rand_bike(&mut rng)
490                 };
491                 if vehicle_spec.length > lane.length() {
492                     continue;
493                 }
494                 let person = sim.random_person(
495                     Scenario::rand_ped_speed(&mut rng),
496                     vec![vehicle_spec.clone()],
497                 );
498                 spawner.schedule_trip(
499                     person,
500                     now,
501                     TripSpec::VehicleAppearing {
502                         start_pos: Position::new(
503                             lane.id,
504                             Scenario::rand_dist(&mut rng, vehicle_spec.length, lane.length()),
505                         ),
506                         goal: DrivingGoal::ParkNear(
507                             map.all_buildings().choose(&mut rng).unwrap().id,
508                         ),
509                         use_vehicle: person.vehicles[0].id,
510                         retry_if_no_room: false,
511                         origin: None,
512                     },
513                     TripEndpoint::Border(lane.src_i, None),
514                     false,
515                     false,
516                     map,
517                 );
518             }
519         } else if lane.is_walkable() {
520             for _ in 0..5 {
521                 spawner.schedule_trip(
522                     sim.random_person(Scenario::rand_ped_speed(&mut rng), Vec::new()),
523                     now,
524                     TripSpec::JustWalking {
525                         start: SidewalkSpot::suddenly_appear(
526                             lane.id,
527                             Scenario::rand_dist(&mut rng, 0.1 * lane.length(), 0.9 * lane.length()),
528                             map,
529                         ),
530                         goal: SidewalkSpot::building(
531                             map.all_buildings().choose(&mut rng).unwrap().id,
532                             map,
533                         ),
534                     },
535                     TripEndpoint::Border(lane.src_i, None),
536                     false,
537                     false,
538                     map,
539                 );
540             }
541         }
542     }
543 
544     sim.flush_spawner(spawner, map, &mut timer);
545     sim.tiny_step(map, &mut app.primary.sim_cb);
546 }
547 
actions(_: &App, id: ID) -> Vec<(Key, String)>548 pub fn actions(_: &App, id: ID) -> Vec<(Key, String)> {
549     match id {
550         ID::Building(_) => vec![(Key::Z, "start a trip here".to_string())],
551         ID::Intersection(_) => vec![(Key::Z, "spawn agents here".to_string())],
552         _ => Vec::new(),
553     }
554 }
555 
execute(ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition556 pub fn execute(ctx: &mut EventCtx, app: &mut App, id: ID, action: String) -> Transition {
557     match (id, action.as_ref()) {
558         (ID::Building(b), "start a trip here") => Transition::Push(AgentSpawner::new(ctx, Some(b))),
559         (ID::Intersection(id), "spawn agents here") => {
560             spawn_agents_around(id, app);
561             Transition::Keep
562         }
563         _ => unreachable!(),
564     }
565 }
566