diff --git a/experiment/src/animation.rs b/experiment/src/animation.rs index 544238f399..b52f64e17f 100644 --- a/experiment/src/animation.rs +++ b/experiment/src/animation.rs @@ -2,17 +2,19 @@ use rand::{Rng, SeedableRng}; use rand_xorshift::XorShiftRng; use geom::{Distance, Duration, PolyLine, Pt2D, Time}; -use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx}; +use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, RewriteColor}; pub struct Animator { active: Vec, - draw_current: Drawable, + draw_mapspace: Drawable, + draw_screenspace: Option, } struct Animation { start: Time, end: Time, effect: Effect, + screenspace: bool, } pub enum Effect { @@ -26,13 +28,19 @@ pub enum Effect { width: Distance, pl: PolyLine, }, + Flash { + orig: GeomBatch, + alpha_scale: (f32, f32), + cycles: usize, + }, } impl Animator { pub fn new(ctx: &EventCtx) -> Animator { Animator { active: Vec::new(), - draw_current: Drawable::empty(ctx), + draw_mapspace: Drawable::empty(ctx), + draw_screenspace: None, } } @@ -42,6 +50,16 @@ impl Animator { start: now, end: now + duration, effect, + screenspace: false, + }); + } + + pub fn add_screenspace(&mut self, now: Time, duration: Duration, effect: Effect) { + self.active.push(Animation { + start: now, + end: now + duration, + effect, + screenspace: true, }); } @@ -49,7 +67,8 @@ impl Animator { if self.active.is_empty() { return; } - let mut batch = GeomBatch::new(); + let mut mapspace = GeomBatch::new(); + let mut screenspace = GeomBatch::new(); self.active.retain(|anim| { let pct = (now - anim.start) / (anim.end - anim.start); if pct < 0.0 { @@ -58,15 +77,29 @@ impl Animator { } else if pct > 1.0 { false } else { - anim.effect.render(pct, &mut batch); + if anim.screenspace { + anim.effect.render(pct, &mut screenspace); + } else { + anim.effect.render(pct, &mut mapspace); + } true } }); - self.draw_current = ctx.upload(batch); + self.draw_mapspace = ctx.upload(mapspace); + if screenspace.is_empty() { + self.draw_screenspace = None; + } else { + self.draw_screenspace = Some(ctx.upload(screenspace)); + } } pub fn draw(&self, g: &mut GfxCtx) { - g.redraw(&self.draw_current); + g.redraw(&self.draw_mapspace); + if let Some(ref d) = self.draw_screenspace { + g.fork_screenspace(); + g.redraw(d); + g.unfork(); + } } pub fn is_done(&self) -> bool { @@ -94,6 +127,19 @@ impl Effect { batch.push(*color, pl.make_polygons(*width)); } } + Effect::Flash { + ref orig, + alpha_scale, + cycles, + } => { + // -1 to 1 + let shift = (pct * (*cycles as f64) * (2.0 * std::f64::consts::PI)).sin() as f32; + let midpt = (alpha_scale.0 + alpha_scale.1) / 2.0; + let half_range = (alpha_scale.1 - alpha_scale.0) / 2.0; + let alpha = midpt + shift * half_range; + + batch.append(orig.clone().color(RewriteColor::ChangeAlpha(alpha))); + } } } } diff --git a/experiment/src/game.rs b/experiment/src/game.rs index 6fd69a0bfb..d514a90f1d 100644 --- a/experiment/src/game.rs +++ b/experiment/src/game.rs @@ -255,6 +255,7 @@ impl Game { let refill = self.state.vehicle.max_energy - self.state.energy; if refill > 0 { self.state.energy += refill; + self.state.warned_low_energy = false; let path_speed = Duration::seconds(0.2); self.animator.add( app.time, @@ -387,6 +388,69 @@ impl State for Game { ); } + if !self.state.warned_low_time + && self.state.level.time_limit - (app.time - Time::START_OF_DAY) + <= Duration::seconds(20.0) + { + self.state.warned_low_time = true; + self.animator.add( + app.time, + Duration::seconds(2.0), + Effect::Flash { + alpha_scale: (0.1, 0.5), + cycles: 2, + orig: GeomBatch::from(vec![( + Color::RED, + app.map.get_boundary_polygon().clone(), + )]), + }, + ); + self.animator.add_screenspace( + app.time, + Duration::seconds(2.0), + Effect::Scale { + lerp_scale: (1.0, 4.0), + center: { + let pt = ctx.canvas.center_to_screen_pt(); + Pt2D::new(pt.x, pt.y / 2.0) + }, + orig: Text::from(Line("Almost out of time!")) + .bg(Color::RED) + .render_autocropped(ctx), + }, + ); + } + + if !self.state.warned_low_energy && self.state.energy < 30 { + self.state.warned_low_energy = true; + self.animator.add( + app.time, + Duration::seconds(2.0), + Effect::Flash { + alpha_scale: (0.1, 0.5), + cycles: 2, + orig: GeomBatch::from(vec![( + Color::RED, + app.map.get_boundary_polygon().clone(), + )]), + }, + ); + self.animator.add_screenspace( + app.time, + Duration::seconds(2.0), + Effect::Scale { + lerp_scale: (1.0, 4.0), + center: { + let pt = ctx.canvas.center_to_screen_pt(); + Pt2D::new(pt.x, pt.y / 2.0) + }, + orig: Text::from(Line("Low on blood sugar, refill soon!")) + .bg(Color::RED) + .render_autocropped(ctx), + }, + ); + } + ctx.request_update(UpdateType::Game); return Transition::Keep; } @@ -504,6 +568,8 @@ struct GameState { idle_time: Duration, game_over: bool, + warned_low_time: bool, + warned_low_energy: bool, } impl GameState { @@ -524,6 +590,8 @@ impl GameState { idle_time: Duration::ZERO, game_over: false, + warned_low_time: false, + warned_low_energy: false, } } diff --git a/experiment/src/vehicles.rs b/experiment/src/vehicles.rs index 1426b34ebf..2341fcecb5 100644 --- a/experiment/src/vehicles.rs +++ b/experiment/src/vehicles.rs @@ -19,7 +19,7 @@ impl Vehicle { name: "bike".to_string(), speed: Speed::miles_per_hour(30.0), - max_energy: 100, + max_energy: 50, draw_frames: vec!["bike1.svg", "bike2.svg", "bike1.svg", "bike3.svg"], scale: 0.05, diff --git a/widgetry/src/geom.rs b/widgetry/src/geom.rs index 88edf23ebb..8e46a6ba54 100644 --- a/widgetry/src/geom.rs +++ b/widgetry/src/geom.rs @@ -123,7 +123,7 @@ impl GeomBatch { } /// True when the batch is empty. - pub(crate) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.list.is_empty() }