use super::{LOG_TOP, card};
use crate::app::i18n::fill;
use crate::app::menu_ui;
use crate::app::palette;
use crate::app::settings::GameSettings;
use crate::app::sim_events::SimEvent;
use crate::sim::CrabKind;
use bevy::prelude::*;
use std::collections::VecDeque;
#[derive(Component)]
pub struct LogLine(pub usize);
pub struct LogEntry {
pub text: String,
pub color: Color,
pub count: u32,
}
impl LogEntry {
pub fn render_into(&self, out: &mut String) {
use std::fmt::Write;
out.clear();
out.push_str(&self.text);
if self.count > 1 {
let _ = write!(out, " ×{}", self.count);
}
}
#[cfg(test)]
pub fn rendered(&self) -> String {
let mut out = String::new();
self.render_into(&mut out);
out
}
}
#[derive(Resource, Default)]
pub struct EventLog(pub VecDeque<LogEntry>);
impl EventLog {
pub fn push(&mut self, text: String, color: Color) {
if let Some(index) = self.0.iter().position(|entry| entry.text == text) {
let mut entry = self.0.remove(index).expect("index came from the deque");
entry.count += 1;
self.0.push_front(entry);
return;
}
self.0.push_front(LogEntry {
text,
color,
count: 1,
});
self.0.truncate(LOG_LINES);
}
}
const LOG_LINES: usize = 9;
pub fn collect_chat(mut online: ResMut<crate::app::net::Online>, mut log: ResMut<EventLog>) {
let Some(session) = &mut online.0 else {
return;
};
for (who, line) in session.heard.drain(..) {
let said = match who.is_empty() {
true => line,
false => format!("{who}: {line}"),
};
log.push(said, palette::IDLE_ROW);
}
}
pub fn collect_log(
mut events: MessageReader<SimEvent>,
settings: Res<GameSettings>,
names: Res<crate::app::SeatNames>,
mut log: ResMut<EventLog>,
) {
let tr = settings.tr();
let seat_label = |seat: u8| names.label(tr, seat);
for event in events.read() {
let line = match event {
SimEvent::CastleRaided { owner, lost, .. } => Some((
fill(
tr.log_raid,
&[("p", &seat_label(*owner)), ("n", &lost.to_string())],
),
palette::INK_RAID,
)),
SimEvent::CrabBanked {
owner,
points,
kind: CrabKind::Golden,
..
} => Some((
fill(
tr.log_golden,
&[("p", &seat_label(*owner)), ("n", &format!("{points:+}"))],
),
palette::GOLD,
)),
SimEvent::LureStarted { owner } => Some((
fill(tr.log_lure, &[("p", &seat_label(*owner))]),
palette::INK_LURE,
)),
SimEvent::TierUp { owner } => Some((
fill(tr.log_tier, &[("p", &seat_label(*owner))]),
palette::player_color(*owner).lighter(0.15),
)),
SimEvent::TideEventFired { event, .. } => Some((
crate::app::hud::event_name(tr, *event).to_string(),
palette::INK_TIDE,
)),
SimEvent::WheelLocked { owner } => Some((
fill(tr.log_wheel_locked, &[("p", &seat_label(*owner))]),
palette::INK_TIDE,
)),
SimEvent::GullArrived => Some((tr.log_gull.to_string(), palette::GULL_INK)),
SimEvent::SurgeStarted => Some((tr.log_surge.to_string(), palette::INK_SURGE)),
SimEvent::GoldenCalled { .. } => Some((tr.log_golden_call.to_string(), palette::GOLD)),
SimEvent::GoldenOut { .. } => Some((tr.log_golden_out.to_string(), palette::GOLD)),
SimEvent::CrabBanked { .. }
| SimEvent::CrabEaten { .. }
| SimEvent::CrabSpawned { .. }
| SimEvent::GullTookOff
| SimEvent::GullLanded { .. }
| SimEvent::GullShooed { .. }
| SimEvent::GullFlewOff { .. }
| SimEvent::SignpostPlaced { .. }
| SimEvent::SignpostRemoved { .. }
| SimEvent::SignpostEvicted { .. }
| SimEvent::RoundEnded => None,
};
if let Some((text, color)) = line {
log.push(text, color);
}
}
}
pub fn update_log(
log: Res<EventLog>,
mut lines: Query<(&LogLine, &mut Text, &mut TextColor)>,
mut value: Local<String>,
) {
for (line, mut text, mut color) in &mut lines {
let target = match log.0.get(line.0) {
Some(entry) => {
let fade = 1.0 - line.0 as f32 * 0.09;
entry.render_into(&mut value);
entry.color.with_alpha(fade)
}
None => {
value.clear();
Color::NONE
}
};
menu_ui::set_text(&mut text, &value);
menu_ui::set_color(&mut color, target);
}
}
pub(super) fn spawn_feed(root: &mut ChildSpawnerCommands) {
let (mut node, edge, fill, shadow) = card(LOG_TOP, None);
node.flex_direction = FlexDirection::Column;
node.align_items = AlignItems::default();
node.justify_content = JustifyContent::default();
node.row_gap = Val::Px(6.0);
node.padding = UiRect::axes(Val::Px(12.0), Val::Px(10.0));
node.overflow = Overflow::clip();
root.spawn((node, edge, fill, shadow))
.with_children(|list| {
for index in 0..LOG_LINES {
list.spawn((
LogLine(index),
Text::new(""),
TextFont {
font_size: FontSize::Px(menu_ui::type_scale::BODY),
..default()
},
TextColor(Color::NONE),
));
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repeated_lines_merge_with_a_counter() {
let mut log = EventLog::default();
for _ in 0..9 {
log.push("a gull swoops in".into(), Color::WHITE);
}
assert_eq!(log.0.len(), 1);
assert_eq!(log.0[0].rendered(), "a gull swoops in ×9");
log.push("P1 raided! -4".into(), Color::WHITE);
assert_eq!(log.0[0].rendered(), "P1 raided! -4");
log.push("a gull swoops in".into(), Color::WHITE);
assert_eq!(log.0.len(), 2, "the gull line was bumped, not duplicated");
assert_eq!(log.0[0].rendered(), "a gull swoops in ×10");
}
#[test]
fn distinct_lines_scroll_and_stay_bounded() {
let mut log = EventLog::default();
for n in 0..LOG_LINES + 5 {
log.push(format!("line {n}"), Color::WHITE);
}
assert_eq!(log.0.len(), LOG_LINES);
assert_eq!(log.0[0].rendered(), format!("line {}", LOG_LINES + 4));
}
}