From 7ba9ce7ba0fec6ae8bd2314f025cffefaa34ba01 Mon Sep 17 00:00:00 2001 From: yukirij Date: Sun, 11 Aug 2024 21:10:05 -0700 Subject: [PATCH] Add game interface. --- server/src/app/mod.rs | 48 +- server/src/app/session.rs | 4 +- server/src/app/user.rs | 1 + server/src/manager/data.rs | 60 ++- server/src/protocol/packet/game_play.rs | 77 +++ server/src/protocol/packet/game_state.rs | 55 ++ server/src/protocol/packet/mod.rs | 5 + server/src/protocol/packet/session_leave.rs | 0 server/src/protocol/packet/session_retire.rs | 0 server/src/system/filesystem/mod.rs | 234 +++++++- server/src/util/pack.rs | 54 +- www/css/game.css | 2 +- www/js/game.js | 536 ++++++++++++------- www/js/interface.js | 349 ++++++++++-- www/js/scene.js | 1 + www/js/util.js | 42 +- 16 files changed, 1179 insertions(+), 289 deletions(-) create mode 100644 server/src/protocol/packet/game_play.rs create mode 100644 server/src/protocol/packet/game_state.rs create mode 100644 server/src/protocol/packet/session_leave.rs create mode 100644 server/src/protocol/packet/session_retire.rs diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index 45d15b0..4743727 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -20,7 +20,6 @@ pub struct App { pub connections:Pool, pub users:Pool, - pub user_next:u32, pub user_id:Sparse, pub user_handle:Trie, pub salts:Sparse<[u8; 16]>, @@ -48,25 +47,58 @@ impl App { let handle_count = filesystem.handle_count()?; for id in 0..handle_count { let (handle, user_id) = filesystem.handle_fetch(id as u32).unwrap(); - println!("got: {} = {}", handle, user_id); user_handle.set(handle.as_bytes(), user_id); } + // Load users + println!("Load users.."); + let mut users = Pool::new(); + let mut user_id = Sparse::new(); + let user_count = filesystem.user_count()?; + for id in 0..user_count { + let user = filesystem.user_fetch(id as u32).unwrap(); + let user_local_id = users.add(user); + user_id.set(user_local_id as isize, id); + } + + // Load sessions + println!("Load sessions.."); + let mut sessions = Trie::new(); + let mut times = Vec::<(u64, SessionToken)>::new(); + let session_count = filesystem.session_count()?; + for id in 0..session_count { + let session = filesystem.session_fetch(id as u32).unwrap(); + + times.push((session.time, session.token.clone())); + + sessions.set(&session.token.clone(), session); + } + + // Organize sessions by most recent + let mut session_time = Chain::new(); + times.sort_by(|(a, _), (b, _)| { + if a < b { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less } + }); + for (_, token) in times { + let id = session_time.add(token); + sessions.get_mut(&token).unwrap().chain_id = id; + } + + println!("Done."); Ok(Self { filesystem:filesystem, connections:Pool::new(), - users:Pool::new(), - user_next:0, - user_id:Sparse::new(), - user_handle:Trie::new(), + users, + user_id, + user_handle, salts, auths:Trie::new(), - sessions:Trie::new(), + sessions, - session_time:Chain::new(), + session_time, }) } else { Err(()) diff --git a/server/src/app/session.rs b/server/src/app/session.rs index f5b8cbe..47e32f2 100644 --- a/server/src/app/session.rs +++ b/server/src/app/session.rs @@ -9,7 +9,8 @@ pub struct Viewer { } pub struct Session { - pub key:SessionToken, + pub id:u32, + pub token:SessionToken, pub secret:SessionSecret, pub game:Game, @@ -18,5 +19,6 @@ pub struct Session { pub p_dusk:Viewer, pub viewers:Vec, + pub time:u64, pub chain_id:usize, } diff --git a/server/src/app/user.rs b/server/src/app/user.rs index 532b6bd..edd94c3 100644 --- a/server/src/app/user.rs +++ b/server/src/app/user.rs @@ -1,5 +1,6 @@ pub struct User { pub id:u32, + pub handle_id:u32, pub handle:String, pub secret:Vec, pub na_key:u32, diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index b9a2b44..fc1066d 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -94,23 +94,28 @@ pub async fn thread_system(mut app:App, bus:Bus) let salt_id = app.filesystem.salt_store(salt).unwrap(); app.salts.set(salt_id as isize, salt); - if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) { - let user_id = app.user_next; - app.user_next += 1; - - // Create user entry - let user_pos = app.users.add(User { - id:user_id, - handle:request.handle.clone(), - secret:hash, - na_key:salt_id, - }); + if let Ok(secret) = argon2::hash_raw(&request.secret, &salt, &argon_config) { + let user_id = app.filesystem.user_count().unwrap() as u32; // Register user pool id and handle - app.filesystem.handle_store(&request.handle, user_id).ok(); - app.user_id.set(user_id as isize, user_pos); + let handle_id = app.filesystem.handle_store(&request.handle, user_id).unwrap(); app.user_handle.set(request.handle.as_bytes(), user_id); + let user_data = User { + id:user_id, + handle_id, + handle:request.handle.clone(), + secret, + na_key:salt_id, + }; + + app.filesystem.user_store(&user_data).ok(); + + // Create user entry + let user_pos = app.users.add(user_data); + + app.user_id.set(user_id as isize, user_pos); + println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id); // Generate authentication token and secret @@ -296,7 +301,7 @@ pub async fn thread_system(mut app:App, bus:Bus) } else { String::new() }; response.records.push(PacketSessionListResponseRecord { - token:session.key, + token:session.token, handles:[ dawn_handle, dusk_handle, @@ -337,9 +342,11 @@ pub async fn thread_system(mut app:App, bus:Bus) rng.fill(&mut secret).ok(); let chain_id = app.session_time.add(token); - app.sessions.set(&token, Session { - key:token, - secret:secret, + + let mut session = Session { + id:0, + token, + secret, game:game::Game::new(), p_dawn:Viewer { connection:None,//Some(qr.id), @@ -350,8 +357,13 @@ pub async fn thread_system(mut app:App, bus:Bus) user:None, }, viewers:Vec::new(), - chain_id:chain_id, - }); + time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64, + chain_id, + }; + + session.id = app.filesystem.session_store(&session).unwrap(); + + app.sessions.set(&token, session); app.session_time.set(chain_id, token); // Set player to Dawn. @@ -397,6 +409,8 @@ pub async fn thread_system(mut app:App, bus:Bus) false } { println!("Add user to session."); + + app.filesystem.session_update(session.id, session).ok(); response.status = STATUS_OK; } } else { @@ -418,6 +432,14 @@ pub async fn thread_system(mut app:App, bus:Bus) Some(QRPacket::new(qr.id, QRPacketData::RSessionJoin(response))) } + // SessionRetire + + // SessionLeave + + // GameState + + // GamePlay + _ => { Some(QRPacket::new(0, QRPacketData::None)) } } } diff --git a/server/src/protocol/packet/game_play.rs b/server/src/protocol/packet/game_play.rs new file mode 100644 index 0000000..352e403 --- /dev/null +++ b/server/src/protocol/packet/game_play.rs @@ -0,0 +1,77 @@ +use crate::util::pack::{pack_u16, unpack_u16}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketGamePlay { + pub handle:String, + pub secret:String, +} +impl PacketGamePlay { + pub fn new() -> Self + { + Self { + handle:String::new(), + secret:String::new(), + } + } +} +impl Packet for PacketGamePlay { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + let mut length = unpack_u16(data, index) as usize; + if data.len() >= *index + length { + match String::from_utf8(data[*index..*index+length].to_vec()) { + Ok(text) => { + *index += length; + result.handle = text; + + length = unpack_u16(data, index) as usize; + if data.len() >= *index + length { + + match String::from_utf8(data[*index..*index+length].to_vec()) { + Ok(text) => { + *index += length; + result.secret = text; + + return Ok(result); + } + Err(_) => { } + } + } + } + Err(_) => { } + } + } + + Err(()) + } +} + + +#[derive(Clone)] +pub struct PacketGamePlayResponse { + pub status:u16, +} +impl PacketGamePlayResponse { + pub fn new() -> Self + { + Self { + status:0, + } + } +} +impl Packet for PacketGamePlayResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + ].concat() + } +} diff --git a/server/src/protocol/packet/game_state.rs b/server/src/protocol/packet/game_state.rs new file mode 100644 index 0000000..7025801 --- /dev/null +++ b/server/src/protocol/packet/game_state.rs @@ -0,0 +1,55 @@ +use crate::{ + app::session::SessionToken, + util::pack::{pack_u16, unpack_u16}, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketGameState { + pub token:SessionToken, +} +impl PacketGameState { + pub fn new() -> Self + { + Self { + token:SessionToken::default(), + } + } +} +impl Packet for PacketGameState { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + Err(()) + } +} + + +#[derive(Clone)] +pub struct PacketGameStateResponse { + pub status:u16, +} +impl PacketGameStateResponse { + pub fn new() -> Self + { + Self { + status:0, + + } + } +} +impl Packet for PacketGameStateResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + + ].concat() + } +} diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index 4901996..d5845ca 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -7,6 +7,11 @@ mod resume; pub use resume::*; mod session_list; pub use session_list::*; mod session_create; pub use session_create::*; mod session_join; pub use session_join::*; +mod session_retire; pub use session_retire::*; +mod session_leave; pub use session_leave::*; + +mod game_state; pub use game_state::*; +mod game_play; pub use game_play::*; mod prelude { pub trait Packet { diff --git a/server/src/protocol/packet/session_leave.rs b/server/src/protocol/packet/session_leave.rs new file mode 100644 index 0000000..e69de29 diff --git a/server/src/protocol/packet/session_retire.rs b/server/src/protocol/packet/session_retire.rs new file mode 100644 index 0000000..e69de29 diff --git a/server/src/system/filesystem/mod.rs b/server/src/system/filesystem/mod.rs index ccedabd..3d1d813 100644 --- a/server/src/system/filesystem/mod.rs +++ b/server/src/system/filesystem/mod.rs @@ -2,23 +2,31 @@ use std::{ fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path }; +use game::Game; + use crate::{ - app::session::Session, - app::user::User, - util::pack::{pack_u32, unpack_u32} + app::{ + session::{self, Session, SessionToken, SessionSecret}, + user::User, + }, + util::pack::*, }; const HANDLE_BUCKET_MASK :u32 = 0xFF; const HANDLE_BUCKET_SIZE :u32 = HANDLE_BUCKET_MASK + 1; +const GENERIC_CONFIG :&str = "c.bin"; +const GENERIC_INDEX :&str = "i.bin"; +const GENERIC_HISTORY :&str = "h.bin"; + const DIR_DATA :&str = "data"; const DIR_HANDLE :&str = const_format::formatcp!("{}/h", DIR_DATA); const DIR_SESSION :&str = const_format::formatcp!("{}/s", DIR_DATA); const DIR_USER :&str = const_format::formatcp!("{}/u", DIR_DATA); -const INDEX_HANDLE :&str = const_format::formatcp!("{}/i.bin", DIR_HANDLE); -const INDEX_SESSION :&str = const_format::formatcp!("{}/i.bin", DIR_SESSION); -const INDEX_USER :&str = const_format::formatcp!("{}/i.bin", DIR_USER); +const INDEX_HANDLE :&str = const_format::formatcp!("{d}/{f}", d= DIR_HANDLE, f= GENERIC_INDEX); +const INDEX_SESSION :&str = const_format::formatcp!("{d}/{f}", d= DIR_SESSION, f= GENERIC_INDEX); +const INDEX_USER :&str = const_format::formatcp!("{d}/{f}", d= DIR_USER, f= GENERIC_INDEX); pub const FILE_SALT :&str = const_format::formatcp!("{}/x.bin", DIR_DATA); @@ -39,7 +47,7 @@ impl FileSystem { // Initialize filesystem if does not exist. // - // Note: does not currently check for corruption. + // Notice: does not currently check for corruption. // if !Path::new(DIR_DATA).exists() { fs::create_dir(DIR_DATA)?; @@ -82,17 +90,219 @@ impl FileSystem { } - pub fn session_store(&mut self, _session:&Session) -> Result<(),()> + pub fn session_store(&mut self, session:&Session) -> Result + { + let size = self.session_count()? as u32; + + // Update size record + self.index_session.seek(SeekFrom::Start(0)).map_err(|_| ())?; + self.index_session.write(&pack_u32(size + 1)).map_err(|_| ())?; + + match self.session_update(size, session) { + Ok(_) => { + let bucket_index = size & !HANDLE_BUCKET_MASK; + let dir_index = size & HANDLE_BUCKET_MASK; + + let bucket_path = Path::new(DIR_SESSION) + .join(format!("{:08x}", bucket_index)) + .join(format!("{:08x}", dir_index)); + fs::write(bucket_path.join(GENERIC_HISTORY), Vec::::new()).map_err(|_| ())?; + Ok(size) + } + Err(_) => Err(()), + } + } + + pub fn session_update(&mut self, id:u32, session:&Session) -> Result<(),()> + { + let bucket_index = id & !HANDLE_BUCKET_MASK; + let dir_index = id & HANDLE_BUCKET_MASK; + + // Create bucket file if not exists + let bucket_path = Path::new(DIR_SESSION) + .join(format!("{:08x}", bucket_index)) + .join(format!("{:08x}", dir_index)); + if !bucket_path.exists() { + fs::create_dir_all(bucket_path.clone()).map_err(|_| ())?; + } + + // Open bucket file for record + if let Ok(mut file) = File::options().write(true).create(true).open(bucket_path.join(GENERIC_CONFIG)) { + + // Write session information + file.write(&session.token).map_err(|_| ())?; + file.write(&session.secret).map_err(|_| ())?; + file.write(&pack_u8(session.p_dawn.user.is_some() as u8)).map_err(|_| ())?; + file.write(&pack_u32(session.p_dawn.user.unwrap_or(0))).map_err(|_| ())?; + file.write(&pack_u8(session.p_dusk.user.is_some() as u8)).map_err(|_| ())?; + file.write(&pack_u32(session.p_dusk.user.unwrap_or(0))).map_err(|_| ())?; + file.write(&pack_u64(session.time)).map_err(|_| ())?; + + Ok(()) + } else { Err(()) } + } + + pub fn session_fetch(&mut self, id:u32) -> Result + { + let bucket_index = id & !HANDLE_BUCKET_MASK; + let dir_index = id & HANDLE_BUCKET_MASK; + + // Create bucket file if not exists + let bucket_path = Path::new(DIR_SESSION) + .join(format!("{:08x}", bucket_index)) + .join(format!("{:08x}", dir_index)); + if !bucket_path.exists() { + fs::create_dir_all(bucket_path.clone()).map_err(|_| ())?; + } + + // Open bucket file for record + if let Ok(mut file) = File::options().read(true).open(bucket_path.join(GENERIC_CONFIG)) { + + // Extract session information + let mut buffer_u8 = [0u8; 1]; + let mut buffer_u32 = [0u8; 4]; + let mut buffer_u64 = [0u8; 8]; + + let mut token = SessionToken::default(); + let mut secret = SessionSecret::default(); + + file.read_exact(&mut token).map_err(|_| ())?; + file.read_exact(&mut secret).map_err(|_| ())?; + + file.read_exact(&mut buffer_u8).map_err(|_| ())?; + file.read_exact(&mut buffer_u32).map_err(|_| ())?; + let dawn = if unpack_u8(&buffer_u8, &mut 0) != 0 { + Some(unpack_u32(&buffer_u32, &mut 0)) + } else { None }; + + file.read_exact(&mut buffer_u8).map_err(|_| ())?; + file.read_exact(&mut buffer_u32).map_err(|_| ())?; + let dusk = if unpack_u8(&buffer_u8, &mut 0) != 0 { + Some(unpack_u32(&buffer_u32, &mut 0)) + } else { None }; + + file.read_exact(&mut buffer_u64).map_err(|_| ())?; + let time = unpack_u64(&buffer_u64, &mut 0); + + Ok(Session { + id, + token, + secret, + game:Game::new(), + p_dawn:session::Viewer { + connection:None, + user:dawn, + }, + p_dusk:session::Viewer { + connection:None, + user:dusk, + }, + viewers:Vec::new(), + time, + chain_id:0, + }) + } else { Err(()) } + } + + pub fn session_history_push(&mut self, _id:u32, _history:()) -> Result<(),()> { Err(()) } - - pub fn user_store(&mut self, _user:&User) -> Result<(),()> + pub fn session_history_fetch(&mut self, _id:u32) -> Result,()> { Err(()) } + pub fn session_count(&mut self) -> Result + // Get number of salts in store. + // + { + Self::get_header_size(&mut self.index_session) + } + + + pub fn user_store(&mut self, user:&User) -> Result + { + let size = self.user_count()? as u32; + + let bucket_index = size & !HANDLE_BUCKET_MASK; + let file_index = size & HANDLE_BUCKET_MASK; + + // Update size record + self.index_user.seek(SeekFrom::Start(0)).map_err(|_| ())?; + self.index_user.write(&pack_u32(size + 1)).map_err(|_| ())?; + + // Create bucket file if not exists + let bucket_path = Path::new(DIR_USER).join(format!("{:08x}", bucket_index)); + if !bucket_path.exists() { + fs::create_dir(bucket_path.clone()).map_err(|_| ())?; + } + + // Open bucket file for record + let file_path = bucket_path.join(format!("{:08x}.bin", file_index)); + if let Ok(mut file) = File::options().write(true).create(true).open(file_path) { + + // Write user information + file.write(&pack_u32(user.handle_id)).map_err(|_| ())?; + file.write(&pack_u32(user.na_key)).map_err(|_| ())?; + file.write(&pack_u16(user.secret.len() as u16)).map_err(|_| ())?; + file.write(&user.secret).map_err(|_| ())?; + + Ok(size) + } else { Err(()) } + } + + pub fn user_fetch(&mut self, id:u32) -> Result + // Retrieve a salt from store. + // + { + let bucket_index = id & !HANDLE_BUCKET_MASK; + let file_index = id & HANDLE_BUCKET_MASK; + + // Open bucket file for record + let file_path = Path::new(DIR_USER) + .join(format!("{:08x}", bucket_index)) + .join(format!("{:08x}.bin", file_index)); + + if let Ok(mut file) = File::options().read(true).open(file_path) { + file.seek(SeekFrom::Start(0)).map_err(|_| ())?; + + // Extract user information + let mut buffer_u16 = [0u8; 2]; + let mut buffer_u32 = [0u8; 4]; + + file.read_exact(&mut buffer_u32).map_err(|_| ())?; + let handle_id = unpack_u32(&buffer_u32, &mut 0); + + file.read_exact(&mut buffer_u32).map_err(|_| ())?; + let na_key = unpack_u32(&buffer_u32, &mut 0); + + file.read_exact(&mut buffer_u16).map_err(|_| ())?; + let secret_length = unpack_u16(&buffer_u16, &mut 0); + + let mut secret = vec![0u8; secret_length as usize]; + file.read_exact(&mut secret).map_err(|_| ())?; + + let (handle, _) = self.handle_fetch(handle_id)?; + + Ok(User { + id, + handle_id, + handle, + secret, + na_key, + }) + } else { Err(()) } + } + + pub fn user_count(&mut self) -> Result + // Get number of salts in store. + // + { + Self::get_header_size(&mut self.index_user) + } + pub fn handle_store(&mut self, handle:&String, user_id:u32) -> Result // Add a salt to store. @@ -111,7 +321,7 @@ impl FileSystem { self.index_handle.write(&pack_u32(size + 1)).map_err(|_| ())?; // Create bucket file if not exists - let bucket_path = Path::new(DIR_HANDLE).join(format!("{:x}.bin", bucket_index)); + let bucket_path = Path::new(DIR_HANDLE).join(format!("{:08x}.bin", bucket_index)); if !bucket_path.exists() { fs::write(bucket_path.clone(), vec![0u8; 12 * HANDLE_BUCKET_SIZE as usize]).map_err(|_| ())?; } @@ -153,7 +363,7 @@ impl FileSystem { let record_offset = 12 * record_index as u64; let path = Path::new(DIR_HANDLE) - .join(format!("{:x}.bin", bucket_index)); + .join(format!("{:08x}.bin", bucket_index)); // Read bucket file for record if let Ok(mut file) = File::open(path) { diff --git a/server/src/util/pack.rs b/server/src/util/pack.rs index bbed1be..2fb68b2 100644 --- a/server/src/util/pack.rs +++ b/server/src/util/pack.rs @@ -5,7 +5,7 @@ pub fn pack_u8(value:u8) -> Vec pub fn unpack_u8(data:&[u8], index:&mut usize) -> u8 { - let mut result :u8 = 0; + let mut result = 0u8; if *index < data.len() { result = data[*index]; *index += 1; @@ -20,7 +20,7 @@ pub fn pack_u16(value:u16) -> Vec pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16 { - let mut result :u16 = 0; + let mut result = 0u16; if *index < data.len() { result = (data[*index] as u16) << 8; *index += 1; @@ -44,22 +44,40 @@ pub fn pack_u32(value:u32) -> Vec pub fn unpack_u32(data:&[u8], index:&mut usize) -> u32 { - let mut result :u32 = 0; - if *index < data.len() { - result = (data[*index] as u32) << 24; - *index += 1; - } - if *index < data.len() { - result = (data[*index] as u32) << 16; - *index += 1; - } - if *index < data.len() { - result = (data[*index] as u32) << 8; - *index += 1; - } - if *index < data.len() { - result |= data[*index] as u32; - *index += 1; + let mut result = 0u32; + for _ in 0..4 { + result <<= 8; + if *index < data.len() { + result |= data[*index] as u32; + *index += 1; + } else { break; } + } + result +} + +pub fn pack_u64(value:u64) -> Vec +{ + vec![ + (value >> 56) as u8, + (value >> 48) as u8, + (value >> 40) as u8, + (value >> 32) as u8, + (value >> 24) as u8, + (value >> 16) as u8, + (value >> 8) as u8, + (value & 0xFF) as u8, + ] +} + +pub fn unpack_u64(data:&[u8], index:&mut usize) -> u64 +{ + let mut result = 0u64; + for _ in 0..8 { + result <<= 8; + if *index < data.len() { + result |= data[*index] as u64; + *index += 1; + } else { break; } } result } diff --git a/www/css/game.css b/www/css/game.css index 16234d1..fd001bb 100644 --- a/www/css/game.css +++ b/www/css/game.css @@ -2,7 +2,7 @@ main>canvas#game { display: block; position: relative; width: 100%; - flex-grow: 1; + height: 100%; background-color: #101010; } diff --git a/www/js/game.js b/www/js/game.js index a0c28a0..c797621 100644 --- a/www/js/game.js +++ b/www/js/game.js @@ -1,212 +1,362 @@ -const GAME_CONST = { - PLAYER_DAWN: 0, - PLAYER_DUSK: 1, +const GAME = { }; +let GAME_DATA = null; - SOURCE_BOARD: 0, - SOURCE_POOL: 1, -}; +GAME.Board = class { + constructor() { + this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile()); } + this.pieces = [ ]; + this.dawn = new GAME.Player(); + this.dusk = new GAME.Player(); -const GAME_CLASS = { - Board: class { - constructor() { - this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME_CLASS.Tile()); } - this.dawn = new GAME_CLASS.Player(); + this.init(); + } + + init() { + this.pieces = [ ]; + + // Describe Dawn layout + let layout = [ + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(0, 1) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(1, 1) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(2, 2) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(3, 2) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(4, 3) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(5, 3) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(6, 4) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(7, 4) }, + { piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(8, 5) }, + + { piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(0, 0) }, + { piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(8, 4) }, + + { piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(1, 0) }, + { piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(7, 3) }, + + { piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(2, 0) }, + { piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(6, 2) }, + + { piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(3, 0) }, + { piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(5, 1) }, + + { piece:GAME.Const.PieceId.Dragon, hex:new MATH.Vec2(4, 1) }, + { piece:GAME.Const.PieceId.Omen, hex:new MATH.Vec2(4, 0) }, + ]; + + // Add Dawn pieces + for(let piece of layout) { + this.place_piece(piece.piece, GAME.Const.Player.Dawn, piece.hex); } - }, - Player: class { - constructor() { - this.handle = ""; - this.pool = new GAME_CLASS.Pool(); + // Add Dusk pieces + for(let piece of layout) { + let hex = new MATH.Vec2(8 - piece.hex.x, 8 - piece.hex.y); + this.place_piece(piece.piece, GAME.Const.Player.Dusk, hex); } - }, + } - Pool: class { - constructor() { - this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(0); } - } - }, + place_piece(piece, player, hex) { + let game_piece = new GAME.Piece(piece, player); + game_piece.tile = this.hex_to_tile(hex); + this.tiles[game_piece.tile].piece = this.pieces.length; + this.pieces.push(game_piece); + } - Tile: class { - constructor() { - this.piece = 0; - } - }, + hex_to_tile(hex) { + let a = ((hex.x + 4) * (hex.x + 5) / 2) - 10; + let b = (hex.x > 4) * ((hex.x - 4) + ((hex.x - 5) * (hex.x - 4))); + return a - b + hex.y; + } - Move: class { - constructor(source, from, to) { - this.source = source; - this.from = from; - this.to = to; - } - }, - - GamePiece: class { - constructor(name, assets, moves, promote_moves) { - this.name = name; - this.assets = assets; - this.moves = moves; - this.pmoves = promote_moves; - } - }, - - Piece: class { - constructor(piece, player) { - this.piece = piece; - this.player = player; - this.promoted = false; - } - }, - - Game: class { - constructor() { - this.board = new GAME_CLASS.Board(); - this.pieces = [ - new GAME_CLASS.GamePiece("Militia", - ["asset/militia_dusk.svg", "asset/militia_dawn.svg"], - new Move() - .add(0) - .add(1) - .add(5), - - new Move() - .add(0) - .add(1) - .add(2) - .add(4) - .add(5) - ), - new GAME_CLASS.GamePiece("Knight", - ["asset/knight_dusk.svg", "asset/knight_dawn.svg"], - new Move() - .add(3) - .add(6) - .add(11) - .add(13) - .add(17), - - new Move() - .add(3) - .add(6) - .add(7) - .add(10) - .add(11) - .add(13) - .add(14) - .add(16) - .add(17) - ), - new GAME_CLASS.GamePiece("Lance", - ["asset/lance_dusk.svg", "asset/lance_dawn.svg"], - new Move() - .add(0, true) - .add(1) - .add(5), - - new Move() - .add(0, true) - .add(1, true) - .add(2, true) - .add(3, true) - .add(4, true) - .add(5, true) - ), - new GAME_CLASS.GamePiece("Tower", - ["asset/tower_dusk.svg", "asset/tower_dawn.svg"], - new Move() - .add(0) - .add(1) - .add(3) - .add(5) - .add(6) - .add(11), - - new Move() - .add(0) - .add(1) - .add(2) - .add(3) - .add(4) - .add(5) - .add(6) - .add(8) - .add(9) - .add(11) - ), - new GAME_CLASS.GamePiece("Castle", - ["asset/castle_dusk.svg", "asset/castle_dawn.svg"], - new Move() - .add(0) - .add(1) - .add(2) - .add(4) - .add(5) - .add(7) - .add(10), - - new Move() - .add(0) - .add(1) - .add(2) - .add(3) - .add(4) - .add(5) - .add(7, true) - .add(10, true) - ), - new GAME_CLASS.GamePiece("Dragon", - ["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"], - new Move() - .add(6, true) - .add(7, true) - .add(8, true) - .add(9, true) - .add(10, true) - .add(11, true), - - new Move() - .add(0, true) - .add(1, true) - .add(2, true) - .add(3, true) - .add(4, true) - .add(5, true) - .add(6, true) - .add(7, true) - .add(8, true) - .add(9, true) - .add(10, true) - .add(11, true) - ), - new GAME_CLASS.GamePiece("Omen", - ["asset/king_dusk.svg", "asset/king_dawn.svg"], - new Move() - .add(0) - .add(1) - .add(2) - .add(3) - .add(4) - .add(5) - .add(7) - .add(10) - ), - ]; - } + tile_to_hex(tile) { + const ROWS = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; + let column = 0; + while(tile >= ROWS[column + 1]) { column += 1; } + let row = tile - ROWS[column] + ((column > 4) * (column - 4)); + return new MATH.Vec2(column, row); } }; -let GAME_DATA = null; +GAME.Player = class { + constructor() { + this.handle = ""; + this.pool = new GAME.Pool(); + } +}; -const GAME = { - init() { - GAME_DATA = new GAME_CLASS.Game(); - }, +GAME.Pool = class { + constructor() { + this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(0); } + } +}; + +GAME.Tile = class { + constructor() { + this.piece = null; + this.threaten = { dawn: 0, dusk: 0 }; + this.checking = { dawn: false, dusk: false }; + } + + reset() { + this.threaten = { dawn: 0, dusk: 0 }; + this.checking = { dawn: false, dusk: false }; + } +}; + +GAME.Move = class { + constructor(source, from, to) { + this.source = source; + this.from = from; + this.to = to; + } +}; + +GAME.GamePiece = class { + constructor(name, assets, moves, promote_moves) { + this.name = name; + this.assets = assets; + this.moves = moves; + this.pmoves = promote_moves; + } +}; + +GAME.PieceMovement = class { + constructor() { + this.direction = 0; + this.stride = 0; + } + + add(direction) { + this.direction |= 1 << direction; + return this; + } + + add_stride(direction) { + this.direction |= 1 << direction; + this.stride |= 1 << direction; + return this; + } + + rotate() { + let copy = new GAME.PieceMovement(); + copy.direction = BITWISE.rotate_blocks(this.direction); + copy.stride = BITWISE.rotate_blocks(this.stride); + return copy; + } +}; + +GAME.Piece = class { + constructor(piece, player) { + this.piece = piece; + this.player = player; + this.promoted = false; + + this.tile = 0; + this.blocking = 0; + } +}; + +GAME.Game = class { + constructor() { + this.turn = 0; + this.board = new GAME.Board(); + } + + update_board() { + // Reset tiles + for(tile of this.board.tiles) { tile.reset(); } + + // Determine threaten, check, and blocking for each piece + for(piece of this.board.pieces) { + + } + } process(move) { - }, + + // Recalculate new board state. + GAME.update_board(); + } validate(move) { - }, + } + + get_move_tiles(piece_id) { + + } +}; + +GAME.Const = { + Player: { + Dawn: 0, + Dusk: 1, + }, + + Source: { + Board: 0, + Pool: 1, + }, + + Direction: [ + new MATH.Vec2(0, 1), + new MATH.Vec2(1, 1), + new MATH.Vec2(1, 0), + new MATH.Vec2(0, -1), + new MATH.Vec2(-1, -1), + new MATH.Vec2(-1, 0), + + new MATH.Vec2(1, 2), + new MATH.Vec2(2, 1), + new MATH.Vec2(1, -1), + new MATH.Vec2(-1, -2), + new MATH.Vec2(-2, -1), + new MATH.Vec2(-1, 1), + ], + + PieceId: { + Militia: 0, + Knight: 1, + Lance: 2, + Tower: 3, + Castle: 4, + Dragon: 5, + Omen: 6, + }, + + Piece: [ + new GAME.GamePiece( + "Militia", + ["asset/militia_dusk.svg", "asset/militia_dawn.svg"], + new GAME.PieceMovement() + .add(0) + .add(1) + .add(5), + new GAME.PieceMovement() + .add(0) + .add(1) + .add(2) + .add(4) + .add(5), + ), + new GAME.GamePiece( + "Knight", + ["asset/knight_dusk.svg", "asset/knight_dawn.svg"], + new GAME.PieceMovement() + .add(3) + .add(6) + .add(11) + .add(13) + .add(17), + new GAME.PieceMovement() + .add(3) + .add(6) + .add(7) + .add(10) + .add(11) + .add(13) + .add(14) + .add(16) + .add(17), + ), + new GAME.GamePiece( + "Lance", + ["asset/lance_dusk.svg", "asset/lance_dawn.svg"], + new GAME.PieceMovement() + .add_stride(0) + .add(1) + .add(5), + new GAME.PieceMovement() + .add_stride(0) + .add_stride(1) + .add_stride(2) + .add_stride(3) + .add_stride(4) + .add_stride(5), + ), + new GAME.GamePiece( + "Tower", + ["asset/tower_dusk.svg", "asset/tower_dawn.svg"], + new GAME.PieceMovement() + .add(0) + .add(1) + .add(3) + .add(5) + .add(6) + .add(11), + new GAME.PieceMovement() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add(6) + .add(8) + .add(9) + .add(11), + ), + new GAME.GamePiece( + "Castle", + ["asset/castle_dusk.svg", "asset/castle_dawn.svg"], + new GAME.PieceMovement() + .add(0) + .add(1) + .add(2) + .add(4) + .add(5) + .add(7) + .add(10), + new GAME.PieceMovement() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add_stride(7) + .add_stride(10), + ), + new GAME.GamePiece( + "Dragon", + ["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"], + new GAME.PieceMovement() + .add_stride(6) + .add_stride(7) + .add_stride(8) + .add_stride(9) + .add_stride(10) + .add_stride(11), + new GAME.PieceMovement() + .add_stride(0) + .add_stride(1) + .add_stride(2) + .add_stride(3) + .add_stride(4) + .add_stride(5) + .add_stride(6) + .add_stride(7) + .add_stride(8) + .add_stride(9) + .add_stride(10) + .add_stride(11), + ), + new GAME.GamePiece( + "Omen", + ["asset/king_dusk.svg", "asset/king_dawn.svg"], + new GAME.PieceMovement() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add(7) + .add(10), + ), + ], +}; + +GAME.init = () => { + GAME_DATA = new GAME.Game(); }; diff --git a/www/js/interface.js b/www/js/interface.js index cfe416d..6570416 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -1,11 +1,32 @@ -let INTERFACE_DATA = { - canvas:null, - context:null, - - scale:1, -}; +let INTERFACE_DATA = null; const INTERFACE = { + Color: { + Background: "#101010", + Text: "#c0c0c0", + + TileBorder: "#606060", + TileLight: "#383838", + TileMedium: "#242424", + TileDark: "#101010", + + Dawn: "#ffe082", + DawnDark: "#ff6d00", + + Dusk: "#f6a1bd", + DuskDark: "#c51162", + + HintHover: "#71a1e8", + HintSelect: "#4a148c", + HintValid: "#1a237e", + HintAllowed: "#6a1b9a", + HintInvalid: "b71c1c", + HintPlay: "#083242", + HintWarn: "#054719", + HintCheck: "#C62828", + + }, + hover(event) { }, @@ -14,66 +35,330 @@ const INTERFACE = { }, - resize() { - INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth; - INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight; - }, - draw() { - this.resize(); + let canvas = INTERFACE_DATA.canvas; + let ctx = INTERFACE_DATA.context; - // Determine - let width = INTERFACE_DATA.canvas.width; - let height = INTERFACE_DATA.canvas.height; - let min_dimension = Math.min(width, height); + // Determine interface configuration + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; - let scale = 1; - //let margin = INTERFACE_DATA.canvas. - - - // Draw indicator gradient if player's turn. + let width = canvas.width; + let height = canvas.height; + // Interface margin + let gui_margin = Math.floor(Math.min(width, height) / 100); + + // Interface width, height, and scale + let gui_width = width - (gui_margin * 2); + let gui_height = height - (gui_margin * 2); + + if(gui_width < gui_height * INTERFACE.Ratio) { + gui_height = Math.floor(gui_width / INTERFACE.Ratio); + } else { + gui_width = Math.floor(gui_height * INTERFACE.Ratio); + } + let gui_scale = gui_height * INTERFACE.Scale; + + // Boundries to center interface + let gui_offset = new MATH.Vec2( + (width - gui_width) / 2, + (height - gui_height) / 2, + ); + + + // Draw background + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = INTERFACE.Color.Background; + ctx.fillRect(0, 0, width, height); // Draw tiles - for(let i = 0; i < GAME.board.tiles.length; ++i) { - // Draw background + let radius = INTERFACE.Radius * gui_scale; + let basis_x = gui_offset.x + radius; + let basis_y = gui_offset.y + (13 * gui_scale); - // Draw piece + const TILE_SCALE = 0.9; + ctx.lineWidth = gui_scale * 0.06; - // Draw + let draw = new INTERFACE.Draw(ctx, TILE_SCALE * gui_scale); + + for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) { + let tile = GAME_DATA.board.tiles[i]; + + let coord = GAME_DATA.board.tile_to_hex(i); + let gui_x = basis_x + (1.5 * radius * coord.x); + let gui_y = basis_y - (2 * gui_scale * coord.y) + (gui_scale * coord.x); + + ctx.save(); + ctx.translate(gui_x, gui_y); + + // Draw background. + // Select indicator color or default to tile color. + if(true) { + switch(MATH.mod(coord.x + coord.y, 3)) { + case 0: ctx.fillStyle = INTERFACE.Color.TileMedium; break; + case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break; + case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break; + } + } else { + + } + ctx.beginPath(); + draw.hex(); + ctx.fill(); + + // Draw tile content + if(tile.piece !== null) { + let piece = GAME_DATA.board.pieces[tile.piece]; + + // Draw border + if(piece.player == GAME.Const.Player.Dawn) { ctx.strokeStyle = INTERFACE.Color.DawnDark; } + else { ctx.strokeStyle = INTERFACE.Color.DuskDark; } + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + + // Draw border hints + draw.hints(piece); + + // Draw piece + + } else { + // Draw standard border + ctx.strokeStyle = INTERFACE.Color.TileBorder; + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + } + + ctx.restore(); } // Draw player pool + for(let i = 0; i < 6; ++i) { + let gui_x = basis_x + (radius * 14); + let gui_y = basis_y - (9 - (2 * i)) * gui_scale; + ctx.save(); + ctx.translate(gui_x, gui_y); + + // Draw background if indicator is present. + if(true) { + ctx.fillStyle = INTERFACE.Color.TileDark; + ctx.beginPath(); + draw.hex(); + ctx.fill(); + } + + // Draw border + ctx.strokeStyle = INTERFACE.Color.Dawn; + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + + ctx.restore(); + } // Draw opponent pool + for(let i = 0; i < 6; ++i) { + let gui_x = basis_x + (radius * 15.5); + let gui_y = basis_y - (8 - (2 * i)) * gui_scale; + ctx.save(); + ctx.translate(gui_x, gui_y); + + // Draw background. + // Select indicator color or default to tile color. + if(true) { + ctx.fillStyle = INTERFACE.Color.TileDark; + } else { + + } + ctx.beginPath(); + draw.hex(); + ctx.fill(); + + // Draw border + ctx.strokeStyle = INTERFACE.Color.Dusk; + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + + ctx.restore(); + } + + + // Draw informational text + ctx.font = Math.ceil(gui_scale / 24) + "px sans-serif"; + + + /* + // Dawn handle + ctx.fillStyle = INTERFACE.Color.Text; + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + ctx.fillText(GAME_DATA.turn, width / 2, gui_margin); + + + // Dusk handle + ctx.fillStyle = INTERFACE.Color.Text; + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin); + */ + + // Number of moves + ctx.fillStyle = INTERFACE.Color.Text; + ctx.textBaseline = "top"; + ctx.textAlign = "right"; + ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin); + + // Game state message + if(INTERFACE_DATA.message !== null) { + ctx.fillStyle = INTERFACE.Color.Text; + ctx.textBaseline = "bottom"; + ctx.textAlign = "left"; + ctx.fillText(INTERFACE_DATA.message, gui_margin, height - gui_margin); + } }, - init() { + Draw: class { + constructor(ctx, scale) { + this.ctx = ctx; + this.scale = scale; + } + + hex() { + this.ctx.moveTo(INTERFACE.HexVertex[5].x * this.scale, INTERFACE.HexVertex[5].y * this.scale); + for(let i = 0; i < INTERFACE.HexVertex.length; ++i) { + this.ctx.lineTo(INTERFACE.HexVertex[i].x * this.scale, INTERFACE.HexVertex[i].y * this.scale); + } + } + + hints(piece) { + let descriptor = GAME.Const.Piece[piece.piece]; + let moves = descriptor.moves; + if(piece.promoted) { moves = descriptor.pmoves; } + if(piece.player == GAME.Const.Player.Dusk) { moves = moves.rotate(); } + moves = moves.direction; + + for(let mask = BITWISE.lsb(moves); moves > 0; mask = BITWISE.lsb(moves)) { + let move = BITWISE.ffs(mask); + let nmove = move % 12; + + this.ctx.strokeStyle = (piece.player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk; + this.ctx.beginPath(); + + // draw edge marker + if(nmove < 6) { + let fr = INTERFACE.HexVertex[nmove]; + let to = INTERFACE.HexVertex[(nmove + 1) % 6]; + + let dx = (to.x - fr.x) / 3.0; + let dy = (to.y - fr.y) / 3.0; + + let fqx = fr.x + dx; + let fqy = fr.y + dy; + + let tqx = fr.x + (2 * dx); + let tqy = fr.y + (2 * dy); + + this.ctx.moveTo(fqx * this.scale, fqy * this.scale); + this.ctx.lineTo(tqx * this.scale, tqy * this.scale); + this.ctx.stroke(); + } + + // draw vertex marker + else { + let fr = INTERFACE.HexVertex[nmove % 6]; + let mid = INTERFACE.HexVertex[(nmove + 1) % 6]; + let to = INTERFACE.HexVertex[(nmove + 2) % 6]; + + let dx1 = (mid.x - fr.x) / 3.0; + let dy1 = (mid.y - fr.y) / 3.0; + + let dx2 = (to.x - mid.x) / 3.0; + let dy2 = (to.y - mid.y) / 3.0; + + let fqx = mid.x - dx1; + let fqy = mid.y - dy1; + + let tqx = mid.x + dx2; + let tqy = mid.y + dy2; + + this.ctx.moveTo(fqx * this.scale, fqy * this.scale); + this.ctx.lineTo(mid.x * this.scale, mid.y * this.scale); + this.ctx.lineTo(tqx * this.scale, tqy * this.scale); + this.ctx.stroke(); + } + moves &= ~mask; + } + } + }, + + init(player_id) { GAME.init(); - INTERFACE_DATA.canvas = document.getElementById("game"); - let canvas = INTERFACE_DATA.canvas; + INTERFACE_DATA = { + canvas: document.getElementById("game"), + context: null, + player_id: player_id, + + message: null, + }; + + let canvas = INTERFACE_DATA.canvas; if(canvas !== undefined) { INTERFACE_DATA.context = canvas.getContext("2d"); canvas.addEventListener("mousemove", INTERFACE.hover); canvas.addEventListener("mousedown", INTERFACE.click); - canvas.addEventListener("resize", INTERFACE.draw); + window.addEventListener("resize", INTERFACE.draw); this.draw(); } }, uninit() { - INTERFACE_DATA.canvas = null; - INTERFACE_DATA.context = null; + MAIN.removeChild(INTERFACE_DATA.canvas); + INTERFACE_DATA = null; }, - message(data) { + message(code, data) { + switch(code) { + case OpCode.GameState: { + // Build game state + INTERFACE.draw(); + } break; + + case OpCode.GamePlay: { + // Apply play to board + GAME_DATA.turn += 1; + + INTERFACE.draw(); + } break; + } }, }; + +INTERFACE.Radius = 2.0 / Math.sqrt(3.0); +INTERFACE.HalfRadius = 1.0 / Math.sqrt(3.0); +INTERFACE.Scale = 1 / 18; +INTERFACE.Ratio = (17.5 * INTERFACE.Radius) * INTERFACE.Scale; +INTERFACE.HexVertex = [ + // top face + new MATH.Vec2(-INTERFACE.HalfRadius, -1), + // top-right face + new MATH.Vec2(INTERFACE.HalfRadius, -1), + // bottom-right face + new MATH.Vec2(INTERFACE.Radius, 0), + // bottom face + new MATH.Vec2(INTERFACE.HalfRadius, 1), + // bottom-left face + new MATH.Vec2(-INTERFACE.HalfRadius, 1), + // top-left face + new MATH.Vec2(-INTERFACE.Radius, 0), +]; diff --git a/www/js/scene.js b/www/js/scene.js index fcfe59c..402b9b1 100644 --- a/www/js/scene.js +++ b/www/js/scene.js @@ -432,6 +432,7 @@ const SCENES = { return true; }, unload() { + INTERFACE.uninit(); MESSAGE_COMPOSE([ PACK.u16(OpCode.SessionLeave), ]); diff --git a/www/js/util.js b/www/js/util.js index 7e560bc..44734a4 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -125,17 +125,49 @@ const BITWISE = { mask = mask - ((mask >> 1) & 0x55555555); mask = (mask & 0x33333333) + ((mask >> 2) & 0x33333333); return ((mask + (mask >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; - } + }, + + rotate_blocks(mask) + { + const r1 = 0x00003F; // first 6 bits + const r2 = 0x000FC0; // second 6 bits + const r3 = 0x03F000; // third 6 bits + + let v1 = (r1 & mask) << 3; + let v2 = (r2 & mask) << 3; + let v3 = (r3 & mask) << 3; + + v1 = (v1 & r1) | ((v1 & ~r1) >> 6); + v2 = (v2 & r2) | ((v2 & ~r2) >> 6); + v3 = (v3 & r3) | ((v3 & ~r3) >> 6); + + return v1 | v2 | v3; + }, }; const MATH = { - sign(a) - { + Vec2: class { + constructor(x, y) { + this.x = x; + this.y = y; + } + }, + + sign(a) { return 1 - ((a < 0) << 1); }, - mod(a, b) - { + mod(a, b) { return ((a % b) + b) % b }, }; + +const COLOR = { + rgba(r, g, b, a) { + let ur = Math.floor(r * 255); + let ug = Math.floor(r * 255); + let ub = Math.floor(r * 255); + let ua = Math.floor(r * 255); + return "rgba(" + ur + "," + ug + "," + ub + "," + ua + ")"; + }, +}