diff --git a/Cargo.lock b/Cargo.lock index 1011852d..bc0c9c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2263,9 +2263,9 @@ dependencies = [ [[package]] name = "graphviz-rust" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc1ec243771bd8dfe7f9a2e75e28d17c66fc3901b2182c5e0eeff067623aef32" +checksum = "5d3adbad2800805b14170067aee88336e12838823086b6a4f70ca8dfb5d971f3" dependencies = [ "dot-generator", "dot-structures", diff --git a/Cargo.toml b/Cargo.toml index f3576888..2bd78f16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ common_story = { path = "common/story" } common_visuals = { path = "common/visuals" } main_game_lib = { path = "main_game_lib" } -graphviz-rust = "0.7" +graphviz-rust = "0.8" itertools = "0.12" lazy_static = "1.4" logos = "0.14" diff --git a/common/assets/src/lib.rs b/common/assets/src/lib.rs index 68c252af..049c1bee 100644 --- a/common/assets/src/lib.rs +++ b/common/assets/src/lib.rs @@ -3,7 +3,7 @@ //! We store e.g. level layouts this way. pub mod ignore_loader; -mod paths; +pub mod paths; pub mod ron_loader; pub mod store; diff --git a/common/assets/src/paths.rs b/common/assets/src/paths.rs index 5bd39f97..adf076d9 100644 --- a/common/assets/src/paths.rs +++ b/common/assets/src/paths.rs @@ -105,3 +105,5 @@ pub mod misc { pub const LOADING_SCREEN_SPACE_ATLAS: &str = "misc/loading_screens/space_atlas.png"; } + +pub const EMOJI_ATLAS: &str = "misc/emoji_atlas.png"; diff --git a/common/loading_screen/src/atlases.rs b/common/loading_screen/src/atlases.rs index dd39bd21..7d667899 100644 --- a/common/loading_screen/src/atlases.rs +++ b/common/loading_screen/src/atlases.rs @@ -43,7 +43,7 @@ impl LoadingScreenAtlas { TextureAtlasLayout::from_grid(tile_size, columns, 1, None, None), AtlasAnimation { last: columns - 1, - on_last_frame: AtlasAnimationEnd::Loop, + on_last_frame: AtlasAnimationEnd::LoopIndefinitely, ..default() }, AtlasAnimationTimer::new_fps(fps), diff --git a/common/story/src/emoji.rs b/common/story/src/emoji.rs new file mode 100644 index 00000000..efacf7e1 --- /dev/null +++ b/common/story/src/emoji.rs @@ -0,0 +1,208 @@ +//! Emoji's are used to express emotions in a visual way. + +use bevy::{ + math::vec2, + prelude::*, + utils::{Duration, Instant}, +}; +use common_assets::paths::EMOJI_ATLAS; +use common_visuals::{ + AtlasAnimation, AtlasAnimationEnd, AtlasAnimationStep, AtlasAnimationTimer, +}; + +use crate::Character; + +/// Don't replace the current emoji too early, would look weird. +/// Since this is just a visual cue to the player, ignoring is not +/// a big deal. +/// +/// If I wanted to be fancy I'd have queued up emojis. +/// But I fancy finishing this game. +const MIN_EMOJI_DURATION: Duration = Duration::from_millis(1000); + +/// How large is a single emoji atlas tile. +const EMOJI_SIZE: Vec2 = vec2(24.0, 22.0); + +/// System in this set consumes [`DisplayEmojiEvent`]s. +#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DisplayEmojiEventConsumer; + +/// Send this event to display an emoji. +/// +/// This event might end up being ignored if +/// - the same emoji is already displayed +/// - the current emoji has not been displayed for long enough +#[derive(Event)] +pub struct DisplayEmojiEvent { + /// Which emoji to display. + pub emoji: EmojiKind, + /// Each entity can only display one emoji at a time. + /// The emoji will insert itself as a child of this entity unless it + /// already exists. + /// Then it will just update the existing emoji. + pub on_parent: Entity, + /// Who is the parent entity? + /// The entity character mustn't change while the emoji is displayed. + pub offset_for: Character, +} + +/// Emojis represent moods or situations that are nice to visually convey to +/// the player. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum EmojiKind { + /// Short emoji animation + Tired, +} + +enum EmojiFrames { + #[allow(dead_code)] + Empty = 0, + + Tired1 = 1, + Tired2 = 2, + Tired3 = 3, +} + +#[derive(Component)] +struct Emoji { + kind: EmojiKind, + started_at: Instant, +} + +pub(crate) struct Plugin; + +impl bevy::app::Plugin for Plugin { + fn build(&self, app: &mut App) { + app.add_event::().add_systems( + Update, + play_next + .in_set(DisplayEmojiEventConsumer) + .run_if(on_event::()), + ); + } +} + +fn play_next( + mut cmd: Commands, + asset_server: Res, + mut layouts: ResMut>, + mut events: EventReader, + + mut existing_emoji: Query<(Entity, &Parent, &mut Emoji, &mut TextureAtlas)>, +) { + for event in events.read() { + // this search is O(n) but there never are many emojis + let existing_emoji = existing_emoji + .iter_mut() + .find(|(_, parent, ..)| parent.get() == event.on_parent); + + let entity = if let Some((entity, _, mut emoji, mut atlas)) = + existing_emoji + { + if emoji.kind == event.emoji + || emoji.started_at.elapsed() < MIN_EMOJI_DURATION + { + // let the current emoji play out + continue; + } + + // set new emoji + *emoji = Emoji { + kind: event.emoji, + started_at: Instant::now(), + }; + atlas.index = event.emoji.initial_frame(); + + entity + } else { + let entity = cmd + .spawn(Name::new("Emoji")) + .insert(Emoji { + kind: event.emoji, + started_at: Instant::now(), + }) + .insert(SpriteBundle { + texture: asset_server.load(EMOJI_ATLAS), + transform: Transform::from_translation( + // the z-index is a dirty hack to make sure the emoji + // is always in front of the character + event.offset_for.emoji_offset().extend(11.0), + ), + ..default() + }) + .insert(TextureAtlas { + layout: layouts.add(TextureAtlasLayout::from_grid( + EMOJI_SIZE, + 4, + 1, + Some(Vec2::splat(2.0)), + Some(Vec2::splat(1.0)), + )), + index: event.emoji.initial_frame(), + }) + .id(); + cmd.entity(event.on_parent).add_child(entity); + + entity + }; + + if let Some(first) = event.emoji.animation_first_frame() { + cmd.entity(entity) + .insert(AtlasAnimation { + first, + last: event.emoji.animation_last_frame(), + play: AtlasAnimationStep::Forward, + on_last_frame: AtlasAnimationEnd::DespawnItself, + extra_steps: event.emoji.extra_steps(), + }) + .insert(AtlasAnimationTimer::new_fps(event.emoji.fps())); + } + } +} + +impl Character { + fn emoji_offset(self) -> Vec2 { + let (size, ..) = self.sprite_atlas().unwrap_or_default(); + + vec2(0.0, size.y + EMOJI_SIZE.y / 2.0) + } +} + +impl EmojiKind { + fn initial_frame(self) -> usize { + match self { + Self::Tired => EmojiFrames::Tired1 as usize, + } + } + + /// Only [`Some`] if an animation. + fn animation_first_frame(self) -> Option { + match self { + Self::Tired => Some(EmojiFrames::Tired2 as usize), + } + } + + fn animation_last_frame(self) -> usize { + match self { + Self::Tired => EmojiFrames::Tired3 as usize, + } + } + + fn fps(self) -> f32 { + match self { + Self::Tired => 3.0, + } + } + + fn extra_steps(self) -> Vec { + match self { + Self::Tired => vec![ + AtlasAnimationStep::Backward, + AtlasAnimationStep::Forward, + AtlasAnimationStep::Backward, + AtlasAnimationStep::Forward, + AtlasAnimationStep::Backward, + ], + } + } +} diff --git a/common/story/src/lib.rs b/common/story/src/lib.rs index 170da6ae..19758e9b 100644 --- a/common/story/src/lib.rs +++ b/common/story/src/lib.rs @@ -8,6 +8,7 @@ #![allow(clippy::too_many_arguments)] pub mod dialog; +pub mod emoji; use std::time::Duration; @@ -75,6 +76,8 @@ pub struct Plugin; impl bevy::app::Plugin for Plugin { fn build(&self, app: &mut App) { + app.add_plugins(emoji::Plugin); + app.add_plugins(dialog::fe::portrait::Plugin) .init_asset_loader::() .init_asset::(); @@ -109,7 +112,7 @@ impl Character { /// How long does it take to move one square. pub fn default_step_time(self) -> Duration { match self { - Character::Winnie => Duration::from_millis(35), + Character::Winnie => Duration::from_millis(15), _ => Duration::from_millis(50), } } @@ -184,6 +187,7 @@ impl Character { } /// Returns arguments to [`TextureAtlasLayout::from_grid`]. + #[inline] fn sprite_atlas(self) -> Option<(Vec2, usize, usize, Vec2)> { const STANDARD_SIZE: Vec2 = Vec2::new(25.0, 46.0); diff --git a/common/visuals/src/systems.rs b/common/visuals/src/systems.rs index fa241245..b21790ad 100644 --- a/common/visuals/src/systems.rs +++ b/common/visuals/src/systems.rs @@ -4,7 +4,7 @@ use bevy::prelude::*; use common_ext::ColorExt; use crate::{ - AtlasAnimation, AtlasAnimationEnd, AtlasAnimationTimer, + AtlasAnimation, AtlasAnimationEnd, AtlasAnimationStep, AtlasAnimationTimer, BeginAtlasAnimation, BeginAtlasAnimationCond, BeginInterpolationEvent, ColorInterpolation, Flicker, OnInterpolationFinished, TranslationInterpolation, UiStyleHeightInterpolation, @@ -27,41 +27,46 @@ pub fn advance_atlas_animation( ) { for (entity, animation, mut timer, mut atlas, mut visibility) in &mut query { - timer.tick(time.delta()); - if timer.just_finished() { - if animation.is_on_last_frame(&atlas) { + timer.inner.tick(time.delta()); + if !timer.inner.just_finished() { + continue; + } + + match animation.next_step_index_and_frame(&atlas, timer.current_step) { + Some((step_index, frame)) => { + atlas.index = frame; + timer.current_step = step_index; + } + None => { match &animation.on_last_frame { - AtlasAnimationEnd::RemoveTimerAndHide => { + AtlasAnimationEnd::RemoveTimerAndHideAndReset => { cmd.entity(entity).remove::(); *visibility = Visibility::Hidden; - atlas.index = if animation.reversed { - animation.last - } else { - animation.first + atlas.index = match animation.play { + AtlasAnimationStep::Forward => animation.first, + AtlasAnimationStep::Backward => animation.last, }; } + AtlasAnimationEnd::DespawnItself => { + cmd.entity(entity).despawn(); + } AtlasAnimationEnd::RemoveTimer => { cmd.entity(entity).remove::(); } AtlasAnimationEnd::Custom { with: Some(fun) } => { - fun(entity, &mut atlas, &mut visibility, &mut cmd); + fun(&mut cmd, entity, &mut atlas, &mut visibility); } AtlasAnimationEnd::Custom { with: None } => { // nothing happens } - AtlasAnimationEnd::Loop => { - atlas.index = if animation.reversed { - animation.last - } else { - animation.first + AtlasAnimationEnd::LoopIndefinitely => { + atlas.index = match animation.play { + AtlasAnimationStep::Forward => animation.first, + AtlasAnimationStep::Backward => animation.last, }; } } - } else if animation.reversed { - atlas.index -= 1; - } else { - atlas.index += 1; - }; + } } } } diff --git a/common/visuals/src/types.rs b/common/visuals/src/types.rs index c55b9a01..6c914c4f 100644 --- a/common/visuals/src/types.rs +++ b/common/visuals/src/types.rs @@ -10,24 +10,39 @@ use crate::EASE_IN_OUT; /// The animation specifically integrates with texture atlas sprites. #[derive(Component, Default, Reflect)] pub struct AtlasAnimation { - /// What should happen when the last frame is reached? - pub on_last_frame: AtlasAnimationEnd, /// The index of the first frame. /// Typically 0. pub first: usize, /// The index of the last frame of the atlas. pub last: usize, - /// If the animation should be played in reverse, going from last to first. - /// When the first frame is reached, the animation still acts in accordance - /// with the [`AtlasAnimationEnd`] strategy. - pub reversed: bool, + /// How should the animation be played? + pub play: AtlasAnimationStep, + /// What should happen when the last frame is reached? + pub on_last_frame: AtlasAnimationEnd, + /// After finishing with [`AtlasAnimation::play`] mode, play these next. + /// Leave empty for just one mode. + /// Allows for stitching animations together. + /// + /// The current step is stored in [`AtlasAnimationTimer`]. + /// The current index is stored in [`TextureAtlas`]. + pub extra_steps: Vec, +} + +/// How should the animation be played? +#[derive(Default, Reflect, Clone, Copy)] +pub enum AtlasAnimationStep { + /// Goes from the first frame to the last. + #[default] + Forward, + /// Reverse version of [`Self::Forward`]. + Backward, } /// Can be used to run custom logic when the last frame of the animation is /// reached. #[allow(clippy::type_complexity)] pub type CustomAtlasAnimationEndFn = Box< - dyn Fn(Entity, &mut TextureAtlas, &mut Visibility, &mut Commands) + dyn Fn(&mut Commands, Entity, &mut TextureAtlas, &mut Visibility) + Send + Sync, >; @@ -37,13 +52,15 @@ pub type CustomAtlasAnimationEndFn = Box< pub enum AtlasAnimationEnd { /// Loops the animation. #[default] - Loop, + LoopIndefinitely, /// Removes the animation timer, hides the entity and sets the index back /// to the first frame. - RemoveTimerAndHide, + RemoveTimerAndHideAndReset, /// Just removes the animation timer. /// Keeps the entity visible and on the last frame. RemoveTimer, + /// Despawns the animated entity. + DespawnItself, /// Can optionally mutate state. Custom { /// The function to call when the last frame is reached. @@ -57,8 +74,14 @@ pub enum AtlasAnimationEnd { } /// Must be present for the systems to actually drive the animation. -#[derive(Component, Deref, DerefMut, Reflect)] -pub struct AtlasAnimationTimer(pub(crate) Timer); +#[derive(Component, Reflect)] +pub struct AtlasAnimationTimer { + pub(crate) inner: Timer, + /// 0 => means current is [`AtlasAnimation::play`]. + /// 1 => extra_steps[0] + /// and so on... + pub(crate) current_step: usize, +} /// Allows to start an animation at random. #[derive(Component, Default, Reflect)] @@ -460,23 +483,47 @@ impl AtlasAnimationTimer { /// Creates a new animation timer. #[inline] pub fn new(duration: Duration, mode: TimerMode) -> Self { - Self(Timer::new(duration, mode)) + Self { + inner: Timer::new(duration, mode), + current_step: 0, + } } /// How many times a second should we go to the next frame. #[inline] pub fn new_fps(fps: f32) -> Self { - Self(Timer::from_seconds(1.0 / fps, TimerMode::Repeating)) + Self { + inner: Timer::from_seconds(1.0 / fps, TimerMode::Repeating), + current_step: 0, + } } } impl AtlasAnimation { - /// Takes into account whether the animation is reversed or not. - pub fn is_on_last_frame(&self, sprite: &TextureAtlas) -> bool { - if self.reversed { - self.first == sprite.index + pub(crate) fn next_step_index_and_frame( + &self, + atlas: &TextureAtlas, + current_step_index: usize, + ) -> Option<(usize, usize)> { + let current_step = if current_step_index == 0 { + self.play } else { - self.last == sprite.index + self.extra_steps.get(current_step_index - 1).copied()? + }; + + match current_step { + AtlasAnimationStep::Forward if atlas.index >= self.last => { + self.next_step_index_and_frame(atlas, current_step_index + 1) + } + AtlasAnimationStep::Forward => { + Some((current_step_index, atlas.index + 1)) + } + AtlasAnimationStep::Backward if atlas.index <= self.first => { + self.next_step_index_and_frame(atlas, current_step_index + 1) + } + AtlasAnimationStep::Backward => { + Some((current_step_index, atlas.index - 1)) + } } } } diff --git a/main_game/assets/downtown/bg.png b/main_game/assets/downtown/bg.png index f75790bb..121de250 100644 Binary files a/main_game/assets/downtown/bg.png and b/main_game/assets/downtown/bg.png differ diff --git a/main_game/assets/downtown/fancy_sloupky.png b/main_game/assets/downtown/fancy_sloupky.png new file mode 100644 index 00000000..2475ef9e Binary files /dev/null and b/main_game/assets/downtown/fancy_sloupky.png differ diff --git a/main_game/assets/downtown/fancy_stairs.png b/main_game/assets/downtown/fancy_stairs.png index d7c3ea01..4d92fbb4 100644 Binary files a/main_game/assets/downtown/fancy_stairs.png and b/main_game/assets/downtown/fancy_stairs.png differ diff --git a/main_game/assets/downtown/mall.png b/main_game/assets/downtown/mall.png new file mode 100644 index 00000000..ad8040ec Binary files /dev/null and b/main_game/assets/downtown/mall.png differ diff --git a/main_game/assets/scenes/building1_player_floor.tscn b/main_game/assets/scenes/building1_player_floor.tscn index b97122e8..f41dfe0c 100644 --- a/main_game/assets/scenes/building1_player_floor.tscn +++ b/main_game/assets/scenes/building1_player_floor.tscn @@ -439,7 +439,10 @@ texture = ExtResource("3_e2w78") position = Vector2(38, -84) texture = ExtResource("4_njet2") -[node name="InspectLabel" type="Node" parent="BackwallFurniture/MeditationChair"] +[node name="Label" type="Node2D" parent="BackwallFurniture/MeditationChair"] +position = Vector2(0, -21) + +[node name="InspectLabel" type="Node" parent="BackwallFurniture/MeditationChair/Label"] metadata/zone = "MeditationZone" metadata/action = "StartMeditation" metadata/label = "Meditate" @@ -456,6 +459,22 @@ self_modulate = Color(1, 1, 1, 0.823529) position = Vector2(-70, -67.5) texture = ExtResource("5_u86a4") +[node name="GoToSleep" type="Node2D" parent="BackwallFurniture"] +position = Vector2(-70, -49) + +[node name="InspectLabel" type="Node" parent="BackwallFurniture/GoToSleep"] +metadata/zone = "BedZone" +metadata/action = "Sleep" +metadata/label = "Sleep" + +[node name="BrewTea" type="Node2D" parent="BackwallFurniture"] +position = Vector2(132, -111) + +[node name="InspectLabel" type="Node" parent="BackwallFurniture/BrewTea"] +metadata/zone = "TeaZone" +metadata/action = "BrewTea" +metadata/label = "Winnie Put the Kettle On" + [node name="Toilet" type="Sprite2D" parent="."] z_index = 2 position = Vector2(-143, -6) @@ -471,7 +490,10 @@ texture = ExtResource("8_uh48f") position = Vector2(-201.5, 49.5) sprite_frames = SubResource("SpriteFrames_33ymd") -[node name="InspectLabel" type="Node" parent="HallwayBg/Elevator"] +[node name="Label" type="Node2D" parent="HallwayBg/Elevator"] +position = Vector2(0, 21) + +[node name="InspectLabel" type="Node" parent="HallwayBg/Elevator/Label"] metadata/zone = "ElevatorZone" metadata/action = "EnterElevator" metadata/label = "Elevator" @@ -550,8 +572,11 @@ z_index = -1 position = Vector2(0, -65) texture = ExtResource("18_fe7mm") -[node name="InspectLabel" type="Node" parent="Package"] -metadata/label = "Open package" +[node name="Label" type="Node2D" parent="Package"] +position = Vector2(0, 10) + +[node name="InspectLabel" type="Node" parent="Package/Label"] +metadata/label = "Open package" [node name="WaterBottleEmpty" type="Sprite2D" parent="."] position = Vector2(-98, -68) diff --git a/main_game/assets/scenes/downtown.tscn b/main_game/assets/scenes/downtown.tscn index bd153d71..a4669362 100644 --- a/main_game/assets/scenes/downtown.tscn +++ b/main_game/assets/scenes/downtown.tscn @@ -1,37 +1,39 @@ -[gd_scene load_steps=37 format=3 uid="uid://dtow2k2vgk8kg"] - -[ext_resource type="Texture2D" uid="uid://b5pepyrbokdp7" path="res://assets/downtown/bg.png" id="1_ac7tb"] -[ext_resource type="Texture2D" uid="uid://dhtpl0f15gojd" path="res://assets/downtown/ac_atlas.png" id="2_vxd7e"] -[ext_resource type="Texture2D" uid="uid://od7278drijk8" path="res://assets/downtown/water_dispenser.png" id="3_mnq47"] -[ext_resource type="Texture2D" uid="uid://bwt8l2nfovdrb" path="res://assets/downtown/bamboo_rooftop.png" id="4_uahdt"] -[ext_resource type="Texture2D" uid="uid://5ywhhdx6rx7r" path="res://assets/downtown/recycle_bin.png" id="5_her3o"] -[ext_resource type="Texture2D" uid="uid://b8xgrwspdhoao" path="res://assets/downtown/cone.png" id="6_4cklq"] -[ext_resource type="Texture2D" uid="uid://clp5emmiqqigh" path="res://assets/downtown/black_bin.png" id="7_p1j5y"] -[ext_resource type="Texture2D" uid="uid://cnd2np6d33oqh" path="res://assets/downtown/robot.png" id="8_s4pbl"] -[ext_resource type="Texture2D" uid="uid://hmoa0kdonp88" path="res://assets/downtown/swing.png" id="9_gwh2v"] -[ext_resource type="Texture2D" uid="uid://bdn0jfgw4tpeg" path="res://assets/downtown/trashcan_tables.png" id="10_nyc25"] -[ext_resource type="Texture2D" uid="uid://cskl5m8h5o3n8" path="res://assets/environment/beer.png" id="11_e3pff"] -[ext_resource type="Texture2D" uid="uid://qjb3dquepnch" path="res://assets/downtown/truck.png" id="12_mwj3x"] -[ext_resource type="Texture2D" uid="uid://cs4xyldoxgtxg" path="res://assets/downtown/tree.png" id="13_7q235"] -[ext_resource type="Texture2D" uid="uid://b7r70khewajh0" path="res://assets/downtown/lamp.png" id="14_8uy80"] -[ext_resource type="Texture2D" uid="uid://d3swa0vc5iitv" path="res://assets/downtown/briza.png" id="16_xr53b"] -[ext_resource type="Texture2D" uid="uid://lgsyrvis4y80" path="res://assets/downtown/wired_fence.png" id="17_5ai3x"] -[ext_resource type="Texture2D" uid="uid://bneahsxtb6wpe" path="res://assets/downtown/front_field.png" id="18_yr0u5"] -[ext_resource type="Texture2D" uid="uid://bq3fw5e27w801" path="res://assets/downtown/house_plantshoptrio.png" id="19_fdu3c"] -[ext_resource type="Texture2D" uid="uid://brvijlucyh2jx" path="res://assets/downtown/tree_pot.png" id="20_kpivv"] -[ext_resource type="Texture2D" uid="uid://dcl4guw8e72nh" path="res://assets/downtown/compound_gate.png" id="20_o0xfm"] -[ext_resource type="Texture2D" uid="uid://bbvuyahml5l4l" path="res://assets/downtown/umbrella_chair.png" id="20_sy6x1"] -[ext_resource type="Texture2D" uid="uid://ufn4rnxo8e62" path="res://assets/environment/water_bottle_half.png" id="21_8m32a"] -[ext_resource type="Texture2D" uid="uid://bmbgymdlgia22" path="res://assets/downtown/praha_building.png" id="21_sq8gl"] -[ext_resource type="Texture2D" uid="uid://bs5503nvkypsp" path="res://assets/downtown/building_duo.png" id="22_bl5xi"] -[ext_resource type="Texture2D" uid="uid://dy8urm75tam2x" path="res://assets/downtown/construction_site_sign.png" id="22_m6eml"] -[ext_resource type="Texture2D" uid="uid://bqhuebro50e5n" path="res://assets/downtown/tower.png" id="23_7ng4s"] -[ext_resource type="Texture2D" uid="uid://closy5vs11rwm" path="res://assets/downtown/barn.png" id="23_av7fi"] -[ext_resource type="Texture2D" uid="uid://m2ycv8ggd4ho" path="res://assets/downtown/clinic.png" id="25_ukjmq"] -[ext_resource type="Texture2D" uid="uid://vyq68glbbaiv" path="res://assets/downtown/zabradli.png" id="26_nm5hp"] -[ext_resource type="Texture2D" uid="uid://uuxhgpjap208" path="res://assets/downtown/big_tree.png" id="28_6tavv"] -[ext_resource type="Texture2D" uid="uid://c8vxwyyvlctic" path="res://assets/downtown/jehlicnan.png" id="29_j62ep"] -[ext_resource type="Texture2D" uid="uid://c85luubin2ham" path="res://assets/downtown/fancy_stairs.png" id="32_01aim"] +[gd_scene load_steps=39 format=3 uid="uid://dtow2k2vgk8kg"] + +[ext_resource type="Texture2D" uid="uid://ctujtb5gwqvre" path="res://assets/downtown/bg.png" id="1_ac7tb"] +[ext_resource type="Texture2D" uid="uid://kqhfsst8cp0b" path="res://assets/downtown/ac_atlas.png" id="2_vxd7e"] +[ext_resource type="Texture2D" uid="uid://fvsk7527x314" path="res://assets/downtown/water_dispenser.png" id="3_mnq47"] +[ext_resource type="Texture2D" uid="uid://ccpux3rqyc01t" path="res://assets/downtown/bamboo_rooftop.png" id="4_uahdt"] +[ext_resource type="Texture2D" uid="uid://cve0p1fj5bjp3" path="res://assets/downtown/recycle_bin.png" id="5_her3o"] +[ext_resource type="Texture2D" uid="uid://c7pfatjf403l5" path="res://assets/downtown/cone.png" id="6_4cklq"] +[ext_resource type="Texture2D" uid="uid://bmy3x3gvi867d" path="res://assets/downtown/black_bin.png" id="7_p1j5y"] +[ext_resource type="Texture2D" uid="uid://d1y5l8pmrdloy" path="res://assets/downtown/robot.png" id="8_s4pbl"] +[ext_resource type="Texture2D" uid="uid://dlc054120cn8l" path="res://assets/downtown/swing.png" id="9_gwh2v"] +[ext_resource type="Texture2D" uid="uid://b75yq8snj53yw" path="res://assets/downtown/trashcan_tables.png" id="10_nyc25"] +[ext_resource type="Texture2D" uid="uid://d2v4j1m6u8ek1" path="res://assets/environment/beer.png" id="11_e3pff"] +[ext_resource type="Texture2D" uid="uid://djc7g7t6dy3wa" path="res://assets/downtown/truck.png" id="12_mwj3x"] +[ext_resource type="Texture2D" uid="uid://dh2sjhvbvlj0y" path="res://assets/downtown/tree.png" id="13_7q235"] +[ext_resource type="Texture2D" uid="uid://c6bjetxf4tjpq" path="res://assets/downtown/mall.png" id="13_jgs6s"] +[ext_resource type="Texture2D" uid="uid://b54rhfp4mdr1d" path="res://assets/downtown/lamp.png" id="14_8uy80"] +[ext_resource type="Texture2D" uid="uid://7iyu03tse1f0" path="res://assets/downtown/briza.png" id="16_xr53b"] +[ext_resource type="Texture2D" uid="uid://cpagv4vgk17ov" path="res://assets/downtown/wired_fence.png" id="17_5ai3x"] +[ext_resource type="Texture2D" uid="uid://d28l4plhavno" path="res://assets/downtown/front_field.png" id="18_yr0u5"] +[ext_resource type="Texture2D" uid="uid://deq70sixg36ra" path="res://assets/downtown/house_plantshoptrio.png" id="19_fdu3c"] +[ext_resource type="Texture2D" uid="uid://dqvep7ugj5h2e" path="res://assets/downtown/tree_pot.png" id="20_kpivv"] +[ext_resource type="Texture2D" uid="uid://dj4h0df73poo0" path="res://assets/downtown/compound_gate.png" id="20_o0xfm"] +[ext_resource type="Texture2D" uid="uid://c3jrebwaemnlt" path="res://assets/downtown/umbrella_chair.png" id="20_sy6x1"] +[ext_resource type="Texture2D" uid="uid://dpj7jnsvxyfvt" path="res://assets/environment/water_bottle_half.png" id="21_8m32a"] +[ext_resource type="Texture2D" uid="uid://bodg01kctbo3d" path="res://assets/downtown/praha_building.png" id="21_sq8gl"] +[ext_resource type="Texture2D" uid="uid://crl8brm7m7obp" path="res://assets/downtown/building_duo.png" id="22_bl5xi"] +[ext_resource type="Texture2D" uid="uid://dm414iyf3bdnr" path="res://assets/downtown/construction_site_sign.png" id="22_m6eml"] +[ext_resource type="Texture2D" uid="uid://cvpb63r4tafm8" path="res://assets/downtown/tower.png" id="23_7ng4s"] +[ext_resource type="Texture2D" uid="uid://cjkde3a6ochx7" path="res://assets/downtown/barn.png" id="23_av7fi"] +[ext_resource type="Texture2D" uid="uid://ctd21n83m0nwc" path="res://assets/downtown/clinic.png" id="25_ukjmq"] +[ext_resource type="Texture2D" uid="uid://cyd2vttksmhrg" path="res://assets/downtown/zabradli.png" id="26_nm5hp"] +[ext_resource type="Texture2D" uid="uid://d4d4x4dbmu2f" path="res://assets/downtown/big_tree.png" id="28_6tavv"] +[ext_resource type="Texture2D" uid="uid://dqqvboiu7sjg4" path="res://assets/downtown/jehlicnan.png" id="29_j62ep"] +[ext_resource type="Texture2D" uid="uid://d2ainwkym4lxy" path="res://assets/downtown/fancy_stairs.png" id="32_01aim"] +[ext_resource type="Texture2D" uid="uid://04yhpl0qtq7g" path="res://assets/downtown/fancy_sloupky.png" id="33_lx3i7"] [sub_resource type="AtlasTexture" id="AtlasTexture_5i4a5"] atlas = ExtResource("2_vxd7e") @@ -165,8 +167,29 @@ texture = ExtResource("14_8uy80") [node name="YSort" type="Node2D" parent="CompoundRoot/Lamp3"] position = Vector2(0, 33) +[node name="BigTree" type="Sprite2D" parent="CompoundRoot"] +position = Vector2(-485, -637) +texture = ExtResource("28_6tavv") + +[node name="YSort" type="Node2D" parent="CompoundRoot/BigTree"] +position = Vector2(0, 75) + +[node name="Jehlicnan" type="Sprite2D" parent="CompoundRoot"] +position = Vector2(-634, -555) +texture = ExtResource("29_j62ep") + +[node name="YSort" type="Node2D" parent="CompoundRoot/Jehlicnan"] +position = Vector2(0, 61) + [node name="MallRoot" type="Node2D" parent="."] +[node name="Mall" type="Sprite2D" parent="MallRoot"] +position = Vector2(-389, 150) +texture = ExtResource("13_jgs6s") + +[node name="YSort" type="Node2D" parent="MallRoot/Mall"] +position = Vector2(-114, -196) + [node name="TrashcanTables6" type="Sprite2D" parent="MallRoot"] position = Vector2(-556, -10) texture = ExtResource("10_nyc25") @@ -261,6 +284,9 @@ texture = ExtResource("7_p1j5y") position = Vector2(-423, 159) texture = ExtResource("20_sy6x1") +[node name="YSort" type="Node2D" parent="MallRoot/UmbrellaChair2"] +position = Vector2(0, 41) + [node name="WaterBottleHalf" type="Sprite2D" parent="MallRoot/UmbrellaChair2"] position = Vector2(-13, 25) texture = ExtResource("21_8m32a") @@ -273,10 +299,16 @@ texture = ExtResource("20_sy6x1") position = Vector2(-10, 24) texture = ExtResource("11_e3pff") +[node name="YSort" type="Node2D" parent="MallRoot/UmbrellaChair"] +position = Vector2(0, 41) + [node name="UmbrellaChair3" type="Sprite2D" parent="MallRoot"] position = Vector2(-432, 31) texture = ExtResource("20_sy6x1") +[node name="YSort" type="Node2D" parent="MallRoot/UmbrellaChair3"] +position = Vector2(0, 41) + [node name="MallEntrance" type="Node2D" parent="MallRoot"] position = Vector2(-149, 342) @@ -347,6 +379,25 @@ z_index = -1 position = Vector2(1, 64) texture = ExtResource("20_kpivv") +[node name="FancyStairs" type="Sprite2D" parent="Block2Root"] +z_index = -2 +position = Vector2(1133, -332) +texture = ExtResource("32_01aim") + +[node name="FancySloupky" type="Sprite2D" parent="Block2Root/FancyStairs"] +position = Vector2(240.5, 78) +texture = ExtResource("33_lx3i7") + +[node name="YSort" type="Node2D" parent="Block2Root/FancyStairs/FancySloupky"] +position = Vector2(0, 8) + +[node name="FancySloupky2" type="Sprite2D" parent="Block2Root/FancyStairs"] +position = Vector2(-240.5, 78) +texture = ExtResource("33_lx3i7") + +[node name="YSort" type="Node2D" parent="Block2Root/FancyStairs/FancySloupky2"] +position = Vector2(0, 8) + [node name="Block1Root" type="Node2D" parent="."] [node name="BuildingDuo" type="Sprite2D" parent="Block1Root"] @@ -496,16 +547,3 @@ texture = ExtResource("9_gwh2v") [node name="YSort" type="Node2D" parent="ShoreRoot/Swing"] position = Vector2(0, 24) - -[node name="BigTree" type="Sprite2D" parent="."] -position = Vector2(-485, -637) -texture = ExtResource("28_6tavv") - -[node name="Jehlicnan" type="Sprite2D" parent="."] -position = Vector2(-634, -555) -texture = ExtResource("29_j62ep") - -[node name="FancyStairs" type="Sprite2D" parent="."] -z_index = -2 -position = Vector2(1133, -332) -texture = ExtResource("32_01aim") diff --git a/main_game_lib/src/cutscene.rs b/main_game_lib/src/cutscene.rs index 23a1a1f6..67851bef 100644 --- a/main_game_lib/src/cutscene.rs +++ b/main_game_lib/src/cutscene.rs @@ -23,7 +23,8 @@ use common_story::dialog::{ }; use common_visuals::{ camera::{order, render_layer}, - AtlasAnimation, AtlasAnimationTimer, BeginInterpolationEvent, + AtlasAnimation, AtlasAnimationStep, AtlasAnimationTimer, + BeginInterpolationEvent, }; use crate::{ @@ -680,7 +681,10 @@ fn reverse_atlas_animation( }; if let Ok(mut animation) = animations.get_mut(*entity) { - animation.reversed = !animation.reversed; + animation.play = match animation.play { + AtlasAnimationStep::Forward => AtlasAnimationStep::Backward, + AtlasAnimationStep::Backward => AtlasAnimationStep::Forward, + }; } cutscene.schedule_next_step_or_despawn(&mut cmd); diff --git a/main_game_lib/src/cutscene/enter_an_elevator.rs b/main_game_lib/src/cutscene/enter_an_elevator.rs index 77a5bcb1..ab7b7ddf 100644 --- a/main_game_lib/src/cutscene/enter_an_elevator.rs +++ b/main_game_lib/src/cutscene/enter_an_elevator.rs @@ -11,7 +11,7 @@ use common_story::{ Character, }; use common_visuals::{ - AtlasAnimation, AtlasAnimationEnd, AtlasAnimationTimer, + AtlasAnimation, AtlasAnimationEnd, AtlasAnimationStep, AtlasAnimationTimer, BeginAtlasAnimation, EASE_IN_OUT, }; use top_down::layout::LAYOUT; @@ -316,10 +316,10 @@ pub fn start_with_open_elevator_and_close_it( let mut a = elevator.get_mut::().unwrap(); // animation runs in reverse - a.reversed = true; + a.play = AtlasAnimationStep::Backward; // on last frame, put everything back to normal a.on_last_frame = - AtlasAnimationEnd::run(Box::new(move |who, atlas, _, cmd| { + AtlasAnimationEnd::run(Box::new(move |cmd, who, atlas, _| { // animation finished in reverse debug_assert_eq!(0, atlas.index); @@ -335,7 +335,7 @@ pub fn start_with_open_elevator_and_close_it( let mut a = e.get_mut::().unwrap(); // back to normal a.on_last_frame = AtlasAnimationEnd::RemoveTimer; - a.reversed = false; + a.play = AtlasAnimationStep::Forward; }); })); } diff --git a/main_game_lib/src/hud/daybar.rs b/main_game_lib/src/hud/daybar.rs index bbffa701..58d2263c 100644 --- a/main_game_lib/src/hud/daybar.rs +++ b/main_game_lib/src/hud/daybar.rs @@ -25,6 +25,13 @@ pub enum IncreaseDayBarEvent { Meditated, } +/// What sort of things are dependent on status of the daybar. +#[derive(Debug)] +pub enum DayBarDependent { + /// The span of time when the mall is open. + MallOpenHours, +} + #[derive(Component)] pub(crate) struct DayBarRoot; #[derive(Component)] @@ -111,4 +118,13 @@ impl DayBar { pub fn is_depleted(&self) -> bool { self.progress >= 1.0 } + + /// Whether it's time for something to happen. + pub fn is_it_time_for(&self, what: DayBarDependent) -> bool { + let range = match what { + DayBarDependent::MallOpenHours => 0.05..0.75, + }; + + range.contains(&self.progress) + } } diff --git a/main_game_lib/src/rscn/spawner.rs b/main_game_lib/src/rscn/spawner.rs index 0e429672..e01e227b 100644 --- a/main_game_lib/src/rscn/spawner.rs +++ b/main_game_lib/src/rscn/spawner.rs @@ -153,7 +153,7 @@ fn node_to_entity( }) .insert(AtlasAnimation { on_last_frame: if animation.should_endless_loop { - AtlasAnimationEnd::Loop + AtlasAnimationEnd::LoopIndefinitely } else { AtlasAnimationEnd::RemoveTimer }, diff --git a/main_game_lib/src/top_down.rs b/main_game_lib/src/top_down.rs index 6e0d11e4..f80f07c0 100644 --- a/main_game_lib/src/top_down.rs +++ b/main_game_lib/src/top_down.rs @@ -25,8 +25,11 @@ pub use inspect_and_interact::{InspectLabel, InspectLabelCategory}; pub use layout::{TileKind, TileMap, TopDownScene}; use leafwing_input_manager::plugin::InputManagerSystem; +use self::inspect_and_interact::ChangeHighlightedInspectLabelEvent; use crate::{ - cutscene::in_cutscene, StandardStateSemantics, WithStandardStateSemantics, + cutscene::in_cutscene, + top_down::inspect_and_interact::ChangeHighlightedInspectLabelEventConsumer, + StandardStateSemantics, WithStandardStateSemantics, }; /// Does not add any systems, only registers generic-less types. @@ -35,11 +38,13 @@ pub struct Plugin; impl bevy::app::Plugin for Plugin { fn build(&self, app: &mut App) { app.add_event::() - .add_event::(); + .add_event::() + .add_event::(); #[cfg(feature = "devtools")] app.register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -131,23 +136,25 @@ where debug!("Adding inspect ability for {}", T::type_path()); - app.register_type::() - .add_systems( - Update, - ( - inspect_and_interact::highlight_what_would_be_interacted_with, - inspect_and_interact::show_all_in_vicinity - .run_if(common_action::inspect_pressed()), - ) - .chain() // easier to think about - .run_if(in_state(running)), + app.add_systems( + Update, + ( + inspect_and_interact::highlight_what_would_be_interacted_with, + inspect_and_interact::change_highlighted_label + .in_set(ChangeHighlightedInspectLabelEventConsumer) + .run_if(on_event::()), + inspect_and_interact::show_all_in_vicinity + .run_if(common_action::inspect_pressed()), ) - .add_systems( - Update, - inspect_and_interact::schedule_hide_all - .run_if(in_state(running)) - .run_if(common_action::inspect_just_released()), - ); + .chain() // easier to reason about + .run_if(in_state(running)), + ) + .add_systems( + Update, + inspect_and_interact::schedule_hide_all + .run_if(in_state(running)) + .run_if(common_action::inspect_just_released()), + ); debug!("Adding interaction systems for {}", T::type_path()); diff --git a/main_game_lib/src/top_down/cameras.rs b/main_game_lib/src/top_down/cameras.rs index 47c18ca8..82cf8943 100644 --- a/main_game_lib/src/top_down/cameras.rs +++ b/main_game_lib/src/top_down/cameras.rs @@ -19,7 +19,7 @@ lazy_static! { /// The box is centered at camera position. pub static ref BOUNDING_BOX_SIZE: Vec2 = { - 2.0 * vec2(PIXEL_VISIBLE_WIDTH, PIXEL_VISIBLE_HEIGHT) + vec2(PIXEL_VISIBLE_WIDTH, PIXEL_VISIBLE_HEIGHT) / //------------------------------------------------------ 3.0 @@ -28,7 +28,7 @@ lazy_static! { /// How smooth is the transition of the camera from wherever it is to the /// player's position. -pub const SYNCING_DURATION: Duration = Duration::from_millis(2000); +pub const SYNCING_DURATION: Duration = Duration::from_millis(1000); /// If the player leaves a bounding box defined with /// [`static@BOUNDING_BOX_SIZE`], this component is attached. diff --git a/main_game_lib/src/top_down/inspect_and_interact.rs b/main_game_lib/src/top_down/inspect_and_interact.rs index a51bb1b6..92ff2470 100644 --- a/main_game_lib/src/top_down/inspect_and_interact.rs +++ b/main_game_lib/src/top_down/inspect_and_interact.rs @@ -32,8 +32,11 @@ use strum::EnumString; use super::actor::player::TakeAwayPlayerControl; use crate::top_down::{ActorMovementEvent, Player, TileKind, TopDownScene}; +/// Useful for error labels. +pub const LIGHT_RED: Color = Color::rgb(1.0, 0.7, 0.7); + /// The label's bg is a rect with a half transparent color. -const HALF_TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.5); +const BG_COLOR: Color = Color::rgba(0.0, 0.0, 0.0, 0.65); /// When the player releases the inspect button, the labels fade out in this /// duration. const FADE_OUT_IN: Duration = Duration::from_millis(5000); @@ -172,6 +175,68 @@ pub(crate) fn match_interact_label_with_action_event( } } +/// System in this set consumes [`ChangeHighlightedInspectLabelEvent`]s. +#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ChangeHighlightedInspectLabelEventConsumer; + +/// Enables changing of the label's appearance. +/// This is only relevant for highlighted labels. +/// Useful to give the player some extra information why the interaction +/// is not actually possible due to some other condition, such as time (e.g. +/// shop after hours). +/// +/// This change of appearance is not permanent and resets on first opportunity. +#[derive(Event)] +pub struct ChangeHighlightedInspectLabelEvent { + /// The entity that has [`InspectLabel`] component + pub entity: Entity, + /// Edit options + pub spawn_params: SpawnLabelBgAndTextParams, +} + +/// Customize the appearance of a label. +#[derive(Default)] +pub struct SpawnLabelBgAndTextParams { + /// Highlight the label visually. + /// (does not overwrite interaction precedence) + pub highlighted: bool, + /// Overwrite the label font color that's by default given by its category. + pub overwrite_font_color: Option, + /// Change the text that's displayed. + pub overwrite_display_text: Option, +} + +/// Respawns the label with provided appearance options. +pub(crate) fn change_highlighted_label( + mut cmd: Commands, + asset_server: Res, + mut events: EventReader, + + highlighted: Query< + (&InspectLabel, &InspectLabelDisplayed), + With, + >, +) { + let ChangeHighlightedInspectLabelEvent { + entity, + spawn_params, + } = events.read().last().expect("At least one event present"); + + let Some((label, displayed)) = highlighted.get_single_or_none() else { + return; + }; + + cmd.entity(displayed.bg).despawn(); + cmd.entity(displayed.text).despawn(); + + let displayed = + spawn_label_bg_and_text(&mut cmd, &asset_server, label, spawn_params); + cmd.entity(*entity) + .add_child(displayed.bg) + .add_child(displayed.text) + .insert(displayed); +} + /// We want the player to know what would be interacted with if they clicked /// the interact button. /// @@ -211,8 +276,12 @@ pub(crate) fn highlight_what_would_be_interacted_with( cmd.entity(old_displayed.bg).despawn(); cmd.entity(old_displayed.text).despawn(); - let mut new_displayed = - spawn_label_bg_and_text(&mut cmd, &asset_server, label, false); + let mut new_displayed = spawn_label_bg_and_text( + &mut cmd, + &asset_server, + label, + &default(), + ); if !controls.pressed(&GlobalAction::Inspect) { new_displayed .schedule_hide(&mut begin_interpolation, highlighted); @@ -238,7 +307,7 @@ pub(crate) fn highlight_what_would_be_interacted_with( // never be interacted with // // the system [`interact`] assumes on this condition - .filter(|(_, label, _, _)| label.emit_event_on_interacted.is_some()) + .filter(|(_, label, ..)| label.emit_event_on_interacted.is_some()) .map(|(entity, label, displayed, transform)| { let distance = transform.translation().truncate().distance(player); (entity, label, displayed, distance) @@ -255,10 +324,9 @@ pub(crate) fn highlight_what_would_be_interacted_with( // 2. // - let highlighted_matches_closest = - highlighted.get_single_or_none().is_some_and( - |(highlighted_entity, _, _)| highlighted_entity == closest, - ); + let highlighted_matches_closest = highlighted + .get_single_or_none() + .is_some_and(|(highlighted_entity, ..)| highlighted_entity == closest); if highlighted_matches_closest { // nothing to do, already in the state we want return; @@ -271,8 +339,15 @@ pub(crate) fn highlight_what_would_be_interacted_with( cmd.entity(*text).despawn(); } - let displayed = - spawn_label_bg_and_text(&mut cmd, &asset_server, label, true); + let displayed = spawn_label_bg_and_text( + &mut cmd, + &asset_server, + label, + &SpawnLabelBgAndTextParams { + highlighted: true, + ..default() + }, + ); cmd.entity(closest) // Q: What if interpolation just finished in this frame and removed this // component? @@ -362,7 +437,7 @@ pub(crate) fn show_all_in_vicinity( &mut cmd, &asset_server, label, - false, + &default(), ); cmd.entity(entity) .add_child(displayed.bg) @@ -379,12 +454,22 @@ fn spawn_label_bg_and_text( cmd: &mut Commands, asset_server: &Res, label: &InspectLabel, - highlighted: bool, + SpawnLabelBgAndTextParams { + highlighted, + overwrite_font_color, + overwrite_display_text, + }: &SpawnLabelBgAndTextParams, ) -> InspectLabelDisplayed { trace!("Displaying label {}", label.display); let font_size = - label.category.font_zone() + if highlighted { 3.0 } else { 0.0 }; + label.category.font_zone() + if *highlighted { 3.0 } else { 0.0 }; + + let text_to_display = if let Some(text) = overwrite_display_text { + text.as_str() + } else { + label.display.as_ref() + }; // We set this to be the zindex of the bg and text. // This is a dirty hack that puts the label always in front of everything. @@ -394,14 +479,15 @@ fn spawn_label_bg_and_text( // this is easier than waiting for the text to be rendered and // then using the logical size, and the impression doesn't // matter for such a short text - let bg_box_width = font_size + font_size / 7.0 * label.display.len() as f32; + let bg_box_width = + font_size + font_size / 7.0 * text_to_display.len() as f32; let bg = cmd .spawn(InspectLabelBg) .insert(Name::new("InspectLabelBg")) .insert(SpriteBundle { transform: Transform::from_translation(Vec3::Z * Z_INDEX), sprite: Sprite { - color: HALF_TRANSPARENT * if highlighted { 1.5 } else { 1.0 }, + color: BG_COLOR * if *highlighted { 1.5 } else { 1.0 }, custom_size: Some(Vec2::new(bg_box_width, font_size / 2.0)), ..default() }, @@ -421,12 +507,13 @@ fn spawn_label_bg_and_text( .with_scale(Vec3::splat(1.0 / PIXEL_ZOOM as f32)), text: Text { sections: vec![TextSection::new( - label.display.clone(), + text_to_display, TextStyle { font: asset_server .load(common_assets::fonts::TINY_PIXEL1), font_size, - color: label.category.color(), + color: overwrite_font_color + .unwrap_or_else(|| label.category.color()), }, )], linebreak_behavior: bevy::text::BreakLineOn::NoWrap, @@ -509,7 +596,7 @@ impl InspectLabelDisplayed { cmd.entity(self.bg).remove::(); bgs.get_mut(self.bg) .expect("BG must exist if display exists") - .color = HALF_TRANSPARENT; + .color = BG_COLOR; cmd.entity(self.text).remove::(); texts diff --git a/scenes/building1_player_floor/src/actor.rs b/scenes/building1_player_floor/src/actor.rs index 2dc6b683..36492df7 100644 --- a/scenes/building1_player_floor/src/actor.rs +++ b/scenes/building1_player_floor/src/actor.rs @@ -1,12 +1,20 @@ //! Player and NPCs. use common_loading_screen::LoadingScreenSettings; -use common_story::dialog::DialogGraph; +use common_story::{ + dialog::DialogGraph, + emoji::{DisplayEmojiEvent, DisplayEmojiEventConsumer, EmojiKind}, +}; use common_visuals::camera::MainCamera; use main_game_lib::{ common_ext::QueryExt, cutscene::{self, in_cutscene}, hud::daybar::DayBar, + top_down::inspect_and_interact::{ + ChangeHighlightedInspectLabelEvent, + ChangeHighlightedInspectLabelEventConsumer, SpawnLabelBgAndTextParams, + ZoneToInspectLabelEntity, LIGHT_RED, + }, }; use top_down::{ actor::{emit_movement_events, movement_event_emitted}, @@ -24,7 +32,12 @@ impl bevy::app::Plugin for Plugin { fn build(&self, app: &mut App) { app.add_systems( Update, - (start_meditation_minigame, enter_the_elevator) + ( + start_meditation_minigame + .before(DisplayEmojiEventConsumer) + .before(ChangeHighlightedInspectLabelEventConsumer), + enter_the_elevator, + ) .run_if(on_event::()) .run_if(Building1PlayerFloor::in_running_state()) .run_if(not(in_cutscene())), @@ -44,9 +57,16 @@ impl bevy::app::Plugin for Plugin { fn start_meditation_minigame( mut cmd: Commands, mut action_events: EventReader, + mut emoji_events: EventWriter, + mut inspect_label_events: EventWriter, mut transition: ResMut, mut next_state: ResMut>, + zone_to_inspect_label_entity: Res< + ZoneToInspectLabelEntity, + >, daybar: Res, + + player: Query>, ) { let is_triggered = action_events.read().any(|action| { matches!(action, Building1PlayerFloorAction::StartMeditation) @@ -54,8 +74,33 @@ fn start_meditation_minigame( if is_triggered { if daybar.is_depleted() { - trace!("Cannot start meditation minigame, daybar is depleted."); - // TODO: https://github.com/porkbrain/dont-count-the-sheep/issues/126 + if let Some(entity) = zone_to_inspect_label_entity + .map + .get(&Building1PlayerFloorTileKind::MeditationZone) + .copied() + { + inspect_label_events.send(ChangeHighlightedInspectLabelEvent { + entity, + spawn_params: SpawnLabelBgAndTextParams { + highlighted: true, + overwrite_font_color: Some(LIGHT_RED), + // LOCALIZATION + overwrite_display_text: Some("(too tired)".to_string()), + }, + }); + } else { + error!("Cannot find meditation zone inspect label entity"); + } + + if let Some(on_parent) = player.get_single_or_none() { + emoji_events.send(DisplayEmojiEvent { + emoji: EmojiKind::Tired, + on_parent, + offset_for: common_story::Character::Winnie, + }); + } else { + error!("Cannot find player entity"); + } return; } @@ -85,6 +130,8 @@ fn enter_the_elevator( camera: Query>, points: Query<(&Name, &rscn::Point)>, ) { + use GlobalGameStateTransition::*; + let is_triggered = action_events.read().any(|action| { matches!(action, Building1PlayerFloorAction::EnterElevator) }); @@ -106,15 +153,10 @@ fn enter_the_elevator( elevator.single(), camera.single(), point_in_elevator, + // LOCALIZATION &[ - ( - GlobalGameStateTransition::Building1PlayerFloorToDowntown, - "go to downtown", - ), - ( - GlobalGameStateTransition::Building1PlayerFloorToBuilding1Basement1, - "go to basement", - ), + (Building1PlayerFloorToDowntown, "go to downtown"), + (Building1PlayerFloorToBuilding1Basement1, "go to basement"), ], ); } diff --git a/scenes/building1_player_floor/src/lib.rs b/scenes/building1_player_floor/src/lib.rs index bbdf3c88..c18a3d1b 100644 --- a/scenes/building1_player_floor/src/lib.rs +++ b/scenes/building1_player_floor/src/lib.rs @@ -86,6 +86,8 @@ pub enum Building1PlayerFloorTileKind { pub enum Building1PlayerFloorAction { EnterElevator, StartMeditation, + Sleep, + BrewTea, } pub fn add(app: &mut App) { diff --git a/scenes/downtown/src/actor.rs b/scenes/downtown/src/actor.rs deleted file mode 100644 index 9b459a33..00000000 --- a/scenes/downtown/src/actor.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Things that player can encounter in this scene. - -use crate::prelude::*; - -pub(crate) struct Plugin; - -impl bevy::app::Plugin for Plugin { - fn build(&self, _: &mut App) {} -} diff --git a/scenes/downtown/src/layout.rs b/scenes/downtown/src/layout.rs index 430eb799..f1119a0f 100644 --- a/scenes/downtown/src/layout.rs +++ b/scenes/downtown/src/layout.rs @@ -3,8 +3,13 @@ use bevy_grid_squared::sq; use common_loading_screen::{LoadingScreenSettings, LoadingScreenState}; use common_visuals::camera::render_layer; use main_game_lib::{ - cutscene::in_cutscene, hud::daybar::IncreaseDayBarEvent, - top_down::inspect_and_interact::ZoneToInspectLabelEntity, + cutscene::in_cutscene, + hud::daybar::{DayBar, DayBarDependent, IncreaseDayBarEvent}, + top_down::inspect_and_interact::{ + ChangeHighlightedInspectLabelEvent, + ChangeHighlightedInspectLabelEventConsumer, SpawnLabelBgAndTextParams, + ZoneToInspectLabelEntity, LIGHT_RED, + }, }; use rscn::{NodeName, TscnSpawner, TscnTree, TscnTreeHandle}; use strum::IntoEnumIterator; @@ -34,7 +39,10 @@ impl bevy::app::Plugin for Plugin { .add_systems(OnExit(Downtown::quitting()), despawn) .add_systems( Update, - (enter_building1, enter_mall) + ( + enter_building1, + enter_mall.before(ChangeHighlightedInspectLabelEventConsumer), + ) .run_if(on_event::()) .run_if(Downtown::in_running_state()) .run_if(not(in_cutscene())), @@ -219,15 +227,42 @@ fn enter_building1( fn enter_mall( mut cmd: Commands, mut action_events: EventReader, + mut inspect_label_events: EventWriter, mut transition: ResMut, mut next_state: ResMut>, mut next_loading_screen_state: ResMut>, + zone_to_inspect_label_entity: Res< + ZoneToInspectLabelEntity, + >, + daybar: Res, ) { let is_triggered = action_events .read() .any(|action| matches!(action, DowntownAction::EnterMall)); if is_triggered { + if !daybar.is_it_time_for(DayBarDependent::MallOpenHours) { + if let Some(entity) = zone_to_inspect_label_entity + .map + .get(&DowntownTileKind::MallEntrance) + .copied() + { + inspect_label_events.send(ChangeHighlightedInspectLabelEvent { + entity, + spawn_params: SpawnLabelBgAndTextParams { + highlighted: true, + overwrite_font_color: Some(LIGHT_RED), + // LOCALIZATION + overwrite_display_text: Some("(closed)".to_string()), + }, + }); + } else { + error!("Cannot find mall entrance zone inspect label entity"); + } + + return; + } + cmd.insert_resource(LoadingScreenSettings { atlas: Some(common_loading_screen::LoadingScreenAtlas::random()), stare_at_loading_screen_for_at_least: Some(from_millis(1000)), diff --git a/scenes/downtown/src/lib.rs b/scenes/downtown/src/lib.rs index f799e9a1..7c72241d 100644 --- a/scenes/downtown/src/lib.rs +++ b/scenes/downtown/src/lib.rs @@ -2,7 +2,6 @@ #![allow(clippy::assertions_on_constants)] #![allow(clippy::type_complexity)] -mod actor; mod autogen; mod layout; mod prelude; @@ -88,7 +87,7 @@ pub fn add(app: &mut App) { debug!("Adding plugins"); - app.add_plugins((layout::Plugin, actor::Plugin)); + app.add_plugins(layout::Plugin); debug!("Adding game loop"); diff --git a/scenes/meditation/src/background.rs b/scenes/meditation/src/background.rs index d01bb0bd..e617fbec 100644 --- a/scenes/meditation/src/background.rs +++ b/scenes/meditation/src/background.rs @@ -92,7 +92,7 @@ fn spawn_shooting_star( ) { let animation = AtlasAnimation { // we schedule it at random - on_last_frame: AtlasAnimationEnd::RemoveTimerAndHide, + on_last_frame: AtlasAnimationEnd::RemoveTimerAndHideAndReset, last: SHOOTING_STAR_FRAMES - 1, ..default() }; diff --git a/scenes/meditation/src/hoshi.rs b/scenes/meditation/src/hoshi.rs index c910b8c8..dc38ce4a 100644 --- a/scenes/meditation/src/hoshi.rs +++ b/scenes/meditation/src/hoshi.rs @@ -158,13 +158,7 @@ fn spawn( HoshiEntity, RenderLayers::layer(render_layer::OBJ), AtlasAnimation { - on_last_frame: AtlasAnimationEnd::run(Box::new( - |entity, atlas, visibility, commands| { - *visibility = Visibility::Hidden; - commands.entity(entity).remove::(); - atlas.index = 0; - }, - )), + on_last_frame: AtlasAnimationEnd::RemoveTimerAndHideAndReset, last: SPARK_FRAMES - 1, ..default() }, diff --git a/scenes/meditation/src/polpos/effects.rs b/scenes/meditation/src/polpos/effects.rs index c37d3c73..3a6b9e54 100644 --- a/scenes/meditation/src/polpos/effects.rs +++ b/scenes/meditation/src/polpos/effects.rs @@ -108,14 +108,14 @@ pub(crate) mod black_hole { // the reason why black hole does not despawn while game is paused is // that we don't run the system while game is paused - let on_last_frame = AtlasAnimationEnd::run(Box::new( - move |entity, _atlas, _visibility, commands| { + let on_last_frame = + AtlasAnimationEnd::run(Box::new(move |cmd, entity, _, _| { debug!("Despawning black hole ({entity:?})"); - commands.entity(entity).despawn_recursive(); + cmd.entity(entity).despawn_recursive(); // remove gravity influence - commands.add(move |world: &mut World| { + cmd.add(move |world: &mut World| { world.send_event( PoissonsEquationUpdateEvent::::new( -BLACK_HOLE_GRAVITY, @@ -123,8 +123,7 @@ pub(crate) mod black_hole { ), ); }); - }, - )); + })); cmd.spawn(( BlackHole, diff --git a/scenes/meditation/src/polpos/react.rs b/scenes/meditation/src/polpos/react.rs index ba514609..804593ad 100644 --- a/scenes/meditation/src/polpos/react.rs +++ b/scenes/meditation/src/polpos/react.rs @@ -205,7 +205,7 @@ pub(super) fn to_environment( let static_entity = cmd .spawn(( AtlasAnimation { - on_last_frame: AtlasAnimationEnd::Loop, + on_last_frame: AtlasAnimationEnd::LoopIndefinitely, first: first_frame, last: STATIC_ATLAS_FRAMES - 1, ..default() diff --git a/wiki/src/SUMMARY.md b/wiki/src/SUMMARY.md index c5966b47..0e226887 100644 --- a/wiki/src/SUMMARY.md +++ b/wiki/src/SUMMARY.md @@ -6,6 +6,7 @@ - [Traits](traits.md) - [Top down scenes](top_down.md) - [Inspect ability](ability_to_inspect.md) + - [Mall scene](scene_mall.md) - [Devtools](devtools.md) - [Godot](devtools_godot.md) - [Dialog](devtools_dialog.md) diff --git a/wiki/src/ability_to_inspect.md b/wiki/src/ability_to_inspect.md index 03511ab7..efbe844e 100644 --- a/wiki/src/ability_to_inspect.md +++ b/wiki/src/ability_to_inspect.md @@ -50,6 +50,7 @@ A natural solution is to gamify the inspection mode, making it feel less like a Their purpose would be to alter the inspection mode in some way. - [x] We highlight the object that is the closest to you. That would be the object you would interact with if you pressed the interaction button. +- [x] If an interaction is not possible, for example because the day bar is depleted or not in the right time range, the label is shortly highlighted in red font color and announce the reason. - [ ] To avoid repositioning you could also change the highlighted object with directional input. Pressing up would change selection to the next object closest to the highlighted object in the upward direction. Allowing you to change the highlighted object you would interact with would help to avoid a bug where you couldn't interact with an object because it was behind another object. diff --git a/wiki/src/daybar.md b/wiki/src/daybar.md index 42878764..e1ebdaca 100644 --- a/wiki/src/daybar.md +++ b/wiki/src/daybar.md @@ -31,8 +31,8 @@ NPCs within the game adhere to their own schedules, often influenced by the time Since time progression is contingent upon player actions, NPC behavior dynamically adjusts based on the player's activities throughout the day. - [ ] There would be various instruments that could change how much time would be consumed by specific actions. - Instruments could include [traits][traits], consumables, events, quests, etc. + Instruments could include [traits](traits.md), consumables, events, quests, etc. - +Shops and other establishments may have varying hours of operation: -[traits]: traits.md +- The [Mall scene](scene_mall.md) diff --git a/wiki/src/scene_mall.md b/wiki/src/scene_mall.md new file mode 100644 index 00000000..e932d631 --- /dev/null +++ b/wiki/src/scene_mall.md @@ -0,0 +1,3 @@ +TODO + +TODO: Opening hours