diff --git a/Cargo.lock b/Cargo.lock index 7400be6..381a06b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "approx" @@ -220,7 +220,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -231,9 +231,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1-rs" @@ -312,7 +312,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] @@ -447,7 +447,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -715,7 +715,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -979,7 +979,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1115,7 +1115,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1172,7 +1172,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1232,7 +1232,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1252,7 +1252,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1440,7 +1440,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -1550,6 +1550,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glam" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" + [[package]] name = "glob" version = "0.3.1" @@ -1945,9 +1951,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -1968,9 +1974,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error 2.0.1", @@ -2052,7 +2058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" dependencies = [ "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -2072,7 +2078,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -2550,7 +2556,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -2759,6 +2765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -3084,7 +3091,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -3157,7 +3164,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -3393,7 +3400,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -3541,14 +3548,14 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -3569,7 +3576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -3592,7 +3599,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -3789,8 +3796,9 @@ checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" [[package]] name = "ratatui" -version = "0.28.1" -source = "git+https://github.com/ratatui/ratatui#23c0d52c29f27547d94448be44aa46e85f49fbb0" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags 2.4.2", "cassowary", @@ -3804,7 +3812,7 @@ dependencies = [ "strum", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.1.13", + "unicode-width 0.2.0", ] [[package]] @@ -3844,15 +3852,16 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error 2.0.1", "rav1e", + "rayon", "rgb", ] @@ -3864,9 +3873,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -3896,7 +3905,7 @@ dependencies = [ [[package]] name = "rebels" -version = "1.0.18" +version = "1.0.19" dependencies = [ "anyhow", "async-trait", @@ -3907,6 +3916,7 @@ dependencies = [ "ed25519-dalek", "futures", "gif", + "glam", "image", "imageproc", "include_dir", @@ -4056,9 +4066,6 @@ name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] [[package]] name = "ring" @@ -4448,9 +4455,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -4467,20 +4474,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -4496,7 +4503,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -4809,7 +4816,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -4880,9 +4887,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -4982,7 +4989,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -5054,9 +5061,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -5078,7 +5085,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -5207,7 +5214,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -5305,8 +5312,9 @@ checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" [[package]] name = "tui-textarea" -version = "0.6.1" -source = "git+https://github.com/ricott1/tui-textarea#3ee0f63aaf7378dd8729ea5f6a44222dd7c24267" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" dependencies = [ "crossterm", "ratatui", @@ -5461,9 +5469,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -5544,7 +5552,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -5578,7 +5586,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6059,7 +6067,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] @@ -6079,7 +6087,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3029f0e..2c26a33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,15 @@ [package] name = "rebels" -version = "1.0.19" +version = "1.0.20" edition = "2021" +authors = ["Alessandro Ricottone "] +license = "GPL-3.0-or-later" +description = "Anarchic spacepirates playing basketball in your terminal" +readme = "README.md" +homepage = "https://github.com/ricott1/rebels-in-the-sky" +repository = "https://github.com/ricott1/rebels-in-the-sky" +keywords = ["cli", "pirates", "rebels", "game"] +categories = ["games"] [profile.release] strip = true @@ -16,17 +24,18 @@ opt-level = 1 opt-level = 3 [dependencies] -anyhow = "1.0.91" +anyhow = "1.0.93" async-trait = "0.1.83" chrono = "0.4.38" clap = { version = "4.5.20", features = ["derive"] } crossterm = "0.28.1" directories = "5.0.1" ed25519-dalek = "2.1.1" +flate2 = { version = "1.0.17", features = ["zlib-ng"], default-features = false } futures = "0.3.30" gif = "0.13.1" -glam = "0.29.0" -image = "0.25.4" +glam = "0.29.2" +image = "0.25.5" imageproc = "0.25.0" include_dir = "0.7.4" itertools = "0.13.0" @@ -48,20 +57,20 @@ rand = "0.8.5" rand_chacha = "0.3.1" rand_distr = "0.4.3" ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] } -rodio = "0.19.0" -russh = "0.45.0" -russh-keys = "0.45.0" -serde = { version = "1.0.212", features = ["derive", "default"] } +rodio = "0.20.1" +russh = "0.46.0" +russh-keys = "0.46.0" +serde = { version = "1.0.214", features = ["derive", "default"] } serde_json = "1.0.132" serde_repr = "0.1.19" sha2 = "0.10.8" -stream-download = { version = "0.9.0", features = ["reqwest-rustls"] } +stream-download = { version = "0.11.2", features = ["reqwest-rustls"] } strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" -tokio = { version = "1.41.0", features = ["full"] } +tokio = { version = "1.41.1", features = ["full"] } tokio-util = "0.7.12" tui-textarea = "0.7.0" unicode-width = "0.2.0" -url = "2.5.2" +url = "2.5.3" uuid = { version = "1.11.0", features = ["v4", "serde"] } void = "1.0.2" diff --git a/README.md b/README.md index 0f64e01..e6febc1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ https://github.com/user-attachments/assets/aaa02f04-06db-4da5-8fa4-732b60083e66 It's the year 2101. Corporations have taken over the world. -The only way to be free is to join a pirate crew and start plundering the galaxy. The only means of survival is to play basketball. +The only way to be free is to join a pirate crew and start plundering the galaxy. The only mean of survival is to play basketball. Now it's your turn to go out there and make a name for yourself. Create your crew and start wandering the galaxy in search of worthy basketball opponents. @@ -19,15 +19,22 @@ Connect via SSH to try the game. Save files are deleted after 2 days of inactivity. -## Download +## Installation -Compiled binaries of the last release can be also downloaded at https://rebels.frittura.org. +### Build -## Music +You need to have the rust toolchain installed --> https://www.rust-lang.org/tools/install. Then you can clone the repo and build the game with -Previous versions had the option to play music directly in the game, but this was removed to reduce the binary size and now music is streamed from internet radios. Nevertheless, you can still listen to the game soundtrack directly by connecting to `https://radio.frittura.org/rebels.ogg`! +`cargo build --release` -You can add more radio stations by including them in `assets/data/stream_data.json`. +### With cargo + +`cargo install rebels` + +### From the latest release page + +- Download the latest release asset for your platform from https://rebels.frittura.org; +- Give execution permissions to the executable with `chmod +x rebels` ### Distro Packages @@ -46,22 +53,12 @@ You can add more radio stations by including them in `assets/data/stream_data.js pacman -S rebels-in-the-sky ``` -## Build - -You need to have the rust toolchain installed --> https://www.rust-lang.org/tools/install. Then you can build the game with - -`cargo build --release` - ## Run This game runs as a terminal application, meaning that you just need to run the executable from your terminal with `./rebels` -If you downloaded the binaries, you will first need to give execution permissions to the executable with - -`chmod +x rebels` - Suggested minimal terminal size: 160x48. Not all terminals support the game colors nicely, so you might need to try different ones. Here is a list of tested terminals: - Linux: whatever the default terminal is, it should work @@ -70,10 +67,17 @@ Suggested minimal terminal size: 160x48. Not all terminals support the game colo **Important**: currently local bot teams are generated by default to make the game more enjoyable. This behaviour can be disabled by passing the `-f` flag to the executable. In the future, when more players will be available, the game will default to online teams only. +## Music + +Previous versions had the option to play music directly in the game, but this was removed to reduce the binary size and now music is streamed from internet radios. Nevertheless, you can still listen to the game soundtrack directly by connecting to `https://radio.frittura.org/rebels.ogg`! + +You can add more radio stations by including them in `assets/data/stream_data.json`. + + ## Credits -- Planet gifs were generated using the [pixel planet generator](https://deep-fold.itch.io/pixel-planet-generator) by [Deep Fold](https://deep-fold.itch.io/). -- Special thanks to [Il Deposito](https://www.ildeposito.org) for inspiration and the great musical archive. +- Planet gifs were generated using the [pixel planet generator](https://deep-fold.itch.io/pixel-planet-generator) by [Deep Fold](https://deep-fold.itch.io/). +- Special thanks to [Il Deposito](https://www.ildeposito.org) for inspiration and the great musical archive. ## Contribution diff --git a/assets/accessories/eye_patch_octopulp_central.png b/assets/accessories/eye_patch_octopulp_central.png new file mode 100644 index 0000000..b209932 Binary files /dev/null and b/assets/accessories/eye_patch_octopulp_central.png differ diff --git a/assets/data/planets_data.json b/assets/data/planets_data.json index 0eeef44..2d78462 100644 --- a/assets/data/planets_data.json +++ b/assets/data/planets_data.json @@ -25,7 +25,7 @@ "resources": {}, "filename": "sol", "rotation_period": 6, - "revolution_period": 180, + "revolution_period": 360, "gravity": 100, "asteroid_probability": 0.0, "planet_type": 1, @@ -48,7 +48,7 @@ "resources": {}, "filename": "proxima", "rotation_period": 3, - "revolution_period": 120, + "revolution_period": 360, "gravity": 80, "asteroid_probability": 0.0, "planet_type": 1, @@ -173,7 +173,7 @@ }, "filename": "earth", "rotation_period": 9, - "revolution_period": 120, + "revolution_period": 180, "gravity": 10, "asteroid_probability": 0.5, "planet_type": 2, @@ -296,7 +296,7 @@ }, "filename": "rocky", "rotation_period": 9, - "revolution_period": 120, + "revolution_period": 360, "gravity": 2, "asteroid_probability": 0.0, "planet_type": 8, @@ -323,7 +323,7 @@ }, "filename": "phobos", "rotation_period": 6, - "revolution_period": 120, + "revolution_period": 180, "gravity": 2, "asteroid_probability": 0.0, "planet_type": 8, diff --git a/assets/head/octopulp1.png b/assets/head/octopulp1.png index d127bc2..037fb03 100644 Binary files a/assets/head/octopulp1.png and b/assets/head/octopulp1.png differ diff --git a/assets/hull/mask_jester_standard.png b/assets/hull/mask_jester_standard.png index 620dbc0..a7b09df 100644 Binary files a/assets/hull/mask_jester_standard.png and b/assets/hull/mask_jester_standard.png differ diff --git a/assets/hull/mask_pincher_large.png b/assets/hull/mask_pincher_large.png index 64041b0..4a1e612 100644 Binary files a/assets/hull/mask_pincher_large.png and b/assets/hull/mask_pincher_large.png differ diff --git a/assets/hull/mask_pincher_standard.png b/assets/hull/mask_pincher_standard.png index 0db467a..f686d50 100644 Binary files a/assets/hull/mask_pincher_standard.png and b/assets/hull/mask_pincher_standard.png differ diff --git a/assets/hull/mask_shuttle_large.png b/assets/hull/mask_shuttle_large.png index 73c47cf..9d4d505 100644 Binary files a/assets/hull/mask_shuttle_large.png and b/assets/hull/mask_shuttle_large.png differ diff --git a/assets/hull/mask_shuttle_small.png b/assets/hull/mask_shuttle_small.png index 7aa61c0..669f03f 100644 Binary files a/assets/hull/mask_shuttle_small.png and b/assets/hull/mask_shuttle_small.png differ diff --git a/assets/hull/mask_shuttle_standard.png b/assets/hull/mask_shuttle_standard.png index 5613fb8..a7b9693 100644 Binary files a/assets/hull/mask_shuttle_standard.png and b/assets/hull/mask_shuttle_standard.png differ diff --git a/assets/hull/pincher_large.png b/assets/hull/pincher_large.png index 0520ec8..d099328 100644 Binary files a/assets/hull/pincher_large.png and b/assets/hull/pincher_large.png differ diff --git a/assets/shooter/jester_double.png b/assets/shooter/jester_double.png new file mode 100644 index 0000000..47ab07c Binary files /dev/null and b/assets/shooter/jester_double.png differ diff --git a/assets/shooter/jester_quadruple.png b/assets/shooter/jester_quadruple.png new file mode 100644 index 0000000..96772e1 Binary files /dev/null and b/assets/shooter/jester_quadruple.png differ diff --git a/assets/shooter/pincher_double1.png b/assets/shooter/pincher_double1.png new file mode 100644 index 0000000..1c6a08e Binary files /dev/null and b/assets/shooter/pincher_double1.png differ diff --git a/assets/shooter/pincher_double2.png b/assets/shooter/pincher_double2.png new file mode 100644 index 0000000..e5f9a73 Binary files /dev/null and b/assets/shooter/pincher_double2.png differ diff --git a/assets/shooter/pincher_quadruple1.png b/assets/shooter/pincher_quadruple1.png new file mode 100644 index 0000000..dced572 Binary files /dev/null and b/assets/shooter/pincher_quadruple1.png differ diff --git a/assets/shooter/pincher_quadruple2.png b/assets/shooter/pincher_quadruple2.png new file mode 100644 index 0000000..dd5cbf4 Binary files /dev/null and b/assets/shooter/pincher_quadruple2.png differ diff --git a/assets/shooter/shuttle_single0.png b/assets/shooter/shuttle_single0.png new file mode 100644 index 0000000..1bc164b Binary files /dev/null and b/assets/shooter/shuttle_single0.png differ diff --git a/assets/shooter/shuttle_single1.png b/assets/shooter/shuttle_single1.png new file mode 100644 index 0000000..b1ee4c4 Binary files /dev/null and b/assets/shooter/shuttle_single1.png differ diff --git a/assets/shooter/shuttle_single2.png b/assets/shooter/shuttle_single2.png new file mode 100644 index 0000000..9c9dcad Binary files /dev/null and b/assets/shooter/shuttle_single2.png differ diff --git a/assets/shooter/shuttle_triple0.png b/assets/shooter/shuttle_triple0.png new file mode 100644 index 0000000..a05e64d Binary files /dev/null and b/assets/shooter/shuttle_triple0.png differ diff --git a/assets/shooter/shuttle_triple1.png b/assets/shooter/shuttle_triple1.png new file mode 100644 index 0000000..410a370 Binary files /dev/null and b/assets/shooter/shuttle_triple1.png differ diff --git a/assets/shooter/shuttle_triple2.png b/assets/shooter/shuttle_triple2.png new file mode 100644 index 0000000..268a24b Binary files /dev/null and b/assets/shooter/shuttle_triple2.png differ diff --git a/assets/space_adventure/asteroid_big0.png b/assets/space_adventure/asteroid_big0.png new file mode 100644 index 0000000..b767453 Binary files /dev/null and b/assets/space_adventure/asteroid_big0.png differ diff --git a/assets/space_adventure/asteroid_big1.png b/assets/space_adventure/asteroid_big1.png index b767453..0bac194 100644 Binary files a/assets/space_adventure/asteroid_big1.png and b/assets/space_adventure/asteroid_big1.png differ diff --git a/assets/space_adventure/asteroid_big2.png b/assets/space_adventure/asteroid_big2.png index 0bac194..894d447 100644 Binary files a/assets/space_adventure/asteroid_big2.png and b/assets/space_adventure/asteroid_big2.png differ diff --git a/assets/space_adventure/asteroid_big3.png b/assets/space_adventure/asteroid_big3.png deleted file mode 100644 index 894d447..0000000 Binary files a/assets/space_adventure/asteroid_big3.png and /dev/null differ diff --git a/assets/space_adventure/asteroid_huge0.png b/assets/space_adventure/asteroid_huge0.png new file mode 100644 index 0000000..a85f378 Binary files /dev/null and b/assets/space_adventure/asteroid_huge0.png differ diff --git a/assets/space_adventure/asteroid_huge1.png b/assets/space_adventure/asteroid_huge1.png index a85f378..0b74451 100644 Binary files a/assets/space_adventure/asteroid_huge1.png and b/assets/space_adventure/asteroid_huge1.png differ diff --git a/assets/space_adventure/asteroid_huge2.png b/assets/space_adventure/asteroid_huge2.png index 0b74451..4904aac 100644 Binary files a/assets/space_adventure/asteroid_huge2.png and b/assets/space_adventure/asteroid_huge2.png differ diff --git a/assets/space_adventure/asteroid_huge3.png b/assets/space_adventure/asteroid_huge3.png deleted file mode 100644 index 4904aac..0000000 Binary files a/assets/space_adventure/asteroid_huge3.png and /dev/null differ diff --git a/assets/space_adventure/asteroid_small0.png b/assets/space_adventure/asteroid_small0.png new file mode 100644 index 0000000..efbb60f Binary files /dev/null and b/assets/space_adventure/asteroid_small0.png differ diff --git a/assets/space_adventure/asteroid_small1.png b/assets/space_adventure/asteroid_small1.png index efbb60f..0e092e6 100644 Binary files a/assets/space_adventure/asteroid_small1.png and b/assets/space_adventure/asteroid_small1.png differ diff --git a/assets/space_adventure/asteroid_small2.png b/assets/space_adventure/asteroid_small2.png index 0e092e6..0f477ba 100644 Binary files a/assets/space_adventure/asteroid_small2.png and b/assets/space_adventure/asteroid_small2.png differ diff --git a/assets/space_adventure/asteroid_small3.png b/assets/space_adventure/asteroid_small3.png deleted file mode 100644 index 0f477ba..0000000 Binary files a/assets/space_adventure/asteroid_small3.png and /dev/null differ diff --git a/assets/storage/mask_pincher_single.png b/assets/storage/mask_pincher_single.png deleted file mode 100644 index 7d892d4..0000000 Binary files a/assets/storage/mask_pincher_single.png and /dev/null differ diff --git a/assets/storage/mask_pincher_single1.png b/assets/storage/mask_pincher_single1.png new file mode 100644 index 0000000..8041133 Binary files /dev/null and b/assets/storage/mask_pincher_single1.png differ diff --git a/assets/storage/mask_pincher_single2.png b/assets/storage/mask_pincher_single2.png new file mode 100644 index 0000000..e5f0a2f Binary files /dev/null and b/assets/storage/mask_pincher_single2.png differ diff --git a/assets/storage/pincher_single.png b/assets/storage/pincher_single.png deleted file mode 100644 index f3bbe65..0000000 Binary files a/assets/storage/pincher_single.png and /dev/null differ diff --git a/assets/storage/pincher_single1.png b/assets/storage/pincher_single1.png new file mode 100644 index 0000000..f427aa4 Binary files /dev/null and b/assets/storage/pincher_single1.png differ diff --git a/assets/storage/pincher_single2.png b/assets/storage/pincher_single2.png new file mode 100644 index 0000000..e71b299 Binary files /dev/null and b/assets/storage/pincher_single2.png differ diff --git a/src/app.rs b/src/app.rs index e8f193b..a81570b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ use stream_download::StreamDownload; use tokio::select; use void::Void; -const NETWORK_HANDLER_INIT_INTERVAL: u128 = 10 * SECONDS; +const NETWORK_HANDLER_INIT_INTERVAL: Tick = 10 * SECONDS; #[derive(Debug, PartialEq)] pub enum AppState { @@ -40,6 +40,7 @@ pub struct App { seed_ip: Option, network_port: Option, store_prefix: String, + pub new_version_notified: bool, } impl App { @@ -194,6 +195,7 @@ impl App { seed_ip, network_port, store_prefix: store_prefix.to_string(), + new_version_notified: false, } } @@ -227,7 +229,6 @@ impl App { Some(streaming_data) = Self::conditional_audio_event(& self.audio_player) => self.handle_streaming_data(streaming_data)?, Some(swarm_event) = Self::conditional_network_event(&mut self.network_handler) => self.handle_network_events(swarm_event)?, app_event = tui.events.next() => { - match app_event{ TerminalEvent::Tick {tick} => { self.handle_tick_events(tick)?; @@ -240,12 +241,13 @@ impl App { if let Err(e) = tui.draw(&mut self.ui, &self.world, self.audio_player.as_ref()).await { error!("Drawing error: {e}"); } - }, - TerminalEvent::Mouse(mouse_event) => {self.handle_mouse_events(mouse_event)?; + }, + TerminalEvent::Mouse(mouse_event) => { + self.handle_mouse_events(mouse_event)?; if let Err(e) = tui.draw(&mut self.ui, &self.world, self.audio_player.as_ref()).await { error!("Drawing error: {e}"); } - }, + }, TerminalEvent::Resize(w, h) => tui.resize((w, h))?, TerminalEvent::Quit => self.quit()?, } diff --git a/src/game_engine/action.rs b/src/game_engine/action.rs index b68e552..51706ca 100644 --- a/src/game_engine/action.rs +++ b/src/game_engine/action.rs @@ -39,6 +39,7 @@ pub enum ActionSituation { AfterOffensiveRebound, AfterLongOffensiveRebound, AfterDefensiveRebound, + AfterSubstitution, MissedShot, Turnover, CloseShot, diff --git a/src/game_engine/game.rs b/src/game_engine/game.rs index 2651c21..d72f9d6 100644 --- a/src/game_engine/game.rs +++ b/src/game_engine/game.rs @@ -189,8 +189,12 @@ impl<'game> Game { game.attendance = attendance as u32; let mut default_output = ActionOutput::default(); default_output.description = format!( - "{} vs {}. Game is about to start here on {}! There are {} people in the stadium.", - home_name, away_name, planet.name, game.attendance + "{} vs {}. Game is about to start here on {}! There are {} {}people in the stadium.", + home_name, + away_name, + planet.name, + game.attendance, + if game.attendance == 69 { "(nice) " } else { "" } ); default_output.random_seed = seed; game.action_results.push(default_output); @@ -280,7 +284,7 @@ impl<'game> Game { ActionSituation::LongShot => Action::LongShot, ActionSituation::MissedShot => Action::Rebound, ActionSituation::EndOfQuarter => Action::StartOfQuarter, - ActionSituation::BallInBackcourt => { + ActionSituation::AfterSubstitution | ActionSituation::BallInBackcourt => { let brawl_probability = BRAWL_ACTION_PROBABILITY * (self.home_team_in_game.tactic.brawl_probability_modifier() + self.away_team_in_game.tactic.brawl_probability_modifier()); @@ -474,9 +478,8 @@ impl<'game> Game { fn get_rng_seed(&self) -> [u8; 32] { let mut seed = [0; 32]; seed[0..16].copy_from_slice(self.id.as_bytes()); - seed[16..32].copy_from_slice(self.starting_at.to_be_bytes().as_ref()); - // Overwrite first two bytes with timer value - seed[0..2].copy_from_slice(self.timer.value.to_be_bytes().as_ref()); + seed[16..24].copy_from_slice(self.starting_at.to_be_bytes().as_ref()); + seed[24..26].copy_from_slice(self.timer.value.to_be_bytes().as_ref()); seed } @@ -719,7 +722,7 @@ impl<'game> Game { ..Default::default() }); } - _ => + (false, false) => // Check if teams make substitutions. Only if ball is out { if let Some(sub) = Substitution::execute(action_input, self, rng) { @@ -740,7 +743,7 @@ impl<'game> Game { mod tests { use super::Game; use crate::game_engine::types::TeamInGame; - use crate::types::GameId; + use crate::types::{AppResult, GameId}; use crate::types::{SystemTimeTick, Tick}; use crate::world::constants::DEFAULT_PLANET_ID; use crate::world::world::World; @@ -749,36 +752,28 @@ mod tests { #[ignore] #[test] - fn test_game() { + fn test_game() -> AppResult<()> { let mut world = World::new(None); let rng = &mut ChaCha8Rng::seed_from_u64(world.seed); - // world.initialize(true); - - // let home_planet = world.planets.get(&DEFAULT_PLANET_ID).unwrap(); - let id0 = world - .generate_random_team( - rng, - DEFAULT_PLANET_ID.clone(), - "Testen".to_string(), - "Tosten".to_string(), - ) - .unwrap(); - let id1 = world - .generate_random_team( - rng, - DEFAULT_PLANET_ID.clone(), - "Holalo".to_string(), - "Halley".to_string(), - ) - .unwrap(); - - let home_team = world.get_team(id0).unwrap().clone(); + let id0 = world.generate_random_team( + rng, + DEFAULT_PLANET_ID.clone(), + "Testen".to_string(), + "Tosten".to_string(), + )?; + let id1 = world.generate_random_team( + rng, + DEFAULT_PLANET_ID.clone(), + "Holalo".to_string(), + "Halley".to_string(), + )?; + + let home_team = world.get_team(&id0).unwrap().clone(); let checked_player_id = home_team.player_ids[0]; let quickness_before = world - .get_player(checked_player_id) - .unwrap() + .get_player_or_err(&checked_player_id)? .athletics .quickness .clone(); @@ -791,7 +786,7 @@ mod tests { home_team_in_game.unwrap(), away_team_in_game.unwrap(), Tick::now(), - &world.get_planet(DEFAULT_PLANET_ID.clone()).unwrap(), + &world.get_planet(&DEFAULT_PLANET_ID).unwrap(), ); game.home_team_in_game @@ -821,11 +816,12 @@ mod tests { let _ = world.handle_tick_events(Tick::now()); } let quickness_after = world - .get_player(checked_player_id) - .unwrap() + .get_player_or_err(&checked_player_id)? .athletics .quickness .clone(); println!("{} {}", quickness_before, quickness_after); + + Ok(()) } } diff --git a/src/game_engine/rebound.rs b/src/game_engine/rebound.rs index 1cd9745..42e43ac 100644 --- a/src/game_engine/rebound.rs +++ b/src/game_engine/rebound.rs @@ -14,7 +14,7 @@ use std::{ collections::HashMap, }; -const MIN_REBOUND_VALUE: u16 = 42; +const MIN_REBOUND_VALUE: u16 = 40; const REBOUND_POSITION_SCALING: f32 = 12.0; fn position_rebound_bonus(idx: usize) -> f32 { diff --git a/src/game_engine/substitution.rs b/src/game_engine/substitution.rs index 6a9af79..6b4660e 100644 --- a/src/game_engine/substitution.rs +++ b/src/game_engine/substitution.rs @@ -1,5 +1,5 @@ use super::{ - action::ActionOutput, + action::{ActionOutput, ActionSituation}, constants::MIN_TIREDNESS_FOR_SUB, game::Game, types::{GameStats, GameStatsMap, Possession}, @@ -178,7 +178,7 @@ impl Substitution { possession: input.possession, attackers: input.attackers.clone(), defenders: input.defenders.clone(), - situation: input.situation.clone(), + situation: ActionSituation::AfterSubstitution, assist_from: input.assist_from, start_at: input.start_at, end_at: input.end_at, diff --git a/src/game_engine/timer.rs b/src/game_engine/timer.rs index 1129b1b..a42a7a0 100644 --- a/src/game_engine/timer.rs +++ b/src/game_engine/timer.rs @@ -154,10 +154,6 @@ impl Timer { return "Q4 00:00".to_string(); } - // if !self.has_started() { - // return "Q1 10:00".to_string(); - // } - if self.is_break() && self.value == self.period().end() { format!("{:2} 10:00", self.period().next(),) } else { diff --git a/src/image/components.rs b/src/image/components.rs index f8282a7..c63702c 100644 --- a/src/image/components.rs +++ b/src/image/components.rs @@ -1,14 +1,41 @@ +use image::RgbaImage; use serde_repr::{Deserialize_repr, Serialize_repr}; use strum::EnumIter; use strum_macros::Display; -use crate::world::spaceship::{Engine, Hull, Storage}; +use crate::{ + types::AppResult, + world::spaceship::{Engine, Hull, Shooter, Storage}, +}; + +use super::utils::open_image; pub trait ImageComponent { + fn select_file(&self) -> String; + fn select_mask_file(&self) -> String { + self.select_file() + } + fn image(&self) -> AppResult { + open_image(&self.select_file()) + } + + fn mask(&self) -> AppResult { + open_image(&self.select_mask_file()) + } +} + +pub trait SizedImageComponent { fn select_file(&self, size: u8) -> String; fn select_mask_file(&self, size: u8) -> String { self.select_file(size) } + fn image(&self, size: u8) -> AppResult { + open_image(&self.select_file(size)) + } + + fn mask(&self, size: u8) -> AppResult { + open_image(&self.select_mask_file(size)) + } } #[derive(Debug, Clone, Copy, Display, Serialize_repr, Deserialize_repr, PartialEq)] @@ -24,7 +51,7 @@ pub enum BeardImage { } impl ImageComponent for BeardImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { format!("beard/{}.png", self.to_string().to_lowercase()) } } @@ -45,7 +72,7 @@ pub enum HairImage { } impl ImageComponent for HairImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { format!("hair/{}.png", self.to_string().to_lowercase()) } } @@ -70,11 +97,11 @@ pub enum HeadImage { } impl ImageComponent for HeadImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { format!("head/{}.png", self.to_string().to_lowercase()) } - fn select_mask_file(&self, _size: u8) -> String { + fn select_mask_file(&self) -> String { format!("head/mask_{}.png", self.to_string().to_lowercase()) } } @@ -89,7 +116,7 @@ pub enum BodyImage { Normal, } -impl ImageComponent for BodyImage { +impl SizedImageComponent for BodyImage { fn select_file(&self, size: u8) -> String { let name = match self { Self::Pupparoll => "pupparoll", @@ -138,7 +165,7 @@ pub enum LegsImage { Normal, } -impl ImageComponent for LegsImage { +impl SizedImageComponent for LegsImage { fn select_file(&self, size: u8) -> String { let number = match size { 0 => 0, @@ -190,7 +217,7 @@ pub enum ShirtImage { PirateGald, } -impl ImageComponent for ShirtImage { +impl SizedImageComponent for ShirtImage { fn select_file(&self, size: u8) -> String { let number = match size { x if x <= 2 => 0, @@ -230,7 +257,7 @@ pub enum ShortsImage { Pupparoll, } -impl ImageComponent for ShortsImage { +impl SizedImageComponent for ShortsImage { fn select_file(&self, size: u8) -> String { if self == &ShortsImage::Pupparoll { return "shorts/pupparoll.png".into(); @@ -262,7 +289,7 @@ pub enum ShoesImage { Classic, } -impl ImageComponent for ShoesImage { +impl SizedImageComponent for ShoesImage { fn select_file(&self, size: u8) -> String { let number = match size { x if x < 7 => 0, @@ -290,7 +317,7 @@ pub enum HatImage { } impl ImageComponent for HatImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { format!("hat/{}.png", self.to_string().to_lowercase()) } } @@ -302,11 +329,13 @@ pub enum WoodenLegImage { Right, } -impl ImageComponent for WoodenLegImage { +impl SizedImageComponent for WoodenLegImage { fn select_file(&self, size: u8) -> String { - match size { - x if x < 7 => "wooden_leg/slim.png".into(), - _ => "wooden_leg/large.png".into(), + match self { + Self::Left | Self::Right => match size { + x if x < 7 => "wooden_leg/slim.png".into(), + _ => "wooden_leg/large.png".into(), + }, } } } @@ -320,10 +349,11 @@ pub enum EyePatchImage { RightHigh, Central, Pupparoll, + OctopulpCentral, } impl ImageComponent for EyePatchImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { match self { EyePatchImage::LeftLow => "accessories/eye_patch_left_low.png".into(), EyePatchImage::RightLow => "accessories/eye_patch_right_low.png".into(), @@ -331,6 +361,7 @@ impl ImageComponent for EyePatchImage { EyePatchImage::RightHigh => "accessories/eye_patch_right_high.png".into(), EyePatchImage::Central => "accessories/eye_patch_central.png".into(), EyePatchImage::Pupparoll => "accessories/eye_patch_pupparoll.png".into(), + EyePatchImage::OctopulpCentral => "accessories/eye_patch_octopulp_central.png".into(), } } } @@ -345,7 +376,7 @@ pub enum HookImage { } impl ImageComponent for HookImage { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { match self { HookImage::Left => "accessories/hook_left.png".into(), HookImage::Right => "accessories/hook_right.png".into(), @@ -356,7 +387,7 @@ impl ImageComponent for HookImage { } impl ImageComponent for Hull { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { match self { Hull::ShuttleSmall => "hull/shuttle_small.png".into(), Hull::ShuttleStandard => "hull/shuttle_standard.png".into(), @@ -367,7 +398,7 @@ impl ImageComponent for Hull { } } - fn select_mask_file(&self, _size: u8) -> String { + fn select_mask_file(&self) -> String { match self { Hull::ShuttleSmall => "hull/mask_shuttle_small.png".into(), Hull::ShuttleStandard => "hull/mask_shuttle_standard.png".into(), @@ -380,7 +411,7 @@ impl ImageComponent for Hull { } impl ImageComponent for Engine { - fn select_file(&self, _size: u8) -> String { + fn select_file(&self) -> String { match self { Engine::ShuttleSingle => "engine/shuttle_single.png".into(), Engine::ShuttleDouble => "engine/shuttle_double.png".into(), @@ -394,21 +425,41 @@ impl ImageComponent for Engine { } } -impl ImageComponent for Storage { +impl SizedImageComponent for Storage { + fn image(&self, size: u8) -> AppResult { + match self { + Self::PincherNone | Self::ShuttleNone | Self::JesterNone => Ok(RgbaImage::new(0, 0)), + _ => open_image(&self.select_file(size)), + } + } + + fn mask(&self, size: u8) -> AppResult { + match self { + Self::PincherNone | Self::ShuttleNone | Self::JesterNone => Ok(RgbaImage::new(0, 0)), + _ => open_image(&self.select_mask_file(size)), + } + } + fn select_file(&self, size: u8) -> String { match self { Storage::ShuttleSingle => match size { 0 => "storage/shuttle_single0.png".into(), 1 => "storage/shuttle_single1.png".into(), - _ => "storage/shuttle_single2.png".into(), + 2 => "storage/shuttle_single2.png".into(), + _ => unreachable!("No image should be required for this component"), }, Storage::ShuttleDouble => match size { 0 => "storage/shuttle_double0.png".into(), 1 => "storage/shuttle_double1.png".into(), - _ => "storage/shuttle_double2.png".into(), + 2 => "storage/shuttle_double2.png".into(), + _ => unreachable!("No image should be required for this component"), + }, + Storage::PincherSingle => match size { + 1 => "storage/pincher_single1.png".into(), + 2 => "storage/pincher_single2.png".into(), + _ => unreachable!("No image should be required for this component"), }, - Storage::PincherSingle => "storage/pincher_single.png".into(), - _ => panic!("No image should be required for this component"), + _ => unreachable!("No image should be required for this component"), } } @@ -417,15 +468,49 @@ impl ImageComponent for Storage { Storage::ShuttleSingle => match size { 0 => "storage/mask_shuttle_single0.png".into(), 1 => "storage/mask_shuttle_single1.png".into(), - _ => "storage/mask_shuttle_single2.png".into(), + 2 => "storage/mask_shuttle_single2.png".into(), + _ => unreachable!("No image should be required for this component"), }, Storage::ShuttleDouble => match size { 0 => "storage/mask_shuttle_double0.png".into(), 1 => "storage/mask_shuttle_double1.png".into(), - _ => "storage/mask_shuttle_double2.png".into(), + 2 => "storage/mask_shuttle_double2.png".into(), + _ => unreachable!("No image should be required for this component"), }, - Storage::PincherSingle => format!("storage/mask_pincher_single.png"), - _ => panic!("No image should be required for this component"), + Storage::PincherSingle => match size { + 1 => "storage/mask_pincher_single1.png".into(), + 2 => "storage/mask_pincher_single2.png".into(), + _ => unreachable!("No image should be required for this component"), + }, + _ => unreachable!("No image should be required for this component"), + } + } +} + +impl SizedImageComponent for Shooter { + fn image(&self, size: u8) -> AppResult { + match self { + Self::PincherNone | Self::ShuttleNone | Self::JesterNone => Ok(RgbaImage::new(0, 0)), + _ => open_image(&self.select_file(size)), + } + } + + fn mask(&self, size: u8) -> AppResult { + match self { + Self::PincherNone | Self::ShuttleNone | Self::JesterNone => Ok(RgbaImage::new(0, 0)), + _ => open_image(&self.select_mask_file(size)), + } + } + + fn select_file(&self, size: u8) -> String { + match self { + Self::ShuttleSingle => format!("shooter/shuttle_single{}.png", size), + Self::ShuttleTriple => format!("shooter/shuttle_triple{}.png", size), + Self::PincherDouble => format!("shooter/pincher_double{}.png", size), + Self::PincherQuadruple => format!("shooter/pincher_quadruple{}.png", size), + Self::JesterDouble => "shooter/jester_double.png".into(), + Self::JesterQuadruple => "shooter/jester_quadruple.png".into(), + _ => unreachable!("No image should be required for this component"), } } } diff --git a/src/image/player.rs b/src/image/player.rs index 61e7fc1..6afd63a 100644 --- a/src/image/player.rs +++ b/src/image/player.rs @@ -6,7 +6,7 @@ use crate::types::AppResult; use crate::world::jersey::{Jersey, JerseyStyle}; use crate::world::player::InfoStats; use crate::world::role::CrewRole; -use crate::world::types::{size_from_info, Population, Pronoun, SIZE_LARGE_OFFSET}; +use crate::world::types::{Population, Pronoun}; use image::RgbaImage; use rand::seq::IteratorRandom; use rand::{Rng, SeedableRng}; @@ -38,6 +38,24 @@ pub struct PlayerImage { } impl PlayerImage { + pub const IMAGE_SIZE_LARGE_OFFSET: u8 = 7; + pub fn size_from_info(info: &InfoStats) -> u8 { + let mut size = match info.height { + x if x <= 184.0 => 0, + x if x <= 190.0 => 1, + x if x <= 196.0 => 2, + x if x <= 202.0 => 3, + x if x <= 208.0 => 4, + x if x <= 214.0 => 5, + _ => 6, + }; + let bmi = info.weight as u32 * 10_000 / (info.height as u32 * info.height as u32); + if bmi >= 27 || info.population == Population::Pupparoll { + size += Self::IMAGE_SIZE_LARGE_OFFSET; + } + size + } + pub fn from_info(info: &InfoStats, rng: &mut ChaCha8Rng) -> Self { let body = match info.population { Population::Polpett => BodyImage::Polpett, @@ -292,10 +310,20 @@ impl PlayerImage { 1 => Some(EyePatchImage::RightLow), _ => Some(EyePatchImage::Central), }, - Population::Polpett | Population::Yardalaim | Population::Octopulp => { - match rng.gen_range(0..=1) { - 0 => Some(EyePatchImage::LeftLow), - _ => Some(EyePatchImage::RightLow), + Population::Polpett | Population::Yardalaim => match rng.gen_range(0..=1) { + 0 => Some(EyePatchImage::LeftLow), + _ => Some(EyePatchImage::RightLow), + }, + Population::Octopulp => { + if self.head == HeadImage::Octopulp1 { + Some(EyePatchImage::OctopulpCentral) + } else if self.head == HeadImage::Octopulp2 { + match rng.gen_range(0..=1) { + 0 => Some(EyePatchImage::LeftLow), + _ => Some(EyePatchImage::RightLow), + } + } else { + unreachable!() } } Population::Pupparoll => Some(EyePatchImage::Pupparoll), @@ -321,7 +349,7 @@ impl PlayerImage { } pub fn compose(&self, info: &InfoStats) -> AppResult { - let size = size_from_info(info); + let size = Self::size_from_info(info); let mut base = RgbaImage::new(PLAYER_IMAGE_WIDTH, PLAYER_IMAGE_HEIGHT); let mut blinking_base = RgbaImage::new(PLAYER_IMAGE_WIDTH, PLAYER_IMAGE_HEIGHT); let img_height = base.height(); @@ -330,8 +358,8 @@ impl PlayerImage { let hair_color_map = self.hair_color_map; let jersey_color_map = self.jersey_color_map; - let mut other = open_image(self.legs.select_file(size).as_str())?; - let mask = open_image(self.legs.select_mask_file(size).as_str())?; + let mut other = self.legs.image(size)?; + let mask = self.legs.mask(size)?; other.apply_color_map_with_shadow_mask(skin_color_map, &mask); @@ -340,8 +368,8 @@ impl PlayerImage { base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; blinking_base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; - if let Some(shoes) = self.shoes.clone() { - let mut other = open_image(shoes.select_file(size).as_str())?; + if let Some(shoes) = self.shoes.as_ref() { + let mut other = shoes.image(size)?; let x = (base.width() - other.width()) / 2; if let Some(color_map) = jersey_color_map { other.apply_color_map(color_map); @@ -350,19 +378,19 @@ impl PlayerImage { blinking_base.copy_non_trasparent_from(&other, x, img_height - other.height())?; } - if let Some(shorts) = self.shorts.clone() { - let mut other = open_image(shorts.select_file(size).as_str())?; + if let Some(shorts) = self.shorts.as_ref() { + let mut other = shorts.image(size)?; let x = (base.width() - other.width()) / 2; if let Some(color_map) = jersey_color_map { - let mask = open_image(shorts.select_mask_file(size).as_str())?; + let mask = shorts.mask(size)?; other.apply_color_map_with_shadow_mask(color_map, &mask); } base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; blinking_base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; } - if let Some(wooden_leg) = self.wooden_leg.clone() { - //Polpett have small legs regardless of size + if let Some(wooden_leg) = self.wooden_leg.as_ref() { + //Polpett and Pupparoll have small legs regardless of size let leg_size = if info.population == Population::Polpett || info.population == Population::Pupparoll { @@ -370,10 +398,11 @@ impl PlayerImage { } else { size }; - let other = open_image(wooden_leg.select_file(leg_size).as_str())?; + + let other = wooden_leg.image(leg_size)?; // Polpett have the wooden leg moved to the side let offset = if info.population == Population::Polpett { - if size >= SIZE_LARGE_OFFSET { + if size >= Self::IMAGE_SIZE_LARGE_OFFSET { 2 } else { 1 @@ -412,16 +441,16 @@ impl PlayerImage { blinking_base.copy_non_trasparent_from(&other, x, img_height - other.height())?; } - let mut other = open_image(self.body.select_file(size).as_str())?; - let mask = open_image(self.body.select_mask_file(size).as_str())?; + let mut other = self.body.image(size)?; + let mask = self.body.mask(size)?; offset_y += other.height() - 1; let body_x = (base.width() - other.width()) / 2; other.apply_color_map_with_shadow_mask(skin_color_map, &mask); base.copy_non_trasparent_from(&other, body_x, img_height - offset_y)?; blinking_base.copy_non_trasparent_from(&other, body_x, img_height - offset_y)?; - if let Some(hook) = self.hook.clone() { - let mut hook_img = open_image(hook.select_file(size).as_str())?; + if let Some(hook) = self.hook.as_ref() { + let mut hook_img = hook.image()?; if let Some(color_map) = jersey_color_map { hook_img.apply_color_map(color_map); @@ -448,20 +477,20 @@ impl PlayerImage { blinking_base.copy_non_trasparent_from(&hook_img, x, y)?; } - if let Some(shirt) = self.shirt.clone() { - let mut other = open_image(shirt.select_file(size).as_str())?; + if let Some(shirt) = self.shirt.as_ref() { + let mut other = shirt.image(size)?; let x = (base.width() - other.width()) / 2; if let Some(color_map) = jersey_color_map { - let mask = open_image(shirt.select_mask_file(size).as_str())?; + let mask = shirt.mask(size)?; other.apply_color_map_with_shadow_mask(color_map, &mask); } base.copy_non_trasparent_from(&other, x, img_height - offset_y + 1)?; blinking_base.copy_non_trasparent_from(&other, x, img_height - offset_y + 1)?; } - let mut other = open_image(self.head.select_file(size).as_str())?; - let mut blinking = open_image(self.head.select_file(size).as_str())?; - let mask = open_image(self.head.select_mask_file(size).as_str())?; + let mut other = self.head.image()?; + let mut blinking = self.head.image()?; + let mask = self.head.mask()?; offset_y += other.height() - 5; let x = (base.width() - other.width()) / 2; let mut cm = skin_color_map; @@ -471,15 +500,15 @@ impl PlayerImage { blinking.apply_color_map_with_shadow_mask(cm, &mask); blinking_base.copy_non_trasparent_from(&blinking, x, img_height - offset_y)?; - if let Some(eye_patch) = self.eye_patch.clone() { - let other = open_image(eye_patch.select_file(size).as_str())?; + if let Some(eye_patch) = self.eye_patch.as_ref() { + let other = eye_patch.image()?; let x = (base.width() - other.width()) / 2; base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; blinking_base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; } - if let Some(hair) = self.hair.clone() { - let mut other = open_image(hair.select_file(size).as_str())?; + if let Some(hair) = self.hair.as_ref() { + let mut other = open_image(hair.select_file().as_str())?; let x = (base.width() - other.width()) / 2; other.apply_color_map(hair_color_map); @@ -493,8 +522,8 @@ impl PlayerImage { blinking_base.copy_non_trasparent_from(&other, x, y)?; } - if let Some(beard) = self.beard.clone() { - let mut other = open_image(beard.select_file(size).as_str())?; + if let Some(beard) = self.beard.as_ref() { + let mut other = open_image(beard.select_file().as_str())?; let x = (base.width() - other.width()) / 2; if info.population == Population::Octopulp { other.apply_color_map(skin_color_map); @@ -505,8 +534,8 @@ impl PlayerImage { blinking_base.copy_non_trasparent_from(&other, x, img_height - offset_y)?; } - if let Some(hat) = self.hat.clone() { - let other = open_image(hat.select_file(size).as_str())?; + if let Some(hat) = self.hat.as_ref() { + let other = open_image(hat.select_file().as_str())?; let x = (base.width() - other.width()) / 2; offset_y += 2; let y = if info.population == Population::Pupparoll { @@ -568,7 +597,7 @@ mod tests { )?; } image::save_buffer( - &Path::new(format!("tests/image_{}.png", population).as_str()), + &Path::new(format!("tests/player_image_{}.png", population).as_str()), &base, PLAYER_IMAGE_WIDTH * n, PLAYER_IMAGE_HEIGHT, diff --git a/src/image/spaceship.rs b/src/image/spaceship.rs index d849bc2..fc31a39 100644 --- a/src/image/spaceship.rs +++ b/src/image/spaceship.rs @@ -5,8 +5,8 @@ use super::components::*; use super::types::Gif; use super::utils::{open_image, ExtraImageUtils}; use crate::types::AppResult; -use crate::world::spaceship::{Engine, Hull, SpaceshipComponent, Storage}; -use image::RgbaImage; +use crate::world::spaceship::{Engine, Hull, Shooter, SpaceshipComponent, Storage}; +use image::{Rgba, RgbaImage}; use serde; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -18,7 +18,7 @@ pub type SpaceshipImageId = Vec; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Hash, Default)] pub struct SpaceshipImage { - color_map: ColorMap, + pub color_map: ColorMap, } impl SpaceshipImage { @@ -44,18 +44,31 @@ impl SpaceshipImage { self.color_map = color_map; } + pub fn size(hull: &Hull) -> u8 { + match hull { + Hull::ShuttleSmall => 0, + Hull::ShuttleStandard => 1, + Hull::ShuttleLarge => 2, + Hull::PincherStandard => 1, + Hull::PincherLarge => 2, + Hull::JesterStandard => 1, + } + } + pub fn compose( &self, - size: u8, hull: Hull, engine: Engine, storage: Storage, + shooter: Shooter, in_shipyard: bool, + shooting: bool, ) -> AppResult { let mut gif = Gif::new(); + let size = Self::size(&hull); - let mut hull_img = open_image(hull.select_file(size).as_str())?; - let mask = open_image(hull.select_mask_file(size).as_str())?; + let mut hull_img = hull.image()?; + let mask = hull.mask()?; hull_img.apply_color_map_with_shadow_mask(self.color_map, &mask); let hull_x = (SPACESHIP_IMAGE_WIDTH - hull_img.width()) / 2; let hull_y = (SPACESHIP_IMAGE_HEIGHT - hull_img.height()) / 2; @@ -67,20 +80,21 @@ impl SpaceshipImage { [ColorPreset::Orange, ColorPreset::Yellow, ColorPreset::Red], [ColorPreset::Red, ColorPreset::Gold, ColorPreset::Yellow], [ColorPreset::Red, ColorPreset::Gold, ColorPreset::Yellow], - [ColorPreset::Red, ColorPreset::Gold, ColorPreset::Red], + [ColorPreset::Red, ColorPreset::Orange, ColorPreset::Yellow], + [ColorPreset::Red, ColorPreset::Gold, ColorPreset::Orange], [ColorPreset::Orange, ColorPreset::Red, ColorPreset::Orange], ]; - let max_tick = if in_shipyard { 1 } else { 32 }; + let max_tick = if in_shipyard { 1 } else { 72 }; for tick in 0..max_tick { - let color_presets = engine_color_presets[tick / 4].clone(); + let color_presets = &engine_color_presets[(tick / 4) % engine_color_presets.len()]; let color_map = ColorMap { red: color_presets[0].to_rgb(), green: color_presets[1].to_rgb(), blue: color_presets[2].to_rgb(), }; - let mut engine = open_image(engine.select_file(size).as_str())?; + let mut engine = engine.image()?; let eng_x = (SPACESHIP_IMAGE_WIDTH - engine.width()) / 2; let eng_y = 0; engine.apply_color_map(color_map); @@ -88,18 +102,13 @@ impl SpaceshipImage { let mut base = RgbaImage::new(SPACESHIP_IMAGE_WIDTH, SPACESHIP_IMAGE_HEIGHT); base.copy_non_trasparent_from(&engine, eng_x, eng_y)?; - match storage { - Storage::PincherNone | Storage::ShuttleNone | Storage::JesterNone => {} - _ => { - let mut storage_img = open_image(storage.select_file(size).as_str())?; - let mask = open_image(storage.select_mask_file(size).as_str())?; - storage_img.apply_color_map_with_shadow_mask(self.color_map, &mask); - let stg_x = (SPACESHIP_IMAGE_WIDTH - storage_img.width()) / 2; - let stg_y = (SPACESHIP_IMAGE_HEIGHT - storage_img.height()) / 2; - storage_img.apply_color_map(self.color_map); - base.copy_non_trasparent_from(&storage_img, stg_x, stg_y)?; - } - } + let mut storage_img = storage.image(size)?; + let mask = storage.mask(size)?; + storage_img.apply_color_map_with_shadow_mask(self.color_map, &mask); + let stg_x = (SPACESHIP_IMAGE_WIDTH - storage_img.width()) / 2; + let stg_y = (SPACESHIP_IMAGE_HEIGHT - storage_img.height()) / 2; + storage_img.apply_color_map(self.color_map); + base.copy_non_trasparent_from(&storage_img, stg_x, stg_y)?; base.copy_non_trasparent_from(&hull_img, hull_x, hull_y)?; if in_shipyard { @@ -117,6 +126,30 @@ impl SpaceshipImage { base.copy_non_trasparent_from(&shipyard_img, x, y)?; } + if shooting { + let shooter_img = shooter.image(size)?; + let mut shooter_positions = vec![]; + for x in 0..shooter_img.width() { + for y in 0..shooter_img.height() { + if let Some(pixel) = shooter_img.get_pixel_checked(x, y) { + // If pixel is blue, it is at the shooter position. + if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 255 && pixel[3] > 0 { + shooter_positions.push((x, y)); + } + } + } + } + + let x_offset = (base.width() - shooter_img.width()) / 2; + let y_offset = (tick as u32 / 2) % (36 / shooter.fire_rate() as u32) + 1; + // Projectiles last for 4 ticks and are generated depending on the shooter firerate. + for (x, y) in shooter_positions.iter() { + if *y >= y_offset { + base.put_pixel(*x + x_offset, *y - y_offset, Rgba([0, 0, 255, 255])); + } + } + } + gif.push(base); } Ok(gif) diff --git a/src/network/handler.rs b/src/network/handler.rs index e2d74f4..bbd608b 100644 --- a/src/network/handler.rs +++ b/src/network/handler.rs @@ -4,6 +4,7 @@ use super::network_callback::NetworkCallback; use super::trade::Trade; use super::types::{NetworkData, NetworkGame, NetworkRequestState, NetworkTeam, SeedInfo}; use crate::game_engine::types::TeamInGame; +use crate::store::serialize; use crate::types::{AppResult, GameId}; use crate::types::{PlayerId, TeamId}; use crate::types::{SystemTimeTick, Tick}; @@ -99,7 +100,7 @@ impl NetworkHandler { } fn _send(&mut self, data: NetworkData) -> AppResult { - let data = serde_json::to_vec(&data)?; + let data = serialize(&data)?; let msg_id = self .swarm .behaviour_mut() @@ -133,28 +134,29 @@ impl NetworkHandler { return Err(anyhow!("No own team")); }; - //If own team is playing with network peer, send the game. + // If own team is playing with network peer, send the game. if let Some(game_id) = world.get_own_team()?.current_game { - let game = world.get_game_or_err(game_id)?; + let game = world.get_game_or_err(&game_id)?; + // FIX BUG?? Send game even if we are playing with local team. + // return self.send_game(world, game_id); + if game.home_team_in_game.peer_id.is_some() || game.away_team_in_game.peer_id.is_some() { - return self.send_game(world, game_id); + return self.send_game(world, &game_id); } } Ok(message_id) } - fn send_game(&mut self, world: &World, game_id: GameId) -> AppResult { + fn send_game(&mut self, world: &World, game_id: &GameId) -> AppResult { let network_game = NetworkGame::from_game_id(&world, game_id)?; self._send(NetworkData::Game(Tick::now(), network_game)) } fn send_team(&mut self, world: &World, team_id: TeamId) -> AppResult { - let mut network_team = NetworkTeam::from_team_id(world, &team_id)?; - // Set the peer_id for team we are sending out - // This means that the team can be challenged online and it will not be stored. - network_team.set_peer_id(self.swarm.local_peer_id().clone()); + let network_team = + NetworkTeam::from_team_id(world, &team_id, self.swarm.local_peer_id().clone())?; self._send(NetworkData::Team(Tick::now(), network_team)) } @@ -202,8 +204,8 @@ impl NetworkHandler { ) -> AppResult { self.send_own_team(world)?; - let proposer_player = world.get_player_or_err(proposer_player_id)?.clone(); - let target_player = world.get_player_or_err(target_player_id)?.clone(); + let proposer_player = world.get_player_or_err(&proposer_player_id)?.clone(); + let target_player = world.get_player_or_err(&target_player_id)?.clone(); let trade = Trade::new( self.swarm.local_peer_id().clone(), @@ -231,6 +233,7 @@ impl NetworkHandler { .connected_peers() .map(|id| id.clone()) .collect::>(); + for peer_id in peers { if self.swarm.is_connected(&peer_id) { let _ = self @@ -244,8 +247,8 @@ impl NetworkHandler { pub fn accept_challenge(&mut self, world: &World, challenge: Challenge) -> AppResult<()> { self.send_own_team(world)?; let mut handle_syn = || -> AppResult<()> { - let home_team = world.get_team_or_err(challenge.home_team_in_game.team_id)?; - let away_team = world.get_team_or_err(challenge.away_team_in_game.team_id)?; + let home_team = world.get_team_or_err(&challenge.home_team_in_game.team_id)?; + let away_team = world.get_team_or_err(&challenge.away_team_in_game.team_id)?; home_team.can_challenge_team(away_team)?; let mut away_team_in_game = @@ -289,7 +292,7 @@ impl NetworkHandler { let mut handle_syn = || -> AppResult<()> { let own_team = world.get_own_team()?; let proposer_team = if let Some(proposer_team_id) = trade.proposer_player.team { - world.get_team_or_err(proposer_team_id)? + world.get_team_or_err(&proposer_team_id)? } else { return Err(anyhow!("Trade target player has no team")); }; @@ -299,7 +302,7 @@ impl NetworkHandler { // and the status of the proposer could have changed considerably // possibly making the trade invalid. let mut trade = trade.clone(); - let target_player = world.get_player_or_err(trade.target_player.id)?.clone(); + let target_player = world.get_player_or_err(&trade.target_player.id)?.clone(); trade.target_player = target_player; proposer_team.can_trade_players( &trade.proposer_player, @@ -382,10 +385,12 @@ impl NetworkHandler { mod tests { use crate::{ network::types::{NetworkData, NetworkTeam}, + store::{deserialize, serialize}, types::{AppResult, SystemTimeTick, Tick}, world::world::World, }; use anyhow::anyhow; + use libp2p::PeerId; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; @@ -397,14 +402,14 @@ mod tests { let team_name = "Testen".to_string(); let ship_name = "Tosten".to_string(); let own_team_id = world.generate_random_team(rng, home_planet, team_name, ship_name); - let network_team = NetworkTeam::from_team_id(&world, &own_team_id.unwrap()).unwrap(); + let network_team = + NetworkTeam::from_team_id(&world, &own_team_id.unwrap(), PeerId::random()).unwrap(); let timestamp = Tick::now(); let serialized_network_data = - serde_json::to_vec(&NetworkData::Team(timestamp, network_team.clone()))?; + serialize(&NetworkData::Team(timestamp, network_team.clone()))?; - let deserialized_network_data = - serde_json::from_slice::(serialized_network_data.as_slice())?; + let deserialized_network_data = deserialize::(&serialized_network_data)?; match deserialized_network_data { NetworkData::Team(deserialized_timestamp, deserialized_team) => { diff --git a/src/network/network_callback.rs b/src/network/network_callback.rs index e4946fc..411b5b7 100644 --- a/src/network/network_callback.rs +++ b/src/network/network_callback.rs @@ -2,6 +2,7 @@ use super::challenge::Challenge; use super::trade::Trade; use super::types::{NetworkData, NetworkGame, NetworkRequestState, NetworkTeam, SeedInfo}; use crate::game_engine::types::TeamInGame; +use crate::store::deserialize; use crate::types::{AppResult, SystemTimeTick, Tick}; use crate::ui::popup_message::PopupMessage; use crate::ui::utils::SwarmPanelEvent; @@ -130,8 +131,8 @@ impl NetworkCallback { text: format!("Closing connection: {}", peer_id), }; app.ui.swarm_panel.push_log_event(event); - // // FIXME: read connection protocol and understand when this is called - // // for example, we could check that num_established >0 or that cause = None + // FIXME: read connection protocol and understand when this is called. + // For example, we could check that num_established >0 or that cause = None } Ok(None) }) @@ -150,6 +151,10 @@ impl NetworkCallback { }; app.ui.swarm_panel.push_log_event(event); + if let Some(id) = peer_id { + app.ui.swarm_panel.add_peer_id(id, network_team.team.id); + } + app.world.add_network_team(network_team.clone())?; let event = SwarmPanelEvent { timestamp, peer_id, @@ -159,10 +164,6 @@ impl NetworkCallback { ), }; app.ui.swarm_panel.push_log_event(event); - if let Some(id) = peer_id { - app.ui.swarm_panel.add_peer_id(id, network_team.team.id); - } - app.world.add_network_team(network_team.clone())?; Ok(None) }) } @@ -228,22 +229,26 @@ impl NetworkCallback { let own_version_minor = env!("CARGO_PKG_VERSION_MINOR").parse()?; let own_version_patch = env!("CARGO_PKG_VERSION_PATCH").parse()?; - if seed_info.version_major > own_version_major - || (seed_info.version_major == own_version_major - && seed_info.version_minor > own_version_minor) - || (seed_info.version_major == own_version_major - && seed_info.version_minor == own_version_minor - && seed_info.version_patch > own_version_patch) - { - let message = format!( - "New version {}.{}.{} available. Download at https://rebels.frittura.org", - seed_info.version_major, seed_info.version_minor, seed_info.version_patch, - ); - app.ui.push_popup(PopupMessage::Ok { - message, - is_skippable: false, - tick: timestamp, - }); + // Notify about new version (only once). + if app.new_version_notified == false { + if seed_info.version_major > own_version_major + || (seed_info.version_major == own_version_major + && seed_info.version_minor > own_version_minor) + || (seed_info.version_major == own_version_major + && seed_info.version_minor == own_version_minor + && seed_info.version_patch > own_version_patch) + { + let message = format!( + "New version {}.{}.{} available. Download at https://rebels.frittura.org", + seed_info.version_major, seed_info.version_minor, seed_info.version_patch, + ); + app.ui.push_popup(PopupMessage::Ok { + message, + is_skippable: false, + tick: timestamp, + }); + app.new_version_notified = true; + } } app.ui @@ -307,7 +312,7 @@ impl NetworkCallback { let mut trade = trade.clone(); let proposer_player = app .world - .get_player_or_err(trade.proposer_player.id)? + .get_player_or_err(&trade.proposer_player.id)? .clone(); trade.proposer_player = proposer_player; @@ -321,7 +326,7 @@ impl NetworkCallback { let own_team = app.world.get_own_team()?; let target_team = app.world.get_team_or_err( - trade + &trade .target_player .team .ok_or(anyhow!("Player in trade should have a team"))?, @@ -405,7 +410,7 @@ impl NetworkCallback { let own_team = app.world.get_own_team()?; let proposer_team = app.world.get_team_or_err( - trade + &trade .proposer_player .team .ok_or(anyhow!("Player in trade should have a team"))?, @@ -716,7 +721,8 @@ impl NetworkCallback { } Self::HandleMessage { message } => { let peer_id = message.source; - let network_data = serde_json::from_slice::(&message.data)?; + + let network_data = deserialize::(&message.data)?; match network_data { NetworkData::Team(timestamp, team) => { Self::handle_team_topic(peer_id, timestamp, team)(app) diff --git a/src/network/trade.rs b/src/network/trade.rs index f3f270d..7ef9ab4 100644 --- a/src/network/trade.rs +++ b/src/network/trade.rs @@ -76,7 +76,7 @@ mod tests { "ship_name".into(), )?; - let mut target_team = world.get_team_or_err(target_team_id)?.clone(); + let mut target_team = world.get_team_or_err(&target_team_id)?.clone(); let target_team_peer_id = PeerId::random(); target_team.peer_id = Some(target_team_peer_id); @@ -87,9 +87,9 @@ mod tests { let own_team_peer_id = PeerId::random(); let proposer_player_id = own_team.player_ids[0]; - let proposer_player = world.get_player_or_err(proposer_player_id)?.clone(); + let proposer_player = world.get_player_or_err(&proposer_player_id)?.clone(); - let target_player = world.get_player_or_err(target_player_id)?.clone(); + let target_player = world.get_player_or_err(&target_player_id)?.clone(); let _trade = Trade::new( own_team_peer_id, diff --git a/src/network/types.rs b/src/network/types.rs index 7f94efe..2c736a3 100644 --- a/src/network/types.rs +++ b/src/network/types.rs @@ -3,7 +3,7 @@ use super::trade::Trade; use crate::game_engine::timer::Timer; use crate::game_engine::types::GameStats; use crate::types::{KartoffelId, PlanetId, Tick}; -use crate::world::planet::{Planet, PlanetType}; +use crate::world::planet::Planet; use crate::world::position::{Position, MAX_POSITION}; use crate::world::skill::Skill; use crate::{ @@ -30,22 +30,6 @@ pub(crate) enum NetworkData { SeedInfo(Tick, SeedInfo), } -impl TryFrom> for NetworkData { - type Error = anyhow::Error; - fn try_from(item: Vec) -> AppResult { - let network_data = serde_json::from_slice::(item.as_slice())?; - Ok(network_data) - } -} - -impl TryInto> for NetworkData { - type Error = anyhow::Error; - fn try_into(self) -> AppResult> { - let data = serde_json::to_vec(&self)?; - Ok(data) - } -} - #[derive(Debug, Clone, Display, Default, Serialize, Deserialize, PartialEq, Hash)] pub enum NetworkRequestState { #[default] @@ -61,40 +45,42 @@ pub enum NetworkRequestState { pub struct NetworkTeam { pub team: Team, pub players: Vec, - pub home_planet: Option, + pub asteroids: Vec, } impl NetworkTeam { - pub fn new(team: Team, players: Vec, home_planet: Option) -> Self { + pub fn new(team: Team, players: Vec, asteroids: Vec) -> Self { Self { team, players, - home_planet, + asteroids, } } - pub fn from_team_id(world: &World, team_id: &TeamId) -> AppResult { - let team = world.get_team_or_err(*team_id)?.clone(); - let players = world.get_players_by_team(&team)?; - let planet = world.get_planet_or_err(team.home_planet_id)?; - let home_planet = if planet.planet_type == PlanetType::Asteroid { - Some(planet) - } else { - None - } - .cloned(); - - Ok(Self::new(team, players, home_planet)) - } - - pub fn set_peer_id(&mut self, peer_id: PeerId) { - self.team.peer_id = Some(peer_id); - for player in self.players.iter_mut() { + pub fn from_team_id(world: &World, team_id: &TeamId, peer_id: PeerId) -> AppResult { + let mut team = world.get_team_or_err(team_id)?.clone(); + let mut players = world.get_players_by_team(&team)?; + let asteroids = team + .asteroid_ids + .iter() + .map(|asteroid_id| { + let mut asteroid = world + .get_planet_or_err(asteroid_id) + .expect("Asteroid should be part of world") + .clone(); + asteroid.peer_id = Some(peer_id); + asteroid + }) + .collect_vec(); + + // Set the peer_id for team we are sending out + // This means that the team can be challenged online and it will not be stored. + team.peer_id = Some(peer_id); + for player in players.iter_mut() { player.peer_id = Some(peer_id.clone()); } - if self.home_planet.is_some() { - self.home_planet.as_mut().unwrap().peer_id = Some(peer_id); - } + + Ok(Self::new(team, players, asteroids)) } } @@ -110,7 +96,7 @@ pub struct NetworkGame { } impl NetworkGame { - pub fn from_game_id(world: &World, game_id: GameId) -> AppResult { + pub fn from_game_id(world: &World, game_id: &GameId) -> AppResult { let game = world.get_game_or_err(game_id)?.clone(); let mut home_team_in_game = game.home_team_in_game.clone(); @@ -171,6 +157,7 @@ impl NetworkGame { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TeamRanking { + team_id: TeamId, pub timestamp: Tick, pub name: String, pub reputation: Skill, @@ -182,6 +169,7 @@ pub struct TeamRanking { impl TeamRanking { pub fn from_network_team(timestamp: Tick, network_team: &NetworkTeam) -> Self { Self { + team_id: network_team.team.id, timestamp, name: network_team.team.name.clone(), reputation: network_team.team.reputation, @@ -203,14 +191,14 @@ pub struct SeedInfo { pub version_minor: usize, pub version_patch: usize, pub message: Option, - pub team_ranking: HashMap, + pub team_ranking: Vec<(TeamId, TeamRanking)>, } impl SeedInfo { pub fn new( connected_peers_count: usize, message: Option, - team_ranking: HashMap, + team_ranking: Vec<(TeamId, TeamRanking)>, ) -> AppResult { Ok(Self { connected_peers_count, diff --git a/src/relayer.rs b/src/relayer.rs index 98a0aa3..2ca1fac 100644 --- a/src/relayer.rs +++ b/src/relayer.rs @@ -3,10 +3,11 @@ use std::collections::HashMap; use crate::network::constants::{DEFAULT_SEED_PORT, TOPIC}; use crate::network::types::{NetworkData, TeamRanking}; use crate::network::{handler::NetworkHandler, types::SeedInfo}; -use crate::store::{load_team_ranking, save_team_ranking}; +use crate::store::{deserialize, load_team_ranking, save_team_ranking}; use crate::types::{AppResult, SystemTimeTick, TeamId, Tick}; use crate::world::constants::*; use futures::StreamExt; +use itertools::Itertools; use libp2p::gossipsub::IdentTopic; use libp2p::{gossipsub, swarm::SwarmEvent}; use tokio::select; @@ -19,9 +20,44 @@ pub struct Relayer { network_handler: NetworkHandler, last_seed_info_tick: Tick, team_ranking: HashMap, + top_team_ranking: Vec<(TeamId, TeamRanking)>, } impl Relayer { + fn update_top_team_ranking( + top_team_ranking: &mut Vec<(TeamId, TeamRanking)>, + team_id: TeamId, + team_ranking: TeamRanking, + ) { + let mut insertion_index = 0; + for (_, top_ranking) in top_team_ranking.iter() { + if team_ranking.reputation > top_ranking.reputation { + break; + } + insertion_index += 1; + } + top_team_ranking.insert(insertion_index, (team_id, team_ranking)); + + let mut unique_team_ids = vec![]; + let mut to_remove = vec![]; + + for (index, (id, _)) in top_team_ranking.iter().enumerate() { + if unique_team_ids.contains(id) { + to_remove.push(index); + } else { + unique_team_ids.push(*id); + } + } + + for index in to_remove { + top_team_ranking.remove(index); + } + + if top_team_ranking.len() > 10 { + top_team_ranking.pop(); + } + } + pub fn new() -> Self { let team_ranking = match load_team_ranking() { Ok(team_ranking) => team_ranking, @@ -30,12 +66,25 @@ impl Relayer { HashMap::new() } }; + + let top_team_ranking = team_ranking + .iter() + .sorted_by(|(_, a), (_, b)| { + b.reputation + .partial_cmp(&a.reputation) + .expect("Reputation should exist") + }) + .take(10) + .map(|(id, ranking)| (id.clone(), ranking.clone())) + .collect(); + Self { running: true, network_handler: NetworkHandler::new(None, DEFAULT_SEED_PORT) .expect("Failed to initialize network handler"), last_seed_info_tick: Tick::now(), team_ranking, + top_team_ranking, } } @@ -56,7 +105,7 @@ impl Relayer { self.network_handler.send_seed_info(SeedInfo::new( self.network_handler.swarm.connected_peers().count(), None, - self.team_ranking.clone(), + self.top_team_ranking.clone(), )?)?; self.last_seed_info_tick = now; } @@ -73,23 +122,41 @@ impl Relayer { SwarmEvent::Behaviour(gossipsub::Event::Subscribed { peer_id, topic }) => { if topic == IdentTopic::new(TOPIC).hash() { println!("Sending info to {}", peer_id); + self.network_handler.send_seed_info(SeedInfo::new( self.network_handler.swarm.connected_peers().count(), None, - self.team_ranking.clone(), + self.top_team_ranking.clone(), )?)?; } } SwarmEvent::Behaviour(gossipsub::Event::Message { message, .. }) => { assert!(message.topic == IdentTopic::new(TOPIC).hash()); - let network_data = serde_json::from_slice::(&message.data)?; + let network_data = deserialize::(&message.data)?; match network_data { NetworkData::Team(timestamp, network_team) => { - self.team_ranking.insert( + let team_ranking = TeamRanking::from_network_team(timestamp, &network_team); + self.team_ranking + .insert(network_team.team.id, team_ranking.clone()); + Self::update_top_team_ranking( + &mut self.top_team_ranking, network_team.team.id, - TeamRanking::from_network_team(timestamp, &network_team), + team_ranking, ); + + // self.top_team_ranking = self + // .team_ranking + // .iter() + // .sorted_by(|(_, a), (_, b)| { + // b.reputation + // .partial_cmp(&a.reputation) + // .expect("Reputation should exist") + // }) + // .take(10) + // .map(|(id, ranking)| (id.clone(), ranking.clone())) + // .collect(); + if let Err(err) = save_team_ranking(&self.team_ranking, true) { println!("Error while saving team ranking: {err}"); } @@ -102,3 +169,41 @@ impl Relayer { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::{ + network::types::{NetworkTeam, TeamRanking}, + relayer::Relayer, + types::{PlanetId, TeamId}, + world::team::Team, + }; + use std::collections::HashMap; + + #[test] + fn test_top_team_ranking() { + let mut team_ranking = HashMap::new(); + let mut top_team_ranking: Vec<(TeamId, TeamRanking)> = vec![]; + + for idx in 0..20 { + let mut team = Team::random(TeamId::new_v4(), PlanetId::new_v4(), "name", "ship_name"); + team.reputation = idx as f32; + let network_team = NetworkTeam::new(team, vec![], vec![]); + let new_team_ranking = TeamRanking::from_network_team(0, &network_team); + + team_ranking.insert(network_team.team.id, new_team_ranking.clone()); + + Relayer::update_top_team_ranking( + &mut top_team_ranking, + network_team.team.id, + new_team_ranking, + ); + } + + for idx in 0..top_team_ranking.len() - 1 { + let (_, ranking) = &top_team_ranking[idx]; + let (_, next_ranking) = &top_team_ranking[idx + 1]; + assert!(ranking.reputation >= next_ranking.reputation) + } + } +} diff --git a/src/space_adventure/asteroid.rs b/src/space_adventure/asteroid.rs index 3c11bc2..5117866 100644 --- a/src/space_adventure/asteroid.rs +++ b/src/space_adventure/asteroid.rs @@ -1,22 +1,24 @@ +use super::networking::ImageType; use super::space_callback::SpaceCallback; use super::visual_effects::VisualEffect; use super::{constants::*, traits::*}; +use crate::image::color_map::AsteroidColorMap; use crate::image::types::Gif; -use crate::image::utils::open_image; +use crate::image::utils::{open_image, ExtraImageUtils}; use crate::register_impl; use crate::space_adventure::utils::{body_data_from_image, EntityState}; use crate::world::resources::Resource; use glam::{I16Vec2, Vec2}; use image::imageops::{rotate180, rotate270, rotate90}; -use image::{Rgba, RgbaImage}; +use image::{Pixel, Rgba, RgbaImage}; use once_cell::sync::Lazy; use rand::seq::IteratorRandom; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::collections::HashMap; use strum::{Display, EnumIter, IntoEnumIterator}; -const MAX_ASTEROID_TYPE_INDEX: usize = 3; const MAX_ROTATION: usize = 4; // Calculate astroid gifs, hit boxes, and contours once to be more efficient. @@ -25,15 +27,27 @@ static ASTEROID_IMAGE_DATA: Lazy base_img.clone(), @@ -42,7 +56,8 @@ static ASTEROID_IMAGE_DATA: Lazy rotate270(&base_img), _ => unreachable!(), }; - let (image, hit_box) = body_data_from_image(&image); + let (image, hit_box) = + body_data_from_image(&image, size == AsteroidSize::Planet); gif.push(image); hit_boxes.push(hit_box); } @@ -53,28 +68,51 @@ static ASTEROID_IMAGE_DATA: Lazy f32 { + fn collision_damage(&self) -> f32 { match self { - AsteroidSize::Huge => 3.0, - AsteroidSize::Big => 1.0, AsteroidSize::Small => 0.2, + AsteroidSize::Big => 1.0, + AsteroidSize::Huge => 3.0, + AsteroidSize::Planet => 0.0, } } - pub fn durability(&self) -> f32 { + fn durability(&self) -> f32 { + match self { + AsteroidSize::Small => 2.0, + AsteroidSize::Big => 10.0, + AsteroidSize::Huge => 30.0, + AsteroidSize::Planet => 0.0, + } + } + + fn max_image_type(&self) -> usize { match self { - AsteroidSize::Huge => 24.0, - AsteroidSize::Big => 12.0, - AsteroidSize::Small => 3.0, + Self::Planet => MAX_ASTEROID_PLANET_IMAGE_TYPE, + _ => 3, } } } @@ -86,9 +124,9 @@ pub struct AsteroidEntity { position: Vec2, velocity: Vec2, size: AsteroidSize, - image_type: usize, durability: f32, - tick: usize, + orientation: f32, + rotation_speed: f32, visual_effects: VisualEffectMap, } @@ -109,36 +147,35 @@ impl Body for AsteroidEntity { self.previous_position = self.position; self.position = self.position + self.velocity * deltatime; - if self.position.x < 0.0 || self.position.x > MAX_SCREEN_WIDTH as f32 { - return vec![SpaceCallback::DestroyEntity { id: self.id() }]; - } - if self.position.y < 0.0 || self.position.y > MAX_SCREEN_HEIGHT as f32 { - return vec![SpaceCallback::DestroyEntity { id: self.id() }]; + if self.size != AsteroidSize::Planet { + if self.position.x < 0.0 || self.position.x > MAX_ENTITY_POSITION.x as f32 { + return vec![SpaceCallback::DestroyEntity { id: self.id() }]; + } + if self.position.y < 0.0 || self.position.y > MAX_ENTITY_POSITION.y as f32 { + return vec![SpaceCallback::DestroyEntity { id: self.id() }]; + } } + self.orientation += self.rotation_speed * deltatime; + vec![] } } impl Sprite for AsteroidEntity { - fn layer(&self) -> usize { - 1 - } - fn image(&self) -> &RgbaImage { let (gif, _) = ASTEROID_IMAGE_DATA - .get(&(self.size, self.image_type)) + .get(&(self.size, self.image_type())) .expect("Asteroid image data should be available"); &gif[self.frame()] } - fn hit_box(&self) -> &HitBox { - let (_, hit_boxes) = ASTEROID_IMAGE_DATA - .get(&(self.size, self.image_type)) - .expect("Asteroid image data should be available"); - - &hit_boxes[self.frame()] + fn network_image_type(&self) -> ImageType { + ImageType::Asteroid { + size: self.size, + image_type: self.image_type(), + } } fn should_apply_visual_effects<'a>(&self) -> bool { @@ -156,6 +193,9 @@ impl Sprite for AsteroidEntity { } fn add_visual_effect(&mut self, duration: f32, effect: VisualEffect) { + if self.size == AsteroidSize::Planet { + return; + } self.visual_effects.insert(effect, duration); } @@ -174,7 +214,7 @@ impl Sprite for AsteroidEntity { } } -register_impl!(!PlayerControlled for AsteroidEntity); +register_impl!(!ControllableSpaceship for AsteroidEntity); register_impl!(!ResourceFragment for AsteroidEntity); impl Collider for AsteroidEntity { @@ -183,7 +223,19 @@ impl Collider for AsteroidEntity { } fn collider_type(&self) -> ColliderType { - ColliderType::Asteroid + if self.size == AsteroidSize::Planet { + ColliderType::AsteroidPlanet + } else { + ColliderType::Asteroid + } + } + + fn hit_box(&self) -> &HitBox { + let (_, hit_boxes) = ASTEROID_IMAGE_DATA + .get(&(self.size, self.image_type())) + .expect("Asteroid image data should be available"); + + &hit_boxes[self.frame()] } } @@ -191,30 +243,48 @@ impl Entity for AsteroidEntity { fn set_id(&mut self, id: usize) { self.id = id; } + fn id(&self) -> usize { self.id } + fn layer(&self) -> usize { + 1 + } + fn handle_space_callback(&mut self, callback: SpaceCallback) -> Vec { match callback { SpaceCallback::DamageEntity { damage, .. } => { self.add_damage(damage); - if self.durability() == 0.0 { + if self.durability() == 0.0 && self.size != AsteroidSize::Planet { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } self.add_visual_effect( VisualEffect::COLOR_MASK_LIFETIME, VisualEffect::ColorMask { - color: Rgba([255, 0, 0, 0]), + color: Rgba([255, 0, 0, 0]).to_rgb().0, }, ); } SpaceCallback::DestroyEntity { .. } => { - let position = self.position().as_vec2(); + // If the asteroid got destroyed by going out-of-screen, don't spawn smaller ones. + let should_emit_fragments = if self.position.x < 0.0 + || self.position.x > MAX_ENTITY_POSITION.x as f32 + || self.position.y < 0.0 + || self.position.y > MAX_ENTITY_POSITION.y as f32 + { + false + } else { + true + }; + let rng = &mut ChaCha8Rng::from_entropy(); let mut callbacks = vec![]; + let position = self.position; + match self.size { + AsteroidSize::Planet => {} AsteroidSize::Huge => { for _ in 0..3 { if rng.gen_bool(0.95) { @@ -262,7 +332,7 @@ impl Entity for AsteroidEntity { layer: rng.gen_range(0..=2), }); } - if rng.gen_bool(0.15) { + if should_emit_fragments && rng.gen_bool(0.15) { callbacks.push(SpaceCallback::GenerateFragment { position, velocity: Vec2::new( @@ -270,7 +340,18 @@ impl Entity for AsteroidEntity { rng.gen_range(-3.5..3.5), ), resource: Resource::SCRAPS, - amount: rng.gen_range(1..=4), + amount: 1, + }); + } + if should_emit_fragments && rng.gen_bool(0.01) { + callbacks.push(SpaceCallback::GenerateFragment { + position, + velocity: Vec2::new( + rng.gen_range(-7.5..7.5), + rng.gen_range(-7.5..7.5), + ), + resource: Resource::GOLD, + amount: 1, }); } } @@ -290,7 +371,7 @@ impl Entity for AsteroidEntity { } for _ in 0..6 { - if rng.gen_bool(0.65) { + if rng.gen_bool(0.75) { callbacks.push(SpaceCallback::GenerateParticle { position, velocity: Vec2::new( @@ -309,7 +390,7 @@ impl Entity for AsteroidEntity { layer: rng.gen_range(0..=2), }); } - if rng.gen_bool(0.75) { + if should_emit_fragments && rng.gen_bool(0.65) { callbacks.push(SpaceCallback::GenerateFragment { position, velocity: Vec2::new( @@ -317,14 +398,33 @@ impl Entity for AsteroidEntity { rng.gen_range(-3.5..3.5), ), resource: Resource::SCRAPS, - amount: rng.gen_range(1..=4), + amount: 1, }); } } } AsteroidSize::Small => { for _ in 0..4 { - if rng.gen_bool(0.75) { + if rng.gen_bool(0.35) { + callbacks.push(SpaceCallback::GenerateParticle { + position, + velocity: Vec2::new( + rng.gen_range(-3.5..3.5), + rng.gen_range(-3.5..3.5), + ), + color: Rgba([ + 55 + rng.gen_range(0..25), + 55 + rng.gen_range(0..25), + 55 + rng.gen_range(0..25), + 255, + ]), + particle_state: EntityState::Decaying { + lifetime: 2.0 + rng.gen_range(0.0..1.5), + }, + layer: rng.gen_range(0..=2), + }); + } + if should_emit_fragments && rng.gen_bool(0.75) { callbacks.push(SpaceCallback::GenerateFragment { position, velocity: Vec2::new( @@ -332,7 +432,7 @@ impl Entity for AsteroidEntity { rng.gen_range(-3.5..3.5), ), resource: Resource::SCRAPS, - amount: rng.gen_range(1..=4), + amount: 1, }); } } @@ -348,22 +448,27 @@ impl Entity for AsteroidEntity { } impl AsteroidEntity { + fn image_type(&self) -> usize { + self.id % self.size.max_image_type() + } fn frame(&self) -> usize { - self.tick + self.orientation as usize % MAX_ROTATION } pub fn new(position: Vec2, velocity: Vec2, size: AsteroidSize) -> Self { let rng = &mut ChaCha8Rng::from_entropy(); - let image_type = rng.gen_range(1..=MAX_ASTEROID_TYPE_INDEX); - // TODO: decide if we like them rotating or not. - let tick = rng.gen_range(0..MAX_ROTATION); + let rotation_speed = if size == AsteroidSize::Planet { + 0.0 + } else { + rng.gen_range(-0.75..0.75) / (1 + size as usize) as f32 + }; Self { id: 0, - tick, + orientation: rng.gen_range(0.0..MAX_ROTATION as f32), + rotation_speed, size, - image_type, durability: size.durability(), position, velocity, @@ -374,21 +479,59 @@ impl AsteroidEntity { pub fn new_at_screen_edge() -> Self { let rng = &mut ChaCha8Rng::from_entropy(); - let size = AsteroidSize::iter() + let &size = [AsteroidSize::Small, AsteroidSize::Big, AsteroidSize::Huge] + .iter() .choose_stable(rng) .expect("There should be at least an asteroid size"); - let x = (SCREEN_WIDTH + 2) as f32; - let y = rng.gen_range(0.15 * SCREEN_HEIGHT as f32..0.85 * SCREEN_HEIGHT as f32); - let vx = rng.gen_range(-1.5..-0.5); - let vy = rng.gen_range(-0.15..0.15); + let (position, velocity) = match rng.gen_range(0..3) { + // Right Edge + 0 => { + let x = MAX_ENTITY_POSITION.x as f32; + let y = rng.gen_range( + 0.15 * MAX_ENTITY_POSITION.y as f32..0.85 * MAX_ENTITY_POSITION.y as f32, + ); + let vx = rng.gen_range(-12.5..-0.5); + let vy = rng.gen_range(-4.5..4.5); + + (Vec2::new(x, y), Vec2::new(vx, vy)) + } + //Top edge + 1 => { + let x = rng.gen_range(0.45..0.85) * MAX_ENTITY_POSITION.x as f32; + let y = 0.0; + let vx = rng.gen_range(-4.5..4.5); + let vy = rng.gen_range(0.5..12.5); + + (Vec2::new(x, y), Vec2::new(vx, vy)) + } + // Bottom edge + 2 => { + let x = rng.gen_range(0.45..0.85) * MAX_ENTITY_POSITION.x as f32; + let y = MAX_ENTITY_POSITION.y as f32; + let vx = rng.gen_range(-4.5..4.5); + let vy = rng.gen_range(-12.5..-0.5); + + (Vec2::new(x, y), Vec2::new(vx, vy)) + } - let position = Vec2::new(x, y); - let velocity = Vec2::new(vx, vy); + _ => unreachable!(), + }; Self::new(position, velocity, size) } + pub fn planet() -> Self { + let rng = &mut ChaCha8Rng::from_entropy(); + + let x = MAX_ENTITY_POSITION.x as f32; + let y = rng.gen_range(0.25..0.45) * MAX_ENTITY_POSITION.y as f32; + let vx = rng.gen_range(-4.0..-3.0); + let vy = rng.gen_range(-0.25..0.25); + + Self::new(Vec2::new(x, y), Vec2::new(vx, vy), AsteroidSize::Planet) + } + pub fn durability(&self) -> f32 { self.durability } @@ -397,3 +540,40 @@ impl AsteroidEntity { self.durability = (self.durability - damage).max(0.0); } } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::{AsteroidSize, ASTEROID_IMAGE_DATA}; + use crate::types::AppResult; + use image::{self, GenericImage, RgbaImage}; + + #[ignore] + #[test] + fn test_generate_asteroid_image() -> AppResult<()> { + let mut base = RgbaImage::new(260, 80); + for (&(size, image_type), (gif, _)) in ASTEROID_IMAGE_DATA.iter() { + if size == AsteroidSize::Planet { + continue; + } + + for (idx, oriented_image) in gif.iter().enumerate() { + base.copy_from( + oriented_image, + image_type as u32 * 88 + idx as u32 * 20, + size as u32 * 20, + )?; + } + } + + image::save_buffer( + &Path::new("tests/asteroid_images.png"), + &base, + 260, + 80, + image::ColorType::Rgba8, + )?; + Ok(()) + } +} diff --git a/src/space_adventure/collector.rs b/src/space_adventure/collector.rs new file mode 100644 index 0000000..bfec0c0 --- /dev/null +++ b/src/space_adventure/collector.rs @@ -0,0 +1,114 @@ +use super::{networking::ImageType, space_callback::SpaceCallback, traits::*}; +use crate::register_impl; +use glam::{I16Vec2, Vec2}; +use image::{Rgba, RgbaImage}; +use std::collections::HashMap; + +const HIT_BOX_RADIUS: i16 = 40; +// const MAGNET_ACCELERATION: f32 = 35.0; + +#[derive(Debug)] +pub struct CollectorEntity { + id: usize, + previous_position: Vec2, + position: Vec2, + velocity: Vec2, + image: RgbaImage, + hit_box: HitBox, +} + +impl Body for CollectorEntity { + fn previous_position(&self) -> I16Vec2 { + self.previous_position.as_i16vec2() + } + + fn position(&self) -> I16Vec2 { + self.position.as_i16vec2() + } + + fn velocity(&self) -> I16Vec2 { + self.velocity.as_i16vec2() + } + + fn update_body(&mut self, _deltatime: f32) -> Vec { + vec![] + } +} + +impl Sprite for CollectorEntity { + fn image(&self) -> &RgbaImage { + &self.image + } + + fn network_image_type(&self) -> ImageType { + ImageType::None + } +} + +impl Collider for CollectorEntity { + fn collider_type(&self) -> ColliderType { + ColliderType::Collector + } + + fn hit_box(&self) -> &HitBox { + &self.hit_box + } +} + +register_impl!(!ControllableSpaceship for CollectorEntity); +register_impl!(!ResourceFragment for CollectorEntity); + +impl Entity for CollectorEntity { + fn set_id(&mut self, id: usize) { + self.id = id; + } + + fn id(&self) -> usize { + self.id + } + + fn layer(&self) -> usize { + 1 + } + + fn handle_space_callback(&mut self, callback: SpaceCallback) -> Vec { + match callback { + SpaceCallback::SetPosition { position, .. } => { + self.previous_position = self.position; + self.position = position.as_vec2(); + } + _ => {} + } + vec![] + } +} + +impl CollectorEntity { + pub fn new() -> Self { + let image = RgbaImage::from_pixel(1, 1, Rgba([0, 0, 0, 0])); + + // The fragment hitbox is larger than the sprite on purpose + // so that when hitting a spaceship it is accelerated towards it. + let mut hit_box = HashMap::new(); + for x in -HIT_BOX_RADIUS..=HIT_BOX_RADIUS { + for y in -HIT_BOX_RADIUS..=HIT_BOX_RADIUS { + let point = I16Vec2::new(x, y); + if point.distance_squared(I16Vec2::ZERO) < HIT_BOX_RADIUS.pow(2) { + hit_box.insert(point, false); + } else if point.distance_squared(I16Vec2::ZERO) == HIT_BOX_RADIUS.pow(2) { + hit_box.insert(point, true); + } + } + } + hit_box.insert(I16Vec2::ZERO, false); + + Self { + id: 0, + previous_position: Vec2::ZERO, + position: Vec2::ZERO, + velocity: Vec2::ZERO, + image, + hit_box: hit_box.into(), + } + } +} diff --git a/src/space_adventure/constants.rs b/src/space_adventure/constants.rs index 9cc5489..eaaeb88 100644 --- a/src/space_adventure/constants.rs +++ b/src/space_adventure/constants.rs @@ -1,10 +1,26 @@ +use glam::UVec2; + +use crate::ui::UI_SCREEN_SIZE; + pub(crate) const FRICTION_COEFF: f32 = 0.1; pub(crate) const THRUST_MOD: f32 = 1.5; -pub(crate) const FUEL_CONSUMPTION_MOD: f32 = 25_000.0; +pub(crate) const FUEL_CONSUMPTION_MOD: f32 = 115_000.0; pub(crate) const MAX_SPACESHIP_SPEED_MOD: f32 = 0.135; -pub(crate) const SCREEN_WIDTH: u16 = 160; -pub(crate) const SCREEN_HEIGHT: u16 = 88; +pub(crate) const ASTEROID_GENERATION_PROBABILITY: f64 = 0.05; +pub(crate) const DIFFICULTY_FOR_ASTEROID_PLANET_GENERATION: usize = 60; + +// There are 3 relevant lengths for the space image: +// 1. the "screen size", which is the size of the cropped space image before rendering on the screen; +// 2. the "entity position size", which indicates where entities can be on the space image. It has an extra buffer +// around the screen size in every four direction so that entities can smoothly 'appear' on screen from every direction; +// 3. the "background total size", which must accomodate drawing enitities at any possible position, +// and hence must have an extra buffer around the max position size (2.) bottom and right. +pub(crate) const SCREEN_SIZE: UVec2 = + UVec2::new(UI_SCREEN_SIZE.0 as u32, UI_SCREEN_SIZE.1 as u32 * 2 - 8); +pub(crate) const MAX_ENTITY_POSITION: UVec2 = UVec2::new(200, 128); +pub(crate) const BACKGROUND_IMAGE_SIZE: UVec2 = UVec2::new(240, 168); + +pub(crate) const MAX_LAYER: usize = 5; -pub(crate) const MAX_SCREEN_WIDTH: u16 = SCREEN_WIDTH + 20; -pub(crate) const MAX_SCREEN_HEIGHT: u16 = SCREEN_HEIGHT + 10; +pub(crate) const MAX_ASTEROID_PLANET_IMAGE_TYPE: usize = 30; diff --git a/src/space_adventure/fragment.rs b/src/space_adventure/fragment.rs index 84034fe..0fa290e 100644 --- a/src/space_adventure/fragment.rs +++ b/src/space_adventure/fragment.rs @@ -1,10 +1,9 @@ -use super::{space_callback::SpaceCallback, traits::*, utils::EntityState}; +use super::{networking::ImageType, space_callback::SpaceCallback, traits::*, utils::EntityState}; use crate::{register_impl, space_adventure::constants::*, world::resources::Resource}; use glam::{I16Vec2, Vec2}; -use image::RgbaImage; +use image::{Pixel, RgbaImage}; use std::collections::HashMap; -const HIT_BOX_RADIUS: i16 = 40; const MAGNET_ACCELERATION: f32 = 35.0; #[derive(Debug)] @@ -56,10 +55,10 @@ impl Body for FragmentEntity { self.position += self.velocity * deltatime; self.acceleration = Vec2::ZERO; - if self.position.x < 0.0 || self.position.x > SCREEN_WIDTH as f32 { + if self.position.x < 0.0 || self.position.x > SCREEN_SIZE.x as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } - if self.position.y < 0.0 || self.position.y > SCREEN_HEIGHT as f32 { + if self.position.y < 0.0 || self.position.y > SCREEN_SIZE.y as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } @@ -68,15 +67,14 @@ impl Body for FragmentEntity { } impl Sprite for FragmentEntity { - fn layer(&self) -> usize { - 1 - } fn image(&self) -> &RgbaImage { &self.image } - fn hit_box(&self) -> &HitBox { - &self.hit_box + fn network_image_type(&self) -> ImageType { + ImageType::Fragment { + color: self.resource.color().to_rgb().0, + } } } @@ -84,9 +82,13 @@ impl Collider for FragmentEntity { fn collider_type(&self) -> ColliderType { ColliderType::Fragment } + + fn hit_box(&self) -> &HitBox { + &self.hit_box + } } -register_impl!(!PlayerControlled for FragmentEntity); +register_impl!(!ControllableSpaceship for FragmentEntity); register_impl!(ResourceFragment for FragmentEntity); impl ResourceFragment for FragmentEntity { fn resource(&self) -> Resource { @@ -107,9 +109,14 @@ impl Entity for FragmentEntity { self.id } + fn layer(&self) -> usize { + 1 + } + fn handle_space_callback(&mut self, callback: SpaceCallback) -> Vec { match callback { - SpaceCallback::AccelerateEntity { acceleration, .. } => { + // FIXME: MAGNET_ACCELERATION should come from the collector. + SpaceCallback::SetAcceleration { acceleration, .. } => { self.acceleration = MAGNET_ACCELERATION * acceleration.as_vec2() } @@ -127,14 +134,6 @@ impl FragmentEntity { // The fragment hitbox is larger than the sprite on purpose // so that when hitting a spaceship it is accelerated towards it. let mut hit_box = HashMap::new(); - for x in -HIT_BOX_RADIUS..=HIT_BOX_RADIUS { - for y in -HIT_BOX_RADIUS..=HIT_BOX_RADIUS { - let point = I16Vec2::new(x, y); - if point.distance_squared(I16Vec2::ZERO) <= HIT_BOX_RADIUS.pow(2) { - hit_box.insert(point, false); - } - } - } hit_box.insert(I16Vec2::ZERO, true); Self { diff --git a/src/space_adventure/mod.rs b/src/space_adventure/mod.rs index b66e1c8..741e306 100644 --- a/src/space_adventure/mod.rs +++ b/src/space_adventure/mod.rs @@ -1,6 +1,8 @@ mod asteroid; +mod collector; mod constants; mod fragment; +mod networking; mod particle; mod projectile; mod space; diff --git a/src/space_adventure/networking.rs b/src/space_adventure/networking.rs new file mode 100644 index 0000000..3881e79 --- /dev/null +++ b/src/space_adventure/networking.rs @@ -0,0 +1,62 @@ +use super::{asteroid::AsteroidSize, constants::MAX_LAYER, visual_effects::VisualEffect, Entity}; +use crate::{ + image::color_map::ColorMap, + world::spaceship::{Engine, Hull, Storage}, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ImageType { + None, + Asteroid { + size: AsteroidSize, + image_type: usize, + }, + Spaceship { + hull: Hull, + engine: Engine, + storage: Storage, + color_map: ColorMap, + }, + Fragment { + color: [u8; 3], + }, + Particle { + color: [u8; 3], + }, + Projectile { + color: [u8; 3], + }, +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct NetworkSpaceData { + new_entities: Vec<(usize, ImageType)>, // [(id, image)] + state: Vec<(usize, u8, u8)>, // (id, x, y) + visual_effects: Vec<(usize, VisualEffect)>, // [(id, visual_effect)] +} + +#[allow(unused)] +impl NetworkSpaceData { + pub fn insert_entity(&mut self, id: usize, image_type: ImageType) { + self.new_entities.push((id, image_type)); + } + + pub fn update_state(&mut self, entities: &[HashMap>; MAX_LAYER]) { + let mut state = vec![]; + // let mut visual_effects = vec![]; + for layer in 0..MAX_LAYER { + for (id, entity) in entities[layer].iter() { + let [x, y] = entity.position().to_array(); + state.push((id, x as u8, y as u8)); + } + } + } + + pub fn reset(&mut self) { + self.new_entities = vec![]; + self.state = vec![]; + self.visual_effects = vec![]; + } +} diff --git a/src/space_adventure/particle.rs b/src/space_adventure/particle.rs index 454f78f..5b4e356 100644 --- a/src/space_adventure/particle.rs +++ b/src/space_adventure/particle.rs @@ -1,12 +1,13 @@ -use super::{space_callback::SpaceCallback, traits::*, utils::EntityState}; +use super::{networking::ImageType, space_callback::SpaceCallback, traits::*, utils::EntityState}; use crate::{register_impl, space_adventure::constants::*}; use glam::{I16Vec2, Vec2}; -use image::{Rgba, RgbaImage}; +use image::{Pixel, Rgba, RgbaImage}; use std::collections::HashMap; #[derive(Debug)] pub struct ParticleEntity { id: usize, + color: Rgba, previous_position: Vec2, position: Vec2, velocity: Vec2, @@ -33,10 +34,10 @@ impl Body for ParticleEntity { self.previous_position = self.position; self.position = self.position + self.velocity * deltatime; - if self.position.x < 0.0 || self.position.x > SCREEN_WIDTH as f32 { + if self.position.x < 0.0 || self.position.x > SCREEN_SIZE.x as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } - if self.position.y < 0.0 || self.position.y > SCREEN_HEIGHT as f32 { + if self.position.y < 0.0 || self.position.y > SCREEN_SIZE.y as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } @@ -59,30 +60,38 @@ impl Body for ParticleEntity { } impl Sprite for ParticleEntity { - fn layer(&self) -> usize { - self.layer - } fn image(&self) -> &RgbaImage { &self.image } + fn network_image_type(&self) -> ImageType { + ImageType::Particle { + color: self.color.to_rgb().0, + } + } +} + +impl Collider for ParticleEntity { fn hit_box(&self) -> &HitBox { &self.hit_box } } -impl Collider for ParticleEntity {} - -register_impl!(!PlayerControlled for ParticleEntity); +register_impl!(!ControllableSpaceship for ParticleEntity); register_impl!(!ResourceFragment for ParticleEntity); impl Entity for ParticleEntity { fn set_id(&mut self, id: usize) { self.id = id; } + fn id(&self) -> usize { self.id } + + fn layer(&self) -> usize { + self.layer + } } impl ParticleEntity { @@ -98,6 +107,7 @@ impl ParticleEntity { hit_box.insert(I16Vec2::ZERO, true); Self { id: 0, + color, previous_position: position, position, velocity, diff --git a/src/space_adventure/projectile.rs b/src/space_adventure/projectile.rs index 27f41c3..2142a14 100644 --- a/src/space_adventure/projectile.rs +++ b/src/space_adventure/projectile.rs @@ -1,7 +1,7 @@ -use super::{space_callback::SpaceCallback, traits::*}; +use super::{networking::ImageType, space_callback::SpaceCallback, traits::*}; use crate::{register_impl, space_adventure::constants::*}; use glam::{I16Vec2, Vec2}; -use image::{Rgba, RgbaImage}; +use image::{Pixel, Rgba, RgbaImage}; use std::collections::HashMap; #[derive(Debug, Clone, Copy)] @@ -16,6 +16,7 @@ pub enum ProjectileState { pub struct ProjectileEntity { id: usize, shot_by_id: usize, + color: Rgba, previous_position: Vec2, position: Vec2, velocity: Vec2, @@ -43,10 +44,10 @@ impl Body for ProjectileEntity { self.previous_position = self.position; self.position = self.position + self.velocity * deltatime; - if self.position.x < 0.0 || self.position.x > MAX_SCREEN_WIDTH as f32 { + if self.position.x < 0.0 || self.position.x > MAX_ENTITY_POSITION.x as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } - if self.position.y < 0.0 || self.position.y > MAX_SCREEN_HEIGHT as f32 { + if self.position.y < 0.0 || self.position.y > MAX_ENTITY_POSITION.y as f32 { return vec![SpaceCallback::DestroyEntity { id: self.id() }]; } @@ -69,15 +70,14 @@ impl Body for ProjectileEntity { } impl Sprite for ProjectileEntity { - fn layer(&self) -> usize { - self.layer - } fn image(&self) -> &RgbaImage { &self.image } - fn hit_box(&self) -> &HitBox { - &self.hit_box + fn network_image_type(&self) -> ImageType { + ImageType::Projectile { + color: self.color.to_rgb().0, + } } } @@ -89,18 +89,31 @@ impl Collider for ProjectileEntity { fn collider_type(&self) -> ColliderType { ColliderType::Projectile } + + fn hit_box(&self) -> &HitBox { + &self.hit_box + } } impl Entity for ProjectileEntity { fn set_id(&mut self, id: usize) { self.id = id; } + fn id(&self) -> usize { self.id } + + fn parent_id(&self) -> Option { + Some(self.shot_by_id) + } + + fn layer(&self) -> usize { + self.layer + } } -register_impl!(!PlayerControlled for ProjectileEntity); +register_impl!(!ControllableSpaceship for ProjectileEntity); register_impl!(!ResourceFragment for ProjectileEntity); impl ProjectileEntity { @@ -117,6 +130,7 @@ impl ProjectileEntity { Self { id: 0, shot_by_id, + color, previous_position: position, position, velocity, diff --git a/src/space_adventure/space.rs b/src/space_adventure/space.rs index 9dab928..313dfd2 100644 --- a/src/space_adventure/space.rs +++ b/src/space_adventure/space.rs @@ -1,5 +1,15 @@ use super::{ - asteroid::{AsteroidEntity, AsteroidSize}, constants::{MAX_SCREEN_HEIGHT, MAX_SCREEN_WIDTH}, fragment::FragmentEntity, particle::ParticleEntity, projectile::ProjectileEntity, spaceship::SpaceshipEntity, traits::{resolve_collision_between, Entity}, utils::EntityState, visual_effects::VisualEffect, PlayerControlled, PlayerInput + asteroid::{AsteroidEntity, AsteroidSize}, + collector::CollectorEntity, + constants::*, + fragment::FragmentEntity, + particle::ParticleEntity, + projectile::ProjectileEntity, + spaceship::SpaceshipEntity, + traits::{resolve_collision_between, Entity}, + utils::EntityState, + visual_effects::VisualEffect, + ControllableSpaceship, PlayerInput, }; use crate::{ image::utils::{ExtraImageUtils, TRAVELLING_BACKGROUND}, @@ -9,45 +19,49 @@ use crate::{ }; use anyhow::anyhow; use glam::Vec2; +use image::imageops::crop_imm; use image::{Rgba, RgbaImage}; use itertools::Itertools; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; -use strum::Display; use std::{ collections::HashMap, time::{Duration, Instant}, }; +use strum::Display; + +#[derive(Debug, Display, Clone, Copy, PartialEq)] +enum SpaceAdventureState { + Starting { time: Instant }, + Running { time: Instant }, + Ending { time: Instant }, +} -const MAX_LAYER: usize = 5; - -#[derive(Default, Debug, Display,Clone, Copy, PartialEq)] -enum SpaceState { - Starting { - time: Instant, - }, - #[default] - Running, - Ending { - time: Instant, - }, +impl SpaceAdventureState { + pub const STARTING_DURATION: Duration = Duration::from_millis(2500); + pub const ENDING_DURATION: Duration = Duration::from_millis(2500); } -impl SpaceState { - pub const STARTING_DURATION:Duration = Duration::from_millis(2500); - pub const ENDING_DURATION:Duration = Duration::from_millis(2500); +#[derive(Debug, Display, Clone, Copy, PartialEq)] +enum AsteroidPlanetState { + NotSpawned { asteroid_planet_probability: f64 }, + Spawned { image_number: usize }, + Landed { image_number: usize }, } -#[derive(Default, Debug)] +#[derive(Debug)] pub struct SpaceAdventure { id: usize, - state: SpaceState, + rng: ChaCha8Rng, + state: SpaceAdventureState, tick: usize, background: RgbaImage, // Layered entities, to allow to draw/interact on separate layers. - entities: [HashMap>; MAX_LAYER], + entities: Vec>>, id_to_layer: HashMap, player_id: Option, + asteroid_planet_state: AsteroidPlanetState, + enemy_ship_spawned: bool, } impl SpaceAdventure { @@ -62,19 +76,40 @@ impl SpaceAdventure { } else { entity.image() }; - base.copy_non_trasparent_from(image, x as u32, y as u32)?; + + let cropped_image = if x as u32 + image.width() > base.width() + && y as u32 + image.height() > base.height() + { + &crop_imm( + image, + 0, + 0, + base.width() - x as u32, + base.height() - y as u32, + ) + .to_image() + } else if x as u32 + image.width() > base.width() { + &crop_imm(image, 0, 0, base.width() - x as u32, image.height()).to_image() + } else if y as u32 + image.height() > base.height() { + &crop_imm(image, 0, 0, image.width(), base.height() - y as u32).to_image() + } else { + image + }; + + base.copy_non_trasparent_from(cropped_image, x as u32, y as u32)?; if debug_view { - let red = Rgba([255, 0, 5, 255]); let gray = Rgba([105, 105, 105, 255]); for (point, &is_border) in entity.hit_box().iter() { let g_point = entity.position() + point; - base.put_pixel( - g_point.x.max(0).min(MAX_SCREEN_WIDTH as i16) as u32, - g_point.y.max(0).min(MAX_SCREEN_HEIGHT as i16) as u32, - if is_border { red } else { gray }, - ); + if is_border { + base.put_pixel( + g_point.x.max(0).min(MAX_ENTITY_POSITION.x as i16) as u32, + g_point.y.max(0).min(MAX_ENTITY_POSITION.y as i16) as u32, + gray, + ); + } } } @@ -85,6 +120,7 @@ impl SpaceAdventure { let id = self.id.clone(); let layer = entity.layer().clone(); entity.set_id(id); + self.entities[layer].insert(entity.id(), entity); self.id_to_layer.insert(id, layer); self.id += 1; @@ -93,14 +129,14 @@ impl SpaceAdventure { pub fn is_starting(&self) -> bool { match self.state { - SpaceState::Starting { .. } => true, + SpaceAdventureState::Starting { .. } => true, _ => false, } } pub fn is_ending(&self) -> bool { match self.state { - SpaceState::Ending { .. } => true, + SpaceAdventureState::Ending { .. } => true, _ => false, } } @@ -150,6 +186,13 @@ impl SpaceAdventure { None } + pub fn generate_enemy_spaceship(&mut self) -> AppResult { + let collector_id = self.insert_entity(Box::new(CollectorEntity::new())); + let enemy_id = self.insert_entity(Box::new(SpaceshipEntity::random_enemy(collector_id)?)); + self.enemy_ship_spawned = true; + Ok(enemy_id) + } + pub fn generate_asteroid( &mut self, position: Vec2, @@ -201,29 +244,69 @@ impl SpaceAdventure { ))) } - pub fn new() -> AppResult { + pub fn asteroid_planet_found(&self) -> Option { + match self.asteroid_planet_state { + AsteroidPlanetState::Landed { image_number } => Some(image_number), + _ => None, + } + } + + pub fn new(asteroid_planet_probability: f64) -> AppResult { let bg = TRAVELLING_BACKGROUND.clone(); - let mut background = RgbaImage::new(bg.width() * 2, bg.height() * 2); + let mut background = RgbaImage::new(bg.width() * 2, bg.height() * 3); background.copy_non_trasparent_from(&bg, 0, 0)?; background.copy_non_trasparent_from(&bg, bg.width(), 0)?; background.copy_non_trasparent_from(&bg, 0, bg.height())?; background.copy_non_trasparent_from(&bg, bg.width(), bg.height())?; + background.copy_non_trasparent_from(&bg, 0, 2 * bg.height())?; + background.copy_non_trasparent_from(&bg, bg.width(), 2 * bg.height())?; + // Crop background + let background = crop_imm( + &background, + 0, + 0, + BACKGROUND_IMAGE_SIZE.x, + BACKGROUND_IMAGE_SIZE.y, + ) + .to_image(); + + let mut entities = vec![]; + for _ in 0..MAX_LAYER { + entities.push(HashMap::new()); + } Ok(Self { + id: 0, + rng: ChaCha8Rng::from_entropy(), + state: SpaceAdventureState::Starting { + time: Instant::now(), + }, + tick: 0, background, - state: SpaceState::Starting { time: Instant::now() }, - ..Default::default() + entities, + id_to_layer: HashMap::new(), + player_id: None, + asteroid_planet_state: AsteroidPlanetState::NotSpawned { + asteroid_planet_probability, + }, + enemy_ship_spawned: false, }) } - pub fn with_spaceship( + pub fn with_player( mut self, spaceship: &Spaceship, + team_speed_bonus: f32, resources: ResourceMap, fuel: u32, ) -> AppResult { + let collector_id = self.insert_entity(Box::new(CollectorEntity::new())); let id = self.insert_entity(Box::new(SpaceshipEntity::from_spaceship( - spaceship, resources, fuel, + spaceship, + team_speed_bonus, + resources, + fuel, + collector_id, )?)); self.player_id = Some(id); @@ -236,47 +319,75 @@ impl SpaceAdventure { } pub fn handle_player_input(&mut self, input: PlayerInput) -> AppResult<()> { - if self.state != SpaceState::Running { - return Ok(()); + match self.state { + SpaceAdventureState::Running { .. } => {} + _ => return Ok(()), } let player = self.get_player_mut().ok_or(anyhow!("No player set"))?; - let player_control: &mut dyn PlayerControlled = player + let player_control: &mut dyn ControllableSpaceship = player .as_trait_mut() - .expect("Player should implement PlayerControlled."); + .expect("Player should implement ControllableSpaceship."); player_control.handle_player_input(input); Ok(()) } pub fn stop_space_adventure(&mut self) { - self.state = SpaceState::Ending { - time: Instant::now(), - }; + match self.state { + SpaceAdventureState::Ending { .. } => {} + _ => { + self.state = SpaceAdventureState::Ending { + time: Instant::now(), + } + } + } } - pub fn update(&mut self, deltatime: f32) -> AppResult> { + pub fn land_on_asteroid(&mut self) { + match self.asteroid_planet_state { + AsteroidPlanetState::NotSpawned { .. } => { + unreachable!("Should not be possible to land on unspawned asteroid planet.") + } + AsteroidPlanetState::Spawned { image_number } => { + self.asteroid_planet_state = AsteroidPlanetState::Landed { image_number } + } + AsteroidPlanetState::Landed { .. } => {} + } match self.state { - SpaceState::Starting { time } => { - if time.elapsed() >= SpaceState::STARTING_DURATION { - self.state = SpaceState::Running; - return Ok(vec![]); + SpaceAdventureState::Ending { .. } => {} + _ => { + self.state = SpaceAdventureState::Ending { + time: Instant::now(), } + } + } + } + pub fn update(&mut self, deltatime: f32) -> AppResult> { + let time = match self.state { + SpaceAdventureState::Starting { time } => { + if time.elapsed() >= SpaceAdventureState::STARTING_DURATION { + self.state = SpaceAdventureState::Running { + time: Instant::now(), + }; + return Ok(vec![]); + } + time } - SpaceState::Running => { + SpaceAdventureState::Running { time } => { if let Some(player) = self.get_player() { - let player_control: &dyn PlayerControlled = player + let player_control: &dyn ControllableSpaceship = player .as_trait_ref() - .expect("Player should implement PlayerControlled."); + .expect("Player should implement ControllableSpaceship."); if player_control.current_durability() == 0 { self.stop_space_adventure(); return Ok(vec![ - UiCallback::PushUiPopup { popup_message: + UiCallback::PushUiPopup { popup_message: PopupMessage::Ok{ message: "Danger! There's a breach in the hull.\nAll the resources in the stiva have been lost,\nyou need to go back to the base...".to_string() , is_skippable:true, tick:Tick::now()} @@ -284,14 +395,16 @@ impl SpaceAdventure { ]); } } + time } - SpaceState::Ending { time } => { - if time.elapsed() >= SpaceState::ENDING_DURATION { + SpaceAdventureState::Ending { time } => { + if time.elapsed() >= SpaceAdventureState::ENDING_DURATION { return Ok(vec![UiCallback::ReturnFromSpaceAdventure]); } + time } - } + }; self.tick += 1; @@ -330,16 +443,36 @@ impl SpaceAdventure { } // Generate asteroids - let rng = &mut ChaCha8Rng::from_entropy(); - if self.entity_count() < 50 && rng.gen_bool(0.01) { + let difficulty_level = time.elapsed().as_secs() as usize; + if self.entity_count() < difficulty_level.min(250) + && self.rng.gen_bool(ASTEROID_GENERATION_PROBABILITY) + { let asteroid = AsteroidEntity::new_at_screen_edge(); self.insert_entity(Box::new(asteroid)); } + if difficulty_level > DIFFICULTY_FOR_ASTEROID_PLANET_GENERATION { + match self.asteroid_planet_state { + AsteroidPlanetState::NotSpawned { + asteroid_planet_probability, + } => { + if asteroid_planet_probability > 0.0 { + let asteroid = AsteroidEntity::planet(); + let id = self.insert_entity(Box::new(asteroid)); + self.asteroid_planet_state = AsteroidPlanetState::Spawned { + image_number: id % MAX_ASTEROID_PLANET_IMAGE_TYPE, + }; + } + } + _ => {} + } + } + + // TODO: spawn enemy ship Ok(vec![]) } - pub fn image(&self, debug_view: bool) -> AppResult { + pub fn image(&self, width: u32, height: u32, debug_view: bool) -> AppResult { let mut base = self.background.clone(); // Draw starting from lowest layer @@ -349,19 +482,30 @@ impl SpaceAdventure { } } - - match self.state { + match self.state { // If adventure is starting, fade in. - SpaceState::Starting { time } => { - VisualEffect::FadeIn.apply_global_effect(&mut base, time.elapsed().as_millis() as f32/1000.0); + SpaceAdventureState::Starting { time } => { + VisualEffect::FadeIn + .apply_global_effect(&mut base, time.elapsed().as_millis() as f32 / 1000.0); } - // If adventure is ending, fade out. - SpaceState::Ending { time } => { - VisualEffect::FadeOut.apply_global_effect(&mut base, time.elapsed().as_millis() as f32/1000.0); + // If adventure is ending, fade out. + SpaceAdventureState::Ending { time } => { + VisualEffect::FadeOut + .apply_global_effect(&mut base, time.elapsed().as_millis() as f32 / 1000.0); } - SpaceState::Running=>{} + SpaceAdventureState::Running { .. } => {} } - Ok(base) + // Crop centered subimage of size SCREEN_SIZE + let image = crop_imm( + &base, + (MAX_ENTITY_POSITION.x - SCREEN_SIZE.x) / 2, + (MAX_ENTITY_POSITION.y - SCREEN_SIZE.y) / 2, + width, + height, + ) + .to_image(); + + Ok(image) } } diff --git a/src/space_adventure/space_callback.rs b/src/space_adventure/space_callback.rs index 3442bbf..83fd789 100644 --- a/src/space_adventure/space_callback.rs +++ b/src/space_adventure/space_callback.rs @@ -8,11 +8,6 @@ use image::Rgba; #[derive(Debug, Clone, Copy)] pub enum SpaceCallback { - AccelerateEntity { - id: usize, - acceleration: I16Vec2, - }, - AddVisualEffect { id: usize, effect: VisualEffect, @@ -62,18 +57,24 @@ pub enum SpaceCallback { color: Rgba, damage: f32, }, + + LandSpaceshipOnAsteroid, + + SetAcceleration { + id: usize, + acceleration: I16Vec2, + }, + + SetPosition { + id: usize, + position: I16Vec2, + }, } impl SpaceCallback { pub fn call(&self, space: &mut SpaceAdventure) { let mut callbacks = vec![]; match *self { - Self::AccelerateEntity { id, .. } => { - if let Some(entity) = space.get_entity_mut(&id) { - callbacks.append(&mut entity.handle_space_callback(*self)); - } - } - Self::AddVisualEffect { id, effect, @@ -139,6 +140,22 @@ impl SpaceCallback { } => { space.generate_projectile(shot_by_id, position, velocity, color, damage); } + + Self::LandSpaceshipOnAsteroid => { + space.land_on_asteroid(); + } + + Self::SetAcceleration { id, .. } => { + if let Some(entity) = space.get_entity_mut(&id) { + callbacks.append(&mut entity.handle_space_callback(*self)); + } + } + + Self::SetPosition { id, .. } => { + if let Some(entity) = space.get_entity_mut(&id) { + callbacks.append(&mut entity.handle_space_callback(*self)); + } + } } for callback in callbacks.iter() { callback.call(space); diff --git a/src/space_adventure/spaceship.rs b/src/space_adventure/spaceship.rs index ff6d0b1..c55f93b 100644 --- a/src/space_adventure/spaceship.rs +++ b/src/space_adventure/spaceship.rs @@ -1,7 +1,10 @@ +use super::networking::ImageType; use super::space_callback::SpaceCallback; use super::utils::{body_data_from_image, EntityState}; use super::{constants::*, traits::*}; -use crate::image::components::ImageComponent; +use crate::image::color_map::ColorMap; +use crate::image::components::{ImageComponent, SizedImageComponent}; +use crate::image::spaceship::SpaceshipImage; use crate::image::utils::open_image; use crate::register_impl; use crate::space_adventure::visual_effects::VisualEffect; @@ -9,13 +12,17 @@ use crate::space_adventure::Direction; use crate::types::*; use crate::world::constants::{FUEL_CONSUMPTION_PER_UNIT_STORAGE, SPEED_PENALTY_PER_UNIT_STORAGE}; use crate::world::resources::Resource; +use crate::world::spaceship::SpaceshipPrefab; use crate::{image::types::Gif, types::AppResult, world::spaceship::Spaceship}; use glam::{I16Vec2, Vec2}; -use image::imageops::rotate90; -use image::{Rgba, RgbaImage}; +use image::imageops::{rotate270, rotate90}; +use image::{Pixel, Rgba, RgbaImage}; +use itertools::Itertools; +use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use std::collections::HashMap; +use strum::IntoEnumIterator; #[derive(Debug, Clone, Copy, PartialEq)] pub enum ShooterState { @@ -25,16 +32,49 @@ pub enum ShooterState { } impl ShooterState { - const MAX_SHOOTING_RECOIL: f32 = 0.125; const SHOOTING_CHARGE_COST: f32 = 1.0; const MAX_CHARGE: f32 = 100.0; - const CHARGE_RECOVERY_SPEED: f32 = 2.0; - const RECHARGE_RECOVERY_SPEED: f32 = 7.5; + const CHARGE_RECOVERY_SPEED: f32 = 5.0; + const RECHARGE_RECOVERY_SPEED: f32 = 3.0; +} + +#[derive(Debug)] +struct Shooter { + pub positions: Vec, + pub damage: f32, + pub max_recoil: f32, + pub state: ShooterState, +} + +impl Shooter { + pub fn new(positions: Vec, damage: f32, fire_rate: f32) -> Self { + Self { + positions, + damage, + max_recoil: 1.0 / fire_rate, + state: ShooterState::Ready { + charge: ShooterState::MAX_CHARGE, + }, + } + } + + pub fn set_state(&mut self, state: ShooterState) { + self.state = state; + } + + pub fn shoot(&mut self, charge: f32) { + self.set_state(ShooterState::Shooting { + charge, + recoil: self.max_recoil, + }) + } } #[derive(Debug)] pub struct SpaceshipEntity { id: usize, + is_player: bool, + base_spaceship: Spaceship, resources: ResourceMap, used_storage_capacity: u32, storage_capacity: u32, @@ -57,10 +97,11 @@ pub struct SpaceshipEntity { tick: usize, gif: Gif, engine_exhaust: Vec, // Position of exhaust in relative coords - shooters: Vec, // Position of shooters in relative coords + shooter: Shooter, auto_shoot: bool, - shooter_state: ShooterState, + collector_id: usize, visual_effects: VisualEffectMap, + releasing_scraps: bool, } impl Body for SpaceshipEntity { @@ -129,9 +170,9 @@ impl Body for SpaceshipEntity { self.position += self.velocity * deltatime; self.acceleration = Vec2::ZERO; - let min_position = Vec2::ZERO; - let max_position = - Vec2::new(SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32) - self.size().as_vec2(); + // The spaceship must always remain on screen + let min_position = (MAX_ENTITY_POSITION - SCREEN_SIZE).as_vec2() / 2.0; + let max_position = min_position + SCREEN_SIZE.as_vec2() - self.size().as_vec2(); if self.position.x < min_position.x { self.position.x = min_position.x; @@ -149,21 +190,27 @@ impl Body for SpaceshipEntity { self.velocity.y = 0.0; } + callbacks.push(SpaceCallback::SetPosition { + id: self.collector_id, + position: self.center(), + }); + callbacks } } impl Sprite for SpaceshipEntity { - fn layer(&self) -> usize { - 1 - } - fn image(&self) -> &RgbaImage { &self.gif[self.frame()] } - fn hit_box(&self) -> &HitBox { - &self.hit_boxes[self.frame()] + fn network_image_type(&self) -> ImageType { + ImageType::Spaceship { + hull: self.base_spaceship.hull, + engine: self.base_spaceship.engine, + storage: self.base_spaceship.storage, + color_map: self.base_spaceship.image.color_map, + } } fn should_apply_visual_effects<'a>(&self) -> bool { @@ -203,92 +250,157 @@ impl Entity for SpaceshipEntity { fn set_id(&mut self, id: usize) { self.id = id; } + fn id(&self) -> usize { self.id } + fn layer(&self) -> usize { + 1 + } + fn update(&mut self, deltatime: f32) -> Vec { + // This is only triggered for enemy ships and not for the player ship. + if !self.is_player && self.current_durability() == 0 { + return vec![SpaceCallback::DestroyEntity { id: self.id }]; + } + + if !self.is_player { + let rng = &mut rand::thread_rng(); + match self.shooter.state { + ShooterState::Ready { charge } => { + if !self.auto_shoot + && charge > ShooterState::MAX_CHARGE * rng.gen_range(0.25..1.0) + { + self.auto_shoot = true; + } + if charge <= 0.25 { + self.auto_shoot = false; + } + } + _ => {} + } + } + let mut callbacks = vec![]; callbacks.append(&mut self.update_body(deltatime)); callbacks.append(&mut self.update_sprite(deltatime)); - match self.shooter_state { + match self.shooter.state { ShooterState::Shooting { charge, recoil } => { - if recoil == ShooterState::MAX_SHOOTING_RECOIL { - for shooter_position in self.shooters.iter() { + if recoil == self.shooter.max_recoil { + for shooter_position in self.shooter.positions.iter() { callbacks.push(SpaceCallback::GenerateProjectile { shot_by_id: self.id(), position: self.position + shooter_position.as_vec2(), - velocity: Vec2::X * 100.0, + velocity: Vec2::X * 100.0 * self.orientation() as f32, color: Rgba([ 25, 125, 55 + (200.0 * charge / ShooterState::MAX_CHARGE) as u8, 255, ]), - damage: 1.5 * (charge / ShooterState::MAX_CHARGE).powf(0.25), + damage: self.shooter.damage, }); } let new_charge = charge - ShooterState::SHOOTING_CHARGE_COST; if new_charge > 0.0 { let new_recoil = recoil - deltatime; - self.shooter_state = ShooterState::Shooting { + self.shooter.set_state(ShooterState::Shooting { charge: new_charge, recoil: new_recoil, - }; + }); } else { - self.shooter_state = ShooterState::Recharging { charge: 0.0 }; + self.shooter + .set_state(ShooterState::Recharging { charge: 0.0 }); } } else { let new_recoil = recoil - deltatime; if new_recoil > 0.0 { - self.shooter_state = ShooterState::Shooting { + self.shooter.set_state(ShooterState::Shooting { charge, recoil: new_recoil, - }; + }); } else { - self.shooter_state = ShooterState::Ready { charge }; + self.shooter.set_state(ShooterState::Ready { charge }); } } } ShooterState::Ready { charge } => { if self.auto_shoot { - self.shooter_state = ShooterState::Shooting { + self.shooter.set_state(ShooterState::Shooting { charge, - recoil: ShooterState::MAX_SHOOTING_RECOIL, - }; + recoil: self.shooter.max_recoil, + }); } else if charge < ShooterState::MAX_CHARGE { let new_charge = (charge + ShooterState::CHARGE_RECOVERY_SPEED * deltatime) .min(ShooterState::MAX_CHARGE); - self.shooter_state = ShooterState::Ready { charge: new_charge }; + self.shooter + .set_state(ShooterState::Ready { charge: new_charge }); } } ShooterState::Recharging { charge } => { let new_charge = charge + ShooterState::RECHARGE_RECOVERY_SPEED * deltatime; if new_charge < ShooterState::MAX_CHARGE { - self.shooter_state = ShooterState::Recharging { charge: new_charge }; + self.shooter + .set_state(ShooterState::Recharging { charge: new_charge }); } else { - self.shooter_state = ShooterState::Ready { + self.shooter.set_state(ShooterState::Ready { charge: ShooterState::MAX_CHARGE, - }; + }); } } } + if self.releasing_scraps { + let rng = &mut rand::thread_rng(); + callbacks.push(SpaceCallback::GenerateParticle { + position: self.center().as_vec2(), + velocity: Vec2::new(-6.0 + rng.gen_range(-0.5..0.5), rng.gen_range(-1.5..1.5)), + color: Resource::SCRAPS.color(), + particle_state: EntityState::Decaying { + lifetime: 3.0 + rng.gen_range(0.0..1.5), + }, + layer: 2, + }); + self.releasing_scraps = false; + } callbacks } fn handle_space_callback(&mut self, callback: SpaceCallback) -> Vec { match callback { + SpaceCallback::DestroyEntity { .. } => { + let rng = &mut rand::thread_rng(); + let mut callbacks = vec![SpaceCallback::DestroyEntity { + id: self.collector_id, + }]; + + let color_map = self.base_spaceship.image.color_map; + let colors = [color_map.red, color_map.green, color_map.blue]; + for _ in 0..24 { + let color = colors.choose(rng).expect("There should be one color"); + callbacks.push(SpaceCallback::GenerateParticle { + position: self.center().as_vec2(), + velocity: self.velocity + + Vec2::new(rng.gen_range(-10.0..10.0), rng.gen_range(-10.0..10.0)), + color: color.to_rgba(), + particle_state: EntityState::Decaying { + lifetime: 5.0 + rng.gen_range(-1.5..1.5), + }, + layer: rng.gen_range(0..=2), + }); + } + return callbacks; + } + SpaceCallback::DamageEntity { damage, .. } => { self.add_damage(damage); self.add_visual_effect( VisualEffect::COLOR_MASK_LIFETIME, - VisualEffect::ColorMask { - color: Rgba([255, 0, 0, 0]), - }, + VisualEffect::ColorMask { color: [255, 0, 0] }, ); } @@ -314,12 +426,20 @@ impl Collider for SpaceshipEntity { fn collider_type(&self) -> ColliderType { ColliderType::Spaceship } + + fn hit_box(&self) -> &HitBox { + &self.hit_boxes[self.frame()] + } } -register_impl!(PlayerControlled for SpaceshipEntity); +register_impl!(ControllableSpaceship for SpaceshipEntity); register_impl!(!ResourceFragment for SpaceshipEntity); -impl PlayerControlled for SpaceshipEntity { +impl ControllableSpaceship for SpaceshipEntity { + fn is_player(&self) -> bool { + self.is_player + } + fn fuel(&self) -> u32 { self.fuel.round() as u32 } @@ -341,7 +461,7 @@ impl PlayerControlled for SpaceshipEntity { } fn charge(&self) -> u32 { - match self.shooter_state { + match self.shooter.state { ShooterState::Ready { charge } => charge, ShooterState::Recharging { charge } => charge, ShooterState::Shooting { charge, .. } => charge, @@ -350,7 +470,7 @@ impl PlayerControlled for SpaceshipEntity { } fn shooter_state(&self) -> ShooterState { - self.shooter_state + self.shooter.state } fn max_charge(&self) -> u32 { @@ -379,13 +499,21 @@ impl PlayerControlled for SpaceshipEntity { PlayerInput::MoveUp => self.accelerate(Direction::UP), PlayerInput::MoveLeft => self.accelerate(Direction::LEFT), PlayerInput::MoveRight => self.accelerate(Direction::RIGHT), - PlayerInput::MainButton => self.shoot(), - PlayerInput::SecondButton => self.auto_shoot = !self.auto_shoot, + PlayerInput::Shoot => self.shoot(), + PlayerInput::ToggleAutofire => self.auto_shoot = !self.auto_shoot, + PlayerInput::ReleaseScraps => self.release_scraps(), } } } impl SpaceshipEntity { + fn orientation(&self) -> i8 { + if self.is_player { + return 1; + } + return -1; + } + fn thrust(&self) -> f32 { self.base_thrust / (1.0 + SPEED_PENALTY_PER_UNIT_STORAGE * self.used_storage_capacity as f32) @@ -422,36 +550,42 @@ impl SpaceshipEntity { } fn shoot(&mut self) { - match self.shooter_state { - ShooterState::Ready { charge } => { - self.shooter_state = ShooterState::Shooting { - charge, - recoil: ShooterState::MAX_SHOOTING_RECOIL, - } - } + match self.shooter.state { + ShooterState::Ready { charge } => self.shooter.shoot(charge), _ => {} } } + fn release_scraps(&mut self) { + if self.resources.sub(Resource::SCRAPS, 1).is_ok() { + self.releasing_scraps = true; + } + } + pub fn from_spaceship( spaceship: &Spaceship, + team_speed_bonus: f32, resources: ResourceMap, fuel: u32, + collector_id: usize, ) -> AppResult { let mut gif = vec![]; let mut hit_boxes = vec![]; let base_gif = spaceship.compose_image()?; for idx in 0..base_gif.len() { let base_image = rotate90(&base_gif[idx]); - let (image, hit_box) = body_data_from_image(&base_image); + let (image, hit_box) = body_data_from_image(&base_image, true); gif.push(image); hit_boxes.push(hit_box); } - let position = Vec2::ZERO.with_y((SCREEN_HEIGHT / 2) as f32); + let position = Vec2::new( + (MAX_ENTITY_POSITION.x - SCREEN_SIZE.x) as f32 / 2.0, + 0.5 * SCREEN_SIZE.y as f32, + ); - let mut engine_img = open_image(&spaceship.engine.select_mask_file(0))?; + let mut engine_img = open_image(&spaceship.engine.select_mask_file())?; engine_img = rotate90(&engine_img); let y_offset = (engine_img.height() - gif[0].height()) / 2; @@ -467,17 +601,18 @@ impl SpaceshipEntity { } } - let mut hull_img = open_image(&spaceship.hull.select_mask_file(0))?; - hull_img = rotate90(&hull_img); + let size = SpaceshipImage::size(&spaceship.hull); + let mut shooter_img = spaceship.shooter.image(size)?; + shooter_img = rotate90(&shooter_img); - let y_offset = (hull_img.height() - gif[0].height()) / 2; - let mut shooters = vec![]; - for x in 0..hull_img.width() { - for y in 0..hull_img.height() { - if let Some(pixel) = hull_img.get_pixel_checked(x, y) { - // If pixel is blue, it is at the exhaust position. + let y_offset = (shooter_img.height() - gif[0].height()) / 2; + let mut shooter_positions = vec![]; + for x in 0..shooter_img.width() { + for y in 0..shooter_img.height() { + if let Some(pixel) = shooter_img.get_pixel_checked(x, y) { + // If pixel is blue, it is at the shooter position. if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 255 && pixel[3] > 0 { - shooters.push(I16Vec2::new(x as i16, y as i16 - y_offset as i16)); + shooter_positions.push(I16Vec2::new(x as i16, y as i16 - y_offset as i16)); } } } @@ -485,8 +620,12 @@ impl SpaceshipEntity { let used_storage_capacity = resources.used_storage_capacity(); + let shooter = Shooter::new(shooter_positions, spaceship.damage(), spaceship.fire_rate()); + Ok(Self { id: 0, + is_player: true, + base_spaceship: spaceship.clone(), resources, used_storage_capacity, storage_capacity: spaceship.storage_capacity(), @@ -497,22 +636,120 @@ impl SpaceshipEntity { current_durability: spaceship.current_durability() as f32, durability: spaceship.durability() as f32, base_thrust: spaceship.speed(0) * THRUST_MOD, - base_speed: spaceship.speed(0) * MAX_SPACESHIP_SPEED_MOD, + base_speed: spaceship.speed(0) * MAX_SPACESHIP_SPEED_MOD * team_speed_bonus, maneuverability: 0.0, fuel: fuel as f32, fuel_capacity: spaceship.fuel_capacity(), base_fuel_consumption: spaceship.fuel_consumption(0) * FUEL_CONSUMPTION_MOD, friction_coeff: FRICTION_COEFF, engine_exhaust, - shooters, + shooter, auto_shoot: false, velocity: Vec2::default(), acceleration: Vec2::default(), tick: 0, - shooter_state: ShooterState::Ready { - charge: ShooterState::MAX_CHARGE, - }, + collector_id, + visual_effects: HashMap::new(), + releasing_scraps: true, + }) + } + + pub fn random_enemy(collector_id: usize) -> AppResult { + let mut gif = vec![]; + let mut hit_boxes = vec![]; + let spaceship = SpaceshipPrefab::iter() + .collect_vec() + .choose(&mut rand::thread_rng()) + .expect("There shiuld be one spaceship available") + .spaceship("Baddy".to_string()) + .with_color_map(ColorMap::random()); + + let base_gif = spaceship.compose_image()?; + for idx in 0..base_gif.len() { + let base_image = rotate270(&base_gif[idx]); + let (image, hit_box) = body_data_from_image(&base_image, true); + + gif.push(image); + hit_boxes.push(hit_box); + } + + let position = Vec2::new(SCREEN_SIZE.x as f32, SCREEN_SIZE.y as f32 / 2.0); + + let mut engine_img = open_image(&spaceship.engine.select_mask_file())?; + engine_img = rotate270(&engine_img); + + let y_offset = (engine_img.height() - gif[0].height()) / 2; + let mut engine_exhaust = vec![]; + for x in 0..engine_img.width() { + for y in 0..engine_img.height() { + if let Some(pixel) = engine_img.get_pixel_checked(x, y) { + // If pixel is blue, it is at the exhaust position. + if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 255 && pixel[3] > 0 { + engine_exhaust.push(I16Vec2::new(x as i16, y as i16 - y_offset as i16)); + } + } + } + } + + let size = SpaceshipImage::size(&spaceship.hull); + let mut shooter_img = spaceship.shooter.image(size)?; + shooter_img = rotate270(&shooter_img); + + let y_offset = (shooter_img.height() - gif[0].height()) / 2; + let mut shooter_positions = vec![]; + for x in 0..shooter_img.width() { + for y in 0..shooter_img.height() { + if let Some(pixel) = shooter_img.get_pixel_checked(x, y) { + // If pixel is blue, it is at the shooter position. + if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 255 && pixel[3] > 0 { + shooter_positions.push(I16Vec2::new(x as i16, y as i16 - y_offset as i16)); + } + } + } + } + + let resources = ResourceMap::new(); + let used_storage_capacity = 0; + let fuel = spaceship.fuel_capacity() as f32; + let storage_capacity = spaceship.storage_capacity(); + let current_durability = spaceship.current_durability() as f32; + let durability = spaceship.durability() as f32; + let base_thrust = spaceship.speed(0) * THRUST_MOD; + let base_speed = spaceship.speed(0) * MAX_SPACESHIP_SPEED_MOD; + let fuel_capacity = spaceship.fuel_capacity(); + let base_fuel_consumption = spaceship.fuel_consumption(0) * FUEL_CONSUMPTION_MOD; + + let shooter = Shooter::new(shooter_positions, spaceship.damage(), spaceship.fire_rate()); + + Ok(Self { + id: 0, + is_player: false, + base_spaceship: spaceship, + resources, + used_storage_capacity, + storage_capacity, + previous_position: position, + position, + gif, + hit_boxes, + current_durability, + durability, + base_thrust, + base_speed, + maneuverability: 0.0, + fuel, + fuel_capacity, + base_fuel_consumption, + friction_coeff: FRICTION_COEFF, + engine_exhaust, + shooter, + auto_shoot: false, + velocity: Vec2::default(), + acceleration: Vec2::default(), + tick: 0, + collector_id, visual_effects: HashMap::new(), + releasing_scraps: true, }) } } diff --git a/src/space_adventure/traits.rs b/src/space_adventure/traits.rs index c5a1713..d9dd3b0 100644 --- a/src/space_adventure/traits.rs +++ b/src/space_adventure/traits.rs @@ -1,9 +1,11 @@ use crate::{types::ResourceMap, world::resources::Resource}; -use super::{space_callback::SpaceCallback, spaceship::ShooterState, visual_effects::VisualEffect}; +use super::{ + networking::ImageType, space_callback::SpaceCallback, spaceship::ShooterState, + visual_effects::VisualEffect, +}; use glam::I16Vec2; -use image::{Rgba, RgbaImage}; -use itertools::Itertools; +use image::{Pixel, RgbaImage}; use std::{ collections::{ hash_map::{Iter, Keys, Values}, @@ -123,7 +125,7 @@ impl HitBox { } } -pub trait Body: Sprite { +pub trait Body: Collider { fn previous_rect(&self) -> (I16Vec2, I16Vec2) { ( self.previous_position() + self.hit_box().top_left, @@ -159,22 +161,7 @@ pub trait Body: Sprite { pub trait Sprite { fn image(&self) -> &RgbaImage; - fn layer(&self) -> usize { - 0 - } - - fn hit_box(&self) -> &HitBox; - - fn hit_box_vec(&self) -> Vec<(I16Vec2, bool)> { - self.hit_box() - .iter() - .map(|(key, value)| (*key, *value)) - .collect_vec() - } - - fn size(&self) -> I16Vec2 { - self.hit_box().size - } + fn network_image_type(&self) -> ImageType; fn should_apply_visual_effects<'a>(&self) -> bool { false @@ -196,19 +183,27 @@ pub trait Sprite { #[derive(Debug, Clone, Copy, PartialEq)] pub enum ColliderType { None, - Spaceship, Asteroid, - Projectile, + AsteroidPlanet, + Collector, Fragment, + Projectile, + Spaceship, } -pub trait Collider: Body { +pub trait Collider { fn collision_damage(&self) -> f32 { - 10.0 + 0.0 } fn collider_type(&self) -> ColliderType { ColliderType::None } + + fn hit_box(&self) -> &HitBox; + + fn size(&self) -> I16Vec2 { + self.hit_box().size + } } fn check_physical_collision(one: &Box, other: &Box) -> bool { @@ -296,6 +291,10 @@ fn are_colliding(one: &Box, other: &Box) -> bool { return false; } + if one.parent_id() == Some(other.id()) || other.parent_id() == Some(one.id()) { + return false; + } + // Broad phase detection, shortcut if rects cannot intersect if !check_broad_phase_collision(one, other) { return false; @@ -336,6 +335,36 @@ pub fn resolve_collision_between( other: &Box, ) -> Vec { match (one.collider_type(), other.collider_type()) { + (ColliderType::AsteroidPlanet, ColliderType::Asteroid) => { + if !are_colliding(one, other) { + return vec![]; + } + return vec![SpaceCallback::DamageEntity { + id: other.id(), + damage: one.collision_damage(), + }]; + } + (ColliderType::Asteroid, ColliderType::AsteroidPlanet) => { + resolve_collision_between(other, one) + } + (ColliderType::AsteroidPlanet, ColliderType::Spaceship) => { + if !are_colliding(one, other) { + return vec![]; + } + + let ship_control: &dyn ControllableSpaceship = other + .as_trait_ref() + .expect("Spaceship should implement ControllableSpaceship"); + + if ship_control.is_player() { + return vec![SpaceCallback::LandSpaceshipOnAsteroid]; + } + + return vec![]; + } + (ColliderType::Spaceship, ColliderType::AsteroidPlanet) => { + resolve_collision_between(other, one) + } (ColliderType::Projectile, ColliderType::Asteroid) => { if !are_colliding(one, other) { return vec![]; @@ -349,6 +378,21 @@ pub fn resolve_collision_between( ]; } (ColliderType::Asteroid, ColliderType::Projectile) => resolve_collision_between(other, one), + (ColliderType::Projectile, ColliderType::Spaceship) => { + if !are_colliding(one, other) { + return vec![]; + } + return vec![ + SpaceCallback::DestroyEntity { id: one.id() }, + SpaceCallback::DamageEntity { + id: other.id(), + damage: one.collision_damage(), + }, + ]; + } + (ColliderType::Spaceship, ColliderType::Projectile) => { + resolve_collision_between(other, one) + } (ColliderType::Spaceship, ColliderType::Asteroid) => { if !are_colliding(one, other) { return vec![]; @@ -367,11 +411,6 @@ pub fn resolve_collision_between( (ColliderType::Asteroid, ColliderType::Spaceship) => resolve_collision_between(other, one), (ColliderType::Spaceship, ColliderType::Fragment) => { - if !are_colliding(one, other) { - return vec![]; - } - - // Two cases: if the fragment actually hits the spaceship hitbox, it is collected. let g_point = other.position() - one.position(); if one.hit_box().contains_key(&g_point) { let resource_fragment: &dyn ResourceFragment = other @@ -384,7 +423,7 @@ pub fn resolve_collision_between( SpaceCallback::AddVisualEffect { id: one.id(), effect: VisualEffect::ColorMask { - color: other.image().get_pixel(0, 0).clone(), + color: other.image().get_pixel(0, 0).to_rgb().0, }, duration: VisualEffect::COLOR_MASK_LIFETIME, }, @@ -396,31 +435,33 @@ pub fn resolve_collision_between( SpaceCallback::DestroyEntity { id: other.id() }, ]; } - // Else, it is accelerated towards the spaceship. - return vec![ - SpaceCallback::AddVisualEffect { - id: other.id(), - effect: VisualEffect::ColorMask { - color: Rgba([0, 255, 0, 255]), - }, - duration: VisualEffect::COLOR_MASK_LIFETIME, - }, - SpaceCallback::AccelerateEntity { - id: other.id(), - acceleration: one.center() - other.center(), - }, - ]; + vec![] } + (ColliderType::Fragment, ColliderType::Spaceship) => resolve_collision_between(other, one), + (ColliderType::Collector, ColliderType::Fragment) => { + if !are_colliding(one, other) { + return vec![]; + } + + // If a fragment touches the collector hit_box, it is accelerated towards it. + return vec![SpaceCallback::SetAcceleration { + id: other.id(), + acceleration: one.center() - other.center(), + }]; + } + (ColliderType::Fragment, ColliderType::Collector) => resolve_collision_between(other, one), + _ => return vec![], } } pub trait Entity: Sprite + + Body + Collider - + MaybeImplements + + MaybeImplements + MaybeImplements + Debug + Send @@ -428,6 +469,13 @@ pub trait Entity: { fn set_id(&mut self, id: usize); fn id(&self) -> usize; + fn set_parent_id(&mut self, _parent_id: usize) {} + fn parent_id(&self) -> Option { + None + } + fn layer(&self) -> usize { + 0 + } fn update(&mut self, deltatime: f32) -> Vec { let mut callbacks = vec![]; callbacks.append(&mut self.update_body(deltatime)); @@ -447,11 +495,13 @@ pub enum PlayerInput { MoveRight, MoveUp, MoveDown, - MainButton, - SecondButton, + ToggleAutofire, + Shoot, + ReleaseScraps, } -pub trait PlayerControlled { +pub trait ControllableSpaceship { + fn is_player(&self) -> bool; fn fuel(&self) -> u32; fn fuel_capacity(&self) -> u32; fn resources(&self) -> &ResourceMap; @@ -476,28 +526,26 @@ pub trait ResourceFragment { mod test { use crate::{ - space_adventure::{fragment::FragmentEntity, traits::*, SpaceshipEntity}, - types::{AppResult, ResourceMap}, - world::{resources::Resource, spaceship::SpaceshipPrefab}, + space_adventure::{collector::CollectorEntity, fragment::FragmentEntity, traits::*}, + types::AppResult, + world::resources::Resource, }; use glam::Vec2; #[test] fn test_spaceship_fragment_collisions() -> AppResult<()> { - let base_ship = SpaceshipPrefab::Ragnarok.spaceship("name".into()); - let spaceship = SpaceshipEntity::from_spaceship(&base_ship, ResourceMap::default(), 100)?; - + let collector = CollectorEntity::new(); let fragment = FragmentEntity::new( Vec2::new( - spaceship.position().x as f32 + 60.0, - spaceship.position().y as f32, + collector.position().x as f32 + 60.0, + collector.position().y as f32, ), Vec2::ZERO, Resource::SCRAPS, 1, ); - let min_distance = spaceship + let min_distance = collector .hit_box() .keys() .map(|point| point.as_vec2().distance(fragment.position().as_vec2())) @@ -505,29 +553,28 @@ mod test { .unwrap(); println!( - "Ship position: {}\nFragment position: {}\nDistance: {}\nHitbox size: {}\n", - spaceship.center(), + "Ship position: {}\nFragment position: {}\nDistance: {}\n", + collector.center(), fragment.center(), min_distance, - fragment.hit_box().size, ); - let trait_spaceship: Box = Box::new(spaceship) as Box; + let trait_collector: Box = Box::new(collector) as Box; let trait_fragment: Box = Box::new(fragment) as Box; - assert!(are_colliding(&trait_spaceship, &trait_fragment) == false); + assert!(are_colliding(&trait_collector, &trait_fragment) == false); - let spaceship = SpaceshipEntity::from_spaceship(&base_ship, ResourceMap::default(), 100)?; + let collector = CollectorEntity::new(); let fragment = FragmentEntity::new( Vec2::new( - spaceship.position().x as f32 + 29.0, - spaceship.position().y as f32, + collector.position().x as f32 + 29.0, + collector.position().y as f32, ), Vec2::ZERO, Resource::SCRAPS, 1, ); - let min_distance = spaceship + let min_distance = collector .hit_box() .keys() .map(|point| point.as_vec2().distance(fragment.position().as_vec2())) @@ -535,14 +582,14 @@ mod test { .unwrap(); println!( - "Ship position: {}\nFragment position: {}\nDistance: {}\nHitbox size: {}\n", - spaceship.center(), + "Ship position: {}\nFragment position: {}\nDistance: {}\n", + collector.center(), fragment.center(), min_distance, - fragment.hit_box().size, ); + let trait_collector: Box = Box::new(collector) as Box; let trait_fragment: Box = Box::new(fragment) as Box; - assert!(are_colliding(&trait_spaceship, &trait_fragment) == true); + assert!(are_colliding(&trait_collector, &trait_fragment) == true); Ok(()) } diff --git a/src/space_adventure/utils.rs b/src/space_adventure/utils.rs index b990384..20b9413 100644 --- a/src/space_adventure/utils.rs +++ b/src/space_adventure/utils.rs @@ -20,7 +20,7 @@ impl Direction { pub const DOWN: Vec2 = Vec2::Y; } -pub fn body_data_from_image(image: &RgbaImage) -> (RgbaImage, HitBox) { +pub fn body_data_from_image(image: &RgbaImage, should_crop: bool) -> (RgbaImage, HitBox) { let gray_img = ConvertBuffer::::convert(image); // Find contours to get minimum rect enclosing image. let mut contours_vec = vec![]; @@ -56,15 +56,19 @@ pub fn body_data_from_image(image: &RgbaImage) -> (RgbaImage, HitBox) { .max_by(|pa, pb| pa.cmp(&pb)) .unwrap_or_default(); - // Crop image to minimum rect. - let cropped_image = crop_imm( - image, - min_x as u32, - min_y as u32, - (max_x - min_x) as u32 + 1, - (max_y - min_y) as u32 + 1, - ) - .to_image(); + let final_image = if should_crop { + // Crop image to minimum rect. + crop_imm( + image, + min_x as u32, + min_y as u32, + (max_x - min_x) as u32 + 1, + (max_y - min_y) as u32 + 1, + ) + .to_image() + } else { + image.clone() + }; // Translate contours. let contour = contours_vec @@ -74,9 +78,9 @@ pub fn body_data_from_image(image: &RgbaImage) -> (RgbaImage, HitBox) { let mut hit_box = HashMap::new(); - for x in 0..cropped_image.width() { - for y in 0..cropped_image.height() { - if let Some(pixel) = cropped_image.get_pixel_checked(x, y) { + for x in 0..final_image.width() { + for y in 0..final_image.height() { + if let Some(pixel) = final_image.get_pixel_checked(x, y) { // If pixel is non-transparent. if pixel[3] > 0 { let point = I16Vec2::new(x as i16, y as i16); @@ -87,5 +91,5 @@ pub fn body_data_from_image(image: &RgbaImage) -> (RgbaImage, HitBox) { } } - (cropped_image, hit_box.into()) + (final_image, hit_box.into()) } diff --git a/src/space_adventure/visual_effects.rs b/src/space_adventure/visual_effects.rs index 76c0b8e..9a06bd5 100644 --- a/src/space_adventure/visual_effects.rs +++ b/src/space_adventure/visual_effects.rs @@ -1,12 +1,12 @@ -use image::{Rgba, RgbaImage}; - use super::Entity; +use image::RgbaImage; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum VisualEffect { FadeIn, FadeOut, - ColorMask { color: Rgba }, + ColorMask { color: [u8; 3] }, } impl VisualEffect { @@ -31,7 +31,7 @@ impl VisualEffect { if is_border { let mut pixel = img.get_pixel(point.x as u32, point.y as u32).clone(); - for idx in 0..4 { + for idx in 0..color.len() { if color[idx] > 0 { pixel.0[idx] = ((1.0 - time / Self::COLOR_MASK_LIFETIME) * pixel.0[idx] as f32 @@ -50,13 +50,13 @@ impl VisualEffect { pub fn apply_global_effect(&self, img: &mut RgbaImage, time: f32) { match &self { VisualEffect::FadeIn => { + let modifier = (time / Self::FADE_IN_LIFETIME).min(1.0).max(0.0); for x in 0..img.width() { for y in 0..img.height() { let mut pixel = img.get_pixel(x, y).clone(); + for idx in 0..4 { - pixel.0[idx] = (time / Self::FADE_IN_LIFETIME * pixel.0[idx] as f32) - .min(255.0) - .max(0.0) as u8; + pixel.0[idx] = (modifier * pixel.0[idx] as f32) as u8; } img.put_pixel(x, y, pixel); } @@ -64,14 +64,13 @@ impl VisualEffect { } VisualEffect::FadeOut => { + let modifier = (1.0 - time / Self::FADE_IN_LIFETIME).min(1.0).max(0.0); + for x in 0..img.width() { for y in 0..img.height() { let mut pixel = img.get_pixel(x, y).clone(); for idx in 0..4 { - pixel.0[idx] = ((1.0 - time / Self::FADE_OUT_LIFETIME) - * pixel.0[idx] as f32) - .min(255.0) - .max(0.0) as u8; + pixel.0[idx] = (modifier * pixel.0[idx] as f32) as u8; } img.put_pixel(x, y, pixel); } diff --git a/src/ssh/server.rs b/src/ssh/server.rs index 6e5bd10..3fd92e6 100644 --- a/src/ssh/server.rs +++ b/src/ssh/server.rs @@ -58,8 +58,7 @@ impl AppServer { println!("Starting SSH server. Press Ctrl-C to exit."); let signing_key = load_keys().unwrap_or_else(|_| { - let key_pair = - russh_keys::key::KeyPair::generate_ed25519().expect("Failed to generate key pair"); + let key_pair = russh_keys::key::KeyPair::generate_ed25519(); let signing_key = match key_pair { russh_keys::key::KeyPair::Ed25519(signing_key) => signing_key, _ => panic!("SSH server: Invalid KeyPair"), diff --git a/src/store.rs b/src/store.rs index 711e38c..9893cf1 100644 --- a/src/store.rs +++ b/src/store.rs @@ -6,19 +6,73 @@ use crate::{ }; use anyhow::anyhow; use directories; +use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; use include_dir::{include_dir, Dir}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs::File, path::PathBuf}; +use std::{ + collections::HashMap, + io::{Read, Write}, + path::PathBuf, +}; pub static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/"); -pub static PERSISTED_WORLD_FILENAME: &str = "world.json"; -pub static PERSISTED_GAMES_PREFIX: &str = "game_"; -pub static PERSISTED_TEAM_RANKING_FILENAME: &str = "team_ranking.json"; +static PERSISTED_WORLD_FILENAME: &str = "world"; +static PERSISTED_GAMES_PREFIX: &str = "game_"; +static PERSISTED_TEAM_RANKING_FILENAME: &str = "team_ranking"; +const COMPRESSION_LEVEL: u32 = 3; -fn path_from_prefix(store_prefix: &str) -> String { +fn prefixed_world_filename(store_prefix: &str) -> String { format!("{}_{}", store_prefix, PERSISTED_WORLD_FILENAME) } +fn save_to_json(filename: &str, data: &T) -> AppResult<()> { + std::fs::write( + store_path(&format!("{}.json.compressed", filename))?, + &serialize(data)?, + )?; + Ok(()) +} + +fn load_from_json Deserialize<'a>>(filename: &str) -> AppResult { + let data: T = + if let Ok(bytes) = std::fs::read(store_path(&format!("{}.json.compressed", filename))?) { + log::info!("Read {} bytes", bytes.len()); + deserialize(&bytes)? + } else { + // This fallback serves to migrate old files to the new compressed format + let file = std::fs::File::open(store_path(&format!("{}.json", filename))?)?; + serde_json::from_reader(file)? + }; + + Ok(data) +} + +fn compress(bytes: &Vec, level: u32) -> AppResult> { + let mut e = ZlibEncoder::new(Vec::new(), Compression::new(level)); + e.write_all(bytes)?; + let compressed_bytes = e.finish()?; + Ok(compressed_bytes) +} + +fn decompress(bytes: &[u8]) -> AppResult> { + let mut d = ZlibDecoder::new(&bytes[..]); + let mut buf = Vec::new(); + d.read_to_end(&mut buf)?; + Ok(buf) +} + +pub fn serialize(value: &T) -> AppResult> { + let bytes = serde_json::to_vec(value)?; + let compressed = compress(&bytes, COMPRESSION_LEVEL)?; + Ok(compressed) +} + +pub fn deserialize Deserialize<'a>>(bytes: &Vec) -> AppResult { + let value = decompress(&bytes)?; + let data = serde_json::from_slice::(&value)?; + Ok(data) +} + pub fn store_path(filename: &str) -> AppResult { let dirs = directories::ProjectDirs::from("org", "frittura", "rebels") .ok_or(anyhow!("Failed to get directories"))?; @@ -31,38 +85,34 @@ pub fn store_path(filename: &str) -> AppResult { } pub fn save_world(world: &World, with_backup: bool, store_prefix: &str) -> AppResult<()> { - let stored_world = world.to_store()?; - let filename = path_from_prefix(store_prefix); - save_to_json(&filename, &stored_world)?; + let data = world.to_store()?; + let filename = prefixed_world_filename(store_prefix); + save_to_json(&filename, &data)?; if with_backup { let backup_filename = format!("{}.back", filename); - save_to_json(&backup_filename, &stored_world)?; + save_to_json(&backup_filename, &data)?; } Ok(()) } pub fn load_world(store_prefix: &str) -> AppResult { - let filename = path_from_prefix(store_prefix); - load_from_json(&filename) + load_from_json::(&prefixed_world_filename(store_prefix)) } pub fn save_game(game: &Game) -> AppResult<()> { - save_to_json( - format!("{}{}.json", PERSISTED_GAMES_PREFIX, game.id).as_str(), - &game, - )?; + save_to_json(&format!("{}{}", PERSISTED_GAMES_PREFIX, game.id), &game)?; Ok(()) } pub fn load_game(game_id: GameId) -> AppResult { - load_from_json(format!("{}{}.json", PERSISTED_GAMES_PREFIX, game_id).as_str()) + load_from_json::(&format!("{}{}", PERSISTED_GAMES_PREFIX, game_id)) } pub fn save_team_ranking( team_ranking: &HashMap, with_backup: bool, ) -> AppResult<()> { - save_to_json(&PERSISTED_TEAM_RANKING_FILENAME, &team_ranking)?; + save_to_json(PERSISTED_TEAM_RANKING_FILENAME, &team_ranking)?; if with_backup { let backup_filename = format!("{}.back", PERSISTED_TEAM_RANKING_FILENAME); save_to_json(&backup_filename, &team_ranking)?; @@ -71,7 +121,7 @@ pub fn save_team_ranking( } pub fn load_team_ranking() -> AppResult> { - load_from_json(&PERSISTED_TEAM_RANKING_FILENAME) + load_from_json::>(PERSISTED_TEAM_RANKING_FILENAME) } pub fn get_world_size(store_prefix: &str) -> AppResult { @@ -79,20 +129,6 @@ pub fn get_world_size(store_prefix: &str) -> AppResult { Ok(size) } -fn save_to_json(filename: &str, data: &T) -> AppResult<()> { - let file = File::create(store_path(filename)?)?; - assert!(file.metadata()?.is_file()); - let buffer = std::io::BufWriter::new(file); - serde_json::to_writer(buffer, data)?; - Ok(()) -} - -fn load_from_json Deserialize<'a>>(filename: &str) -> AppResult { - let file = File::open(store_path(filename)?)?; - let data: T = serde_json::from_reader(file)?; - Ok(data) -} - pub fn reset() -> AppResult<()> { let dirs = directories::ProjectDirs::from("org", "frittura", "rebels") .ok_or(anyhow!("Failed to get directories"))?; @@ -105,22 +141,36 @@ pub fn reset() -> AppResult<()> { } pub fn world_exists(store_prefix: &str) -> bool { - let filename = path_from_prefix(store_prefix); - let path = store_path(&filename); + let filename = prefixed_world_filename(store_prefix); + let path = store_path(&format!("{}.json", filename)); path.is_ok() && path.unwrap().exists() } pub fn world_file_data(store_prefix: &str) -> AppResult { - let filename = path_from_prefix(store_prefix); - let path = store_path(&filename)?; - let metadata = std::fs::metadata(path)?; - Ok(metadata) + let filename = prefixed_world_filename(store_prefix); + + if let Ok(compressed_metadata) = + std::fs::metadata(store_path(&format!("{}.json.compressed", filename))?) + { + Ok(compressed_metadata) + } else { + let metadata = std::fs::metadata(store_path(&format!("{}.json", filename))?)?; + Ok(metadata) + } } #[cfg(test)] mod tests { - use crate::world::world::World; + use super::{deserialize, serialize}; + use crate::{ + network::types::{NetworkData, NetworkTeam}, + types::{AppResult, PlanetId, PlayerId, TeamId}, + world::{planet::Planet, player::Player, team::Team, world::World}, + }; use directories; + use itertools::Itertools; + use rand::SeedableRng; + use rand_chacha::ChaCha8Rng; use std::fs::File; #[test] @@ -143,9 +193,44 @@ mod tests { } #[test] - fn test_save() { - let world = World::new(None); - let result = super::save_to_json("test", &world); - assert!(result.is_ok()); + fn test_store_world() -> AppResult<()> { + let store_prefix = "test"; + let mut world = World::new(None); + world.initialize(true)?; + world.own_team_id = world.teams.keys().collect_vec()[0].clone(); + super::save_world(&world, false, store_prefix)?; + let _ = super::load_world(store_prefix)?; + Ok(()) + } + + #[test] + fn test_serialize_network_data() -> AppResult<()> { + let value = NetworkData::Message(0, "Hello".to_string()); + let serialized_data = serialize(&value)?; + let deserialized_data = deserialize(&serialized_data)?; + assert!(value == deserialized_data); + + let mut team = Team::random( + TeamId::new_v4(), + PlanetId::new_v4(), + "name".to_string(), + "ship_name".to_string(), + ); + + let mut players = vec![]; + let rng = &mut ChaCha8Rng::from_entropy(); + for _ in 0..5 { + let player = Player::random(rng, PlayerId::new_v4(), None, &Planet::default(), 0.0); + team.player_ids.push(player.id); + players.push(player); + } + + let value = NetworkData::Team(0, NetworkTeam::new(team, players, vec![])); + let serialized_data = serialize(&value)?; + println!("Team size: {}", serialized_data.len()); + + let deserialized_data = deserialize(&serialized_data)?; + assert!(value == deserialized_data); + Ok(()) } } diff --git a/src/tui.rs b/src/tui.rs index a3ff479..5afef66 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -15,8 +15,6 @@ use futures::Future; use ratatui::layout::Rect; use ratatui::prelude::CrosstermBackend; use ratatui::Terminal; -use ratatui::TerminalOptions; -use ratatui::Viewport; use std::io::{self}; use std::panic; use std::pin::Pin; @@ -85,16 +83,7 @@ impl Tui { impl Tui { pub fn new_ssh(writer: SSHWriterProxy, events: SSHEventHandler) -> AppResult { let backend = CrosstermBackend::new(writer); - let opts = TerminalOptions { - viewport: Viewport::Fixed(Rect { - x: 0, - y: 0, - width: 160, - height: 48, - }), - }; - - let terminal = Terminal::with_options(backend, opts)?; + let terminal = Terminal::new(backend)?; let mut tui = Self { tui_type: TuiType::SSH, terminal, diff --git a/src/types.rs b/src/types.rs index 9891a26..0c259e0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,7 +17,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; // A Tick represents a unit of time in the game world. // It corresponds to a millisecond in the real world. -pub type Tick = u128; +pub type Tick = u64; pub type PlayerId = uuid::Uuid; pub type TeamId = uuid::Uuid; @@ -180,13 +180,13 @@ impl SystemTimeTick for Tick { SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Invalid system time") - .as_millis() + .as_millis() as Tick } fn from_system_time(time: SystemTime) -> Tick { time.duration_since(UNIX_EPOCH) .expect("Invalid system time") - .as_millis() + .as_millis() as Tick } fn as_secs(&self) -> Tick { @@ -229,10 +229,10 @@ impl SystemTimeTick for Tick { fn formatted(&self) -> String { let seconds = self.as_secs() % 60; - let minutes = (self.as_minutes() as f32) as u128 % 60; - let hours = (self.as_hours() as f32) as u128 % 24; - let days = (self.as_secs() as f32 / 60.0 / 60.0 / 24.0) as u128 % 365; - let years = (self.as_secs() as f32 / 60.0 / 60.0 / 24.0 / 365.2425) as u128; + let minutes = (self.as_minutes() as f32) as Tick % 60; + let hours = (self.as_hours() as f32) as Tick % 24; + let days = (self.as_secs() as f32 / 60.0 / 60.0 / 24.0) as Tick % 365; + let years = (self.as_secs() as f32 / 60.0 / 60.0 / 24.0 / 365.2425) as Tick; if years > 0 { format!( diff --git a/src/ui/button.rs b/src/ui/button.rs index 4804ddf..1b5690f 100644 --- a/src/ui/button.rs +++ b/src/ui/button.rs @@ -1,5 +1,6 @@ use super::{ constants::UiStyle, + traits::HoverableWidget, ui_callback::{CallbackRegistry, UiCallback}, widgets::default_block, }; @@ -7,27 +8,27 @@ use crossterm::event::KeyCode; use ratatui::{ layout::{Margin, Rect}, style::{Style, Styled, Stylize}, + symbols::border, text::{Line, Span, Text}, - widgets::{Clear, Paragraph, Widget}, + widgets::{Block, Paragraph, Widget}, }; -use std::{sync::Arc, sync::Mutex}; -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct Button<'a> { text: Text<'a>, hotkey: Option, on_click: UiCallback, - callback_registry: Arc>, disabled: bool, - disabled_text: Option, + selected: bool, + is_hovered: bool, + disabled_text: Option>, text_alignemnt: ratatui::layout::Alignment, style: Style, hover_style: Style, - box_style: Option