game/sandbox/gameplay/freeform/
spawner.rs

1use crate::ID;
2use abstutil::Timer;
3use geom::{Polygon, Pt2D};
4use map_model::{BuildingID, NORMAL_LANE_THICKNESS};
5use synthpop::{IndividTrip, PersonSpec, Scenario, TripEndpoint, TripMode, TripPurpose};
6use widgetry::tools::PopupMsg;
7use widgetry::{
8    Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, Spinner,
9    State, TextExt, VerticalAlignment, Widget,
10};
11
12use crate::app::{App, Transition};
13use crate::common::CommonState;
14use crate::debug::PathCostDebugger;
15
16pub struct AgentSpawner {
17    panel: Panel,
18    start: Option<(TripEndpoint, Pt2D)>,
19    // (goal, point on the map, feasible path, draw the path). Even if we can't draw the path,
20    // remember if the path exists at all.
21    goal: Option<(TripEndpoint, Pt2D, bool, Option<Polygon>)>,
22    confirmed: bool,
23}
24
25impl AgentSpawner {
26    pub fn new_state(
27        ctx: &mut EventCtx,
28        app: &App,
29        start: Option<BuildingID>,
30    ) -> Box<dyn State<App>> {
31        let mut spawner = AgentSpawner {
32            start: None,
33            goal: None,
34            confirmed: false,
35            panel: Panel::new_builder(Widget::col(vec![
36                Widget::row(vec![
37                    Line("New trip").small_heading().into_widget(ctx),
38                    ctx.style().btn_close_widget(ctx),
39                ]),
40                "Click a building or border to specify start"
41                    .text_widget(ctx)
42                    .named("instructions"),
43                Widget::row(vec![
44                    "Type of trip:".text_widget(ctx),
45                    Widget::dropdown(
46                        ctx,
47                        "mode",
48                        TripMode::Drive,
49                        TripMode::all()
50                            .into_iter()
51                            .map(|m| Choice::new(m.ongoing_verb(), m))
52                            .collect(),
53                    ),
54                ]),
55                Widget::row(vec![
56                    "Number of trips:".text_widget(ctx).centered_vert(),
57                    Spinner::widget(ctx, "number", (1, 1000), 1, 1),
58                ]),
59                if app.opts.dev {
60                    ctx.style()
61                        .btn_plain_destructive
62                        .text("Debug all costs")
63                        .build_def(ctx)
64                } else {
65                    Widget::nothing()
66                },
67                ctx.style()
68                    .btn_solid_primary
69                    .text("Confirm")
70                    .disabled(true)
71                    .build_def(ctx),
72            ]))
73            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
74            .build(ctx),
75        };
76        if let Some(b) = start {
77            let endpt = TripEndpoint::Building(b);
78            let pt = endpt.pt(&app.primary.map);
79            spawner.start = Some((endpt, pt));
80            spawner.panel.replace(
81                ctx,
82                "instructions",
83                "Click a building or border to specify end".text_widget(ctx),
84            );
85        }
86        Box::new(spawner)
87    }
88}
89
90impl State<App> for AgentSpawner {
91    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
92        match self.panel.event(ctx) {
93            Outcome::Clicked(x) => match x.as_ref() {
94                "close" => {
95                    return Transition::Pop;
96                }
97                "Confirm" => {
98                    let map = &app.primary.map;
99                    let mut scenario = Scenario::empty(map, "one-shot");
100                    let from = self.start.take().unwrap().0;
101                    let to = self.goal.take().unwrap().0;
102                    for _ in 0..self.panel.spinner("number") {
103                        scenario.people.push(PersonSpec {
104                            orig_id: None,
105                            trips: vec![IndividTrip::new(
106                                app.primary.sim.time(),
107                                TripPurpose::Shopping,
108                                from,
109                                to,
110                                self.panel.dropdown_value("mode"),
111                            )],
112                        });
113                    }
114                    let mut rng = app.primary.current_flags.sim_flags.make_rng();
115                    app.primary.sim.instantiate(
116                        &scenario,
117                        map,
118                        &mut rng,
119                        &mut Timer::new("spawn trip"),
120                    );
121                    app.primary.sim.tiny_step(map, &mut app.primary.sim_cb);
122                    app.recalculate_current_selection(ctx);
123                    return Transition::Pop;
124                }
125                "Debug all costs" => {
126                    if let Some(state) = self
127                        .goal
128                        .as_ref()
129                        .and_then(|(to, _, _, _)| {
130                            TripEndpoint::path_req(
131                                self.start.unwrap().0,
132                                *to,
133                                self.panel.dropdown_value("mode"),
134                                &app.primary.map,
135                            )
136                        })
137                        .and_then(|req| app.primary.map.pathfind(req).ok())
138                        .and_then(|path| {
139                            path.trace(&app.primary.map).map(|pl| {
140                                (
141                                    path.get_req().clone(),
142                                    pl.make_polygons(NORMAL_LANE_THICKNESS),
143                                )
144                            })
145                        })
146                        .and_then(|(req, draw_path)| {
147                            PathCostDebugger::maybe_new(ctx, app, req, draw_path)
148                        })
149                    {
150                        return Transition::Push(state);
151                    } else {
152                        return Transition::Push(PopupMsg::new_state(
153                            ctx,
154                            "Error",
155                            vec!["Couldn't launch cost debugger for some reason"],
156                        ));
157                    }
158                }
159                _ => unreachable!(),
160            },
161            Outcome::Changed(_) => {
162                // We need to recalculate the path to see if this is sane. Otherwise we could trick
163                // a pedestrian into wandering on/off a highway border.
164                if self.goal.is_some() {
165                    let to = self.goal.as_ref().unwrap().0;
166                    if let Some(path) = TripEndpoint::path_req(
167                        self.start.unwrap().0,
168                        to,
169                        self.panel.dropdown_value("mode"),
170                        &app.primary.map,
171                    )
172                    .and_then(|req| app.primary.map.pathfind(req).ok())
173                    {
174                        self.goal = Some((
175                            to,
176                            to.pt(&app.primary.map),
177                            true,
178                            path.trace(&app.primary.map)
179                                .map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS)),
180                        ));
181                    } else {
182                        self.goal = None;
183                        self.confirmed = false;
184                        self.panel.replace(
185                            ctx,
186                            "instructions",
187                            "Click a building or border to specify end".text_widget(ctx),
188                        );
189                        self.panel.replace(
190                            ctx,
191                            "Confirm",
192                            ctx.style()
193                                .btn_solid_primary
194                                .text("Confirm")
195                                .disabled(true)
196                                .build_def(ctx),
197                        );
198                    }
199                }
200            }
201            _ => {}
202        }
203
204        ctx.canvas_movement();
205        let map = &app.primary.map;
206
207        if self.confirmed {
208            return Transition::Keep;
209        }
210
211        if ctx.redo_mouseover() {
212            app.primary.current_selection = app.mouseover_unzoomed_everything(ctx);
213            if match app.primary.current_selection {
214                Some(ID::Intersection(i)) => !map.get_i(i).is_border(),
215                Some(ID::Building(_)) => false,
216                _ => true,
217            } {
218                app.primary.current_selection = None;
219            }
220        }
221        if let Some(hovering) = match app.primary.current_selection {
222            Some(ID::Intersection(i)) => Some(TripEndpoint::Border(i)),
223            Some(ID::Building(b)) => Some(TripEndpoint::Building(b)),
224            None => None,
225            _ => unreachable!(),
226        } {
227            if self.start.is_none() && app.per_obj.left_click(ctx, "start here") {
228                self.start = Some((hovering, hovering.pt(map)));
229                self.panel.replace(
230                    ctx,
231                    "instructions",
232                    "Click a building or border to specify end".text_widget(ctx),
233                );
234            } else if self.start.is_some() && self.start.map(|(x, _)| x != hovering).unwrap_or(true)
235            {
236                if self
237                    .goal
238                    .as_ref()
239                    .map(|(to, _, _, _)| to != &hovering)
240                    .unwrap_or(true)
241                {
242                    if let Some(path) = TripEndpoint::path_req(
243                        self.start.unwrap().0,
244                        hovering,
245                        self.panel.dropdown_value("mode"),
246                        map,
247                    )
248                    .and_then(|req| map.pathfind(req).ok())
249                    {
250                        self.goal = Some((
251                            hovering,
252                            hovering.pt(map),
253                            true,
254                            path.trace(map)
255                                .map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS)),
256                        ));
257                    } else {
258                        // Don't constantly recalculate a failed path
259                        self.goal = Some((hovering, hovering.pt(map), false, None));
260                    }
261                }
262
263                if self.goal.as_ref().map(|(_, _, ok, _)| *ok).unwrap_or(false)
264                    && app.per_obj.left_click(ctx, "end here")
265                {
266                    app.primary.current_selection = None;
267                    self.confirmed = true;
268                    self.panel.replace(
269                        ctx,
270                        "instructions",
271                        "Confirm the trip settings".text_widget(ctx),
272                    );
273                    self.panel.replace(
274                        ctx,
275                        "Confirm",
276                        ctx.style()
277                            .btn_solid_primary
278                            .text("Confirm")
279                            .hotkey(Key::Enter)
280                            .build_def(ctx),
281                    );
282                }
283            }
284        } else {
285            self.goal = None;
286        }
287
288        Transition::Keep
289    }
290
291    fn draw(&self, g: &mut GfxCtx, app: &App) {
292        self.panel.draw(g);
293        CommonState::draw_osd(g, app);
294
295        if let Some((_, center)) = self.start {
296            map_gui::tools::start_marker(g, center, 2.0).draw(g);
297        }
298        if let Some((_, center, _, ref path_poly)) = self.goal {
299            map_gui::tools::goal_marker(g, center, 2.0).draw(g);
300            if let Some(p) = path_poly {
301                g.draw_polygon(Color::PURPLE, p.clone());
302            }
303        }
304    }
305}