Remove wasm; add js client.

This commit is contained in:
yukirij 2024-08-08 00:37:58 -07:00
parent 26fe6c86e7
commit 7ea088737d
21 changed files with 406 additions and 400 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
/data
Cargo.lock
*.pem

View File

@ -3,6 +3,5 @@ resolver = "2"
members = [
"game",
"client-web",
"server",
]

View File

@ -1,24 +0,0 @@
[package]
name = "client-web"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.92"
game = { path = "../game" }
[dependencies.web-sys]
version = "0.3.69"
features = [
'Document',
'Element',
'HtmlElement',
'Node',
'Window',
'Event',
]

View File

@ -1,67 +0,0 @@
use crate::prelude::Scene;
pub mod session; use session::Session;
pub mod scene;
pub struct App {
pub session:Session,
pub document:web_sys::Document,
pub menu:web_sys::Element,
pub main:web_sys::Element,
unload:Option<fn()>,
}
impl App {
pub fn init() -> Result<Self,()>
{
match web_sys::window() {
Some(window) => match window.document() {
Some(document) => {
let menu = document.create_element("nav");
let container = document.create_element("main");
if menu.is_ok() && container.is_ok() {
Ok(Self {
session:Session::new(),
document:document,
menu:menu.unwrap(),
main:container.unwrap(),
unload:None,
})
} else {
Err(())
}
}
None => Err(())
}
None => Err(())
}
}
pub fn load<S:Scene>(&mut self)
{
match self.unload {
Some(proc) => { proc(); }
None => { }
}
self.document_clear();
S::load();
self.unload = Some(S::unload);
}
pub fn document_clear(&self)
{
match self.document.body() {
Some(body) => {
while let Some(child) = body.last_child() {
body.remove_child(&child).ok();
}
}
None => { }
}
}
}

View File

@ -1,79 +0,0 @@
use crate::{
scene::{ui, util},
session::SessionState,
APP,
};
/* List Online
**
** Displays a table of recent, ongoing contests.
*/
pub struct SceneListOnline { }
impl crate::prelude::Scene for SceneListOnline {
fn load()
{
let mut app = unsafe {APP.borrow_mut()};
match &mut *app {
Some(app) => {
util::load_mainmenu();
// Create header
match app.document.create_element("header") {
Ok(header) => {
// Left-side header
match app.document.create_element("section") {
Ok(section_left) => {
// Button: Start
if app.session.state == SessionState::User {
match ui::button(&app.document, "Start", |_e|{ }) {
Ok(button) => { section_left.append_child(&button).ok(); }
Err(_) => { }
}
}
header.append_child(&section_left).ok();
}
Err(_) => { }
}
// Right-side header
match app.document.create_element("section") {
Ok(section_right) => {
// Button: Previous Page
match ui::button(&app.document, "", |_e|{ }) {
Ok(button) => { section_right.append_child(&button).ok(); }
Err(_) => { }
}
// Text: Page Number
match ui::button(&app.document, "", |_e|{ }) {
Ok(elem) => {
elem.set_id("pn");
section_right.append_child(&elem).ok();
}
Err(_) => { }
}
// Button: Next Page
match ui::button(&app.document, "", |_e|{ }) {
Ok(button) => { section_right.append_child(&button).ok(); }
Err(_) => { }
}
}
Err(_) => { }
}
app.main.append_child(&header).ok();
}
Err(_) => { }
}
}
None => { }
}
}
}

View File

@ -1,3 +0,0 @@
pub mod util;
pub mod ui;
mod list_online; pub use list_online::*;

View File

@ -1,27 +0,0 @@
use wasm_bindgen::{prelude::Closure, JsCast};
pub fn button(document:&web_sys::Document, text:&str, callback:fn(web_sys::Event)) -> Result<web_sys::Element,()>
{
match document.create_element("button") {
Ok(element) => {
element.set_text_content(Some(text));
let cl = Closure::wrap(Box::new(callback) as Box<dyn FnMut(_)>);
element.add_event_listener_with_callback("click", cl.as_ref().unchecked_ref()).ok();
cl.forget();
Ok(element)
}
Err(_) => Err(())
}
}
pub fn text_block(document:&web_sys::Document, text:&str) -> Result<web_sys::Element,()>
{
match document.create_element("div") {
Ok(element) => {
element.set_text_content(Some(text));
Ok(element)
}
Err(_) => Err(())
}
}

