let MAIN = null; let MENU = null; let SCENE = null; let CONNECTED = false; let SOCKET = null; let CONTEXT = { Scene: null, Auth: null, Data: null, }; const Status = { Ok: 0, Error: 1, NotImplement: 2, BadHandle: 1, BadSecret: 2, BadCode: 3, }; const OpCode = { Register :0x0010, Authenticate :0x0011, Resume :0x0012, Deauthenticate :0x0013, }; class Message { constructor(code, data) { this.code = code; this.data = data; } } const PACK = { u8:(value) => { return new Uint8Array([ value & 0xFF ]); }, u16:(value) => { return new Uint8Array([ (value >> 8) & 0xFF, value & 0xFF ]); }, u32:(value) => { return new Uint8Array([ (value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF ]); }, }; const UI = { text:(value) => { return document.createTextNode(value); }, button:(text, callback) => { let button = document.createElement("button"); button.innerText = text; if(callback !== null) { button.addEventListener("click", callback); } return button; }, textbox:(id, placeholder) => { let input = document.createElement("input"); input.setAttribute("type", "text"); if(id !== null) { input.setAttribute("id", id); } input.setAttribute("placeholder", placeholder); return input; }, password:(id) => { let input = document.createElement("input"); input.setAttribute("type", "password"); if(id !== null) { input.setAttribute("id", id); } return input; }, label:(name, id) => { let label = document.createElement("label"); label.setAttribute("for", id); label.innerText = name; return label; }, div:(children) => { let div = document.createElement("div"); for(child of children) { div.appendChild(child); } return div; }, table:(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:(left_children, right_children) => { let header = document.createElement("nav"); let left = document.createElement("section"); if(CONTEXT.Auth === null) { left.appendChild(UI.button("Register", () => { LOAD(SCENES.Register) })); left.appendChild(UI.button("Log In", () => { LOAD(SCENES.Authenticate) })); } 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); }, mainmenu:() => { if(SOCKET !== null) { MENU.appendChild(UI.button("Online", () => { LOAD(SCENES.Online); })); if(CONTEXT.Auth !== null) { MENU.appendChild(UI.button("Continue", () => { LOAD(SCENES.Continue); })); MENU.appendChild(UI.button("Join", () => { LOAD(SCENES.Join); })); } MENU.appendChild(UI.button("Live", () => { LOAD(SCENES.Live); })); MENU.appendChild(UI.button("History", () => { LOAD(SCENES.History); })); MENU.appendChild(UI.button("Guide", () => { LOAD(SCENES.Guide); })); MENU.appendChild(UI.button("About", () => { LOAD(SCENES.About); })); if(CONTEXT.Auth !== null) { MENU.appendChild(UI.button("Logout", () => { MESSAGE_COMPOSE([ PACK.u16(OpCode.Deauthenticate), ]); CONTEXT.Auth = null; LOAD(SCENE); })); } } }, }; const SCENES = { Init:{ load:() => { LOAD_OFFLINE(); CONTEXT.Scene = SCENES.Online; RECONNECT(); return true; }, }, Offline:{ load:() => { MENU.appendChild(UI.button("Reconnect", () => { RECONNECT(); })) return true; }, }, Register:{ load:() => { if(CONTEXT.Auth !== null) return false; UI.mainmenu(); UI.mainnav([], []); let container = document.createElement("section"); let form = document.createElement("div"); form.appendChild(UI.table(null, [ [ UI.label("Handle", "handle"), UI.textbox("handle", "") ], [ UI.label("Secret", "secret"), UI.password("secret") ], [ UI.label("Code", "code"), UI.password("code") ], ])); let button = UI.button("Register", (event) => { let handle = document.getElementById("handle"); let secret = document.getElementById("secret"); let code = document.getElementById("code"); handle.removeAttribute("class"); secret.removeAttribute("class"); code.removeAttribute("class"); event.target.removeAttribute("class"); if(handle.value.length > 0 && secret.value.length > 0 && code.value.length > 0) { event.target.setAttribute("disabled", ""); let enc = new TextEncoder(); let enc_handle = enc.encode(handle.value); let enc_secret = enc.encode(secret.value); let enc_code = enc.encode(code.value); MESSAGE_COMPOSE([ PACK.u16(OpCode.Register), PACK.u16(enc_handle.length), enc_handle, PACK.u16(enc_secret.length), enc_secret, PACK.u16(enc_code.length), enc_code, ]); } else { if(handle.value.length == 0) { handle.setAttribute("class", "error"); } if(secret.value.length == 0) { secret.setAttribute("class", "error"); } if(code.value.length == 0) { code.setAttribute("class", "error"); } } }); button.setAttribute("id", "submit"); form.appendChild(button); container.appendChild(form); MAIN.appendChild(container); MAIN.setAttribute("class", "form"); return true; }, message:(code, data) => { if(code == OpCode.Register && data !== null) { let submit = document.getElementById("submit"); switch(data.status) { case Status.Ok: { CONTEXT.Auth = data; LOAD(SCENES.Online); } break; default: { submit.removeAttribute("disabled"); switch(data.status) { case Status.BadHandle: { document.getElementById("handle").setAttribute("class", "error"); submit.setAttribute("class", "error"); } break; case Status.BadSecret: { document.getElementById("secret").setAttribute("class", "error"); submit.setAttribute("class", "error"); } break; case Status.BadCode: { document.getElementById("code").setAttribute("class", "error"); submit.setAttribute("class", "error"); } break; } } } } }, }, Authenticate:{ load:() => { if(CONTEXT.Auth !== null) return false; UI.mainmenu(); UI.mainnav([], []); let container = document.createElement("section"); let form = document.createElement("div"); form.appendChild(UI.table(null, [ [ UI.label("Handle", "handle"), UI.textbox("handle", "") ], [ UI.label("Secret", "secret"), UI.password("secret") ], ])); let button = UI.button("Register", (event) => { let handle = document.getElementById("handle"); let secret = document.getElementById("secret"); handle.removeAttribute("class"); secret.removeAttribute("class"); event.target.removeAttribute("class"); if(handle.value.length > 0 && secret.value.length > 0) { event.target.setAttribute("disabled", ""); let enc = new TextEncoder(); let enc_handle = enc.encode(handle.value); let enc_secret = enc.encode(secret.value); MESSAGE_COMPOSE([ PACK.u16(OpCode.Authenticate), PACK.u16(enc_handle.length), enc_handle, PACK.u16(enc_secret.length), enc_secret, ]); } else { if(handle.value.length == 0) { handle.setAttribute("class", "error"); } if(secret.value.length == 0) { secret.setAttribute("class", "error"); } } }); button.setAttribute("id", "submit"); form.appendChild(button); container.appendChild(form); MAIN.appendChild(container); MAIN.setAttribute("class", "form"); return true; }, message:(code, data) => { if(code == OpCode.Authenticate && data !== null) { let submit = document.getElementById("submit"); switch(data.status) { case Status.Ok: { CONTEXT.Auth = data; LOAD(SCENES.Online); } break; case Status.Error: { submit.removeAttribute("disabled"); document.getElementById("handle").setAttribute("class", "error"); document.getElementById("secret").setAttribute("class", "error"); submit.setAttribute("class", "error"); } } } }, }, Online:{ load:() => { UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { left_buttons.push(UI.button("Start", null)); } UI.mainnav( left_buttons, [ 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); MAIN.setAttribute("class", "list"); SCENE.refresh(); return true; }, refresh:() => { let request = new Uint8Array(); //SERVER.send() SCENE.message(0, 0, null); }, message:(code, data) => { let table = document.getElementById("content"); MAIN.removeChild(table); let rows = [ [ 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", "" ], rows, )); } }, Continue:{ load:() => { if(CONTEXT.Auth === null) return false; UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { left_buttons.push(UI.button("Start", null)); } UI.mainnav( left_buttons, [ 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); MAIN.setAttribute("class", "list"); SCENE.refresh(); return true; }, refresh:() => { }, }, Join:{ load:() => { if(CONTEXT.Auth === null) return false; UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { left_buttons.push(UI.button("Start", null)); } UI.mainnav( left_buttons, [ 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); MAIN.setAttribute("class", "list"); SCENE.refresh(); return true; }, refresh:() => { }, }, Live:{ load:() => { UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { left_buttons.push(UI.button("Start", null)); } UI.mainnav( left_buttons, [ 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); MAIN.setAttribute("class", "list"); SCENE.refresh(); return true; }, refresh:() => { }, }, History:{ load:() => { UI.mainmenu(); UI.mainnav( [ ], [ 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); MAIN.setAttribute("class", "list"); SCENE.refresh(); return true; }, refresh:() => { }, }, Guide:{ load:() => { UI.mainmenu(); UI.mainnav([], []); return true; }, refresh:() => { }, }, About:{ load:() => { UI.mainmenu(); UI.mainnav([], []); return true; }, refresh:() => { }, }, Game:{ load:() => { MENU.appendChild(UI.button("Back", () => { LOAD(SCENES.Online) })); MENU.appendChild(UI.button("Retire", () => { })); return true; }, unload:() => { }, refresh:() => { }, }, }; function RECONNECT() { if(SOCKET === null) { console.log("Websocket connecting.."); SOCKET = new WebSocket("wss://omen.kirisame.com:38612"); SOCKET.binaryType = "arraybuffer"; SOCKET.addEventListener("error", (event) => { SOCKET = null; LOAD(SCENES.Offline) }); SOCKET.addEventListener("open", (event) => { if(SOCKET.readyState === WebSocket.OPEN) { console.log("Websocket connected."); SOCKET.addEventListener("message", MESSAGE); SOCKET.addEventListener("close", (event) => { console.log("Websocket closed."); SOCKET = null; RECONNECT(); }); RESUME(); } }); } } function RESUME() { LOAD(CONTEXT.Scene); } 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(event) { console.log("Message received."); if(SCENE.message !== undefined) { let bytes = new Uint8Array(event.data); let code = 0; let index = 2; let data = null; if(bytes.length >= 2) { code = (bytes[0] << 8) + bytes[1]; } switch(code) { case OpCode.Register: { console.log("Register response."); if(bytes.length == 28) { console.log("Good size"); data = { status:(bytes[2] << 8) + bytes[3], token:new Uint8Array(), secret:new Uint8Array(), }; index += 2; for(let i = 0; i < 8; ++i) { data.token += bytes[index++]; } for(let i = 0; i < 16; ++i) { data.secret += bytes[index++]; } } else { console.log("Register bad length:" + bytes.length); return; } } break; case OpCode.Authenticate: { console.log("Authenticate response."); if(bytes.length == 28) { console.log("Good size"); data = { status:(bytes[2] << 8) + bytes[3], token:new Uint8Array(), secret:new Uint8Array(), }; index += 2; for(let i = 0; i < 8; ++i) { data.token += bytes[index++]; } for(let i = 0; i < 16; ++i) { data.secret += bytes[index++]; } } else { console.log("Authenticate bad length:" + bytes.length); return; } } break; case OpCode.Resume: { } break; case OpCode.Deauthenticate: { } break; default: return; } SCENE.message(code, data); } } function MESSAGE_COMPOSE(data) { if(SOCKET !== null) { let length = 0; for(let i = 0; i < data.length; ++i) { length += data[i].length; } let raw = new Uint8Array(length); length = 0; for(let i = 0; i < data.length; ++i) { raw.set(data[i], length); length += data[i].length; } SOCKET.send(raw); } } function LOAD(scene) { if(SCENE.unload !== undefined) { SCENE.unload(); } while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } REBUILD(); SCENE = scene; CONTEXT.Scene = SCENE; if(!SCENE.load()) { LOAD(SCENES.Online); } } function LOAD_OFFLINE() { if(SCENE.unload !== undefined) { SCENE.unload(); } while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } REBUILD(); SCENE = SCENES.Offline; if(!SCENE.load()) { LOAD(SCENES.Online); } } document.addEventListener("DOMContentLoaded", () => { SCENE = SCENES.Offline; LOAD(SCENES.Init); });