diff --git a/server/Cargo.toml b/server/Cargo.toml
index 9931c8d..ce37c1c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -20,6 +20,7 @@ futures = "0.3.30"
rust-argon2 = "2.1.0"
ring = "0.17.8"
const_format = "0.2.32"
+markdown = "0.3.0"
game = { path = "../game" }
diff --git a/server/src/main.rs b/server/src/main.rs
index 08d96e5..01129e4 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -186,6 +186,26 @@ async fn main()
}
}
+ let about_path = std::path::Path::new("www/about");
+ for doc in [
+ "main",
+ ] {
+ if cache.cache_md(&format!("/about/{}.html", doc), about_path.join(format!("{}.md", doc))).is_err() {
+ println!("error: failed to load: {}", doc);
+ }
+ }
+
+ let guide_path = std::path::Path::new("www/guide");
+ for doc in [
+ "game",
+ "pieces",
+ "interface",
+ ] {
+ if cache.cache_md(&format!("/guide/{}.html", doc), guide_path.join(format!("{}.md", doc))).is_err() {
+ println!("error: failed to load: {}", doc);
+ }
+ }
+
let mut tcp_server = TcpServer::new();
match tcp_server.bind("127.0.0.1:38611").await {
Ok(_) => {
diff --git a/server/src/system/cache/mod.rs b/server/src/system/cache/mod.rs
index 8d7d992..45036c1 100644
--- a/server/src/system/cache/mod.rs
+++ b/server/src/system/cache/mod.rs
@@ -77,6 +77,28 @@ impl WebCache {
}
}
+ pub fn cache_md
>(&self, object:&str, path:P) -> Result<(),()>
+ {
+ match self.data.write() {
+ Ok(mut writer) => {
+ match markdown::file_to_html(&path.as_ref()) {
+ Ok(text) => {
+ let text :Vec = text.trim().lines().map(|line| line.trim().to_string()).collect();
+ let data = text.concat().as_bytes().to_vec();
+
+ writer.objects.set(object.as_bytes(), CacheData {
+ mime:String::from("text/html"),
+ data,
+ });
+ Ok(())
+ }
+ Err(_) => Err(())
+ }
+ }
+ Err(_) => Err(()),
+ }
+ }
+
pub fn cache_whitespace_minimize(&self, object:&str) -> Result<(),()>
{
match self.data.write() {
diff --git a/www/about/main.md b/www/about/main.md
new file mode 100644
index 0000000..cdd51b9
--- /dev/null
+++ b/www/about/main.md
@@ -0,0 +1,13 @@
+# About
+
+Omen is a work of [Project Kirisame](https://kirisame.com).
+
+ - [Project License](https://ykr.info/license)
+
+© 2024 Yukiri Corporation
+
+
+## User Privacy
+
+This website does not collect any information beyond that used to implement user accounts and gameplay.
+Information is not provided to third parties except in serving the application to clients.
diff --git a/www/css/main.css b/www/css/main.css
index eac6cff..bc6b389 100644
--- a/www/css/main.css
+++ b/www/css/main.css
@@ -107,6 +107,7 @@ main>nav{
justify-content:flex-start;
width:100%;
height:3rem;
+ flex-shrink: 0;
background-color:#282828;
border-bottom:1px solid #404040;
diff --git a/www/css/ui.css b/www/css/ui.css
index 0c1e5c6..e6e0a80 100644
--- a/www/css/ui.css
+++ b/www/css/ui.css
@@ -52,3 +52,64 @@ main>table.list td:last-child>button{
main>table.list td:last-child>button:hover{
background-color:#343434;
}
+
+main>article{
+ display: block;
+ position: relative;
+ width: 100%;
+ flex-grow: 1;
+
+ padding: 1rem;
+ margin: 0;
+
+ line-height: 1.4em;
+ font-size: 1.2rem;
+ color: #c0c0c0;
+
+ overflow: auto;
+}
+main>article>h1{
+ padding: 0.5rem;
+ margin: 0 0 0.5rem 0;
+
+ font-size: 1.8rem;
+ font-weight: bold;
+ background-color: #a0a0a0;
+ color: #202020;
+}
+main>article>h2{
+ padding: 0 0 0.5rem 0.25rem;
+ margin: 1.5rem 0 0.5rem 0;
+
+ font-size: 1.6rem;
+ font-weight: bold;
+ border-bottom: 2px solid #c0c0c0;
+ color: #c0c0c0;
+}
+main>article>h3{
+ padding: 0 0 0 0.25rem;
+ margin: 1.5rem 0 0.5rem 0;
+
+ font-size: 1.5rem;
+ font-weight: bold;
+ text-decoration: underline;
+ color: #b0b0b0;
+}
+main>article>h4{
+ padding: 0 0 0 0.25rem;
+ margin: 1.5rem 0 0.5rem 0;
+
+ font-size: 1.3rem;
+ text-decoration: underline;
+ color: #a0a0a0;
+}
+main>article>p{
+ padding: 0 0.5rem 0 0.5rem;
+}
+main>article a{
+ color: #db758e;
+ text-decoration: none;
+}
+main>article>a:hover{
+ text-decoration: underline;
+}
diff --git a/www/guide/game.md b/www/guide/game.md
new file mode 100644
index 0000000..7dacb1f
--- /dev/null
+++ b/www/guide/game.md
@@ -0,0 +1,78 @@
+# Game
+
+Omen is an abstract strategy game, similar to chess and shogi, in which players take turns moving and placing pieces on a hexagon grid to capture the opponent's king (omen).
+
+
+## Setup
+
+### Board
+
+Omen is played on a hexagon grid that is 9 tiles across and 5 tiles on each side, with opposite corners facing the players, such that the hexagon tiles form columns between them.
+
+ - Each player has 9 Militia placed between the 2nd ranks of the edges, leaving one or two tiles between opposing Militia.
+ - The back line has the Omen at the center, flanked on either side by first Towers, then Castles, Knights, and Lances, such that the Lances are in the adjacent corners.
+ - The Behemoth is placed in front of the Omen and the Dragon in front of the Behemoth.
+
+
+### Turns
+
+At the start of the game, each player is assigned either the Dawn or Dusk seat—either randomly or alternating, if more than one game is played.
+
+Players take turns making actions on the board until the game is over, with the Dawn player making the first move.
+
+
+## Actions
+
+Each turn, the acting player must make one of three actions on the board: Move, Swap, or Drop.
+
+
+### Move
+
+Each piece has a specific set of moves it may make, with each move falling into one of two categories: Direct and Stride.
+
+Direct moves are limited to a single tile in the given direction but are not blocked by other pieces.
+Stride moves are not limited in distance but may not jump over other pieces.
+
+Pieces may move onto any tile that is empty, contains an opposing piece, or contains a swappable piece.
+
+
+#### Capturing
+
+If a piece is moved onto the tile of an opposing piece, the opposing piece is captured and added to the capturing player's pool.
+
+
+#### Promotion
+
+A piece that lands on the opponent's back line is promoted, gaining a new move set.
+
+
+### Swap
+
+Two pieces that can each jump to each others tiles may swap places as a single action.
+
+
+### Drop
+
+A player may place a piece from their pool back onto the board as their own.
+
+
+#### Militia Stacking
+
+Militia may not be placed onto a column in which the player already has a Militia, with the exception of promoted Militia.
+
+
+#### Off-sides
+
+A piece may not be placed behind the first opposing piece in a column.
+
+
+## Check
+
+If an opposing piece can make a move that captures your omen, then you are in check.
+
+While in check, a player may only make actions that remove check.
+
+
+### Checkmate
+
+If a player is in check and can make no valid action, then the game is over and the checking player wins.
diff --git a/www/guide/interface.md b/www/guide/interface.md
new file mode 100644
index 0000000..6e8995f
--- /dev/null
+++ b/www/guide/interface.md
@@ -0,0 +1,16 @@
+# Interface
+
+## Game
+
+### Board
+
+
+### Pools
+
+
+### Indicators
+
+ - The player's turn is indicated by the presence of borders around the player's pool tiles.
+ - The turn counter is displayed in the top-right corner.
+ - The status of the game may be displayed in the bottom-left corner.
+ - If the user hovers over a tile, the position and piece are displayed in the top-left corner.
diff --git a/www/guide/pieces.md b/www/guide/pieces.md
new file mode 100644
index 0000000..281ee51
--- /dev/null
+++ b/www/guide/pieces.md
@@ -0,0 +1,69 @@
+# Pieces
+
+## Militia
+
+Militia are the primary offensive pieces, capable of moving in the three forward directions.
+
+### Promotion
+
+The promoted Militia may move onto any adjacent tile.
+
+
+## Lance
+
+The Lance is similar to the Militia but may move any number of tiles in the forward direction.
+
+### Promotion
+
+The promoted Lance may move onto any adjacent tile, similar to the Militia.
+
+
+## Knight
+
+The Knight may move to the forward and back diagonal tiles, as well as the second adjacent side tiles.
+
+
+### Promotion
+
+The Knight has no promotion.
+
+
+## Tower
+
+The Tower may move to the three forward adjacent tiles, two forward diagonal tiles, and back tile.
+
+### Promotion
+
+The promoted Tower may additionally move backwards with its forward move set.
+
+
+## Castle
+
+The Castle may move onto any of the adjacent tiles, except back, as well as to the diagonal sides.
+
+### Promotion
+
+The promoted Castle gains movement onto the back tile and may move any number of tiles to the diagonal sides.
+
+
+## Dragon
+
+The Dragon may move any number of tiles in the diagonal directions.
+
+### Promotion
+
+The promoted Dragon may additionally move to any of the adjacent tiles.
+
+
+## Behemoth
+
+The Behemoth may move any number of tiles in the adjacent directions.
+
+### Promotion
+
+The promoted Behemoth may additionally move to the second tiles in the adjacent directions.
+
+
+## Omen
+
+The Omen may move onto any of the adjacent tiles, as wella s to the diagonal sides.
diff --git a/www/js/scene.js b/www/js/scene.js
index bb3c617..de8d40c 100644
--- a/www/js/scene.js
+++ b/www/js/scene.js
@@ -189,16 +189,8 @@ const SCENES = {
};
UI.mainmenu("browse");
-
- let left_buttons = [ ];
- if(CONTEXT.Auth !== null) {
- left_buttons.push(UI.button("Start", () => {
- MESSAGE_SESSION_START();
- }));
- }
-
- UI.mainnav(
- left_buttons,
+ UI.mainnav_std(
+ [ ],
[
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
@@ -251,16 +243,8 @@ const SCENES = {
};
UI.mainmenu("continue");
-
- let left_buttons = [ ];
- if(CONTEXT.Auth !== null) {
- left_buttons.push(UI.button("Start", () => {
- MESSAGE_SESSION_START();
- }));
- }
-
- UI.mainnav(
- left_buttons,
+ UI.mainnav_std(
+ [ ],
[
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
@@ -313,16 +297,8 @@ const SCENES = {
};
UI.mainmenu("join");
-
- let left_buttons = [ ];
- if(CONTEXT.Auth !== null) {
- left_buttons.push(UI.button("Start", () => {
- MESSAGE_SESSION_START();
- }));
- }
-
- UI.mainnav(
- left_buttons,
+ UI.mainnav_std(
+ [ ],
[
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
@@ -373,16 +349,8 @@ const SCENES = {
};
UI.mainmenu("live");
-
- let left_buttons = [ ];
- if(CONTEXT.Auth !== null) {
- left_buttons.push(UI.button("Start", () => {
- MESSAGE_SESSION_START();
- }));
- }
-
- UI.mainnav(
- left_buttons,
+ UI.mainnav_std(
+ [ ],
[
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
@@ -433,11 +401,8 @@ const SCENES = {
};
UI.mainmenu("history");
-
- let left_buttons = [ ];
-
- UI.mainnav(
- left_buttons,
+ UI.mainnav_std(
+ [ ],
[
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
@@ -483,18 +448,28 @@ const SCENES = {
load() {
UI.mainmenu("guide");
UI.mainnav([
- UI.button("Game", () => { }),
- UI.button("Interface", () => { }),
- UI.button("Pieces", () => { }),
- UI.button("Dropping", () => { }),
+ UI.button("Game", () => { SCENE.refresh("game.html"); }),
+ UI.button("Pieces", () => { SCENE.refresh("pieces.html"); }),
+ UI.button("Interface", () => { SCENE.refresh("interface.html"); }),
], []);
-
+ let body = document.createElement("article");
+ body.setAttribute("id", "article");
+ MAIN.appendChild(body);
+ this.refresh("game.html");
return true;
},
- refresh() {
-
+ refresh(page) {
+ fetch("/guide/" + page)
+ .then((response) => {
+ return response.text();
+ })
+ .then((text) => {
+ let parser = new DOMParser();
+ let body = parser.parseFromString(text, "text/html");
+ document.getElementById("article").innerHTML = body.body.innerHTML;
+ });
},
},
@@ -503,12 +478,23 @@ const SCENES = {
UI.mainmenu("about");
UI.mainnav([], []);
-
+ let body = document.createElement("article");
+ body.setAttribute("id", "article");
+ MAIN.appendChild(body);
+ this.refresh("main.html");
return true;
},
- refresh() {
-
+ refresh(page) {
+ fetch("/about/" + page)
+ .then((response) => {
+ return response.text();
+ })
+ .then((text) => {
+ let parser = new DOMParser();
+ let body = parser.parseFromString(text, "text/html");
+ document.getElementById("article").innerHTML = body.body.innerHTML;
+ });
},
},
@@ -590,19 +576,11 @@ const SCENES = {
UI.button("Mirror", () => { INTERFACE.mirror(); }),
], buttons_bottom);
-
- let left_buttons = [ ];
- if(CONTEXT.Auth !== null) {
- left_buttons.push(UI.button("Start", () => {
- MESSAGE_SESSION_START();
- }));
- }
-
let ind_turn = UI.div([UI.text("0 of 0")]);
ind_turn.setAttribute("id", "ind_turn");
UI.mainnav(
- left_buttons,
+ [ ],
[
ind_turn,
UI.button("◀", () => { INTERFACE.replay_prev(); }),
diff --git a/www/js/ui.js b/www/js/ui.js
index 625faf1..db8ec65 100644
--- a/www/js/ui.js
+++ b/www/js/ui.js
@@ -82,11 +82,28 @@ const UI = {
mainnav(left_children, right_children) {
let header = document.createElement("nav");
+
+ let left = document.createElement("section");
+ for(child of left_children) { left.appendChild(child); }
+
+ let right = document.createElement("section");
+ for(child of right_children) { right.appendChild(child); }
+
+ header.appendChild(left);
+ header.appendChild(right);
+
+ MAIN.appendChild(header);
+ },
+
+ mainnav_std(left_children, right_children) {
+ let header = document.createElement("nav");
let left = document.createElement("section");
if(CONTEXT.Auth === null) {
- left.appendChild(UI.button("Register", () => { LOAD_STACK(SCENES.Register) }));
- left.appendChild(UI.button("Log In", () => { LOAD_STACK(SCENES.Authenticate) }));
+ left.appendChild(UI.button("Register", () => { LOAD_STACK(SCENES.Register); }));
+ left.appendChild(UI.button("Log In", () => { LOAD_STACK(SCENES.Authenticate); }));
+ } else {
+ left.push(UI.button("Start", () => { MESSAGE_SESSION_START(); }));
}
for(child of left_children) { left.appendChild(child); }