use crate::app::App;
use crate::challenges::ChallengesPicker;
use crate::devtools::DevToolsMode;
use crate::edit::apply_map_edits;
use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
use crate::helpers::open_browser;
use crate::sandbox::gameplay::Tutorial;
use crate::sandbox::{GameplayMode, SandboxMode};
use abstutil::Timer;
use geom::{Duration, Line, Percent, Pt2D, Speed};
use instant::Instant;
use map_model::PermanentMapEdits;
use rand::Rng;
use rand_xorshift::XorShiftRng;
use sim::ScenarioGenerator;
use std::collections::HashMap;
use widgetry::{
hotkey, hotkeys, Btn, Color, EventCtx, GfxCtx, Key, Line, Outcome, Panel, RewriteColor, Text,
UpdateType, Widget,
};
pub struct TitleScreen {
panel: Panel,
screensaver: Screensaver,
rng: XorShiftRng,
}
impl TitleScreen {
pub fn new(ctx: &mut EventCtx, app: &mut App) -> TitleScreen {
let mut rng = app.primary.current_flags.sim_flags.make_rng();
let mut timer = Timer::new("screensaver traffic");
ScenarioGenerator::small_run(&app.primary.map)
.generate(&app.primary.map, &mut rng, &mut timer)
.instantiate(&mut app.primary.sim, &app.primary.map, &mut rng, &mut timer);
TitleScreen {
panel: Panel::new(
Widget::col(vec![
Widget::draw_svg(ctx, "system/assets/pregame/logo.svg"),
Btn::text_bg2("PLAY").build(
ctx,
"start game",
hotkeys(vec![Key::Space, Key::Enter]),
),
])
.bg(app.cs.grass)
.padding(16)
.outline(3.0, Color::BLACK)
.centered(),
)
.build_custom(ctx),
screensaver: Screensaver::bounce(ctx, app, &mut rng),
rng,
}
}
}
impl State for TitleScreen {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
match self.panel.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"start game" => {
app.primary.clear_sim();
return Transition::Replace(MainMenu::new(ctx, app));
}
_ => unreachable!(),
},
_ => {}
}
self.screensaver.update(&mut self.rng, ctx, app);
ctx.request_update(UpdateType::Game);
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
}
}
pub struct MainMenu {
panel: Panel,
}
impl MainMenu {
pub fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
let col = vec![
Btn::svg_def("system/assets/pregame/quit.svg")
.build(ctx, "quit", hotkey(Key::Escape))
.align_left(),
{
let mut txt = Text::from(Line("A/B STREET").display_title());
txt.add(Line("Created by Dustin Carlino and Yuwen Li"));
txt.draw(ctx).centered_horiz()
},
Widget::row(vec![
Btn::svg(
"system/assets/pregame/tutorial.svg",
RewriteColor::Change(Color::WHITE, app.cs.hovering),
)
.tooltip({
let mut txt = Text::tooltip(ctx, hotkey(Key::T), "Tutorial");
txt.add(Line("Learn how to play the game").small());
txt
})
.build(ctx, "Tutorial", hotkey(Key::T)),
Btn::svg(
"system/assets/pregame/sandbox.svg",
RewriteColor::Change(Color::WHITE, app.cs.hovering),
)
.tooltip({
let mut txt = Text::tooltip(ctx, hotkey(Key::S), "Sandbox");
txt.add(Line("No goals, try out any idea here").small());
txt
})
.build(ctx, "Sandbox mode", hotkey(Key::S)),
Btn::svg(
"system/assets/pregame/challenges.svg",
RewriteColor::Change(Color::WHITE, app.cs.hovering),
)
.tooltip({
let mut txt = Text::tooltip(ctx, hotkey(Key::C), "Challenges");
txt.add(Line("Fix specific problems").small());
txt
})
.build(ctx, "Challenges", hotkey(Key::C)),
])
.centered(),
Widget::row(vec![
Btn::text_bg2("Community Proposals")
.tooltip({
let mut txt = Text::tooltip(ctx, hotkey(Key::P), "Community Proposals");
txt.add(Line("See existing ideas for improving traffic").small());
txt
})
.build_def(ctx, hotkey(Key::P)),
Btn::text_bg2("Contribute parking data to OpenStreetMap")
.tooltip({
let mut txt = Text::tooltip(
ctx,
hotkey(Key::M),
"Contribute parking data to OpenStreetMap",
);
txt.add(Line("Improve parking data in OpenStreetMap").small());
txt
})
.build_def(ctx, hotkey(Key::M)),
Btn::text_bg2("Internal Dev Tools").build_def(ctx, hotkey(Key::D)),
])
.centered(),
Widget::col(vec![
Widget::row(vec![
Btn::text_bg2("About").build_def(ctx, None),
Btn::text_bg2("Feedback").build_def(ctx, None),
]),
built_info::time().draw(ctx),
])
.centered(),
];
Box::new(MainMenu {
panel: Panel::new(Widget::col(col).evenly_spaced())
.exact_size_percent(90, 85)
.build_custom(ctx),
})
}
}
impl State for MainMenu {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
match self.panel.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"quit" => {
std::process::exit(0);
}
"Tutorial" => {
return Tutorial::start(ctx, app);
}
"Sandbox mode" => {
let map_path = if abstutil::list_all_objects(abstutil::path_all_maps())
.contains(app.primary.map.get_name())
{
abstutil::path_map(app.primary.map.get_name())
} else {
abstutil::path_map("montlake")
};
let scenario = if abstutil::file_exists(abstutil::path_scenario(
app.primary.map.get_name(),
"weekday",
)) {
"weekday"
} else {
"home_to_work"
};
return Transition::Push(SandboxMode::new(
ctx,
app,
GameplayMode::PlayScenario(map_path, scenario.to_string(), Vec::new()),
));
}
"Challenges" => {
return Transition::Push(ChallengesPicker::new(ctx, app));
}
"About" => {
return Transition::Push(About::new(ctx, app));
}
"Feedback" => {
open_browser("https://forms.gle/ocvbek1bTaZUr3k49".to_string());
}
"Community Proposals" => {
return Transition::Push(Proposals::new(ctx, app, None));
}
"Contribute parking data to OpenStreetMap" => {
return Transition::Push(crate::devtools::mapping::ParkingMapper::new(
ctx, app,
));
}
"Internal Dev Tools" => {
return Transition::Push(DevToolsMode::new(ctx, app));
}
_ => unreachable!(),
},
_ => {}
}
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::Custom
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
g.clear(app.cs.grass);
self.panel.draw(g);
}
}
struct About {
panel: Panel,
}
impl About {
fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
let col = vec![
Btn::svg_def("system/assets/pregame/back.svg")
.build(ctx, "back", hotkey(Key::Escape))
.align_left(),
{
Text::from_multiline(vec![
Line("A/B STREET").display_title(),
Line("Created by Dustin Carlino, UX by Yuwen Li"),
Line("Character art by Holly Hansel"),
Line(""),
Line(
"Data from OpenStreetMap, King County GIS, and Puget Sound Regional \
Council",
),
Line(""),
Line(
"Disclaimer: This game is based on imperfect data, heuristics concocted \
under the influence of cold brew, a simplified traffic simulation model, \
and a deeply flawed understanding of how much articulated buses can bend \
around tight corners. Use this as a conversation starter with your city \
government, not a final decision maker. Any resemblance of in-game \
characters to real people is probably coincidental, unless of course you \
stumble across the elusive \"Dustin Bikelino\". Have the appropriate \
amount of fun.",
),
])
.wrap_to_pct(ctx, 50)
.draw(ctx)
.centered_horiz()
.align_vert_center()
.bg(app.cs.panel_bg)
.padding(16)
},
Btn::text_bg2("See full credits")
.build_def(ctx, None)
.centered_horiz(),
];
Box::new(About {
panel: Panel::new(Widget::custom_col(col))
.exact_size_percent(90, 85)
.build_custom(ctx),
})
}
}
impl State for About {
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
match self.panel.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"back" => {
return Transition::Pop;
}
"See full credits" => {
open_browser("https://github.com/dabreegster/abstreet#credits".to_string());
}
_ => unreachable!(),
},
_ => {}
}
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::Custom
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
g.clear(app.cs.grass);
self.panel.draw(g);
}
}
struct Proposals {
panel: Panel,
proposals: HashMap<String, PermanentMapEdits>,
current: Option<String>,
}
impl Proposals {
fn new(ctx: &mut EventCtx, app: &App, current: Option<String>) -> Box<dyn State> {
let mut proposals = HashMap::new();
let mut buttons = Vec::new();
let mut current_tab = Vec::new();
for (name, edits) in
abstutil::load_all_objects::<PermanentMapEdits>(abstutil::path("system/proposals"))
{
if current == Some(name.clone()) {
let mut txt = Text::new();
txt.add(Line(&edits.proposal_description[0]).small_heading());
for l in edits.proposal_description.iter().skip(1) {
txt.add(Line(l));
}
current_tab.push(
txt.wrap_to_pct(ctx, 70)
.draw(ctx)
.margin_below(15)
.margin_above(15),
);
if edits.proposal_link.is_some() {
current_tab.push(
Btn::text_bg2("Read detailed write-up")
.build_def(ctx, None)
.margin_below(10),
);
}
current_tab.push(Btn::text_bg2("Try out this proposal").build_def(ctx, None));
buttons.push(Btn::text_bg2(&edits.proposal_description[0]).inactive(ctx));
} else {
buttons.push(
Btn::text_bg2(&edits.proposal_description[0])
.tooltip(Text::new())
.build(ctx, &name, None)
.margin_below(10),
);
}
proposals.insert(name, edits);
}
let mut col = vec![
{
let mut txt = Text::from(Line("A/B STREET").display_title());
txt.add(Line("PROPOSALS").big_heading_styled());
txt.add(Line(""));
txt.add(Line(
"These are proposed changes to Seattle made by community members.",
));
txt.add(Line("Contact dabreegster@gmail.com to add your idea here!"));
txt.draw(ctx).centered_horiz().margin_below(20)
},
Widget::custom_row(buttons).flex_wrap(ctx, Percent::int(80)),
];
col.extend(current_tab);
Box::new(Proposals {
proposals,
panel: Panel::new(Widget::custom_col(vec![
Btn::svg_def("system/assets/pregame/back.svg")
.build(ctx, "back", hotkey(Key::Escape))
.align_left()
.margin_below(20),
Widget::col(col).bg(app.cs.panel_bg).padding(16),
]))
.exact_size_percent(90, 85)
.build_custom(ctx),
current,
})
}
}
impl State for Proposals {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
match self.panel.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"back" => {
return Transition::Pop;
}
"Try out this proposal" => {
let edits = &self.proposals[self.current.as_ref().unwrap()];
let map_name = edits.map_name.clone();
let edits = edits.clone();
let maybe_err = ctx.loading_screen("apply edits", |ctx, mut timer| {
if &edits.map_name != app.primary.map.get_name() {
app.switch_map(ctx, abstutil::path_map(&edits.map_name));
}
match PermanentMapEdits::from_permanent(edits, &app.primary.map) {
Ok(edits) => {
apply_map_edits(ctx, app, edits);
app.primary
.map
.recalculate_pathfinding_after_edits(&mut timer);
None
}
Err(err) => Some(err),
}
});
if let Some(err) = maybe_err {
return Transition::Push(PopupMsg::new(
ctx,
"Can't load proposal",
vec![err],
));
} else {
app.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app)));
return Transition::Push(SandboxMode::new(
ctx,
app,
GameplayMode::PlayScenario(
abstutil::path_map(&map_name),
"weekday".to_string(),
Vec::new(),
),
));
}
}
"Read detailed write-up" => {
open_browser(
self.proposals[self.current.as_ref().unwrap()]
.proposal_link
.clone()
.unwrap(),
);
}
x => {
return Transition::Replace(Proposals::new(ctx, app, Some(x.to_string())));
}
},
_ => {}
}
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::Custom
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
g.clear(app.cs.grass);
self.panel.draw(g);
}
}
struct Screensaver {
line: Line,
started: Instant,
}
impl Screensaver {
fn bounce(ctx: &mut EventCtx, app: &mut App, rng: &mut XorShiftRng) -> Screensaver {
let at = ctx.canvas.center_to_map_pt();
let bounds = app.primary.map.get_bounds();
let line = loop {
let goto = Pt2D::new(
rng.gen_range(0.0, bounds.max_x),
rng.gen_range(0.0, bounds.max_y),
);
if let Some(l) = Line::new(at, goto) {
break l;
}
};
ctx.canvas.cam_zoom = 10.0;
Screensaver {
line,
started: Instant::now(),
}
}
fn update(&mut self, rng: &mut XorShiftRng, ctx: &mut EventCtx, app: &mut App) {
const SIM_SPEED: f64 = 3.0;
const PAN_SPEED: Speed = Speed::const_meters_per_second(20.0);
if let Some(dt) = ctx.input.nonblocking_is_update_event() {
ctx.input.use_update_event();
if let Some(pt) = self
.line
.dist_along(Duration::realtime_elapsed(self.started) * PAN_SPEED)
{
ctx.canvas.center_on_map_pt(pt);
} else {
*self = Screensaver::bounce(ctx, app, rng);
}
app.primary.sim.time_limited_step(
&app.primary.map,
SIM_SPEED * dt,
Duration::seconds(0.033),
&mut app.primary.sim_cb,
);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused)]
mod built_info {
use widgetry::{Color, Line, Text};
include!(concat!(env!("OUT_DIR"), "/built.rs"));
pub fn time() -> Text {
let t = built::util::strptime(BUILT_TIME_UTC);
let mut txt = Text::from(Line(format!(
"This version built on {}",
t.date().naive_local()
)));
if (chrono::Utc::now() - t).num_days() > 8 {
txt.append(Line(format!(" (get the new release from abstreet.org)")).fg(Color::RED));
}
txt
}
}
#[cfg(target_arch = "wasm32")]
mod built_info {
pub fn time() -> widgetry::Text {
widgetry::Text::new()
}
}