View File

@ -1,56 +0,0 @@
use crate::{
scene::{self, ui},
session::SessionState,
APP
};
pub fn load_mainmenu()
{
let mut app = unsafe {APP.borrow_mut()};
match &mut *app {
Some(app) => {
if app.session.state == SessionState::User {
// Button: Online
match ui::button(&app.document, "Online", |_e|{
let mut app = unsafe {APP.borrow_mut()};
match &mut *app {
Some(app) => { app.load::<scene::SceneListOnline>(); }
None => { }
}
}) {
Ok(button) => { app.menu.append_child(&button).ok(); }
Err(_) => { }
}
}
// Button: Continue
match ui::button(&app.document, "Continue", |_e|{
let mut app = unsafe {APP.borrow_mut()};
match &mut *app {
Some(app) => { app.load::<scene::SceneListOnline>(); }
None => { }
}
}) {
Ok(button) => { app.menu.append_child(&button).ok(); }
Err(_) => { }
}
// Button: Join
match ui::button(&app.document, "Continue", |_e|{
let mut app = unsafe {APP.borrow_mut()};
match &mut *app {
Some(app) => { app.load::<scene::SceneListOnline>(); }
None => { }
}
}) {
Ok(button) => { app.menu.append_child(&button).ok(); }
Err(_) => { }
}
} None => { }
}
}

View File

@ -1,23 +0,0 @@
#[derive(Clone, Copy, PartialEq)]
pub enum SessionState {
Anonymous,
User,
}
pub struct Session {
pub state:SessionState,
pub handle:String,
pub token:[u8; 8],
pub secret:[u8; 16],
}
impl Session {
pub fn new() -> Self
{
Self {
state:SessionState::Anonymous,
handle:String::new(),
token:[0; 8],
secret:[0; 16],
}
}
}

View File

@ -1,4 +0,0 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" { }

View File

@ -1,32 +0,0 @@
#![allow(dead_code)]
use std::cell::RefCell;
use wasm_bindgen::prelude::*;
mod ext;
mod prelude;
mod app; use app::*;
static mut APP :RefCell<Option<App>> = RefCell::new(None);
#[wasm_bindgen(start)]
fn main() -> Result<(),JsValue>
{
match App::init() {
Ok(app) => {
unsafe {
APP = RefCell::new(Some(app));
let mut app = APP.borrow_mut();
match &mut *app {
Some(app) => {
app.load::<scene::SceneListOnline>();
} None => { }
}
}
}
Err(_) => { }
}
Ok(())
}

View File

@ -1,8 +0,0 @@
pub trait Scene {
fn load();
fn unload() { }
fn hover() { }
fn click() { }
fn keydown() { }
}

View File

@ -15,6 +15,7 @@ webpki-roots = "0.26"
opaque-ke = "2.0.0"
hyper = { version = "1.4.1", features = ["full"] }
hyper-util = { version = "0.1.7", features = ["tokio"] }
http-body-util = "0.1.2"
game = { path = "../game" }

View File

