game/sandbox/gameplay/freeform/
spawner.rs1use 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: 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 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 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}