diff --git a/.gitignore b/.gitignore index 3746e40..c47e23d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ build/ .history/ -.vs-code/ +.vscode/ .clangd/ + .env compile_commands.json diff --git a/client/index.html b/client/index.html index 19fdaf1..0c4efe6 100644 --- a/client/index.html +++ b/client/index.html @@ -22,16 +22,26 @@ . + +
+
-
-
Name:
- +
+
+
Name
+ +
-
+
+
+ SCOREBOARD +
+
+
diff --git a/client/input.js b/client/input.js index 1890edd..70aaacc 100644 --- a/client/input.js +++ b/client/input.js @@ -30,7 +30,9 @@ class Input { this.onInput = onInput; this.maxDragLength = maxDragLength; - element.addEventListener("touchstart", this.onTouchStart.bind(this)); + element.addEventListener("touchstart", this.onTouchStart.bind(this), { + passive: false, + }); element.addEventListener("touchend", this.onTouchEnd.bind(this)); element.addEventListener( "touchmove", diff --git a/client/main.css b/client/main.css index 0b84f47..05ace1f 100644 --- a/client/main.css +++ b/client/main.css @@ -6,6 +6,9 @@ body { background-repeat: no-repeat; background-attachment: fixed; background-size: 100% 100%; + font-size: 175%; + font-family: "Roboto Mono"; + color: white; } /* hide the element used to preload the fonts */ @@ -17,20 +20,15 @@ body { position: absolute; } -#container { - display: flex; - flex-direction: row; - flex-wrap: nowrap; -} - #canvas { position: absolute; z-index: -1; } -#inner-canvas { - width: 100px; - height: 100%; +#container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; } #details { @@ -38,25 +36,59 @@ body { height: 100%; padding: 20px; box-sizing: border-box; - color: white; - font-size: 125%; - font-family: "Roboto Mono"; +} + +#name-flex-container { + display: flex; + justify-content: center; +} + +#input-name-label { + padding-right: 20px; + font-size: 100%; } #input-name { width: 100%; max-width: 400px; - color: white; - font-size: 100%; - font-family: "Roboto Mono"; - background-color: black; - border: 2px solid rgb(255, 255, 255); + padding: 2px; + background-color: rgba(0, 0, 0, 0); + border: 1px solid rgb(255, 255, 255); border-radius: 2px; + color: white; + font-size: 90%; } #scoreboard { - margin-top: 20px; - background-color: rgb(50, 50, 50); + width: 100%; + max-width: 500px; + min-height: 100px; + margin: 20px auto 0px auto; + box-sizing: border-box; + border-radius: 2px; + font-size: smaller; +} + +.scoreboard-title { + font-weight: bold; + padding-left: 20px; +} + +.scoreboard-line { + overflow: hidden; + display: flex; +} + +.scoreboard-name { + width: 75%; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; +} + +.scoreboard-score { + width: 25%; + text-align: right; } .unselectable { diff --git a/client/main.js b/client/main.js index 2353727..b2378e4 100644 --- a/client/main.js +++ b/client/main.js @@ -226,6 +226,8 @@ class GameEngine { this.inputRefY = null; this.gameIsOver = false; this.canvas = canvasManager; + this.scoreboardSize = 5; + this.createScoreboard(); this.sock = new WebSocket(url); @@ -286,6 +288,7 @@ class GameEngine { return; } + this.updateScoreboard(msg.scoreboard); this.canvas.clear(); this.canvas.drawTicksPerSecond(); this.canvas.drawLimits(); @@ -328,6 +331,45 @@ class GameEngine { this.gameIsOver = false; this.send({ command: { respawn: true } }); } + + createScoreboard() { + let parent = document.getElementById("scoreboard-body"); + parent.innerHTML = ""; + + for (let idx = 0; idx < this.scoreboardSize; ++idx) { + let row = document.createElement("div"); + row.classList.add("scoreboard-line"); + + let name = document.createElement("div"); + name.classList.add("scoreboard-name"); + name.id = "scoreboard-player-name-" + idx; + row.appendChild(name); + + let score = document.createElement("div"); + score.classList.add("scoreboard-score"); + score.id = "scoreboard-player-score-" + idx; + row.appendChild(score); + + parent.appendChild(row); + } + } + + updateScoreboard(scoreboard) { + function update(elementId, value) { + let element = document.getElementById(elementId); + if (element.innerText != value) { + element.innerText = value; + } + } + for ( + let idx = 0; + idx < Math.min(this.scoreboardSize, scoreboard.length); + ++idx + ) { + update("scoreboard-player-name-" + idx, scoreboard[idx].name); + update("scoreboard-player-score-" + idx, scoreboard[idx].score.toFixed()); + } + } } class GameManager { diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index e514ea2..c9f3b95 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -39,6 +39,7 @@ if (NOT CONAN_ONLY) session.cpp player.cpp world.cpp + scoreboard.cpp ) target_link_libraries( server diff --git a/server/config.h b/server/config.h index 850b29e..44fed65 100644 --- a/server/config.h +++ b/server/config.h @@ -26,6 +26,7 @@ class session_t; class listener_t; class world_t; class player_t; +class scoreboard_t; using player_id_t = boost::uuids::uuid; using player_handle_t = std::unique_ptr>; diff --git a/server/main.cpp b/server/main.cpp index c2c8c24..da6c0d9 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -2,6 +2,7 @@ #include #include "listener.h" +#include "scoreboard.h" #include "world.h" using namespace sd; @@ -38,9 +39,10 @@ int main(int argc, char* argv[]) // The io_context is required for all I/O net::io_context ioc{1}; + auto scoreboard = std::make_shared(); std::vector> worlds; for (int i = 0; i < nworlds; ++i) { - worlds.emplace_back(std::make_shared(ioc)); + worlds.emplace_back(std::make_shared(ioc, scoreboard)); worlds.back()->run(); } std::make_shared(ioc, std::move(worlds), tcp::endpoint{address, port}) diff --git a/server/scoreboard.cpp b/server/scoreboard.cpp new file mode 100644 index 0000000..296fbf7 --- /dev/null +++ b/server/scoreboard.cpp @@ -0,0 +1,62 @@ +#include "scoreboard.h" +#include "player.h" + +namespace sd { + +const score_t& scoreboard_t::lowest_score() const +{ + return scores_.back(); +} + +void scoreboard_t::push_score(const score_t& score) +{ + if (!scores_.empty() && score.score <= lowest_score().score) { + return; + } + + bool inserted = false; + for (auto it = begin(scores_); it < end(scores_); ++it) { + if (!inserted) { + if (score.score > it->score) { + inserted = true; + + if (score.id == it->id) { + // just replace and bail out + *it = score; + break; + } + else { + // insert, shift the other scores + scores_.insert(it, score); + } + } + if (score.id == it->id) { + // same player already has a better score + break; + } + } + else { + if (score.id == it->id) { + // remove previous highest score + scores_.erase(it--); + } + } + } + + if (!inserted && scores_.size() < max_size) { + scores_.push_back(score); + } + + while (scores_.size() > max_size) { + scores_.pop_back(); + } +} + +void scoreboard_t::push_score(const player_t& player) +{ + if (scores_.empty() || player.score() > lowest_score().score) { + return push_score({player.id(), player.name(), player.score()}); + } +} + +} // sd diff --git a/server/scoreboard.h b/server/scoreboard.h new file mode 100644 index 0000000..f59a1a5 --- /dev/null +++ b/server/scoreboard.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "config.h" + +namespace sd { + +struct score_t { + player_id_t id; + std::string name; + double score; +}; + +class scoreboard_t { +public: + static constexpr auto max_size = 10; + + const std::vector& scores() const { return scores_; } + void push_score(const score_t& score); + void push_score(const player_t& player); + +private: + const score_t& lowest_score() const; + + std::vector scores_; +}; + +} // sd \ No newline at end of file diff --git a/server/world.cpp b/server/world.cpp index d9227d9..c99b772 100644 --- a/server/world.cpp +++ b/server/world.cpp @@ -1,5 +1,6 @@ #include "world.h" #include "player.h" +#include "scoreboard.h" #include @@ -38,7 +39,10 @@ std::string get_fake_player_name( } -world_t::world_t(net::io_context& ioc) : ioc_{ioc} {} +world_t::world_t(net::io_context& ioc, std::shared_ptr scoreboard) + : ioc_{ioc}, scoreboard_{scoreboard} +{ +} world_t::~world_t() = default; @@ -149,7 +153,10 @@ void world_t::run() nlohmann::json world_t::game_state_for_player(const player_handle_t& player) { - nlohmann::json state = {{"players", nlohmann::json::array()}}; + nlohmann::json state = { + {"players", nlohmann::json::array()}, + {"scoreboard", nlohmann::json::array()}, + }; bool game_over = false; transform( @@ -166,7 +173,6 @@ nlohmann::json world_t::game_state_for_player(const player_handle_t& player) p->lifetime()) .count(); return nlohmann::json({ - {"id", p->id()}, {"name", p->name()}, {"x", p->state().x}, {"y", p->state().y}, @@ -184,6 +190,18 @@ nlohmann::json world_t::game_state_for_player(const player_handle_t& player) }); state["game_over"] = game_over; + + transform( + begin(scoreboard_->scores()), + end(scoreboard_->scores()), + back_inserter(state["scoreboard"]), + [&](const score_t& score) { + return nlohmann::json({ + {"name", score.name}, + {"score", score.score}, + }); + }); + return state; } @@ -221,10 +239,12 @@ void world_t::update(std::chrono::nanoseconds dt) continue; } + // kill players that are outside the world if (!player.is_in_world()) { player.kill(); } + // compute collisions for (auto other_it = ++decltype(player_it)(player_it); other_it != end(players_); ++other_it) { @@ -242,6 +262,11 @@ void world_t::update(std::chrono::nanoseconds dt) player.kill(); } } + + // update scoreboard + if (!player.fake()) { + scoreboard_->push_score(player); + } } // remove killed fake players diff --git a/server/world.h b/server/world.h index d656035..24f8ba0 100644 --- a/server/world.h +++ b/server/world.h @@ -25,7 +25,7 @@ class world_t : public std::enable_shared_from_this { static constexpr auto refresh_dt = std::chrono::milliseconds{20}; static constexpr std::size_t max_players = 8; - world_t(net::io_context& ioc); + world_t(net::io_context& ioc, std::shared_ptr scorboard); ~world_t(); void run(); @@ -52,6 +52,7 @@ class world_t : public std::enable_shared_from_this { std::vector> players_; std::list fake_players_; boost::uuids::random_generator uuid_generator_; + std::shared_ptr scoreboard_; }; } // sd