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 @@
+
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