@ -8,6 +8,7 @@ mod system;
mod protocol;
use app::App;
use hyper::body::Bytes;
use system::{cache::WebCache, net::Stream};
use tokio_stream::StreamExt;
use tokio_tungstenite::WebSocketStream;
@ -60,6 +61,8 @@ async fn handle_ws(mut ws_stream:WebSocketStream<TlsStream<TcpStream>>, args:Htt
packet::*,
};
println!("ws ready");
let bus_ds = args.bus.mailbox(1).unwrap_or(1);
while match ws_stream.try_next().await {
@ -102,41 +105,62 @@ async fn handle_ws(mut ws_stream:WebSocketStream<TlsStream<TcpStream>>, args:Htt
ws_stream.close(None).await.ok();
}
async fn service_http(request:hyper::Request<hyper::body::Incoming>, args:HttpServiceArgs) -> Result<hyper::Response<String>, std::convert::Infallible>
async fn service_http(request:hyper::Request<hyper::body::Incoming>, args:HttpServiceArgs) -> Result<hyper::Response<http_body_util::Full<Bytes>>, std::convert::Infallible>
{
use hyper::Response;
use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL, UPGRADE}};
use http_body_util::Full;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::handshake::server::create_response_with_body;
println!("Serving: {}", request.uri().path());
match request.uri().path() {
"" => Ok(Response::new(args.cache.html())),
".css" => Ok(Response::new(args.cache.css())),
".js" => Ok(Response::new(args.cache.js())),
".wasm" => Ok(Response::new(args.cache.wasm())),
//"favicon.png" => Ok(Response::new(String::new())),
"ws" => {
if request.headers().get(hyper::header::UPGRADE).map(|h| h == "websocket").unwrap_or(false) {
match hyper::upgrade::on(request).await {
Ok(upgraded) => {
match upgraded.downcast::<TokioIo<TlsStream<TcpStream>>>() {
Ok(parts) => {
match accept_async(parts.io.into_inner()).await {
Ok(ws_stream) => { handle_ws(ws_stream, args).await }
Err(_) => { }
"/.css" => Ok(Response::builder()
.header(CONTENT_TYPE, "text/css")
.header(CACHE_CONTROL, "no-cache")
.body(Full::new(Bytes::from(args.cache.css()))).unwrap()),
"/.js" => Ok(Response::builder()
.header(CONTENT_TYPE, "text/javascript")
.header(CACHE_CONTROL, "no-cache")
.body(Full::new(Bytes::from(args.cache.js()))).unwrap()),
"/favicon.png" => Ok(Response::builder()
.header(CONTENT_TYPE, "image/png")
.body(Full::new(Bytes::from(args.cache.favicon()))).unwrap()),
_ => {
if request.headers().get(UPGRADE).map(|h| h == "websocket").unwrap_or(false) {
let response = create_response_with_body(&request, || Full::new(Bytes::new())).unwrap();
tokio::task::spawn(async move {
match hyper::upgrade::on(request).await {
Ok(upgraded) => {
match upgraded.downcast::<TokioIo<TlsStream<TcpStream>>>() {
Ok(parts) => {
match accept_async(parts.io.into_inner()).await {
Ok(ws_stream) => {
println!("here");
handle_ws(ws_stream, args).await
}
Err(e) => { println!("ws not accepted: {}", e.to_string()); }
}
}
Err(_) => { println!("transfer error"); }
}
Err(_) => { }
}
Err(e) => { println!("upgrade error: {}", e.to_string()); }
}
Err(_) => { }
}
});
Ok(response)
} else {
Ok(Response::builder()
.header(CONTENT_TYPE, "text/html")
.header(CACHE_CONTROL, "no-cache")
.body(Full::new(Bytes::from(args.cache.html()))).unwrap())
}
Ok(Response::builder()
.status(101)
.header(hyper::header::UPGRADE, "websocket")
.body(String::new())
.unwrap())
}
_ => Ok(Response::new(String::new())),
}
}
@ -149,11 +173,12 @@ async fn handle_http(stream:system::net::tls::TlsStream, addr:SocketAddr, args:H
let io = TokioIo::new(stream.to_stream());
http1::Builder::new()
let conn = http1::Builder::new()
.serve_connection(io, service_fn(move |req| {
service_http(req, args.clone())
}))
.await.ok();
}));
conn.with_upgrades().await.ok();
Ok(())
}
@ -195,9 +220,12 @@ async fn main()
let cache = WebCache::init();
let mut server = TlsServer::new();
server.add_cert("omen.kirisame.com", "cert/fullchain.pem", "cert/privkey.pem").await.ok();
if server.add_cert("omen.kirisame.com", "cert/fullchain.pem", "cert/privkey.pem").await.is_ok() {
println!("Loaded cert file.");
}
match server.bind("0.0.0.0:38612").await {
Ok(_) => {
println!("Listener bind successful.");
tokio::spawn(async move {
while server.accept(handle_http, HttpServiceArgs {
bus:bus.connect().unwrap(),

View File

@ -6,7 +6,7 @@ struct WebCacheData {
html:String,
css:String,
js:String,
wasm:String,
favicon:Vec<u8>,
}
#[derive(Clone)]
@ -21,7 +21,7 @@ impl WebCache {
let mut html = String::new();
let mut css = String::new();
let mut js = String::new();
let mut wasm = String::new();
let mut favicon = Vec::<u8>::new();
// Cache html file
match File::open("www/.html") {
@ -45,23 +45,22 @@ impl WebCache {
match File::open("www/.js") {
Ok(mut file) => {
file.read_to_string(&mut js).ok();
js = minimize_whitespace(&js);
//js = minimize_whitespace(&js);
}
Err(_) => { }
}
// Cache wasm file
match File::open("www/.wasm") {
// Cache favicon file
match File::open("www/favicon.png") {
Ok(mut file) => {
file.read_to_string(&mut wasm).ok();
wasm = minimize_whitespace(&wasm);
file.read_to_end(&mut favicon).ok();
}
Err(_) => { }
}
Self {
data:Arc::new(RwLock::new(WebCacheData {
html, css, js, wasm
html, css, js, favicon,
})),
}
}
@ -90,11 +89,11 @@ impl WebCache {
}
}
pub fn wasm(&self) -> String
pub fn favicon(&self) -> Vec<u8>
{
match self.data.read() {
Ok(reader) => reader.wasm.to_string(),
Err(_) => String::new(),
Ok(reader) => reader.favicon.clone(),
Err(_) => Vec::new(),
}
}
}

View File

@ -63,8 +63,7 @@ impl CertificateStore {
key:key.unwrap(),
}));
Ok(())
}
else {
} else {
Err(())
}
}
@ -73,8 +72,7 @@ impl CertificateStore {
{
if self.certs.unset(domain) {
Ok(())
}
else {
} else {
Err(())
}
}

View File

@ -138,7 +138,7 @@ impl Server for TlsServer {
else { Err(()) }
}
Err(e) => {
println!("[conn failure] {:}", e.to_string());
println!("[connection failure] {:}", e.to_string());
Err(())
}
}

View File

@ -1,8 +1,8 @@
* {
*{
box-sizing:border-box;
}
html {
html{
display:block;
position:relative;
width:100%;
@ -12,7 +12,7 @@ html {
overflow:hidden;
}
body {
body{
display:flex;
position:relative;
flex-flow:row nowrap;
@ -27,7 +27,7 @@ body {
font-family:sans-serif;
}
body>nav {
body>nav{
display:block;
position:relative;
width:9rem;
@ -35,7 +35,7 @@ body>nav {
background-color:#282828;
}
body>nav>button {
body>nav>button{
display:block;
position:relative;
width:100%;
@ -49,12 +49,12 @@ body>nav>button {
background-color:#282828;
border:0;
color:#c0c0c0;
} body>nav>button:hover {
} body>nav>button:hover{
background-color:#383838;
color:#e0e0e0;
}
body>nav>header {
body>nav>header{
display:block;
position:relative;
width:100%;
@ -70,7 +70,7 @@ body>nav>header {
border-bottom:1px solid #404040;
}
main {
main{
display:block;
position:relative;
width:100%;
@ -81,7 +81,7 @@ main {
background-color:#202020;
}
main>nav {
main>nav{
display:flex;
position:relative;
flex-flow:row nowrap;
@ -91,9 +91,10 @@ main>nav {
height:3rem;
background-color:#282828;
border-bottom:1px solid #404040;
}
main>nav>section:first-child {
main>nav>section:first-child{
display:flex;
position:relative;
flex-flow:row nowrap;
@ -102,7 +103,7 @@ main>nav>section:first-child {
height:100%;
}
main>nav>section:last-child {
main>nav>section:last-child{
display:flex;
position:relative;
flex-flow:row nowrap;
@ -112,7 +113,7 @@ main>nav>section:last-child {
flex-grow:1;
}
main>nav>section>button {
main>nav>section>button{
display:block;
position:relative;
width:auto;
@ -125,12 +126,13 @@ main>nav>section>button {
background-color:#282828;
border:0;
color:#c0c0c0;
} main>nav>section>button:hover {
}
main>nav>section>button:hover{
background-color:#383838;
color:#e0e0e0;
}
main>nav>section>div {
main>nav>section>div{
display:block;
position:relative;
width:auto;
@ -144,14 +146,14 @@ main>nav>section>div {
color:#e0e0e0;
}
main table {
main table{
width:100%;
border-collapse:collapse;
}
main table tr {
main table tr{
height:2.5rem;
}
main table th {
main table th{
padding:0 1rem 0 1rem;
text-align:center;
@ -162,7 +164,7 @@ main table th {
color:#f0f0f0;
border-bottom:1px solid #404040;
}
main table td {
main table td{
height:100%;
padding:0 1rem 0 1rem;
@ -171,7 +173,7 @@ main table td {
background-color:#303030;
color:#f0f0f0;
}
main table td:last-child {
main table td:last-child{
display:flex;
flex-flow:row nowrap;
align-items:flex-start;
@ -179,12 +181,12 @@ main table td:last-child {
flex-grow:1;
}
main table td>img {
main table td>img{
height:2rem;
vertical-align:middle;
}
main table td:last-child>button {
main table td:last-child>button{
display:block;
position:relative;
width:auto;
@ -198,11 +200,12 @@ main table td:last-child>button {
background-color:#303030;
border:0;
color:#e0e0e0;
} main table td:last-child>button:hover {
}
main table td:last-child>button:hover{
background-color:#343434;
}
main>canvas {
main>canvas{
display:block;
position:relative;
width:100%;
@ -213,8 +216,7 @@ main>canvas {
background-color:#202020;
}
/* Scene:game */
main.game>div.sidemenu {
main.game>div.sidemenu{
display:flex;
position:absolute;
bottom:0px;
@ -232,7 +234,7 @@ main.game>div.sidemenu {
overflow:hidden;
}
main.game>div.sidemenu>button {
main.game>div.sidemenu>button{
display:block;
position:relative;
padding:1rem;
@ -245,10 +247,12 @@ main.game>div.sidemenu>button {
font-size:1rem;
cursor:pointer;
} main.game>div.sidemenu>button:hover {
}
main.game>div.sidemenu>button:hover{
background-color:#404040;
} main.game>div.sidemenu>button.warn:hover {
}
main.game>div.sidemenu>button.warn:hover{
background-color:#602020;
}
span.text-system { color:#909090; }
span.text-system{color:#909090;}

View File

@ -6,8 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="application-name" content="Omen">
<meta name="description" content="Abstract strategy game mixing chess and shogi on a hexagon grid.">
<link rel="icon" href="/favicon.png">
<link rel="stylesheet" href=".css">
<script src=".js" async></script>
<script src=".js"></script>
</head>
<body>
<noscript>This application requires JavaScript to function.</noscript>

300
www/.js
View File

@ -1 +1,299 @@
WebAssembly.instantiateStreaming(fetch(".wasm"),ap);
let MAIN = null;
let MENU = null;
let SCENE = null;
let CONNECTED = false;
let SOCKET = null;
let USER = null;
let CONTEXT = null;
let UI = {
text:function(value) {
return document.createTextNode(value);
},
button:function(text, callback) {
let b = document.createElement("button");
b.innerText = text;
if(callback !== null) { b.addEventListener("click", callback); }
return b;
},
div:function(children) {
let div = document.createElement("div");
for(child of children) { div.appendChild(child); }
return div;
},
table:function(header, rows) {
let table = document.createElement("table");
let tbody = document.createElement("tbody");
if(header !== null) {
let row = document.createElement("tr");
for(head of header) {
let cell = document.createElement("th");
cell.innerText = head;
row.appendChild(cell);
}
tbody.appendChild(row);
}
for(row of rows) {
let tr = document.createElement("tr");
for(node of row) {
let cell = document.createElement("td");
if(Array.isArray(node)) { for(item of node) { cell.appendChild(item); } }
else { cell.appendChild(node); }
tr.appendChild(cell);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
return table;
},
mainnav:function(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);
return header;
},
mainmenu:function() {
if(SOCKET !== null) {
MENU.appendChild(UI.button("Online", function() { LOAD(SCENE.Online); }));
MENU.appendChild(UI.button("Continue", function() { LOAD(SCENE.Continue); }));
MENU.appendChild(UI.button("Join", function() { LOAD(SCENE.Continue); }));
MENU.appendChild(UI.button("Live", function() { LOAD(SCENE.Continue); }));
MENU.appendChild(UI.button("History", function() { LOAD(SCENE.Continue); }));
MENU.appendChild(UI.button("Guide", function() { LOAD(SCENE.Continue); }));
MENU.appendChild(UI.button("About", function() { LOAD(SCENE.Continue); }));
}
},
};
function RECONNECT() {
if(SOCKET === null) {
console.log("Connecting..");
SOCKET = new WebSocket("wss://omen.kirisame.com:38612");
SOCKET.binaryType = 'blob';
SOCKET.addEventListener("error", function(e) {
console.log("Failed");
SOCKET = null;
});
SOCKET.addEventListener("open", function(e) {
if(SOCKET.readyState === WebSocket.OPEN) {
console.log("Connected.");
SOCKET.addEventListener("message", function(e) {
MESSAGE(e.data);
});
SOCKET.addEventListener("close", function(e) {
console.log("Closed.");
SOCKET = null;
RECONNECT();
});
RESUME();
SOCKET.send(new Uint8Array([ 0 ]));
}
});
}
}
function RESUME() {
LOAD(SCENES.Online);
}
const SCENES = {
Init:{
load:function() {
LOAD(SCENES.Offline);
RECONNECT();
return true;
},
},
Offline:{
load:function() {
MENU.appendChild(UI.button("Reconnect", function() { RECONNECT(); }))
return true;
},
},
Online:{
load:function() {
UI.mainmenu();
MAIN.appendChild(UI.mainnav([
UI.button("Start", null),
], [
UI.div([UI.text("0 - 0 of 0")]),
UI.button("◀", null),
UI.button("▶", null),
]));
let table = document.createElement("table");
table.setAttribute("id", "content");
MAIN.appendChild(table);
this.refresh();
return true;
},
refresh:function() {
let request = new Uint8Array();
//SERVER.send()
let cb = function() {
let table = document.getElementById("content");
MAIN.removeChild(table);
let data = [
[ UI.text("Player1"), UI.text("Player2"), UI.text("0"), UI.text("Ha1-D ◈ Ba1-M"), UI.text("0"), [ UI.button("Join", null), UI.button("Spectate", null) ] ],
];
MAIN.appendChild(UI.table(
[ "Dawn", "Dusk", "Turn", "Move", "Spectators", "" ],
data,
));
};
cb();
},
},
Continue:{
load:function() {
if(USER === null) return false;
UI.mainmenu();
MAIN.appendChild(UI.mainnav([
UI.button("Start", null),
], [
UI.button("◀", null),
UI.button("▶", null),
]));
return true;
},
refresh:function() {
},
},
Join:{
load:function() {
if(USER === null) return false;
UI.mainmenu();
MAIN.appendChild(UI.mainnav([
UI.button("Start", null),
], [
UI.button("◀", null),
UI.button("▶", null),
]));
return true;
},
refresh:function() {
},
},
Live:{
load:function() {
UI.mainmenu();
MAIN.appendChild(UI.mainnav([
UI.button("Start", null),
], [
UI.button("◀", null),
UI.button("▶", null),
]));
return true;
},
refresh:function() {
},
},
History:{
load:function() {
UI.mainmenu();
MAIN.appendChild(UI.mainnav([ ], [
UI.button("◀", null),
UI.button("▶", null),
]));
return true;
},
refresh:function() {
},
},
Guide:{
load:function() {
UI.mainmenu();
return true;
},
refresh:function() {
},
},
About:{
load:function() {
UI.mainmenu();
return true;
},
refresh:function() {
},
},
Game:{
load:function() {
MENU.appendChild(UI.button("Back", function() { LOAD(SCENES.Online) }));
MENU.appendChild(UI.button("Retire", function() { }));
return true;
},
unload:function() {
},
refresh:function() {
},
},
};
function REBUILD() {
MENU = document.createElement("nav");
let title = document.createElement("header");
title.innerText = "Omen";
MENU.appendChild(title);
MAIN = document.createElement("main");
document.body.appendChild(MENU);
document.body.appendChild(MAIN);
}
function MESSAGE() {
}
function LOAD(scene) {
if(SCENE.unload !== undefined) { SCENE.unload(); }
while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); }
REBUILD();
SCENE = scene;
if(!SCENE.load()) { LOAD(SCENES.Online); }
}
document.addEventListener("DOMContentLoaded", function() {
SCENE = SCENES.Offline;
LOAD(SCENES.Init);
});

BIN
www/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB