Initial commit
commit
9102608b37
@ -0,0 +1,2 @@
|
||||
/target
|
||||
example.config.toml
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "spleef"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.17.1"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
toml = "0.7.3"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
valence = { git = "https://github.com/valence-rs/valence" }
|
||||
valence_protocol = { git = "https://github.com/valence-rs/valence" }
|
@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
@ -0,0 +1,9 @@
|
||||
# TODO
|
||||
- Implement waiting room system [✔️]
|
||||
- Chat [✔️]
|
||||
- Make the game automatically start
|
||||
- Make spectator mode work properly
|
||||
- Maybe make the floors use a random block each round?
|
||||
- Leaderboards using a database, probably Surreal
|
||||
- Maybe some kind of events that can randomly happen? Could be a cool mechanic that other spleef servers (probably) don't have
|
||||
- Eventually add world support for nicer looking arenas
|
@ -0,0 +1,11 @@
|
||||
no_auto_stop = true
|
||||
|
||||
spawn_y = 60
|
||||
|
||||
platforms = 2
|
||||
platform_gap = 5
|
||||
platform_size = 15
|
||||
|
||||
operators = [
|
||||
"dc524fc6-bdba-4f55-9f7b-cefc824cc77e"
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
use valence::prelude::{event::ChatMessage, *};
|
||||
|
||||
pub fn handle_messages(
|
||||
mut clients: Query<(&mut Client, &Username)>,
|
||||
mut messages: EventReader<ChatMessage>,
|
||||
) {
|
||||
for message in messages.iter() {
|
||||
let username = match clients.get_component::<Username>(message.client) {
|
||||
Ok(username) => username.to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
let message = format!("{}: {}", username, message.message);
|
||||
|
||||
for (mut client, _) in &mut clients {
|
||||
client.send_message(message.clone());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
use valence::prelude::*;
|
||||
|
||||
use crate::{GameState, CONFIG};
|
||||
|
||||
use super::Command;
|
||||
|
||||
pub fn run(
|
||||
command: Command,
|
||||
client: &mut Client,
|
||||
uuid: Uuid,
|
||||
next_state: &mut ResMut<NextState<GameState>>,
|
||||
state: &Res<State<GameState>>
|
||||
) {
|
||||
if CONFIG.operators.contains(&uuid) {
|
||||
if let Some(args) = command.args {
|
||||
match args[0].as_str() {
|
||||
"start" => next_state.set(GameState::Started),
|
||||
"stop" => next_state.set(GameState::Stopped),
|
||||
_ => client.send_message("Invalid game state!"),
|
||||
}
|
||||
} else {
|
||||
client.send_message(format!(
|
||||
"The game is currently {}",
|
||||
match state.0 {
|
||||
GameState::Started => "started",
|
||||
GameState::Stopped => "stopped",
|
||||
}
|
||||
));
|
||||
}
|
||||
} else {
|
||||
client.send_message("You do not have the permission to execute this command.");
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
mod game;
|
||||
|
||||
use valence::prelude::{event::CommandExecution, *};
|
||||
|
||||
use crate::GameState;
|
||||
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn parse(command: String) -> Self {
|
||||
let command = command
|
||||
.split(" ")
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let name = command[0].clone();
|
||||
let args = if command.len() > 1 {
|
||||
Some(command.as_slice()[1..].to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self { name, args }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_executor(
|
||||
mut clients: Query<&mut Client>,
|
||||
uuids: Query<&UniqueId, With<Client>>,
|
||||
mut events: EventReader<CommandExecution>,
|
||||
mut next_state: ResMut<NextState<GameState>>,
|
||||
state: Res<State<GameState>>
|
||||
) {
|
||||
for event in events.iter() {
|
||||
let cmd = Command::parse(event.command.to_string());
|
||||
let mut client = clients.get_mut(event.client).unwrap();
|
||||
let uuid = uuids.get(event.client).unwrap();
|
||||
|
||||
match cmd.name.as_str() {
|
||||
"game" => game::run(cmd, &mut client, uuid.0, &mut next_state, &state),
|
||||
_ => client.send_message(format!("Command {} not found!", cmd.name)),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use std::fs;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use valence::uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
// USE ONLY FOR DEVELOPMENT PURPOSES
|
||||
pub no_auto_stop: Option<bool>,
|
||||
|
||||
pub spawn_y: i32,
|
||||
|
||||
pub platforms: i32,
|
||||
pub platform_gap: i32,
|
||||
pub platform_size: i32,
|
||||
|
||||
pub operators: Vec<Uuid>,
|
||||
}
|
||||
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||
let config = fs::read_to_string("./config.toml").expect("Failed to read config.toml!");
|
||||
|
||||
toml::from_str(&config).unwrap()
|
||||
});
|
@ -0,0 +1,94 @@
|
||||
use rand::prelude::*;
|
||||
use valence::prelude::*;
|
||||
|
||||
use crate::{player::Alive, GameState, CONFIG};
|
||||
|
||||
pub fn start_game(
|
||||
mut clients: Query<(&mut Client, &mut Position, &mut GameMode, &mut Alive)>,
|
||||
mut instances: Query<&mut Instance>,
|
||||
) {
|
||||
tracing::debug!("Starting game!");
|
||||
|
||||
let mut instance = instances.single_mut();
|
||||
|
||||
for (mut client, mut pos, mut gamemode, mut alive) in &mut clients {
|
||||
*gamemode = GameMode::Survival;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let x = rng.gen_range(0..=CONFIG.platform_size);
|
||||
let z = rng.gen_range(0..=CONFIG.platform_size);
|
||||
|
||||
pos.set([x.into(), CONFIG.spawn_y as f64 + 1.0, z.into()]);
|
||||
alive.0 = true;
|
||||
|
||||
// TODO: add colors
|
||||
client.set_title("Game started!", "Have fun!", None);
|
||||
}
|
||||
|
||||
for platform in -CONFIG.platforms..CONFIG.platforms * CONFIG.platform_gap {
|
||||
if platform % CONFIG.platform_gap == 0 {
|
||||
for z in -CONFIG.platform_size..CONFIG.platform_size {
|
||||
for x in -CONFIG.platform_size..CONFIG.platform_size {
|
||||
instance.set_block([x, CONFIG.spawn_y - platform, z], BlockState::SNOW_BLOCK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_game(
|
||||
mut clients: Query<(
|
||||
&mut Client,
|
||||
&mut Position,
|
||||
&mut GameMode,
|
||||
&mut Alive,
|
||||
&Username,
|
||||
)>,
|
||||
) {
|
||||
tracing::debug!("Stopping game!");
|
||||
|
||||
let mut alive_players = vec![];
|
||||
|
||||
for (mut client, mut pos, mut gamemode, mut alive, username) in &mut clients {
|
||||
if alive.0 {
|
||||
alive_players.push(username);
|
||||
}
|
||||
|
||||
*gamemode = GameMode::Spectator;
|
||||
pos.set([0.0, CONFIG.spawn_y as f64 + 10.0, 0.0]);
|
||||
alive.0 = false;
|
||||
|
||||
// TODO: add colors
|
||||
client.set_title(
|
||||
"Game has ended!",
|
||||
format!(
|
||||
"{} wins!",
|
||||
// if statement to not panic if somehow no one was alive when the game ended
|
||||
if alive_players.len() >= 1 {
|
||||
// let's assume there's only one alive player
|
||||
alive_players[0].0.clone()
|
||||
} else {
|
||||
"Somehow, no one".to_string()
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_stop(
|
||||
clients: Query<&Alive, With<Client>>,
|
||||
mut next_state: ResMut<NextState<GameState>>,
|
||||
state: Res<State<GameState>>,
|
||||
) {
|
||||
if state.0 == GameState::Started {
|
||||
let alive_clients = clients
|
||||
.iter()
|
||||
.filter(|alive| alive.0)
|
||||
.collect::<Vec<&Alive>>();
|
||||
|
||||
if alive_clients.len() <= 1 {
|
||||
next_state.set(GameState::Stopped);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
mod chat;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod game;
|
||||
mod player;
|
||||
mod world;
|
||||
|
||||
use config::CONFIG;
|
||||
use once_cell::sync::Lazy;
|
||||
use valence::bevy_ecs;
|
||||
use valence::prelude::*;
|
||||
|
||||
#[derive(States, Default, Debug, Hash, PartialEq, Eq, Clone)]
|
||||
pub enum GameState {
|
||||
Started,
|
||||
#[default]
|
||||
Stopped,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Lazy::force(&CONFIG);
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
App::new()
|
||||
.add_state::<GameState>()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(world::setup)
|
||||
.add_system(player::init_clients)
|
||||
.add_systems(
|
||||
(
|
||||
default_event_handler,
|
||||
player::break_block,
|
||||
commands::command_executor,
|
||||
chat::handle_messages,
|
||||
)
|
||||
.in_schedule(EventLoopSchedule),
|
||||
)
|
||||
.add_systems((
|
||||
despawn_disconnected_clients,
|
||||
player::death,
|
||||
game::auto_stop.run_if(|| !CONFIG.no_auto_stop.unwrap_or(false)),
|
||||
))
|
||||
.add_system(game::start_game.in_schedule(OnEnter(GameState::Started)))
|
||||
.add_system(game::stop_game.in_schedule(OnExit(GameState::Started)))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
use valence::bevy_ecs;
|
||||
use valence::{
|
||||
entity::player::PlayerEntityBundle,
|
||||
prelude::{event::StartDigging, *},
|
||||
};
|
||||
|
||||
use crate::{GameState, CONFIG};
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct Alive(pub bool);
|
||||
|
||||
pub fn init_clients(
|
||||
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (entity, uuid, mut game_mode) in &mut clients {
|
||||
*game_mode = GameMode::Spectator;
|
||||
|
||||
commands.entity(entity).insert((
|
||||
Alive(false),
|
||||
PlayerEntityBundle {
|
||||
location: Location(instances.single()),
|
||||
position: Position::new([0.0, CONFIG.spawn_y as f64 + 10.0, 0.0]),
|
||||
uuid: *uuid,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
pub fn break_block(mut instances: Query<&mut Instance>, mut events: EventReader<StartDigging>) {
|
||||
let mut instance = instances.single_mut();
|
||||
|
||||
for event in events.iter() {
|
||||
instance.set_block(event.position, BlockState::AIR);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn death(
|
||||
mut clients: Query<(&mut Client, &mut Position, &mut GameMode, &mut Alive)>,
|
||||
state: Res<State<GameState>>,
|
||||
) {
|
||||
for (mut client, mut pos, mut gamemode, mut alive) in &mut clients {
|
||||
if pos.0.y <= 0.0 && state.0 == GameState::Started {
|
||||
*gamemode = GameMode::Spectator;
|
||||
pos.set([0.0, (CONFIG.spawn_y + 10).into(), 0.0]);
|
||||
alive.0 = false;
|
||||
|
||||
client.send_message("You died!".italic().bold().color(Color::GRAY));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
use valence::prelude::*;
|
||||
|
||||
pub fn setup(
|
||||
mut commands: Commands,
|
||||
server: Res<Server>,
|
||||
dimensions: Query<&DimensionType>,
|
||||
biomes: Query<&Biome>,
|
||||
) {
|
||||
let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);
|
||||
|
||||
for z in -5..5 {
|
||||
for x in -5..5 {
|
||||
instance.insert_chunk([x, z], Chunk::default());
|
||||
}
|
||||
}
|
||||
|
||||
commands.spawn(instance);
|
||||
|
||||
tracing::info!("Finished setting up world!");
|
||||
}
|
Loading…
Reference in new issue