use super::{ACHIEVEMENTS, Stats, Unlocked};
use crate::app::i18n::fill;
use crate::app::palette;
use crate::app::settings::GameSettings;
use bevy::prelude::*;
#[derive(Component)]
pub struct Toast {
age: f32,
}
const TOAST_LIFE: f32 = 4.0;
const TOAST_TOP: f32 = 52.0;
const TOAST_GAP: f32 = 6.0;
const TOAST_GUESS: f32 = 64.0;
pub(super) fn spawn_toast(commands: &mut Commands, name: &str, desc: &str) {
commands
.spawn((
Toast { age: 0.0 },
GlobalZIndex(30),
Node {
position_type: PositionType::Absolute,
top: Val::Px(52.0),
right: Val::Px(12.0),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(2.0),
padding: UiRect::axes(Val::Px(14.0), Val::Px(10.0)),
..default()
},
BackgroundColor(Color::srgb(0.1, 0.12, 0.16)),
))
.with_children(|toast| {
toast.spawn((
Text::new(format!("* {name}")),
TextFont {
font_size: FontSize::Px(19.0),
..default()
},
TextColor(palette::GOLD),
));
toast.spawn((
Text::new(desc.to_string()),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextColor(palette::PARCHMENT.with_alpha(0.9)),
));
});
}
pub fn update_toasts(
time: Res<Time>,
mut commands: Commands,
mut toasts: Query<(Entity, &mut Toast, &mut Node, &ComputedNode)>,
) {
let mut top = TOAST_TOP;
for (entity, mut toast, mut node, computed) in &mut toasts {
toast.age += time.delta_secs();
if toast.age >= TOAST_LIFE {
commands.entity(entity).despawn();
continue;
}
let target = Val::Px(top);
if node.top != target {
node.top = target;
}
let height = computed.size().y * computed.inverse_scale_factor();
top += if height > 0.0 { height } else { TOAST_GUESS } + TOAST_GAP;
}
}
#[derive(Component)]
pub struct AchievementsUi;
fn unlock_mark(done: bool) -> (Node, BackgroundColor, BorderColor) {
(
Node {
width: Val::Px(14.0),
height: Val::Px(14.0),
flex_shrink: 0.0,
border: UiRect::all(Val::Px(2.0)),
border_radius: BorderRadius::all(Val::Px(7.0)),
..default()
},
BackgroundColor(if done { palette::GOLD } else { Color::NONE }),
BorderColor::all(if done {
palette::GOLD
} else {
Color::srgba(1.0, 1.0, 1.0, 0.22)
}),
)
}
const COLS: usize = 2;
fn spawn_trophy(
column: &mut ChildSpawnerCommands,
tr: &crate::app::i18n::Tr,
index: usize,
done: bool,
now: u32,
goal: u32,
) {
let ink = if done {
palette::SELECTED_ROW
} else {
palette::IDLE_ROW
};
column
.spawn((
Node {
width: Val::Px(580.0),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(3.0),
padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)),
border_radius: BorderRadius::all(Val::Px(6.0)),
..default()
},
BackgroundColor(if done {
palette::GOLD.with_alpha(0.10)
} else {
Color::NONE
}),
))
.with_children(|tile| {
tile.spawn(Node {
column_gap: Val::Px(8.0),
align_items: AlignItems::Center,
width: Val::Percent(100.0),
..default()
})
.with_children(|line| {
line.spawn(unlock_mark(done));
line.spawn((
Node {
width: Val::Px(204.0),
flex_shrink: 0.0,
overflow: Overflow::clip_x(),
..default()
},
children![(
Text::new(tr.ach_names[index].to_string()),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextLayout::no_wrap(),
TextColor(ink),
)],
));
line.spawn((
Node {
flex_grow: 1.0,
flex_basis: Val::Px(0.0),
min_width: Val::Px(0.0),
overflow: Overflow::clip_x(),
..default()
},
children![(
Text::new(tr.ach_descs[index].to_string()),
TextFont {
font_size: FontSize::Px(12.5),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(if done { 0.7 } else { 0.45 })),
)],
));
line.spawn((
Node {
flex_shrink: 0.0,
..default()
},
children![(
Text::new(format!("{now}/{goal}")),
TextFont {
font_size: FontSize::Px(12.5),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(if done { 0.7 } else { 0.4 })),
)],
));
});
let filled = if goal == 0 {
100.0
} else {
(now.min(goal) as f32 / goal as f32) * 100.0
};
tile.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Px(3.0),
border_radius: BorderRadius::all(Val::Px(2.0)),
..default()
},
BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.08)),
children![(
Node {
width: Val::Percent(filled),
height: Val::Percent(100.0),
border_radius: BorderRadius::all(Val::Px(2.0)),
..default()
},
BackgroundColor(if done {
palette::GOLD
} else {
palette::GOLD.with_alpha(0.45)
}),
)],
));
});
}
pub fn enter_achievements(
mut commands: Commands,
settings: Res<GameSettings>,
stats: Res<Stats>,
unlocked: Res<Unlocked>,
) {
let tr = settings.tr();
let per_column = ACHIEVEMENTS.len().div_ceil(COLS);
commands
.spawn((
AchievementsUi,
Node {
position_type: PositionType::Absolute,
top: Val::Px(50.0),
bottom: Val::Px(50.0),
left: Val::Px(0.0),
right: Val::Px(0.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
))
.with_children(|wrap| {
wrap.spawn((
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: Val::Px(8.0),
padding: UiRect::axes(Val::Px(20.0), Val::Px(14.0)),
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(16.0)),
..default()
},
BackgroundColor(palette::CARD_FILL),
BorderColor::all(palette::CARD_EDGE),
))
.with_children(|card| {
let earned = ACHIEVEMENTS
.iter()
.filter(|a| unlocked.0.contains(a.id))
.count();
card.spawn((
Text::new(fill(
tr.ach_progress,
&[
("a", &earned.to_string()),
("b", &ACHIEVEMENTS.len().to_string()),
],
)),
TextFont {
font_size: FontSize::Px(18.0),
..default()
},
TextColor(palette::PARCHMENT.with_alpha(0.75)),
));
card.spawn(Node {
column_gap: Val::Px(16.0),
..default()
})
.with_children(|grid| {
for chunk in 0..COLS {
grid.spawn(Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(2.0),
..default()
})
.with_children(|column| {
let first = chunk * per_column;
let last = (first + per_column).min(ACHIEVEMENTS.len());
for (index, achievement) in
ACHIEVEMENTS.iter().enumerate().take(last).skip(first)
{
let done = unlocked.0.contains(achievement.id);
let (now, goal) = achievement.progress(&stats);
spawn_trophy(column, tr, index, done, now, goal);
}
});
}
});
let footer = fill(
tr.stats_footer,
&[
("banked", &stats.banked.to_string()),
("golden", &stats.golden.to_string()),
("wins", &stats.wins.to_string()),
("rounds", &stats.rounds.to_string()),
("puzzles", &stats.puzzles.to_string()),
],
);
card.spawn((
Text::new(footer),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextColor(palette::PARCHMENT.with_alpha(0.6)),
));
});
});
}
pub fn achievements_input(
keys: Res<ButtonInput<KeyCode>>,
mut next_screen: ResMut<NextState<crate::app::Screen>>,
) {
if keys.just_pressed(KeyCode::Escape) || keys.just_pressed(KeyCode::Enter) {
next_screen.set(crate::app::Screen::Menu);
}
}