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 store::{EndGameReason, GameEvent, GameState};
|
|
|
|
|
use renet::transport::{NetcodeClientTransport, NetcodeTransportError, NETCODE_USER_DATA_BYTES};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use bevy::prelude::*;
|
|
|
|
|
use bevy::window::PrimaryWindow;
|
2023-10-31 16:29:05 +01:00
|
|
|
use bevy_renet::{
|
|
|
|
|
renet::{transport::ClientAuthentication, ConnectionConfig, RenetClient},
|
|
|
|
|
transport::NetcodeClientPlugin,
|
|
|
|
|
RenetClientPlugin,
|
2023-10-29 20:49:01 +01:00
|
|
|
};
|
2023-11-01 14:20:34 +01:00
|
|
|
|
|
|
|
|
#[derive(Resource)]
|
|
|
|
|
struct BevyGameState(GameState);
|
|
|
|
|
|
|
|
|
|
impl Default for BevyGameState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
0: GameState::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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-10-31 14:32:21 +01:00
|
|
|
let (client, transport) = 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())
|
|
|
|
|
.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-10-31 17:37:02 +01:00
|
|
|
.add_systems(Startup, setup)
|
|
|
|
|
.add_systems(Update, update_waiting_text)
|
2023-11-01 14:20:34 +01:00
|
|
|
.add_systems(Update, input)
|
2023-10-31 14:32:21 +01:00
|
|
|
.add_systems(Update, panic_on_error_system)
|
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;
|
|
|
|
|
|
|
|
|
|
////////// UPDATE SYSTEMS //////////
|
|
|
|
|
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>,
|
|
|
|
|
) {
|
|
|
|
|
// let window = windows.get_primary().unwrap();
|
|
|
|
|
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
|
|
|
|
|
// NOTE: This calculation assumes a fixed window size.
|
|
|
|
|
// That's fine for now, but consider using the windows size instead.
|
|
|
|
|
let x_tile: usize = (mouse_position.x / 83.0).floor() as usize;
|
|
|
|
|
let y_tile: usize = (mouse_position.y / 540.0).floor() as usize;
|
|
|
|
|
let tile = x_tile + y_tile * 13;
|
|
|
|
|
|
|
|
|
|
// If mouse is outside of board we do nothing
|
|
|
|
|
if 25 < tile {
|
|
|
|
|
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-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| {
|
|
|
|
|
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-10-31 14:32:21 +01:00
|
|
|
fn new_renet_client(username: &String) -> anyhow::Result<(RenetClient, NetcodeClientTransport)> {
|
|
|
|
|
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-10-31 14:32:21 +01:00
|
|
|
Ok((client, transport))
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|