use ponko::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default)]
struct Pad {
dir: Vec2,
shove: bool,
}
fn read_keys(ctx: &mut Ctx) {
let mut pad = Pad::default();
for (key, d) in [
(Key::Left, -Vec2::X),
(Key::Right, Vec2::X),
(Key::Up, -Vec2::Y),
(Key::Down, Vec2::Y),
] {
if ctx.key_down(key) {
pad.dir += d;
}
}
pad.shove = ctx.key_down(Key::Space);
ctx.net.set_input(&pad);
}
#[derive(Serialize, Deserialize, Clone, Copy)]
struct Crab {
player: u32,
push: Vec2,
}
fn move_crabs(ctx: &mut Ctx) {
let pads: Vec<Pad> = (0..ctx.net.players())
.map(|p| ctx.net.input::<Pad>(p).unwrap_or_default())
.collect();
let dt = ctx.dt();
let crabs: Vec<(Entity, Vec2, u32)> = ctx
.world
.query::<(Entity, &Transform2D, &Crab)>()
.iter()
.map(|(e, t, c)| (e, t.position, c.player))
.collect();
for &(e, at, player) in &crabs {
let pad = &pads[player as usize];
if pad.shove {
for &(other, other_at, _) in &crabs {
if other != e && at.distance(other_at) < 120.0 {
ctx.world.get_mut::<Crab>(other).unwrap().push += (other_at - at).normalize_or_zero() * 900.0;
}
}
}
let crab = *ctx.world.get::<Crab>(e).unwrap();
let t = ctx.world.get_mut::<Transform2D>(e).unwrap();
t.position += (pad.dir.normalize_or_zero() * 250.0 + crab.push) * dt;
t.position = t.position.clamp(vec2(40.0, 40.0), vec2(760.0, 560.0));
ctx.world.get_mut::<Crab>(e).unwrap().push = crab.push * 0.9;
}
}
fn draw(ctx: &mut Ctx) {
ctx.clear(Color::hex(0x20304a));
let crabs: Vec<(Vec2, u32)> = ctx
.world
.query::<(&Transform2D, &Crab)>()
.iter()
.map(|(t, c)| (t.position, c.player))
.collect();
for (at, player) in crabs {
let color = [Color::hex(0xf56565), Color::hex(0x63b3ed)][player as usize % 2];
ctx.draw_circle(at, 30.0, color);
}
let net = &ctx.net;
let status = match net.state() {
NetState::Connecting => "waiting for the other player...".to_string(),
NetState::Running => format!(
"you are player {} ({}), tick {}{}",
net.player() + 1,
["red", "blue"][net.player() % 2],
ctx.sim_tick(),
net.desync().map_or(String::new(), |d| format!(" DESYNC: {d}"))
),
NetState::Lost => format!("connection lost: {}", net.error().unwrap_or("?")),
NetState::Offline => "offline".into(),
_ => String::new(),
};
ctx.draw_text(&status, vec2(20.0, 20.0), 20.0, Color::WHITE);
#[cfg(feature = "steam")]
if ctx.net.state() == NetState::Connecting
&& let Some(lobby) = ctx.net.steam_lobby()
{
let text = format!("Steam lobby {}: press I to invite a friend", lobby.raw());
ctx.draw_text(&text, vec2(20.0, 50.0), 20.0, Color::WHITE);
if ctx.key_pressed(Key::I) {
ctx.net.steam_invite();
}
}
if ctx.key_pressed(Key::Escape) {
ctx.quit();
}
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
#[cfg(feature = "steam")]
if let Some(net) = steam_session(&args) {
return play(net);
}
let net = match args.first().map(String::as_str) {
Some("host") => Net::host(format!("0.0.0.0:{}", args.get(1).map_or("7777", |p| p)), 2),
Some("join") => Net::join(args.get(1).map_or("127.0.0.1:7777", |a| a)),
_ => {
eprintln!("usage: lockstep host [port] | join <host:port> | steam | steam-join");
return;
}
}
.expect("could not open a socket");
play(net);
}
fn play(net: Net) {
Game::new("Lockstep", 800, 600)
.lockstep(net)
.system(read_keys) .sim_system(move_crabs) .system(draw)
.run(|ctx| {
ctx.world.reflect::<Crab>(); for player in 0..2 {
ctx.world.spawn((
Transform2D::at(vec2(250.0 + 300.0 * player as f32, 300.0)),
Crab {
player,
push: Vec2::ZERO,
},
));
}
});
}
#[cfg(feature = "steam")]
fn steam_session(args: &[String]) -> Option<Net> {
let mode = args.first().map(String::as_str);
let invited = steam_lobby_from_args(); if !matches!(mode, Some("steam" | "steam-join")) && invited.is_none() {
return None;
}
let client = steamworks::Client::init_app(480).expect("Steam must be running and logged in");
if let Some(lobby) = invited {
return Some(Net::steam_join(&client, lobby));
}
if mode == Some("steam") {
return Some(Net::steam_host(&client, 2));
}
println!("waiting for you to accept a Steam invite...");
let lobby = steam_wait_for_invite(&client, std::time::Duration::from_secs(300))?;
Some(Net::steam_join(&client, lobby))
}