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