/* * This file is part of laurelin_client * Copyright (C) 2023 Jonni Liljamo * * 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, AppState, Global, }; use super::{GameData, RefreshGameEvent}; pub mod log; mod state_button; pub struct GameUIPlugin; impl Plugin for GameUIPlugin { fn build(&self, app: &mut App) { app.add_plugin(log::LogPlugin) .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) { 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>, game_data: Res, ) { 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>, game_data: Res, global: Res, ) { 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>, game_data: Res, global: Res, ) { 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>, game_data: Res, global: Res, ) { 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>, game_data: Res, global: Res, ) { 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>, game_data: Res, global: Res, ) { 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>, game_data: Res, global: Res, ) { 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, game_data: Res, mut create_action_ev_w: EventWriter, mut rg_ev_w: EventWriter, ) { egui::Window::new("Game Details").title_bar(false).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)); }); }); } fn hardcoded_init(game: &Game, create_action_ev_w: &mut EventWriter) { // 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, }, None, ), }); 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 { amount: 1, sides: 6 }, }], }, amount: 10, }, None, ), }); 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, }, None, ), }); 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, }, None, ), }); // 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 {}, None, ), }); }