Compare commits
2 Commits
abb3f4c2da
...
2ba48354ac
Author | SHA1 | Date | |
---|---|---|---|
2ba48354ac | |||
457d6cee96 |
@ -6,8 +6,8 @@ pub type NewUserState {
|
||||
}
|
||||
|
||||
pub type NewUserClientMessages {
|
||||
Register(game_code: String, username: String)
|
||||
CreateRoom(game_code: String, username: String)
|
||||
JoinAsPlayer(token: String, user_id: Int, game_id: String)
|
||||
JoinAsHost(token: String, user_id: Int, game_id: String)
|
||||
}
|
||||
|
||||
pub fn new_user_client_messages_decoder() -> decode.Decoder(
|
||||
@ -15,40 +15,32 @@ pub fn new_user_client_messages_decoder() -> decode.Decoder(
|
||||
) {
|
||||
use variant <- decode.field("type", decode.string)
|
||||
case variant {
|
||||
"Register" -> {
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
decode.success(Register(game_code:, username:))
|
||||
"JoinAsPlayer" -> {
|
||||
use token <- decode.field("token", decode.string)
|
||||
use user_id <- decode.field("user_id", decode.int)
|
||||
use game_id <- decode.field("game_id", decode.string)
|
||||
decode.success(JoinAsPlayer(token:, user_id:, game_id:))
|
||||
}
|
||||
"CreateRoom" -> {
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
decode.success(CreateRoom(game_code:, username:))
|
||||
"JoinAsHost" -> {
|
||||
use token <- decode.field("token", decode.string)
|
||||
use user_id <- decode.field("user_id", decode.int)
|
||||
use game_id <- decode.field("game_id", decode.string)
|
||||
decode.success(JoinAsHost(token:, user_id:, game_id:))
|
||||
}
|
||||
_ -> decode.failure(Register("", ""), "NewUserClientMessages")
|
||||
_ -> decode.failure(JoinAsPlayer("", 0, ""), "NewUserClientMessages")
|
||||
}
|
||||
}
|
||||
|
||||
pub type NewUserServerMessages {
|
||||
RegisterResponse(username: String, token: String)
|
||||
HostRegisterResponse(username: String, token: String)
|
||||
RegisterResponse
|
||||
HostRegisterResponse
|
||||
}
|
||||
|
||||
pub fn encode_new_user_server_messages(
|
||||
new_user_server_messages: NewUserServerMessages,
|
||||
) -> json.Json {
|
||||
case new_user_server_messages {
|
||||
RegisterResponse(..) ->
|
||||
json.object([
|
||||
#("type", json.string("RegisterResponse")),
|
||||
#("username", json.string(new_user_server_messages.username)),
|
||||
#("token", json.string(new_user_server_messages.token)),
|
||||
])
|
||||
HostRegisterResponse(..) ->
|
||||
json.object([
|
||||
#("type", json.string("HostRegisterResponse")),
|
||||
#("username", json.string(new_user_server_messages.username)),
|
||||
#("token", json.string(new_user_server_messages.token)),
|
||||
])
|
||||
RegisterResponse -> json.string("register_response")
|
||||
HostRegisterResponse -> json.string("host_register_response")
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,30 @@
|
||||
import app/clients/new_user_client
|
||||
import app/web
|
||||
import gleam/bit_array
|
||||
import gleam/bytes_tree
|
||||
import gleam/dict
|
||||
import gleam/erlang/process
|
||||
import gleam/http/cookie
|
||||
import gleam/http/request.{type Request}
|
||||
import gleam/http/response.{type Response}
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
import gleam/option.{None, Some}
|
||||
import gleam/otp/actor
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import login
|
||||
import mist.{type Connection, type ResponseData}
|
||||
import player
|
||||
import session
|
||||
import socket_manager
|
||||
import storail
|
||||
|
||||
type JoinAs {
|
||||
JoinAsHost
|
||||
JoinAsPlayer
|
||||
}
|
||||
|
||||
pub fn handle_request(
|
||||
req: Request(Connection),
|
||||
@ -25,10 +38,75 @@ pub fn handle_request(
|
||||
["static", "frontend_bg.wasm"] ->
|
||||
serve_wasm(ctx, "frontend_bg.wasm", "application/wasm")
|
||||
["ws"] -> serve_websocket(ctx, req)
|
||||
["login", "host"] -> server_login(ctx, req, JoinAsHost)
|
||||
["login", "join"] -> server_login(ctx, req, JoinAsPlayer)
|
||||
_ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new()))
|
||||
}
|
||||
}
|
||||
|
||||
fn server_login(
|
||||
ctx: web.Context,
|
||||
req: Request(Connection),
|
||||
join_type: JoinAs,
|
||||
) -> Response(ResponseData) {
|
||||
let assert Ok(req) = mist.read_body(req, 1000)
|
||||
let assert Ok(body) = bit_array.to_string(req.body)
|
||||
|
||||
let assert Ok(login_req) = json.parse(body, login.login_request_decoder())
|
||||
let #(token, user_id) =
|
||||
web.register_user(ctx, login_req.username, login_req.game_code)
|
||||
|
||||
let _ = case join_type {
|
||||
JoinAsHost -> {
|
||||
let key = storail.key(ctx.sessions, login_req.game_code)
|
||||
let session = session.Session(login_req.game_code, user_id, dict.new())
|
||||
let assert Ok(Nil) = storail.write(key, session)
|
||||
}
|
||||
JoinAsPlayer -> {
|
||||
let key = storail.key(ctx.sessions, login_req.game_code)
|
||||
let assert Ok(session) = storail.read(key)
|
||||
|
||||
let player =
|
||||
player.Player(
|
||||
name: login_req.username,
|
||||
id: user_id,
|
||||
score: 0,
|
||||
buzz_in_time: None,
|
||||
)
|
||||
let session =
|
||||
session.Session(
|
||||
..session,
|
||||
players: dict.insert(session.players, player.id, player),
|
||||
)
|
||||
|
||||
let assert Ok(Nil) = storail.write(key, session)
|
||||
}
|
||||
}
|
||||
|
||||
let join_type_name = case join_type {
|
||||
JoinAsHost -> "host"
|
||||
JoinAsPlayer -> "player"
|
||||
}
|
||||
|
||||
response.new(200)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/token",
|
||||
token,
|
||||
cookie.defaults(req.scheme),
|
||||
)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/client_type",
|
||||
join_type_name,
|
||||
cookie.defaults(req.scheme),
|
||||
)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/username",
|
||||
login_req.username,
|
||||
cookie.defaults(req.scheme),
|
||||
)
|
||||
|> response.set_body(mist.Bytes(bytes_tree.new()))
|
||||
}
|
||||
|
||||
fn serve_wasm(
|
||||
ctx: web.Context,
|
||||
file: String,
|
||||
@ -100,6 +178,7 @@ fn handle_ws_message(
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Custom(resp) -> {
|
||||
echo resp
|
||||
let assert Ok(_) = mist.send_text_frame(conn, resp)
|
||||
actor.continue(state)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import gleam/int
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
import gleam/option.{None, Some}
|
||||
import gleam/otp/actor
|
||||
import player
|
||||
import player_session
|
||||
@ -18,6 +19,10 @@ import session
|
||||
import socket_manager
|
||||
import storail
|
||||
|
||||
pub type Error {
|
||||
InvalidToken
|
||||
}
|
||||
|
||||
pub type Context {
|
||||
Context(
|
||||
config: config.Config,
|
||||
@ -53,6 +58,32 @@ pub fn handle_client_msg(
|
||||
}
|
||||
}
|
||||
|
||||
fn set_buzz_in(
|
||||
socket_state: SocketState,
|
||||
player_state: player_client.PlayerState,
|
||||
time: Float,
|
||||
) {
|
||||
let key = storail.key(socket_state.ctx.sessions, player_state.game_id)
|
||||
let assert Ok(session) = storail.read(key)
|
||||
|
||||
let players =
|
||||
dict.upsert(session.players, player_state.user_id, fn(player) {
|
||||
let assert Some(player) = player
|
||||
|
||||
player.Player(..player, buzz_in_time: Some(time))
|
||||
})
|
||||
|
||||
let session = session.Session(..session, players:)
|
||||
|
||||
let assert Ok(_) = storail.write(key, session)
|
||||
|
||||
send_message_to_host(
|
||||
socket_state,
|
||||
session.host_user_id,
|
||||
host_client.UpdatePlayerStates(session.players),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_player(
|
||||
socket_state: SocketState,
|
||||
player_state: player_client.PlayerState,
|
||||
@ -69,6 +100,9 @@ fn handle_player(
|
||||
<> " at "
|
||||
<> float.to_string(time),
|
||||
)
|
||||
|
||||
set_buzz_in(socket_state, player_state, time)
|
||||
|
||||
#(
|
||||
socket_state,
|
||||
json.to_string(player_client.encode_player_server_messages(
|
||||
@ -79,8 +113,8 @@ fn handle_player(
|
||||
}
|
||||
}
|
||||
|
||||
fn register_user(
|
||||
state: SocketState,
|
||||
pub fn register_user(
|
||||
ctx: Context,
|
||||
username: String,
|
||||
game_code: String,
|
||||
) -> #(String, Int) {
|
||||
@ -97,7 +131,7 @@ fn register_user(
|
||||
|
||||
let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8)
|
||||
|
||||
let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
|
||||
let key = storail.key(ctx.player_sessions, int.to_base16(user_id))
|
||||
|
||||
let assert Ok(Nil) =
|
||||
storail.write(
|
||||
@ -106,13 +140,40 @@ fn register_user(
|
||||
id: user_id,
|
||||
token_hash:,
|
||||
username:,
|
||||
socket_id: state.id,
|
||||
socket_id: None,
|
||||
),
|
||||
)
|
||||
|
||||
#(token, user_id)
|
||||
}
|
||||
|
||||
pub fn attach_socket_to_user(
|
||||
state: SocketState,
|
||||
user_id: Int,
|
||||
token: String,
|
||||
) -> Result(Nil, Error) {
|
||||
let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
|
||||
let assert Ok(player_state) = storail.read(key)
|
||||
|
||||
let hashed_token = player_session.hash_token(token)
|
||||
|
||||
case hashed_token == player_state.token_hash {
|
||||
False -> Error(InvalidToken)
|
||||
True -> {
|
||||
let assert Ok(Nil) =
|
||||
storail.write(
|
||||
key,
|
||||
player_session.PlayerSession(
|
||||
..player_state,
|
||||
socket_id: Some(state.id),
|
||||
),
|
||||
)
|
||||
|
||||
Ok(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_new_user(
|
||||
socket_state: SocketState,
|
||||
_new_user_state: new_user_client.NewUserState,
|
||||
@ -122,42 +183,25 @@ fn handle_new_user(
|
||||
json.parse(msg, new_user_client.new_user_client_messages_decoder())
|
||||
|
||||
let #(socket_state, new_user_msg) = case msg {
|
||||
new_user_client.CreateRoom(game_code, username) -> {
|
||||
let #(token, user_id) = register_user(socket_state, username, game_code)
|
||||
|
||||
let key = storail.key(socket_state.ctx.sessions, game_code)
|
||||
let session = session.Session(game_code, user_id, dict.new())
|
||||
let assert Ok(Nil) = storail.write(key, session)
|
||||
new_user_client.JoinAsHost(token, user_id, game_id) -> {
|
||||
let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
|
||||
|
||||
#(
|
||||
SocketState(
|
||||
..socket_state,
|
||||
state: Host(host_client.HostState(user_id, game_code)),
|
||||
state: Player(player_client.PlayerState(user_id:, game_id:)),
|
||||
),
|
||||
new_user_client.HostRegisterResponse(username, token),
|
||||
new_user_client.HostRegisterResponse,
|
||||
)
|
||||
}
|
||||
new_user_client.Register(game_code, username) -> {
|
||||
let #(token, user_id) = register_user(socket_state, username, game_code)
|
||||
|
||||
let key = storail.key(socket_state.ctx.sessions, game_code)
|
||||
let assert Ok(session) = storail.read(key)
|
||||
|
||||
let player = player.Player(name: username, id: user_id, score: 0)
|
||||
let session =
|
||||
session.Session(
|
||||
..session,
|
||||
players: dict.insert(session.players, player.id, player),
|
||||
)
|
||||
|
||||
let assert Ok(Nil) = storail.write(key, session)
|
||||
|
||||
new_user_client.JoinAsPlayer(token, user_id, game_id) -> {
|
||||
let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
|
||||
#(
|
||||
SocketState(
|
||||
..socket_state,
|
||||
state: Player(player_client.PlayerState(user_id, game_code)),
|
||||
state: Host(host_client.HostState(user_id:, game_id:)),
|
||||
),
|
||||
new_user_client.RegisterResponse(token, username),
|
||||
new_user_client.HostRegisterResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -181,8 +225,9 @@ fn get_player_session_subject(
|
||||
ctx: Context,
|
||||
player: player_session.PlayerSession,
|
||||
) -> process.Subject(String) {
|
||||
let assert Some(id) = player.socket_id
|
||||
let assert Ok(subject) =
|
||||
socket_manager.get_socket_subj_by_id(ctx.socket_manager, player.socket_id)
|
||||
socket_manager.get_socket_subj_by_id(ctx.socket_manager, id)
|
||||
subject
|
||||
}
|
||||
|
||||
@ -212,6 +257,41 @@ fn send_message_to_player(
|
||||
|> actor.send(json.to_string(client_msg))
|
||||
}
|
||||
|
||||
fn send_message_to_host(
|
||||
socket_state: SocketState,
|
||||
id: Int,
|
||||
msg: host_client.HostServerMessages,
|
||||
) {
|
||||
let client_msg = host_client.encode_host_server_messages(msg)
|
||||
|
||||
get_player_session_from_id(socket_state.ctx, id)
|
||||
|> get_player_session_subject(socket_state.ctx, _)
|
||||
|> actor.send(json.to_string(client_msg))
|
||||
}
|
||||
|
||||
fn reset_player_buzzer(
|
||||
socket_state: SocketState,
|
||||
session_key: storail.Key(session.Session),
|
||||
id: Int,
|
||||
) {
|
||||
let assert Ok(session) = storail.read(session_key)
|
||||
|
||||
let assert Ok(player) = dict.get(session.players, id)
|
||||
let player = player.Player(..player, buzz_in_time: None)
|
||||
|
||||
let players = dict.insert(session.players, id, player)
|
||||
|
||||
let session = session.Session(..session, players: players)
|
||||
|
||||
let assert Ok(_) = storail.write(session_key, session)
|
||||
|
||||
send_message_to_host(
|
||||
socket_state,
|
||||
session.host_user_id,
|
||||
host_client.UpdatePlayerStates(session.players),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_host(
|
||||
socket_state: SocketState,
|
||||
host_state: host_client.HostState,
|
||||
@ -225,6 +305,11 @@ fn handle_host(
|
||||
|
||||
let #(socket_state, resp) = case msg {
|
||||
host_client.ResetAllBuzers -> {
|
||||
dict.to_list(session.players)
|
||||
|> list.each(fn(player_entry) {
|
||||
reset_player_buzzer(socket_state, session_key, player_entry.0)
|
||||
})
|
||||
|
||||
broadcast_message_to_players(
|
||||
socket_state,
|
||||
session,
|
||||
@ -233,12 +318,13 @@ fn handle_host(
|
||||
#(socket_state, host_client.Ack)
|
||||
}
|
||||
host_client.ResetPlayerBuzzer(id) -> {
|
||||
reset_player_buzzer(socket_state, session_key, id)
|
||||
send_message_to_player(socket_state, id, player_client.ResetBuzzer)
|
||||
#(socket_state, host_client.Ack)
|
||||
}
|
||||
host_client.UpdateScore(id, score) -> {
|
||||
let assert Ok(player) = dict.get(session.players, id)
|
||||
let player = player.Player(..player, score:)
|
||||
let player = player.Player(..player, score: player.score + score)
|
||||
let session =
|
||||
session.Session(
|
||||
..session,
|
||||
@ -252,6 +338,12 @@ fn handle_host(
|
||||
player_client.UpdatePointTotal(player.score),
|
||||
)
|
||||
|
||||
send_message_to_host(
|
||||
socket_state,
|
||||
session.host_user_id,
|
||||
host_client.UpdatePlayerStates(session.players),
|
||||
)
|
||||
|
||||
#(socket_state, host_client.Ack)
|
||||
}
|
||||
}
|
||||
|
72
frontend/index/index.html
Normal file
72
frontend/index/index.html
Normal file
@ -0,0 +1,72 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Play of the Game</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
canvas {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; height: 100vh; width: 100vw">
|
||||
<canvas id="glcanvas" tabindex="1" hidden></canvas>
|
||||
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
|
||||
<script type="module">
|
||||
import init, { set_wasm } from "./static/frontend.js";
|
||||
async function impl_run() {
|
||||
let wbg = await init();
|
||||
miniquad_add_plugin({
|
||||
register_plugin: (a) => (a.wbg = wbg),
|
||||
on_init: () => set_wasm(wasm_exports),
|
||||
version: "0.0.1",
|
||||
name: "wbg",
|
||||
});
|
||||
load("./static/frontend_bg.wasm");
|
||||
}
|
||||
window.run = function (join_type) {
|
||||
console.log("Joining as: ", join_type);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "./login/" + join_type);
|
||||
xhr.onload = function (event) {
|
||||
console.log("Starting game...", event.target);
|
||||
document.getElementById("run-container").remove();
|
||||
document
|
||||
.getElementById("glcanvas")
|
||||
.removeAttribute("hidden");
|
||||
document.getElementById("glcanvas").focus();
|
||||
impl_run();
|
||||
};
|
||||
// or onerror, onabort
|
||||
var formData = new FormData(document.getElementById("login"));
|
||||
xhr.send(JSON.stringify(Object.fromEntries(formData));
|
||||
};
|
||||
</script>
|
||||
<div
|
||||
id="run-container"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
"
|
||||
>
|
||||
<form id="login">
|
||||
<label for="username">Username:</label><br />
|
||||
<input type="text" id="username" name="username" /><br />
|
||||
<label for="gamecode">Gamecode:</label><br />
|
||||
<input type="text" id="gamecode" name="gamecode" /><br />
|
||||
</form>
|
||||
<button onclick="run('join')">Join Game</button>
|
||||
<button onclick="run('host')">Host Game</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
35
frontend/src/font.rs
Normal file
35
frontend/src/font.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use macroquad::{
|
||||
text::{TextDimensions, measure_text},
|
||||
window::screen_width,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FontSize {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl FontSize {
|
||||
pub fn size(&self) -> u16 {
|
||||
let size1 = measure_text("A", None, 10, 1.0);
|
||||
let size2 = measure_text("A", None, 20, 1.0);
|
||||
|
||||
let scale_factor = size2.width - size1.width / 10.0;
|
||||
|
||||
let offset = size2.width - (10.0 * scale_factor);
|
||||
|
||||
let base_font_size = screen_width() * 0.01 - offset / scale_factor;
|
||||
|
||||
(match self {
|
||||
FontSize::Small => base_font_size,
|
||||
FontSize::Medium => base_font_size * 1.0,
|
||||
FontSize::Large => base_font_size * 2.0,
|
||||
}) as u16
|
||||
}
|
||||
|
||||
pub fn measure(&self, text: &str) -> TextDimensions {
|
||||
measure_text(text, None, self.size(), 1.0)
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use macroquad::{
|
||||
prelude::*,
|
||||
ui::{Skin, Ui, hash, root_ui, widgets::Window},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{model::player::Player, screen::Screen};
|
||||
use crate::{font::FontSize, model::player::Player, screen::Screen, ui_scaling::window_width};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize)]
|
||||
@ -11,35 +14,121 @@ use crate::{model::player::Player, screen::Screen};
|
||||
enum HostClientMessages {
|
||||
ResetAllBuzers,
|
||||
ResetPlayerBuzzer { user_id: u64 },
|
||||
UpdateScore { user_id: u64, diff: u32 },
|
||||
UpdateScore { user_id: u64, diff: i32 },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum HostServerMessages {
|
||||
Ack,
|
||||
UpdatePlayerStates { players: HashMap<u64, Player> },
|
||||
UpdatePlayerStates { players: HashMap<String, Player> },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HostScreen {
|
||||
players: HashMap<u64, Player>,
|
||||
players: HashMap<String, Player>,
|
||||
}
|
||||
|
||||
impl HostScreen {
|
||||
fn draw_user(&self, ctx: &mut crate::context::Context, user_id: &str, ui: &mut Ui, width: f32) {
|
||||
let player = &self.players[user_id];
|
||||
let size = Vec2::new(width, screen_height() * 0.05);
|
||||
|
||||
let first_buzzed_in_player = self.get_first_buzzed_in_player();
|
||||
|
||||
let is_first = if let Some(first_player) = first_buzzed_in_player {
|
||||
first_player == user_id
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
ui.group(hash!(user_id), size, |ui| {
|
||||
let player_txt = player.name.clone();
|
||||
let player_txt_size = FontSize::Medium.measure(&player_txt);
|
||||
|
||||
let score_txt = format!("Score: {}", player.score);
|
||||
ui.label(Vec2::new(0.0, 0.0), &player_txt);
|
||||
ui.label(
|
||||
Vec2::new(0.0, player_txt_size.height + size.y * 0.025),
|
||||
&score_txt,
|
||||
);
|
||||
|
||||
if is_first {
|
||||
ui.label(Vec2::new(size.x * 0.45, 0.0), "BUZZED IN");
|
||||
}
|
||||
|
||||
if ui.button(Vec2::new(size.x * 0.70, 0.0), "+") {
|
||||
let msg = HostClientMessages::UpdateScore {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
diff: 1,
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
if ui.button(Vec2::new(size.x * 0.80, 0.0), "-") {
|
||||
let msg = HostClientMessages::UpdateScore {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
diff: -1,
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
if ui.button(Vec2::new(size.x * 0.90, 0.0), "C") {
|
||||
let msg = HostClientMessages::ResetPlayerBuzzer {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn menu_skin() -> Skin {
|
||||
let label_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Medium.size())
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
|
||||
Skin {
|
||||
label_style,
|
||||
..root_ui().default_skin()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_buzzed_in_player(&self) -> Option<String> {
|
||||
self.players
|
||||
.iter()
|
||||
.filter_map(|(id, player)| player.buzz_in_time.map(|buzz_in| (id, buzz_in)))
|
||||
.min_by(|(_, buzz_in), (_, other_buzz_in)| buzz_in.total_cmp(other_buzz_in))
|
||||
.map(|(id, _)| id)
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen for HostScreen {
|
||||
fn handle_frame(&mut self, ctx: &mut crate::context::Context) {
|
||||
clear_background(WHITE);
|
||||
clear_background(BEIGE);
|
||||
|
||||
draw_text(
|
||||
"I'm the host!",
|
||||
screen_width() / 2.0,
|
||||
screen_height() / 2.0,
|
||||
100.0,
|
||||
BLACK,
|
||||
);
|
||||
let width = window_width();
|
||||
|
||||
let button_size = screen_width() * 0.01;
|
||||
let button_location = Vec2::new(screen_width() * 0.50, screen_height() * 0.70);
|
||||
let menu_skin = &Self::menu_skin();
|
||||
root_ui().push_skin(menu_skin);
|
||||
|
||||
let window_position =
|
||||
Vec2::new(screen_width() * 0.5 - (width * 0.5), screen_height() * 0.1);
|
||||
let window_size = Vec2::new(width, screen_height() * 0.70);
|
||||
|
||||
Window::new(hash!("HostWindow"), window_position, window_size)
|
||||
.movable(false)
|
||||
.ui(&mut root_ui(), |ui| {
|
||||
let mut players: Vec<String> = self.players.keys().cloned().collect();
|
||||
players.sort();
|
||||
|
||||
for user_id in &players {
|
||||
self.draw_user(ctx, user_id, ui, width);
|
||||
}
|
||||
});
|
||||
|
||||
let button_size = screen_width() * 0.025;
|
||||
let button_location = Vec2::new(screen_width() * 0.50, screen_height() * 0.90);
|
||||
|
||||
draw_circle(
|
||||
button_location.x,
|
||||
@ -56,6 +145,8 @@ impl Screen for HostScreen {
|
||||
ctx.send_msg(&HostClientMessages::ResetAllBuzers);
|
||||
}
|
||||
}
|
||||
|
||||
root_ui().pop_skin();
|
||||
}
|
||||
|
||||
fn handle_messages(
|
||||
@ -68,6 +159,7 @@ impl Screen for HostScreen {
|
||||
// pass
|
||||
}
|
||||
HostServerMessages::UpdatePlayerStates { players } => {
|
||||
info!("Got new player state: {:?}", players);
|
||||
self.players = players;
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,10 @@
|
||||
use crate::StateTransition;
|
||||
use crate::context::Context;
|
||||
use crate::font::FontSize;
|
||||
use crate::screen::Screen;
|
||||
use crate::ui_scaling::window_width;
|
||||
use macroquad::hash;
|
||||
use macroquad::ui::widgets::Window;
|
||||
use macroquad::{prelude::*, ui::root_ui};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -31,22 +34,24 @@ pub struct LoginScreen {
|
||||
|
||||
impl Screen for LoginScreen {
|
||||
fn handle_frame(&mut self, ctx: &mut Context) {
|
||||
clear_background(WHITE);
|
||||
clear_background(BEIGE);
|
||||
|
||||
let group_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.05);
|
||||
let window_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.25);
|
||||
let group_size = Vec2::new(window_width(), screen_height() * 0.05);
|
||||
let window_size = Vec2::new(window_width(), screen_height() * 0.4);
|
||||
|
||||
let mut pressed_join = false;
|
||||
let mut pressed_host = false;
|
||||
|
||||
root_ui().window(
|
||||
Window::new(
|
||||
hash!("Window"),
|
||||
Vec2::new(
|
||||
screen_width() * 0.5 - window_size.x * 0.5,
|
||||
screen_height() * 0.5 - window_size.x * 0.5,
|
||||
),
|
||||
Vec2::new(screen_width() * 0.25, screen_height() * 0.25),
|
||||
|ui| {
|
||||
window_size,
|
||||
)
|
||||
.movable(false)
|
||||
.ui(&mut root_ui(), |ui| {
|
||||
ui.group(hash!("User Group"), group_size, |ui| {
|
||||
ui.input_text(hash!("Username In"), "Username", &mut self.new_username);
|
||||
});
|
||||
@ -54,7 +59,11 @@ impl Screen for LoginScreen {
|
||||
ui.input_text(hash!("Room Code In"), "Room Code", &mut self.new_room_code);
|
||||
});
|
||||
ui.group(hash!("Join Group"), group_size, |ui| {
|
||||
if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Join") {
|
||||
let dim = FontSize::Large.measure("Join");
|
||||
if ui.button(
|
||||
Vec2::new(group_size.x * 0.50, 0.0) - Vec2::new(dim.width * 0.5, 0.0),
|
||||
"Join",
|
||||
) {
|
||||
info!(
|
||||
"User pressed joined with name={} room_code={}",
|
||||
self.new_username, self.new_room_code
|
||||
@ -64,7 +73,11 @@ impl Screen for LoginScreen {
|
||||
}
|
||||
});
|
||||
ui.group(hash!("Host Group"), group_size, |ui| {
|
||||
if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Host Game") {
|
||||
let dim = FontSize::Large.measure("Host Game");
|
||||
if ui.button(
|
||||
Vec2::new(group_size.x * 0.50, 0.0) - Vec2::new(dim.width * 0.5, 0.0),
|
||||
"Host Game",
|
||||
) {
|
||||
info!(
|
||||
"User pressed host with name={} room_code={}",
|
||||
self.new_username, self.new_room_code
|
||||
@ -79,8 +92,7 @@ impl Screen for LoginScreen {
|
||||
ui.label(Vec2::new(0.0, 0.0), "Failed to join game, try again...");
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if pressed_join {
|
||||
ctx.send_msg(&NewUserClientMessages::Register {
|
||||
|
@ -1,16 +1,22 @@
|
||||
use context::Context;
|
||||
use font::FontSize;
|
||||
use host_screen::HostScreen;
|
||||
use login_screen::LoginScreen;
|
||||
use macroquad::prelude::*;
|
||||
use macroquad::{
|
||||
prelude::*,
|
||||
ui::{Skin, root_ui},
|
||||
};
|
||||
use player_screen::PlayerScreen;
|
||||
use screen::Screen;
|
||||
|
||||
mod context;
|
||||
mod font;
|
||||
mod host_screen;
|
||||
mod login_screen;
|
||||
mod model;
|
||||
mod player_screen;
|
||||
mod screen;
|
||||
mod ui_scaling;
|
||||
|
||||
pub enum State {
|
||||
Init,
|
||||
@ -58,6 +64,38 @@ pub enum StateTransition {
|
||||
LeaveGame,
|
||||
}
|
||||
|
||||
pub fn default_skin() -> Skin {
|
||||
let label_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
let editbox_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.color(BROWN)
|
||||
.color_selected(BROWN)
|
||||
.color_clicked(BROWN)
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
let window_style = root_ui().style_builder().color(DARKBROWN).build();
|
||||
let button_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.color(GOLD)
|
||||
.text_color(BLACK)
|
||||
.color_hovered(YELLOW)
|
||||
.build();
|
||||
|
||||
Skin {
|
||||
label_style,
|
||||
editbox_style,
|
||||
button_style,
|
||||
window_style,
|
||||
..root_ui().default_skin()
|
||||
}
|
||||
}
|
||||
|
||||
#[macroquad::main("Play of the Game")]
|
||||
async fn main() {
|
||||
let mut context = Context::new("ws://127.0.0.1:8080/ws");
|
||||
@ -66,12 +104,16 @@ async fn main() {
|
||||
state.transition_state(StateTransition::Init);
|
||||
|
||||
loop {
|
||||
let skin = default_skin();
|
||||
root_ui().push_skin(&skin);
|
||||
|
||||
if let Some(transition) = state.handle_messages(&mut context) {
|
||||
state = state.transition_state(transition);
|
||||
}
|
||||
|
||||
state.handle_frame(&mut context);
|
||||
|
||||
root_ui().pop_skin();
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ pub struct Player {
|
||||
pub name: String,
|
||||
pub id: u64,
|
||||
pub score: i32,
|
||||
pub buzz_in_time: Option<f64>,
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use web_time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::StateTransition;
|
||||
use crate::context::Context;
|
||||
use crate::font::FontSize;
|
||||
use crate::screen::Screen;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
@ -59,11 +60,24 @@ impl Screen for PlayerScreen {
|
||||
RED
|
||||
};
|
||||
|
||||
let text_measure = FontSize::Large.measure(&ctx.username);
|
||||
|
||||
draw_text(
|
||||
&format!("Score: {}", self.score),
|
||||
screen_width() * 0.5,
|
||||
&ctx.username,
|
||||
screen_width() * 0.5 - text_measure.width * 0.5,
|
||||
screen_height() * 0.15,
|
||||
FontSize::Large.size() as f32,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
let score_text = format!("Score: {}", self.score);
|
||||
let text_measure = FontSize::Large.measure(&score_text);
|
||||
|
||||
draw_text(
|
||||
&score_text,
|
||||
screen_width() * 0.5 - text_measure.width * 0.5,
|
||||
screen_height() * 0.25,
|
||||
50.0,
|
||||
FontSize::Large.size() as f32,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
|
5
frontend/src/ui_scaling.rs
Normal file
5
frontend/src/ui_scaling.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use macroquad::window::{screen_height, screen_width};
|
||||
|
||||
pub fn window_width() -> f32 {
|
||||
(screen_width() * 0.8).min(screen_height() * 0.5)
|
||||
}
|
36
shared/src/login.gleam
Normal file
36
shared/src/login.gleam
Normal file
@ -0,0 +1,36 @@
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
|
||||
pub type LoginRequest {
|
||||
LoginRequest(username: String, game_code: String)
|
||||
}
|
||||
|
||||
pub fn encode_login_request(login_request: LoginRequest) -> json.Json {
|
||||
json.object([
|
||||
#("username", json.string(login_request.username)),
|
||||
#("game_code", json.string(login_request.game_code)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn login_request_decoder() -> decode.Decoder(LoginRequest) {
|
||||
use username <- decode.field("username", decode.string)
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
decode.success(LoginRequest(username:, game_code:))
|
||||
}
|
||||
|
||||
pub type LoginResponse {
|
||||
LoginResponse(username: String, token: String)
|
||||
}
|
||||
|
||||
pub fn encode_login_response(login_response: LoginResponse) -> json.Json {
|
||||
json.object([
|
||||
#("username", json.string(login_response.username)),
|
||||
#("token", json.string(login_response.token)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn login_response_decoder() -> decode.Decoder(LoginResponse) {
|
||||
use username <- decode.field("username", decode.string)
|
||||
use token <- decode.field("token", decode.string)
|
||||
decode.success(LoginResponse(username:, token:))
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option
|
||||
|
||||
pub type Player {
|
||||
Player(name: String, id: Int, score: Int)
|
||||
Player(name: String, id: Int, score: Int, buzz_in_time: option.Option(Float))
|
||||
}
|
||||
|
||||
pub fn serialize(player: Player) -> json.Json {
|
||||
@ -10,14 +11,17 @@ pub fn serialize(player: Player) -> json.Json {
|
||||
#("name", json.string(player.name)),
|
||||
#("id", json.int(player.id)),
|
||||
#("score", json.int(player.score)),
|
||||
#("buzz_in_time", json.nullable(player.buzz_in_time, json.float)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn decoder() -> decode.Decoder(Player) {
|
||||
{
|
||||
use name <- decode.field("name", decode.string)
|
||||
use id <- decode.field("id", decode.int)
|
||||
use score <- decode.field("score", decode.int)
|
||||
decode.success(Player(name:, id:, score:))
|
||||
}
|
||||
use buzz_in_time <- decode.field(
|
||||
"buzz_in_time",
|
||||
decode.optional(decode.float),
|
||||
)
|
||||
decode.success(Player(name:, id:, score:, buzz_in_time:))
|
||||
}
|
||||
|
@ -2,9 +2,15 @@ import gleam/bit_array
|
||||
import gleam/crypto
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option.{type Option}
|
||||
|
||||
pub type PlayerSession {
|
||||
PlayerSession(id: Int, token_hash: String, username: String, socket_id: Int)
|
||||
PlayerSession(
|
||||
id: Int,
|
||||
token_hash: String,
|
||||
username: String,
|
||||
socket_id: Option(Int),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
|
||||
@ -12,7 +18,10 @@ pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
|
||||
#("id", json.int(player_session.id)),
|
||||
#("token_hash", json.string(player_session.token_hash)),
|
||||
#("username", json.string(player_session.username)),
|
||||
#("socket_id", json.int(player_session.socket_id)),
|
||||
#("socket_id", case player_session.socket_id {
|
||||
option.None -> json.null()
|
||||
option.Some(value) -> json.int(value)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
@ -20,12 +29,11 @@ pub fn player_session_decoder() -> decode.Decoder(PlayerSession) {
|
||||
use id <- decode.field("id", decode.int)
|
||||
use token_hash <- decode.field("token_hash", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
use socket_id <- decode.field("socket_id", decode.int)
|
||||
use socket_id <- decode.field("socket_id", decode.optional(decode.int))
|
||||
decode.success(PlayerSession(id:, token_hash:, username:, socket_id:))
|
||||
}
|
||||
|
||||
pub fn hash_token(token: String) -> String {
|
||||
let token =
|
||||
crypto.new_hasher(crypto.Sha512)
|
||||
|> crypto.hash_chunk(bit_array.from_string(token))
|
||||
|> crypto.digest
|
||||
|
@ -1,5 +0,0 @@
|
||||
import score_update
|
||||
|
||||
pub type UpdateScore {
|
||||
BuzzIn(user_id: Int, score_update: score_update.ScoreUpdateType)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user