game/pregame/
proposals.rs

1use std::collections::HashMap;
2
3use geom::Percent;
4use map_gui::load::MapLoader;
5use map_model::PermanentMapEdits;
6use synthpop::Scenario;
7use widgetry::tools::{open_browser, PopupMsg};
8use widgetry::{EventCtx, Key, Line, Panel, SimpleState, State, Text, Widget};
9
10use crate::app::{App, Transition};
11use crate::edit::apply_map_edits;
12use crate::sandbox::{GameplayMode, SandboxMode};
13
14pub struct Proposals {
15    proposals: HashMap<String, PermanentMapEdits>,
16    current: Option<String>,
17}
18
19impl Proposals {
20    pub fn new_state(ctx: &mut EventCtx, current: Option<String>) -> Box<dyn State<App>> {
21        let mut proposals = HashMap::new();
22        let mut tab_buttons = Vec::new();
23        let mut current_tab_rows = Vec::new();
24        // If a proposal has fallen out of date, it'll be skipped with an error logged. Since these
25        // are under version control, much more likely to notice when they break (or we could add a
26        // step to data/regen.sh).
27        for (name, edits) in
28            abstio::load_all_objects::<PermanentMapEdits>(abstio::path("system/proposals"))
29        {
30            if current == Some(name.clone()) {
31                let mut txt = Text::new();
32                txt.add_line(Line(edits.get_title()).small_heading());
33                for l in edits.proposal_description.iter().skip(1) {
34                    txt.add_line(l);
35                }
36                current_tab_rows.push(
37                    txt.wrap_to_pct(ctx, 70)
38                        .into_widget(ctx)
39                        .margin_below(15)
40                        .margin_above(15),
41                );
42
43                if edits.proposal_link.is_some() {
44                    current_tab_rows.push(
45                        ctx.style()
46                            .btn_plain
47                            .btn()
48                            .label_underlined_text("Read detailed write-up")
49                            .build_def(ctx)
50                            .margin_below(10),
51                    );
52                }
53                current_tab_rows.push(
54                    ctx.style()
55                        .btn_solid_primary
56                        .text("Try out this proposal")
57                        .hotkey(Key::Enter)
58                        .build_def(ctx),
59                );
60
61                tab_buttons.push(
62                    ctx.style()
63                        .btn_tab
64                        .text(edits.get_title())
65                        .disabled(true)
66                        .build_def(ctx)
67                        .margin_below(10),
68                );
69            } else {
70                let hotkey = Key::NUM_KEYS
71                    .get(proposals.len())
72                    .map(|key| widgetry::MultiKey::from(*key));
73                tab_buttons.push(
74                    ctx.style()
75                        .btn_outline
76                        .text(edits.get_title())
77                        .no_tooltip()
78                        .hotkey(hotkey)
79                        .build_widget(ctx, &name)
80                        .margin_below(10),
81                );
82            }
83
84            proposals.insert(name, edits);
85        }
86
87        let panel = Panel::new_builder(Widget::col(vec![
88            Widget::row(vec![
89                Line("Community proposals").small_heading().into_widget(ctx),
90                ctx.style().btn_close_widget(ctx),
91            ]),
92            {
93                let mut txt =
94                    Text::from("These are proposed changes to Seattle made by community members.");
95                txt.add_line("Contact dabreegster@gmail.com to add your idea here!");
96                txt.into_widget(ctx).centered_horiz()
97            },
98            Widget::custom_row(tab_buttons)
99                .flex_wrap(ctx, Percent::int(80))
100                .margin_above(60),
101            Widget::col(current_tab_rows),
102        ]))
103        .build(ctx);
104        <dyn SimpleState<_>>::new_state(panel, Box::new(Proposals { proposals, current }))
105    }
106}
107
108impl SimpleState<App> for Proposals {
109    fn on_click(
110        &mut self,
111        ctx: &mut EventCtx,
112        app: &mut App,
113        x: &str,
114        _: &mut Panel,
115    ) -> Transition {
116        match x {
117            "close" => Transition::Pop,
118            "Try out this proposal" => launch(
119                ctx,
120                app,
121                self.proposals[self.current.as_ref().unwrap()].clone(),
122            ),
123            "Read detailed write-up" => {
124                open_browser(
125                    self.proposals[self.current.as_ref().unwrap()]
126                        .proposal_link
127                        .clone()
128                        .unwrap(),
129                );
130                Transition::Keep
131            }
132            x => Transition::Replace(Proposals::new_state(ctx, Some(x.to_string()))),
133        }
134    }
135}
136
137fn launch(ctx: &mut EventCtx, app: &App, edits: PermanentMapEdits) -> Transition {
138    #[cfg(not(target_arch = "wasm32"))]
139    {
140        if !abstio::file_exists(edits.map_name.path()) {
141            return map_gui::tools::prompt_to_download_missing_data(
142                ctx,
143                edits.map_name.clone(),
144                Box::new(move |ctx, app| launch(ctx, app, edits)),
145            );
146        }
147    }
148
149    Transition::Push(MapLoader::new_state(
150        ctx,
151        app,
152        edits.map_name.clone(),
153        Box::new(move |ctx, app| {
154            // Apply edits before setting up the sandbox, for simplicity
155            let maybe_err = ctx.loading_screen("apply edits", |ctx, timer| {
156                match edits.into_edits(&app.primary.map) {
157                    Ok(edits) => {
158                        apply_map_edits(ctx, app, edits);
159                        app.primary.map.recalculate_pathfinding_after_edits(timer);
160                        None
161                    }
162                    Err(err) => Some(err),
163                }
164            });
165            if let Some(err) = maybe_err {
166                Transition::Replace(PopupMsg::new_state(
167                    ctx,
168                    "Can't load proposal",
169                    vec![err.to_string()],
170                ))
171            } else {
172                app.primary.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app)));
173                Transition::Replace(SandboxMode::simple_new(
174                    app,
175                    GameplayMode::PlayScenario(
176                        app.primary.map.get_name().clone(),
177                        Scenario::default_scenario_for_map(app.primary.map.get_name()),
178                        Vec::new(),
179                    ),
180                ))
181            }
182        }),
183    ))
184}