/*
* This file is part of laurelin_client
* Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
*
* Licensed under GPL-3.0-only.
* See LICENSE for licensing information.
*/
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use crate::{
api::game::{Action, Command, Game},
game_status::{Card, CardAction, PlayerState},
plugins::GameActionCreateCallEvent,
seed_gen, AppState, Global,
};
use super::{GameData, RefreshGameEvent};
mod state_button;
pub struct GameUIPlugin;
impl Plugin for GameUIPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(state_button::StateButtonPlugin)
.add_system(dev_details_ui.run_if(in_state(AppState::InGame)))
.add_system(setup_details.in_schedule(OnEnter(AppState::InGame)))
.add_systems((
update_game_state_text,
update_currency_text,
update_deck_text,
update_discard_text,
update_plays_text,
update_buys_text,
update_vp_text,
));
}
}
#[derive(Component)]
struct GameStateText;
#[derive(Component)]
struct CurrencyText;
#[derive(Component)]
struct DeckText;
#[derive(Component)]
struct DiscardText;
#[derive(Component)]
struct PlaysText;
#[derive(Component)]
struct BuysText;
#[derive(Component)]
struct VPText;
fn setup_details(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/FiraMono-Bold.ttf");
let font_size = 40.;
let text_style = TextStyle {
font,
font_size,
color: Color::WHITE,
};
// game state
commands.spawn((
TextBundle::from_sections([
TextSection::new("State: ", text_style.clone()),
TextSection::from_style(text_style.clone()),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: Val::Px(20.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
GameStateText,
));
// plays
commands.spawn((
TextBundle::from_sections([
TextSection::new("Plays: ", text_style.clone()),
TextSection::from_style(text_style.clone()),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(220.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
PlaysText,
));
// buys
commands.spawn((
TextBundle::from_sections([
TextSection::new("Buys: ", text_style.clone()),
TextSection::from_style(text_style.clone()),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(180.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
BuysText,
));
// currency
commands.spawn((
TextBundle::from_sections([
TextSection::new("Currency: ", text_style.clone()),
TextSection::from_style(TextStyle {
color: Color::GOLD,
..text_style.clone()
}),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(140.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
CurrencyText,
));
// deck
commands.spawn((
TextBundle::from_sections([
TextSection::new("Deck: ", text_style.clone()),
TextSection::from_style(text_style.clone()),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(100.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
DeckText,
));
// discard
commands.spawn((
TextBundle::from_sections([
TextSection::new("Discard: ", text_style.clone()),
TextSection::from_style(text_style.clone()),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(60.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
DiscardText,
));
// vp
commands.spawn((
TextBundle::from_sections([
TextSection::new("VP: ", text_style.clone()),
TextSection::from_style(text_style),
])
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
bottom: Val::Px(20.),
left: Val::Px(20.),
..Default::default()
},
..Default::default()
}),
VPText,
));
}
fn update_game_state_text(
mut text_query: Query<&mut Text, With<GameStateText>>,
game_data: Res<GameData>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.values().find(|p| p.state != PlayerState::Idle) else {
return;
};
text.sections[1].value = format!("{} - {:?}", player.display_name, player.state);
}
}
fn update_currency_text(
mut text_query: Query<&mut Text, With<CurrencyText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
text.sections[1].value = player.currency.to_string();
}
}
fn update_deck_text(
mut text_query: Query<&mut Text, With<DeckText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
text.sections[1].value = player.deck.len().to_string();
}
}
fn update_discard_text(
mut text_query: Query<&mut Text, With<DiscardText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
text.sections[1].value = player.discard.len().to_string();
}
}
fn update_plays_text(
mut text_query: Query<&mut Text, With<PlaysText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
if player.plays == 0 {
text.sections[1].style.color = Color::RED;
} else {
text.sections[1].style.color = Color::GREEN;
}
text.sections[1].value = player.plays.to_string();
}
}
fn update_buys_text(
mut text_query: Query<&mut Text, With<BuysText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
if player.buys == 0 {
text.sections[1].style.color = Color::RED;
} else {
text.sections[1].style.color = Color::GREEN;
}
text.sections[1].value = player.buys.to_string();
}
}
fn update_vp_text(
mut text_query: Query<&mut Text, With<VPText>>,
game_data: Res<GameData>,
global: Res<Global>,
) {
for mut text in &mut text_query {
let Some(status) = &game_data.game_status else {
return;
};
let Some(player) = status.players.get(&global.user.as_ref().unwrap().id) else {
return;
};
text.sections[1].value = player.vp.to_string();
}
}
pub fn dev_details_ui(
mut contexts: EguiContexts,
global: Res<Global>,
game_data: Res<GameData>,
mut create_action_ev_w: EventWriter<GameActionCreateCallEvent>,
mut rg_ev_w: EventWriter<RefreshGameEvent>,
) {
egui::Window::new("Game Details").show(contexts.ctx_mut(), |ui| {
let Some(game) = &game_data.game else {
// early return if game is None
return;
};
let Some(status) = &game_data.game_status else {
// early return if game_status is None
return;
};
ui.add_enabled_ui(!game_data.locked, |ui| {
#[allow(clippy::collapsible_if)]
if status.actions.is_empty() && game.host_id == global.user.as_ref().unwrap().id {
if ui.button("Init Game").clicked() {
// NOTE/FIXME: hardcoded game init
hardcoded_init(game, &mut create_action_ev_w);
rg_ev_w.send(RefreshGameEvent);
}
}
if ui.button("Force Refresh").clicked() {
rg_ev_w.send(RefreshGameEvent);
}
});
ui.separator();
egui::CollapsingHeader::new("Game")
.default_open(true)
.show(ui, |ui| {
ui.label(format!("Host: {}", game.host.as_ref().unwrap().username));
ui.label(format!("Guest: {}", game.guest.as_ref().unwrap().username));
ui.label(format!("State: {:?}", game.state));
});
egui::CollapsingHeader::new("Supply Piles")
.default_open(false)
.show(ui, |ui| {
for pile in &status.supply_piles {
egui::CollapsingHeader::new(&pile.card.name)
.default_open(true)
.show(ui, |ui| {
ui.label(format!("Amount: {}", pile.amount));
});
}
});
egui::CollapsingHeader::new("Log")
.default_open(false)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.max_width(f32::INFINITY)
.show(ui, |ui| {
for (i, action) in status.actions.iter().enumerate() {
egui::CollapsingHeader::new(format!("{}", i))
.default_open(false)
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(
&mut serde_json::to_string_pretty(action).unwrap(),
)
.code_editor()
.interactive(false)
.desired_width(f32::INFINITY),
);
});
}
});
});
});
}
fn hardcoded_init(game: &Game, create_action_ev_w: &mut EventWriter<GameActionCreateCallEvent>) {
// first, piles
create_action_ev_w.send(GameActionCreateCallEvent {
action: Action::new(
&game.id,
&game.host_id,
&game.host_id,
&Command::InitSupplyPile {
card: Card {
name: "Narcissistic Cannibal".to_string(),
short_details: vec![],
long_details: vec![],
cost: 4,
actions: vec![],
},
amount: 10,
},
seed_gen!(),
),
});
create_action_ev_w.send(GameActionCreateCallEvent {
action: Action::new(
&game.id,
&game.host_id,
&game.host_id,
&Command::InitSupplyPile {
card: Card {
name: "Gib Möney".to_string(),
short_details: vec!["Roll 1d6 for currency".to_string()],
long_details: vec![],
cost: 0,
actions: vec![CardAction {
command: Command::RollForCurrency { min: 1, max: 6 },
}],
},
amount: 10,
},
seed_gen!(),
),
});
create_action_ev_w.send(GameActionCreateCallEvent {
action: Action::new(
&game.id,
&game.host_id,
&game.host_id,
&Command::InitSupplyPile {
card: Card {
name: "Test Card 3".to_string(),
short_details: vec![],
long_details: vec![],
cost: 4,
actions: vec![],
},
amount: 10,
},
seed_gen!(),
),
});
create_action_ev_w.send(GameActionCreateCallEvent {
action: Action::new(
&game.id,
&game.host_id,
&game.host_id,
&Command::InitSupplyPile {
card: Card {
name: "Test Card 4".to_string(),
short_details: vec![],
long_details: vec![],
cost: 2,
actions: vec![],
},
amount: 10,
},
seed_gen!(),
),
});
// second, set a player to the action phase, to start the game
create_action_ev_w.send(GameActionCreateCallEvent {
action: Action::new(
&game.id,
&game.host_id,
&game.host_id,
&Command::StartTurn {},
seed_gen!(),
),
});
}