trictrac/client/src/main.rs

333 lines
10 KiB
Rust
Raw Normal View History

2023-10-30 14:25:05 +01:00
use std::{net::UdpSocket, time::SystemTime};
2023-10-29 20:49:01 +01:00
2023-11-01 14:20:34 +01:00
use renet::transport::{NetcodeClientTransport, NetcodeTransportError, NETCODE_USER_DATA_BYTES};
2024-01-27 19:11:23 +01:00
use store::{GameEvent, GameState};
2023-11-01 14:20:34 +01:00
2024-01-27 19:11:23 +01:00
use bevy::{prelude::*};
2023-11-01 14:20:34 +01:00
use bevy::window::PrimaryWindow;
2023-10-31 16:29:05 +01:00
use bevy_renet::{
renet::{transport::ClientAuthentication, ConnectionConfig, RenetClient},
2023-11-01 20:02:53 +01:00
transport::{client_connected, NetcodeClientPlugin},
2023-10-31 16:29:05 +01:00
RenetClientPlugin,
2023-10-29 20:49:01 +01:00
};
2023-11-01 14:20:34 +01:00
2023-11-01 20:02:53 +01:00
#[derive(Debug, Resource)]
struct CurrentClientId(u64);
2023-11-01 14:20:34 +01:00
#[derive(Resource)]
struct BevyGameState(GameState);
impl Default for BevyGameState {
fn default() -> Self {
Self {
2023-11-01 20:02:53 +01:00
0: GameState::default(),
}
}
}
#[derive(Resource, Deref, DerefMut)]
struct GameUIState {
selected_tile: Option<usize>,
}
impl Default for GameUIState {
fn default() -> Self {
Self {
selected_tile: None,
2023-11-01 14:20:34 +01:00
}
}
}
#[derive(Event)]
struct BevyGameEvent(GameEvent);
2023-10-29 20:49:01 +01:00
// This id needs to be the same as the server is using
const PROTOCOL_ID: u64 = 2878;
2022-12-01 18:04:03 +01:00
fn main() {
2023-10-29 20:49:01 +01:00
// Get username from stdin args
let args = std::env::args().collect::<Vec<String>>();
let username = &args[1];
2023-11-01 20:02:53 +01:00
let (client, transport, client_id) = new_renet_client(&username).unwrap();
2023-10-29 20:49:01 +01:00
App::new()
// Lets add a nice dark grey background color
.insert_resource(ClearColor(Color::hex("282828").unwrap()))
2023-10-30 14:25:05 +01:00
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
// Adding the username to the window title makes debugging a whole lot easier.
title: format!("TricTrac <{}>", username),
2023-10-31 17:37:02 +01:00
resolution: (1080.0, 1080.0).into(),
2023-10-30 14:25:05 +01:00
..default()
}),
..default()
}))
2023-11-01 14:20:34 +01:00
// Add our game state and register GameEvent as a bevy event
.insert_resource(BevyGameState::default())
2023-11-01 20:02:53 +01:00
.insert_resource(GameUIState::default())
2023-11-01 14:20:34 +01:00
.add_event::<BevyGameEvent>()
2023-10-29 20:49:01 +01:00
// Renet setup
2023-10-30 14:25:05 +01:00
.add_plugins(RenetClientPlugin)
2023-10-31 16:29:05 +01:00
.add_plugins(NetcodeClientPlugin)
2023-10-30 14:25:05 +01:00
.insert_resource(client)
2023-10-31 14:32:21 +01:00
.insert_resource(transport)
2023-11-01 20:02:53 +01:00
.insert_resource(CurrentClientId(client_id))
2023-10-31 17:37:02 +01:00
.add_systems(Startup, setup)
2023-11-05 17:14:58 +01:00
.add_systems(Update, (update_waiting_text, input, update_board, panic_on_error_system))
2023-11-01 20:02:53 +01:00
.add_systems(
PostUpdate,
receive_events_from_server.run_if(client_connected()),
)
2023-10-29 20:49:01 +01:00
.run();
}
2023-10-31 17:37:02 +01:00
////////// COMPONENTS //////////
#[derive(Component)]
struct UIRoot;
#[derive(Component)]
struct WaitingText;
2023-11-05 17:14:58 +01:00
#[derive(Component)]
struct Board {
squares: [Square; 26]
}
impl Default for Board {
fn default() -> Self {
Self {
squares: [Square { count: 0, color: None, position: 0}; 26]
}
}
}
impl Board {
fn square_at(&self, position: usize) -> Square {
self.squares[position]
}
}
#[derive(Component, Clone, Copy)]
struct Square {
count: usize,
color: Option<bool>,
position: usize,
}
2023-10-31 17:37:02 +01:00
////////// UPDATE SYSTEMS //////////
2023-11-05 17:14:58 +01:00
fn update_board(
mut commands: Commands,
game_state: Res<BevyGameState>,
mut game_events: EventReader<BevyGameEvent>,
asset_server: Res<AssetServer>,
) {
for event in game_events.iter() {
match event.0 {
2024-01-27 19:11:23 +01:00
GameEvent::Move { player_id, from: _, to } => {
2023-11-05 17:14:58 +01:00
// backgammon postions, TODO : dépend de player_id
let (x, y) = if to < 13 { (13 - to, 1) } else { (to - 13, 0)};
let texture =
asset_server.load(match game_state.0.players[&player_id].color {
store::Color::Black => "tac.png",
store::Color::White => "tic.png",
});
info!("spawning tictac sprite");
commands.spawn(SpriteBundle {
transform: Transform::from_xyz(
83.0 * (x as f32 - 1.0),
-30.0 + 540.0 * (y as f32 - 1.0),
0.0,
),
sprite: Sprite {
custom_size: Some(Vec2::new(83.0, 83.0)),
..default()
},
texture: texture.into(),
..default()
});
}
_ => {}
}
}
}
2023-10-31 17:37:02 +01:00
fn update_waiting_text(mut text_query: Query<&mut Text, With<WaitingText>>, time: Res<Time>) {
if let Ok(mut text) = text_query.get_single_mut() {
let num_dots = (time.elapsed_seconds() as usize % 3) + 1;
text.sections[0].value = format!(
"Waiting for an opponent{}{}",
".".repeat(num_dots as usize),
// Pad with spaces to avoid text changing width and dancing all around the screen 🕺
" ".repeat(3 - num_dots as usize)
);
}
}
2023-11-01 14:20:34 +01:00
fn input(
primary_query: Query<&Window, With<PrimaryWindow>>,
// windows: Res<Windows>,
input: Res<Input<MouseButton>>,
game_state: Res<BevyGameState>,
2023-11-01 20:02:53 +01:00
mut game_ui_state: ResMut<GameUIState>,
mut client: ResMut<RenetClient>,
client_id: Res<CurrentClientId>,
2023-11-01 14:20:34 +01:00
) {
2023-11-01 20:02:53 +01:00
// We only want to handle inputs once we are ingame
if game_state.0.stage != store::Stage::InGame {
return;
}
2023-11-01 14:20:34 +01:00
let window = primary_query.get_single().unwrap();
if let Some(mouse_position) = window.cursor_position() {
// Determine the index of the tile that the mouse is currently over
2023-11-01 20:02:53 +01:00
// NOTE: This calculation assumes a fixed window size.
2023-11-01 14:20:34 +01:00
// That's fine for now, but consider using the windows size instead.
2023-11-02 21:28:31 +01:00
let mut tile_x: usize = (mouse_position.x / 83.0).floor() as usize;
let tile_y: usize = (mouse_position.y / 540.0).floor() as usize;
if tile_x > 5 {
// remove the middle bar offset
tile_x = tile_x - 1
}
2023-11-05 17:14:58 +01:00
// let tile = tile_x + tile_y * 12;
// traduction en position backgammon
let tile = if tile_y == 0 {
13 + tile_x
} else {
12 - tile_x
};
2023-11-01 14:20:34 +01:00
// If mouse is outside of board we do nothing
2023-11-02 21:28:31 +01:00
if 23 < tile {
2023-11-01 14:20:34 +01:00
return;
}
// If left mouse button is pressed, send a place tile event to the server
if input.just_pressed(MouseButton::Left) {
info!("select piece at tile {:?}", tile);
2023-11-01 20:02:53 +01:00
if game_ui_state.selected_tile.is_some() {
let from_tile = game_ui_state.selected_tile.unwrap();
info!("sending movement from: {:?} to: {:?} ", from_tile, tile);
let event = GameEvent::Move {
player_id: client_id.0,
from: from_tile,
to: tile,
};
client.send_message(0, bincode::serialize(&event).unwrap());
}
game_ui_state.selected_tile = if game_ui_state.selected_tile.is_some() {
None
} else {
Some(tile)
}
2023-11-01 14:20:34 +01:00
}
}
}
2023-10-31 17:37:02 +01:00
////////// SETUP //////////
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Tric Trac is a 2D game
// To show 2D sprites we need a 2D camera
commands.spawn(Camera2dBundle::default());
// Spawn board background
commands.spawn(SpriteBundle {
transform: Transform::from_xyz(0.0, -30.0, 0.0),
sprite: Sprite {
2023-11-01 14:20:34 +01:00
custom_size: Some(Vec2::new(1080.0, 927.0)),
2023-10-31 17:37:02 +01:00
..default()
},
texture: asset_server.load("board.png").into(),
..default()
});
// Spawn pregame ui
commands
// A container that centers its children on the screen
.spawn(NodeBundle {
style: Style {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
..default()
})
.insert(UIRoot)
.with_children(|parent| {
2023-11-05 17:14:58 +01:00
// parent.spawn(Board::default()); // panic
2023-10-31 17:37:02 +01:00
parent
.spawn(TextBundle::from_section(
"Waiting for an opponent...",
TextStyle {
font: asset_server.load("Inconsolata.ttf"),
font_size: 24.0,
color: Color::hex("ebdbb2").unwrap(),
},
))
.insert(WaitingText);
});
}
2023-10-29 20:49:01 +01:00
////////// RENET NETWORKING //////////
// Creates a RenetClient thats already connected to a server.
// Returns an Err if connection fails
2023-11-01 20:02:53 +01:00
fn new_renet_client(
username: &String,
) -> anyhow::Result<(RenetClient, NetcodeClientTransport, u64)> {
2023-10-31 14:32:21 +01:00
let client = RenetClient::new(ConnectionConfig::default());
2023-10-29 20:49:01 +01:00
let server_addr = "127.0.0.1:5000".parse()?;
let socket = UdpSocket::bind("127.0.0.1:0")?;
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let client_id = current_time.as_millis() as u64;
// Place username in user data
let mut user_data = [0u8; NETCODE_USER_DATA_BYTES];
if username.len() > NETCODE_USER_DATA_BYTES - 8 {
panic!("Username is too big");
}
user_data[0..8].copy_from_slice(&(username.len() as u64).to_le_bytes());
user_data[8..username.len() + 8].copy_from_slice(username.as_bytes());
2023-10-30 14:25:05 +01:00
let authentication = ClientAuthentication::Unsecure {
server_addr,
2023-10-29 20:49:01 +01:00
client_id,
2023-10-30 14:25:05 +01:00
user_data: Some(user_data),
protocol_id: PROTOCOL_ID,
};
let transport = NetcodeClientTransport::new(current_time, authentication, socket).unwrap();
2023-10-29 20:49:01 +01:00
2023-11-01 20:02:53 +01:00
Ok((client, transport, client_id))
}
fn receive_events_from_server(
mut client: ResMut<RenetClient>,
mut game_state: ResMut<BevyGameState>,
mut game_events: EventWriter<BevyGameEvent>,
) {
while let Some(message) = client.receive_message(0) {
// Whenever the server sends a message we know that it must be a game event
let event: GameEvent = bincode::deserialize(&message).unwrap();
trace!("{:#?}", event);
// We trust the server - It's always been good to us!
// No need to validate the events it is sending us
game_state.0.consume(&event);
// Send the event into the bevy event system so systems can react to it
game_events.send(BevyGameEvent(event));
}
2023-10-29 20:49:01 +01:00
}
2023-10-31 14:32:21 +01:00
// If any error is found we just panic
fn panic_on_error_system(mut renet_error: EventReader<NetcodeTransportError>) {
for e in renet_error.iter() {
panic!("{}", e);
}
}