game/sandbox/gameplay/
actdev.rs

1use std::collections::BTreeSet;
2
3use maplit::btreeset;
4use rand::seq::SliceRandom;
5use rand::SeedableRng;
6use rand_xorshift::XorShiftRng;
7
8use geom::Duration;
9use map_gui::tools::{grey_out_map, nice_map_name};
10use map_model::AreaType;
11use sim::{AgentType, PersonID, TripID};
12use synthpop::TripEndpoint;
13use widgetry::tools::{open_browser, PopupMsg};
14use widgetry::{
15    lctrl, ControlState, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
16    SimpleState, Text, TextExt, Toggle, VerticalAlignment, Widget,
17};
18
19use crate::app::{App, Transition};
20use crate::common::jump_to_time_upon_startup;
21use crate::edit::EditMode;
22use crate::info::{OpenTrip, Tab};
23use crate::sandbox::gameplay::{GameplayMode, GameplayState};
24use crate::sandbox::{Actions, SandboxControls, SandboxMode, SpeedSetting};
25
26/// A gameplay mode with specific controls for integration with
27/// https://cyipt.github.io/acton/articles/the-actdev-project.html.
28pub struct Actdev {
29    top_right: Panel,
30    scenario_name: String,
31    bg_traffic: bool,
32    once: bool,
33}
34
35impl Actdev {
36    pub fn new_state(
37        ctx: &mut EventCtx,
38        scenario_name: String,
39        bg_traffic: bool,
40    ) -> Box<dyn GameplayState> {
41        Box::new(Actdev {
42            top_right: Panel::empty(ctx),
43            scenario_name,
44            bg_traffic,
45            once: true,
46        })
47    }
48}
49
50impl GameplayState for Actdev {
51    fn event(
52        &mut self,
53        ctx: &mut EventCtx,
54        app: &mut App,
55        controls: &mut SandboxControls,
56        actions: &mut Actions,
57    ) -> Option<Transition> {
58        if self.once {
59            self.once = false;
60
61            if self.bg_traffic {
62                let mut highlight = BTreeSet::new();
63                let study_area = &app
64                    .primary
65                    .map
66                    .all_areas()
67                    .iter()
68                    .find(|a| a.area_type == AreaType::StudyArea)
69                    .unwrap()
70                    .polygon;
71
72                for person in app.primary.sim.get_all_people() {
73                    if let TripEndpoint::Building(b) =
74                        app.primary.sim.trip_info(person.trips[0]).start
75                    {
76                        if study_area.contains_pt(app.primary.map.get_b(b).polygon.center()) {
77                            highlight.insert(person.id);
78                        }
79                    }
80                }
81                app.primary.sim.set_highlighted_people(highlight);
82            }
83
84            // The top-right panel never changes height, so we can set this just once.
85            controls.time_panel.as_mut().unwrap().override_height =
86                Some(self.top_right.panel_dims().height);
87
88            controls
89                .time_panel
90                .as_mut()
91                .unwrap()
92                .resume(ctx, app, SpeedSetting::Faster);
93        }
94
95        match self.top_right.event(ctx) {
96            Outcome::Clicked(x) => match x.as_ref() {
97                "change scenario" => {
98                    let scenario = if self.scenario_name == "base" {
99                        "go_active"
100                    } else {
101                        "base"
102                    };
103                    return Some(Transition::Replace(SandboxMode::async_new(
104                        app,
105                        GameplayMode::Actdev(
106                            app.primary.map.get_name().clone(),
107                            scenario.to_string(),
108                            self.bg_traffic,
109                        ),
110                        jump_to_time_upon_startup(Duration::hours(8)),
111                    )));
112                }
113                "Edit map" => Some(Transition::Push(EditMode::new_state(
114                    ctx,
115                    app,
116                    GameplayMode::Actdev(
117                        app.primary.map.get_name().clone(),
118                        self.scenario_name.clone(),
119                        self.bg_traffic,
120                    ),
121                ))),
122                "about A/B Street" => {
123                    let panel = Panel::new_builder(Widget::col(vec![
124                        Widget::row(vec![
125                            Line("About A/B Street").small_heading().into_widget(ctx),
126                            ctx.style().btn_close_widget(ctx),
127                        ]),
128                        Line("Created by Dustin Carlino, Yuwen Li, & Michael Kirk")
129                            .small()
130                            .into_widget(ctx),
131                        Text::from(
132                            "A/B Street is a traffic simulation game based on OpenStreetMap. You \
133                             can modify roads and intersections, measure the effects on different \
134                             groups, and advocate for your proposal.",
135                        )
136                        .wrap_to_pct(ctx, 50)
137                        .into_widget(ctx),
138                        "This is a simplified version. Check out the full version below."
139                            .text_widget(ctx),
140                        ctx.style().btn_outline.text("abstreet.org").build_def(ctx),
141                    ]))
142                    .build(ctx);
143                    Some(Transition::Push(<dyn SimpleState<_>>::new_state(
144                        panel,
145                        Box::new(About),
146                    )))
147                }
148                "Follow someone" => {
149                    if let Some((person, trip)) = find_active_trip(app) {
150                        // The user may not realize they have to close layers; do it for them.
151                        app.primary.layer = None;
152                        ctx.canvas.cam_zoom = 40.0;
153                        controls.common.as_mut().unwrap().launch_info_panel(
154                            ctx,
155                            app,
156                            Tab::PersonTrips(person, OpenTrip::single(trip)),
157                            actions,
158                        );
159                        None
160                    } else {
161                        return Some(Transition::Push(PopupMsg::new_state(
162                            ctx,
163                            "Nobody's around...",
164                            vec!["There are no active trips right now"],
165                        )));
166                    }
167                }
168                "Cycling" => {
169                    app.primary.layer =
170                        Some(Box::new(crate::layer::map::BikeActivity::new(ctx, app)));
171                    None
172                }
173                "Walking" => {
174                    app.primary.layer = Some(Box::new(crate::layer::traffic::Throughput::new(
175                        ctx,
176                        app,
177                        btreeset! { AgentType::Pedestrian },
178                    )));
179                    None
180                }
181                _ => unreachable!(),
182            },
183            Outcome::Changed(_) => {
184                // Background traffic was toggled
185                return Some(Transition::Replace(SandboxMode::async_new(
186                    app,
187                    GameplayMode::Actdev(
188                        app.primary.map.get_name().clone(),
189                        self.scenario_name.clone(),
190                        !self.bg_traffic,
191                    ),
192                    jump_to_time_upon_startup(Duration::hours(8)),
193                )));
194            }
195            _ => None,
196        }
197    }
198
199    fn draw(&self, g: &mut GfxCtx, _: &App) {
200        self.top_right.draw(g);
201    }
202
203    fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
204        let col = Widget::col(vec![
205            Widget::row(vec![
206                ctx.style()
207                    .btn_plain
208                    .btn()
209                    .image_path("system/assets/pregame/logo.svg")
210                    .image_dims(50.0)
211                    .build_widget(ctx, "about A/B Street"),
212                Line(nice_map_name(app.primary.map.get_name()))
213                    .small_heading()
214                    .into_widget(ctx),
215                ctx.style()
216                    .btn_outline
217                    .icon_text("system/assets/tools/pencil.svg", "Edit map")
218                    .hotkey(lctrl(Key::E))
219                    .build_def(ctx),
220            ])
221            .centered(),
222            Widget::row(vec![
223                ctx.style()
224                    .btn_popup_icon_text("system/assets/tools/calendar.svg", "scenario")
225                    .label_styled_text(
226                        match self.scenario_name.as_ref() {
227                            "base" => Text::from_all(vec![
228                                Line("Baseline / "),
229                                Line("Go Active").secondary(),
230                            ]),
231                            "go_active" => Text::from_all(vec![
232                                Line("Baseline").secondary(),
233                                Line(" / Go Active"),
234                            ]),
235                            _ => unreachable!(),
236                        },
237                        ControlState::Default,
238                    )
239                    .build_widget(ctx, "change scenario"),
240                Toggle::checkbox(ctx, "background traffic", None, self.bg_traffic),
241            ]),
242            Widget::row(vec![
243                ctx.style()
244                    .btn_plain
245                    .icon_text("system/assets/tools/location.svg", "Follow someone")
246                    .build_def(ctx),
247                ctx.style()
248                    .btn_plain
249                    .icon_text("system/assets/meters/pedestrian.svg", "Walking")
250                    .build_def(ctx),
251                ctx.style()
252                    .btn_plain
253                    .icon_text("system/assets/meters/bike.svg", "Cycling")
254                    .build_def(ctx),
255            ]),
256        ]);
257
258        self.top_right = Panel::new_builder(col)
259            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
260            .build(ctx);
261    }
262
263    fn has_tool_panel(&self) -> bool {
264        // Get rid of the home button, which would allow escaping to the title screen
265        false
266    }
267}
268
269struct About;
270
271impl SimpleState<App> for About {
272    fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &mut Panel) -> Transition {
273        if x == "close" {
274            return Transition::Pop;
275        } else if x == "abstreet.org" {
276            open_browser("https://abstreet.org");
277        }
278        Transition::Keep
279    }
280
281    fn draw(&self, g: &mut GfxCtx, app: &App) {
282        grey_out_map(g, app);
283    }
284}
285
286fn find_active_trip(app: &App) -> Option<(PersonID, TripID)> {
287    let mut all = Vec::new();
288    for agent in app.primary.sim.active_agents() {
289        if let Some(trip) = app.primary.sim.agent_to_trip(agent) {
290            if let Some(person) = app.primary.sim.trip_to_person(trip) {
291                all.push((person, trip));
292            }
293        }
294    }
295    all.choose(&mut XorShiftRng::from_entropy()).cloned()
296}