From d2daf695a6d4ca33f5ae08d149508aed2a388912 Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 1 Dec 2022 17:16:53 +0000 Subject: [PATCH] add combo rom --- examples/combo/.cargo/config.toml | 14 + examples/combo/Cargo.lock | 554 ++++ examples/combo/Cargo.toml | 22 + examples/combo/gba.ld | 113 + examples/combo/gba_mb.ld | 111 + examples/combo/gfx/games.aseprite | Bin 0 -> 10151 bytes examples/combo/gfx/games.toml | 16 + examples/combo/gfx/hat.png | Bin 0 -> 4508 bytes examples/combo/gfx/hyperspace.png | Bin 0 -> 3758 bytes examples/combo/gfx/purple.png | Bin 0 -> 3334 bytes examples/combo/rust-toolchain.toml | 3 + examples/combo/src/lib.rs | 131 + examples/combo/src/main.rs | 10 + examples/hyperspace-roll/src/lib.rs | 226 ++ examples/hyperspace-roll/src/main.rs | 224 +- .../the-hat-chooses-the-wizard/src/lib.rs | 994 +++++++ .../the-hat-chooses-the-wizard/src/main.rs | 993 +------ examples/the-purple-night/src/lib.rs | 2344 +++++++++++++++++ examples/the-purple-night/src/main.rs | 2339 +--------------- 19 files changed, 4543 insertions(+), 3551 deletions(-) create mode 100644 examples/combo/.cargo/config.toml create mode 100644 examples/combo/Cargo.lock create mode 100644 examples/combo/Cargo.toml create mode 100644 examples/combo/gba.ld create mode 100644 examples/combo/gba_mb.ld create mode 100644 examples/combo/gfx/games.aseprite create mode 100644 examples/combo/gfx/games.toml create mode 100644 examples/combo/gfx/hat.png create mode 100644 examples/combo/gfx/hyperspace.png create mode 100644 examples/combo/gfx/purple.png create mode 100644 examples/combo/rust-toolchain.toml create mode 100644 examples/combo/src/lib.rs create mode 100644 examples/combo/src/main.rs create mode 100644 examples/hyperspace-roll/src/lib.rs create mode 100644 examples/the-hat-chooses-the-wizard/src/lib.rs create mode 100644 examples/the-purple-night/src/lib.rs diff --git a/examples/combo/.cargo/config.toml b/examples/combo/.cargo/config.toml new file mode 100644 index 00000000..b3276236 --- /dev/null +++ b/examples/combo/.cargo/config.toml @@ -0,0 +1,14 @@ +[unstable] +build-std = ["core", "alloc"] +build-std-features = ["compiler-builtins-mem"] + +[build] +target = "thumbv4t-none-eabi" + +[target.thumbv4t-none-eabi] +rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-qt" + +[target.armv4t-none-eabi] +rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-qt" diff --git a/examples/combo/Cargo.lock b/examples/combo/Cargo.lock new file mode 100644 index 00000000..c544a557 --- /dev/null +++ b/examples/combo/Cargo.lock @@ -0,0 +1,554 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "agb" +version = "0.12.2" +dependencies = [ + "agb_fixnum", + "agb_image_converter", + "agb_macros", + "agb_sound_converter", + "bare-metal", + "bitflags", + "modular-bitfield", + "rustc-hash", +] + +[[package]] +name = "agb_fixnum" +version = "0.12.2" +dependencies = [ + "agb_macros", +] + +[[package]] +name = "agb_image_converter" +version = "0.12.2" +dependencies = [ + "asefile", + "fontdue", + "image", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + +[[package]] +name = "agb_macros" +version = "0.12.2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "agb_sound_converter" +version = "0.12.2" +dependencies = [ + "hound", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "asefile" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a71de7aecd2d0a76ec90fde2c443d12667c737d92de76bd187f101eca37891" +dependencies = [ + "bitflags", + "byteorder", + "flate2", + "image", + "log", + "nohash", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bare-metal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603" + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combo" +version = "0.1.0" +dependencies = [ + "agb", + "hyperspace-roll", + "the-hat-chooses-the-wizard", + "the-purple-night", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide 0.6.2", +] + +[[package]] +name = "fontdue" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a62391ecb864cf12ed06b2af4eda2e609b97657950d6a8f06841b17726ab253" +dependencies = [ + "hashbrown", + "ttf-parser", +] + +[[package]] +name = "generational-arena" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + +[[package]] +name = "hyperspace-roll" +version = "0.1.0" +dependencies = [ + "agb", + "bare-metal", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libflate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd" +dependencies = [ + "adler32", + "crc32fast", + "rle-decode-fast", + "take_mut", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nohash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "the-hat-chooses-the-wizard" +version = "0.1.0" +dependencies = [ + "agb", + "serde", + "serde_json", +] + +[[package]] +name = "the-purple-night" +version = "0.1.0" +dependencies = [ + "agb", + "generational-arena", + "quote", + "tiled", +] + +[[package]] +name = "tiled" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2c30aeea9d8159cb461a17dba23ad28980a2a9c217a6784a14e931e4979d6f" +dependencies = [ + "base64", + "libflate", + "xml-rs", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "ttf-parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/examples/combo/Cargo.toml b/examples/combo/Cargo.toml new file mode 100644 index 00000000..2d556216 --- /dev/null +++ b/examples/combo/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "combo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +agb = { version = "0.12.2", path = "../../agb" } +the-purple-night = { path = "../the-purple-night" } +the-hat-chooses-the-wizard = { path = "../the-hat-chooses-the-wizard" } +hyperspace-roll = { path = "../hyperspace-roll" } + + +[profile.dev] +opt-level = 2 +debug = true + +[profile.release] +panic = "abort" +lto = true +debug = true diff --git a/examples/combo/gba.ld b/examples/combo/gba.ld new file mode 100644 index 00000000..639d0d7f --- /dev/null +++ b/examples/combo/gba.ld @@ -0,0 +1,113 @@ +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") +OUTPUT_ARCH(arm) + +ENTRY(__start) +EXTERN(__AGBRS_ASYNC_INTERRUPT_HANDLER) + +EXTERN(__agbabi_memset) +EXTERN(__agbabi_memcpy) + +MEMORY { + ewram (w!x) : ORIGIN = 0x02000000, LENGTH = 256K + iwram (w!x) : ORIGIN = 0x03000000, LENGTH = 32K + rom (rx) : ORIGIN = 0x08000000, LENGTH = 32M +} + +__text_start = ORIGIN(rom); + +INPUT (agb.a) + +SECTIONS { + . = __text_start; + + + .text : { + KEEP(*(.crt0)); + *(.crt0 .crt0*); + *(.text .text*); + . = ALIGN(4); + } > rom + __text_end = .; + + .rodata : { + *(.rodata .rodata.*); + . = ALIGN(4); + } > rom + + __iwram_rom_start = .; + .iwram : { + __iwram_data_start = ABSOLUTE(.); + + *(.iwram .iwram.*); + . = ALIGN(4); + + *(.text_iwram .text_iwram.*); + . = ALIGN(4); + + __iwram_data_end = ABSOLUTE(.); + } > iwram AT>rom + + . = __iwram_rom_start + (__iwram_data_end - __iwram_data_start); + + __ewram_rom_start = .; + .ewram : { + __ewram_data_start = ABSOLUTE(.); + + *(.ewram .ewram.*); + . = ALIGN(4); + + *(.data .data.*); + . = ALIGN(4); + + __ewram_data_end = ABSOLUTE(.); + } > ewram AT>rom + + .bss : { + *(.bss .bss.*); + . = ALIGN(4); + __iwram_end = ABSOLUTE(.); + } > iwram + + __iwram_rom_length_bytes = __iwram_data_end - __iwram_data_start; + __iwram_rom_length_halfwords = (__iwram_rom_length_bytes + 1) / 2; + + __ewram_rom_length_bytes = __ewram_data_end - __ewram_data_start; + __ewram_rom_length_halfwords = (__ewram_rom_length_bytes + 1) / 2; + + /* debugging sections */ + /* Stabs */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + + .debug_ranges 0 : { *(.debug_ranges) } + + /* discard anything not already mentioned */ + /DISCARD/ : { *(*) } +} \ No newline at end of file diff --git a/examples/combo/gba_mb.ld b/examples/combo/gba_mb.ld new file mode 100644 index 00000000..4a27b9ab --- /dev/null +++ b/examples/combo/gba_mb.ld @@ -0,0 +1,111 @@ +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") +OUTPUT_ARCH(arm) + +ENTRY(__start) +EXTERN(__AGBRS_ASYNC_INTERRUPT_HANDLER) + +EXTERN(__agbabi_memset) +EXTERN(__agbabi_memcpy) + +MEMORY { + ewram (w!x) : ORIGIN = 0x02000000, LENGTH = 256K + iwram (w!x) : ORIGIN = 0x03000000, LENGTH = 32K +} + +__text_start = ORIGIN(ewram); + +INPUT (agb.a) + +SECTIONS { + . = __text_start; + + .text : { + KEEP(*(.crt0)); + *(.crt0 .crt0*); + *(.text .text*); + . = ALIGN(4); + } > rom + __text_end = .; + + .rodata : { + *(.rodata .rodata.*); + . = ALIGN(4); + } > ewram + + __iwram_rom_start = .; + .iwram : { + __iwram_data_start = ABSOLUTE(.); + + *(.iwram .iwram.*); + . = ALIGN(4); + + *(.text_iwram .text_iwram.*); + . = ALIGN(4); + + __iwram_data_end = ABSOLUTE(.); + } > iwram AT>ewram + + . = __iwram_rom_start + (__iwram_data_end - __iwram_data_start); + + __ewram_rom_start = .; + .ewram : { + __ewram_data_start = ABSOLUTE(.); + + *(.ewram .ewram.*); + . = ALIGN(4); + + *(.data .data.*); + . = ALIGN(4); + + __ewram_data_end = ABSOLUTE(.); + } > ewram AT>ewram + + .bss : { + *(.bss .bss.*); + . = ALIGN(4); + __iwram_end = ABSOLUTE(.); + } > iwram + + __iwram_rom_length_bytes = __iwram_data_end - __iwram_data_start; + __iwram_rom_length_halfwords = (__iwram_rom_length_bytes + 1) / 2; + + __ewram_rom_length_bytes = __ewram_data_end - __ewram_data_start; + __ewram_rom_length_halfwords = (__ewram_rom_length_bytes + 1) / 2; + + /* debugging sections */ + /* Stabs */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + + .debug_ranges 0 : { *(.debug_ranges) } + + /* discard anything not already mentioned */ + /DISCARD/ : { *(*) } +} \ No newline at end of file diff --git a/examples/combo/gfx/games.aseprite b/examples/combo/gfx/games.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..5f2832bd213759638c4c47a366ae0bea2ec0b0e3 GIT binary patch literal 10151 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 QboZY7eVu@l>h($ literal 0 HcmV?d00001 diff --git a/examples/combo/gfx/hyperspace.png b/examples/combo/gfx/hyperspace.png new file mode 100644 index 0000000000000000000000000000000000000000..59aae5444b551e4170acb4bad14013bb6042fe6e GIT binary patch literal 3758 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 YA1=@p4a16U1poj507*qoM6N<$f*DLN9RL6T literal 0 HcmV?d00001 diff --git a/examples/combo/gfx/purple.png b/examples/combo/gfx/purple.png new file mode 100644 index 0000000000000000000000000000000000000000..f578a61b47da6390aeeac7ada1527b6acfe8dba1 GIT binary patch literal 3334 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 J>+LXU{{a?MaQ6TJ literal 0 HcmV?d00001 diff --git a/examples/combo/rust-toolchain.toml b/examples/combo/rust-toolchain.toml new file mode 100644 index 00000000..06842486 --- /dev/null +++ b/examples/combo/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/examples/combo/src/lib.rs b/examples/combo/src/lib.rs new file mode 100644 index 00000000..d44f8d3e --- /dev/null +++ b/examples/combo/src/lib.rs @@ -0,0 +1,131 @@ +#![no_std] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] +#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] + +extern crate alloc; +use alloc::boxed::Box; + +use agb::{ + display::{ + tiled::{InfiniteScrolledMap, RegularBackgroundSize, TileFormat, TileSet, TileSetting}, + Priority, + }, + fixnum::{Num, Vector2D}, + include_gfx, + input::Button, +}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Game { + TheHatChoosesTheWizard, + ThePurpleNight, + HyperspaceRoll, +} + +impl Game { + fn launch_game(self, gba: agb::Gba) -> ! { + match self { + Game::TheHatChoosesTheWizard => the_hat_chooses_the_wizard::main(gba), + Game::ThePurpleNight => the_purple_night::main(gba), + Game::HyperspaceRoll => hyperspace_roll::main(gba), + } + } + + fn from_index(index: i32) -> Game { + match index.rem_euclid(3) { + 0 => Game::TheHatChoosesTheWizard, + 1 => Game::ThePurpleNight, + 2 => Game::HyperspaceRoll, + _ => unreachable!("game out of index in an unreachable manner"), + } + } +} + +include_gfx!("gfx/games.toml"); + +fn get_game(gba: &mut agb::Gba) -> Game { + let mut input = agb::input::ButtonController::new(); + let vblank = agb::interrupt::VBlank::get(); + + let (tile, mut vram) = gba.display.video.tiled0(); + + let hat = TileSet::new(games::hat.tiles, TileFormat::FourBpp); + let purple = TileSet::new(games::purple.tiles, TileFormat::FourBpp); + let hyperspace = TileSet::new(games::hyperspace.tiles, TileFormat::FourBpp); + + let tiles = [hat, purple, hyperspace]; + + let palette_assignments = &[ + games::hat.palette_assignments, + games::purple.palette_assignments, + games::hyperspace.palette_assignments, + ]; + + vram.set_background_palettes(games::PALETTES); + + let mut bg = InfiniteScrolledMap::new( + tile.background(Priority::P0, RegularBackgroundSize::Background32x32), + Box::new(|pos| { + let y = pos.y.rem_euclid(20); + let x = pos.x.rem_euclid(30); + + let game = (pos.x).rem_euclid(90) as usize / 30; + let tile_id = (y * 30 + x) as usize; + ( + &tiles[game], + TileSetting::new( + tile_id as u16, + false, + false, + palette_assignments[game][tile_id], + ), + ) + }), + ); + + bg.init(&mut vram, (0, 0).into(), &mut || {}); + + bg.set_pos(&mut vram, (0, 0).into()); + bg.commit(&mut vram); + bg.show(); + + let mut position: Vector2D> = (0, 0).into(); + let mut game_idx = 0; + let game = loop { + let lr: agb::input::Tri = ( + input.is_just_pressed(Button::LEFT), + input.is_just_pressed(Button::RIGHT), + ) + .into(); + + game_idx += lr as i32; + + if (position.x - game_idx * 30 * 8).abs() < Num::new(1) / 2 { + position.x = Num::new(game_idx * 30 * 8); + } + + position.x += + ((Num::new(game_idx * 30 * 8) - position.x) / 8).clamp(-Num::new(8), Num::new(8)); + + bg.set_pos(&mut vram, position.floor()); + + vblank.wait_for_vblank(); + bg.commit(&mut vram); + input.update(); + + if input.is_just_pressed(Button::A) { + break Game::from_index(game_idx); + } + }; + + bg.hide(); + bg.clear(&mut vram); + bg.commit(&mut vram); + + game +} + +pub fn main(mut gba: agb::Gba) -> ! { + get_game(&mut gba).launch_game(gba) +} diff --git a/examples/combo/src/main.rs b/examples/combo/src/main.rs new file mode 100644 index 00000000..21c89b41 --- /dev/null +++ b/examples/combo/src/main.rs @@ -0,0 +1,10 @@ +#![no_std] +#![no_main] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] +#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] + +#[agb::entry] +fn entry(gba: agb::Gba) -> ! { + combo::main(gba); +} diff --git a/examples/hyperspace-roll/src/lib.rs b/examples/hyperspace-roll/src/lib.rs new file mode 100644 index 00000000..269f3231 --- /dev/null +++ b/examples/hyperspace-roll/src/lib.rs @@ -0,0 +1,226 @@ +// Games made using `agb` are no_std which means you don't have access to the standard +// rust library. This is because the game boy advance doesn't really have an operating +// system, so most of the content of the standard library doesn't apply. +// +// Provided you haven't disabled it, agb does provide an allocator, so it is possible +// to use both the `core` and the `alloc` built in crates. +#![no_std] +// `agb` defines its own `main` function, so you must declare your game's main function +// using the #[agb::entry] proc macro. Failing to do so will cause failure in linking +// which won't be a particularly clear error message. +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] +#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] + +use agb::display::object::ObjectController; +use agb::display::tiled::{TiledMap, VRamManager}; +use agb::display::Priority; +use agb::interrupt::VBlank; +use agb::{display, sound::mixer::Frequency}; + +extern crate alloc; +use alloc::vec; +use alloc::vec::Vec; + +mod background; +mod battle; +mod customise; +mod graphics; +mod level_generation; +mod save; +mod sfx; + +use background::{show_title_screen, StarBackground}; +use battle::BattleResult; +use graphics::NumberDisplay; +use sfx::Sfx; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum Face { + Shoot, + Shield, + Malfunction, + Heal, + Bypass, + DoubleShot, + TripleShot, + Blank, + Disrupt, + MalfunctionShot, + DoubleShield, + TripleShield, + DoubleShieldValue, + DoubleShotValue, + TripleShotValue, + BurstShield, + Invert, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum Ship { + Player, + Drone, + PilotedShip, + Shield, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum EnemyAttackType { + Attack, + Shield, + Heal, +} + +#[derive(Debug, Clone)] +pub struct Die { + faces: [Face; 6], +} + +impl Die { + /// roll this die (potentially using the custom probabilities, should we implement that) and return which face index is showing + fn roll(&self) -> Face { + let n = agb::rng::gen().rem_euclid(6); + self.faces[n as usize] + } +} + +#[derive(Debug, Clone)] +pub struct PlayerDice { + dice: Vec, +} + +struct Agb<'a> { + obj: ObjectController, + vblank: VBlank, + star_background: StarBackground<'a>, + vram: VRamManager, + sfx: Sfx<'a>, +} + +pub fn main(mut gba: agb::Gba) -> ! { + save::init_save(&mut gba).expect("Could not initialize save game"); + + if save::load_high_score() > 1000 { + save::save_high_score(&mut gba, 0).expect("Could not reset high score"); + } + + let gfx = gba.display.object.get(); + let vblank = agb::interrupt::VBlank::get(); + + let (tiled, mut vram) = gba.display.video.tiled0(); + let mut background0 = tiled.background( + Priority::P0, + display::tiled::RegularBackgroundSize::Background64x32, + ); + let mut background1 = tiled.background( + Priority::P0, + display::tiled::RegularBackgroundSize::Background64x32, + ); + let mut card_descriptions = tiled.background( + Priority::P1, + display::tiled::RegularBackgroundSize::Background32x32, + ); + + let mut help_background = tiled.background( + Priority::P1, + display::tiled::RegularBackgroundSize::Background32x32, + ); + + let basic_die = Die { + faces: [ + Face::Shoot, + Face::Shield, + Face::Blank, + Face::Malfunction, + Face::Blank, + Face::Blank, + ], + }; + + let mut star_background = StarBackground::new(&mut background0, &mut background1, &mut vram); + star_background.commit(&mut vram); + + let mut mixer = gba.mixer.mixer(Frequency::Hz32768); + mixer.enable(); + let _interrupt_handler = mixer.setup_interrupt_handler(); + + let sfx = Sfx::new(&mut mixer); + + let mut agb = Agb { + obj: gfx, + vblank, + star_background, + vram, + sfx, + }; + + loop { + let mut dice = PlayerDice { + dice: vec![basic_die.clone(); 2], + }; + + let mut current_level = 1; + + agb.sfx.title_screen(); + + { + show_title_screen(&mut help_background, &mut agb.vram, &mut agb.sfx); + let mut score_display = NumberDisplay::new((216, 9).into()); + score_display.set_value(Some(save::load_high_score()), &agb.obj); + agb.obj.commit(); + agb.star_background.hide(); + + let mut input = agb::input::ButtonController::new(); + loop { + let _ = agb::rng::gen(); + input.update(); + if input.is_just_pressed(agb::input::Button::all()) { + break; + } + agb.vblank.wait_for_vblank(); + agb.sfx.frame(); + } + } + + agb.obj.commit(); + + help_background.hide(); + help_background.clear(&mut agb.vram); + help_background.commit(&mut agb.vram); + agb.sfx.frame(); + + background::load_palettes(&mut agb.vram); + agb.star_background.show(); + + loop { + dice = customise::customise_screen( + &mut agb, + dice.clone(), + &mut card_descriptions, + &mut help_background, + current_level, + ); + + let result = + battle::battle_screen(&mut agb, dice.clone(), current_level, &mut help_background); + match result { + BattleResult::Win => {} + BattleResult::Loss => { + agb.obj.commit(); + agb.sfx.customise(); + if save::load_high_score() < current_level { + save::save_high_score(&mut gba, current_level) + .expect("Could not save high score"); + } + break; + } + } + + current_level += 1; + + if current_level % 5 == 0 && dice.dice.len() < 5 { + dice.dice.push(basic_die.clone()); + } + } + } +} diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs index 3d1d46c7..51ca0413 100644 --- a/examples/hyperspace-roll/src/main.rs +++ b/examples/hyperspace-roll/src/main.rs @@ -1,232 +1,10 @@ -// Games made using `agb` are no_std which means you don't have access to the standard -// rust library. This is because the game boy advance doesn't really have an operating -// system, so most of the content of the standard library doesn't apply. -// -// Provided you haven't disabled it, agb does provide an allocator, so it is possible -// to use both the `core` and the `alloc` built in crates. #![no_std] -// `agb` defines its own `main` function, so you must declare your game's main function -// using the #[agb::entry] proc macro. Failing to do so will cause failure in linking -// which won't be a particularly clear error message. #![no_main] #![cfg_attr(test, feature(custom_test_frameworks))] #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] -use agb::display::object::ObjectController; -use agb::display::tiled::{TiledMap, VRamManager}; -use agb::display::Priority; -use agb::interrupt::VBlank; -use agb::{display, sound::mixer::Frequency}; - -extern crate alloc; -use alloc::vec; -use alloc::vec::Vec; - -mod background; -mod battle; -mod customise; -mod graphics; -mod level_generation; -mod save; -mod sfx; - -use background::{show_title_screen, StarBackground}; -use battle::BattleResult; -use graphics::NumberDisplay; -use sfx::Sfx; - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub enum Face { - Shoot, - Shield, - Malfunction, - Heal, - Bypass, - DoubleShot, - TripleShot, - Blank, - Disrupt, - MalfunctionShot, - DoubleShield, - TripleShield, - DoubleShieldValue, - DoubleShotValue, - TripleShotValue, - BurstShield, - Invert, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub enum Ship { - Player, - Drone, - PilotedShip, - Shield, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub enum EnemyAttackType { - Attack, - Shield, - Heal, -} - -#[derive(Debug, Clone)] -pub struct Die { - faces: [Face; 6], -} - -impl Die { - /// roll this die (potentially using the custom probabilities, should we implement that) and return which face index is showing - fn roll(&self) -> Face { - let n = agb::rng::gen().rem_euclid(6); - self.faces[n as usize] - } -} - -#[derive(Debug, Clone)] -pub struct PlayerDice { - dice: Vec, -} - -struct Agb<'a> { - obj: ObjectController, - vblank: VBlank, - star_background: StarBackground<'a>, - vram: VRamManager, - sfx: Sfx<'a>, -} - -fn main(mut gba: agb::Gba) -> ! { - save::init_save(&mut gba).expect("Could not initialize save game"); - - if save::load_high_score() > 1000 { - save::save_high_score(&mut gba, 0).expect("Could not reset high score"); - } - - let gfx = gba.display.object.get(); - let vblank = agb::interrupt::VBlank::get(); - - let (tiled, mut vram) = gba.display.video.tiled0(); - let mut background0 = tiled.background( - Priority::P0, - display::tiled::RegularBackgroundSize::Background64x32, - ); - let mut background1 = tiled.background( - Priority::P0, - display::tiled::RegularBackgroundSize::Background64x32, - ); - let mut card_descriptions = tiled.background( - Priority::P1, - display::tiled::RegularBackgroundSize::Background32x32, - ); - - let mut help_background = tiled.background( - Priority::P1, - display::tiled::RegularBackgroundSize::Background32x32, - ); - - let basic_die = Die { - faces: [ - Face::Shoot, - Face::Shield, - Face::Blank, - Face::Malfunction, - Face::Blank, - Face::Blank, - ], - }; - - let mut star_background = StarBackground::new(&mut background0, &mut background1, &mut vram); - star_background.commit(&mut vram); - - let mut mixer = gba.mixer.mixer(Frequency::Hz32768); - mixer.enable(); - let _interrupt_handler = mixer.setup_interrupt_handler(); - - let sfx = Sfx::new(&mut mixer); - - let mut agb = Agb { - obj: gfx, - vblank, - star_background, - vram, - sfx, - }; - - loop { - let mut dice = PlayerDice { - dice: vec![basic_die.clone(); 2], - }; - - let mut current_level = 1; - - agb.sfx.title_screen(); - - { - show_title_screen(&mut help_background, &mut agb.vram, &mut agb.sfx); - let mut score_display = NumberDisplay::new((216, 9).into()); - score_display.set_value(Some(save::load_high_score()), &agb.obj); - agb.obj.commit(); - agb.star_background.hide(); - - let mut input = agb::input::ButtonController::new(); - loop { - let _ = agb::rng::gen(); - input.update(); - if input.is_just_pressed(agb::input::Button::all()) { - break; - } - agb.vblank.wait_for_vblank(); - agb.sfx.frame(); - } - } - - agb.obj.commit(); - - help_background.hide(); - help_background.clear(&mut agb.vram); - help_background.commit(&mut agb.vram); - agb.sfx.frame(); - - background::load_palettes(&mut agb.vram); - agb.star_background.show(); - - loop { - dice = customise::customise_screen( - &mut agb, - dice.clone(), - &mut card_descriptions, - &mut help_background, - current_level, - ); - - let result = - battle::battle_screen(&mut agb, dice.clone(), current_level, &mut help_background); - match result { - BattleResult::Win => {} - BattleResult::Loss => { - agb.obj.commit(); - agb.sfx.customise(); - if save::load_high_score() < current_level { - save::save_high_score(&mut gba, current_level) - .expect("Could not save high score"); - } - break; - } - } - - current_level += 1; - - if current_level % 5 == 0 && dice.dice.len() < 5 { - dice.dice.push(basic_die.clone()); - } - } - } -} - #[agb::entry] fn entry(mut gba: agb::Gba) -> ! { - main(gba) + hyperspace_roll::main(gba) } diff --git a/examples/the-hat-chooses-the-wizard/src/lib.rs b/examples/the-hat-chooses-the-wizard/src/lib.rs new file mode 100644 index 00000000..26a8c9ca --- /dev/null +++ b/examples/the-hat-chooses-the-wizard/src/lib.rs @@ -0,0 +1,994 @@ +#![no_std] +#![no_main] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] +#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] + +extern crate alloc; + +use agb::{ + display::{ + object::{Graphics, Object, ObjectController, Tag, TagMap}, + tiled::{ + InfiniteScrolledMap, PartialUpdateStatus, RegularBackgroundSize, TileFormat, TileSet, + TileSetting, TiledMap, VRamManager, + }, + Priority, HEIGHT, WIDTH, + }, + fixnum::{FixedNum, Vector2D}, + input::{self, Button, ButtonController}, + sound::mixer::Frequency, +}; +use alloc::boxed::Box; + +mod enemies; +mod level_display; +mod sfx; +mod splash_screen; + +pub struct Level { + background: &'static [u16], + foreground: &'static [u16], + dimensions: Vector2D, + collision: &'static [u32], + + slimes: &'static [(i32, i32)], + snails: &'static [(i32, i32)], + enemy_stops: &'static [(i32, i32)], + start_pos: (i32, i32), +} + +mod map_tiles { + + use super::Level; + pub const LEVELS: &[Level] = &[ + l1_1::get_level(), + l1_2::get_level(), + l1_3::get_level(), + l1_4::get_level(), + l1_5::get_level(), + l1_7::get_level(), // these are intentionally this way round + l1_6::get_level(), + l1_8::get_level(), + l2_3::get_level(), // goes 2-3, 2-1 then 2-2 + l2_1::get_level(), + l2_2::get_level(), + l2_4::get_level(), + ]; + + pub mod l1_1 { + include!(concat!(env!("OUT_DIR"), "/1-1.json.rs")); + } + pub mod l1_2 { + include!(concat!(env!("OUT_DIR"), "/1-2.json.rs")); + } + pub mod l1_3 { + include!(concat!(env!("OUT_DIR"), "/1-3.json.rs")); + } + pub mod l1_4 { + include!(concat!(env!("OUT_DIR"), "/1-4.json.rs")); + } + pub mod l1_5 { + include!(concat!(env!("OUT_DIR"), "/1-5.json.rs")); + } + pub mod l1_6 { + include!(concat!(env!("OUT_DIR"), "/1-6.json.rs")); + } + pub mod l1_7 { + include!(concat!(env!("OUT_DIR"), "/1-7.json.rs")); + } + pub mod l2_1 { + include!(concat!(env!("OUT_DIR"), "/2-1.json.rs")); + } + + pub mod l1_8 { + include!(concat!(env!("OUT_DIR"), "/1-8.json.rs")); + } + pub mod l2_2 { + include!(concat!(env!("OUT_DIR"), "/2-2.json.rs")); + } + pub mod l2_3 { + include!(concat!(env!("OUT_DIR"), "/2-3.json.rs")); + } + + pub mod l2_4 { + include!(concat!(env!("OUT_DIR"), "/2-4.json.rs")); + } + + pub mod tilemap { + include!(concat!(env!("OUT_DIR"), "/tilemap.rs")); + } +} + +agb::include_gfx!("gfx/tile_sheet.toml"); + +const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite"); +const TAG_MAP: &TagMap = GRAPHICS.tags(); + +const WALKING: &Tag = TAG_MAP.get("Walking"); +const JUMPING: &Tag = TAG_MAP.get("Jumping"); +const FALLING: &Tag = TAG_MAP.get("Falling"); +const PLAYER_DEATH: &Tag = TAG_MAP.get("Player Death"); +const HAT_SPIN_1: &Tag = TAG_MAP.get("HatSpin"); +const HAT_SPIN_2: &Tag = TAG_MAP.get("HatSpin2"); +const HAT_SPIN_3: &Tag = TAG_MAP.get("HatSpin3"); + +type FixedNumberType = FixedNum<10>; + +pub struct Entity<'a> { + sprite: Object<'a>, + position: Vector2D, + velocity: Vector2D, + collision_mask: Vector2D, +} + +impl<'a> Entity<'a> { + pub fn new(object: &'a ObjectController, collision_mask: Vector2D) -> Self { + let dummy_sprite = object.sprite(WALKING.sprite(0)); + let mut sprite = object.object(dummy_sprite); + sprite.set_priority(Priority::P1); + Entity { + sprite, + collision_mask, + position: (0, 0).into(), + velocity: (0, 0).into(), + } + } + + fn something_at_point bool>( + &self, + position: Vector2D, + something_fn: T, + ) -> bool { + let left = (position.x - self.collision_mask.x as i32 / 2).floor() / 8; + let right = (position.x + self.collision_mask.x as i32 / 2 - 1).floor() / 8; + let top = (position.y - self.collision_mask.y as i32 / 2).floor() / 8; + let bottom = (position.y + self.collision_mask.y as i32 / 2 - 1).floor() / 8; + + for x in left..=right { + for y in top..=bottom { + if something_fn(x, y) { + return true; + } + } + } + false + } + + fn collision_at_point(&self, level: &Level, position: Vector2D) -> bool { + self.something_at_point(position, |x, y| level.collides(x, y)) + } + + fn killision_at_point(&self, level: &Level, position: Vector2D) -> bool { + self.something_at_point(position, |x, y| level.kills(x, y)) + } + + fn completion_at_point(&self, level: &Level, position: Vector2D) -> bool { + self.something_at_point(position, |x, y| level.wins(x, y)) + } + + fn enemy_collision_at_point( + &self, + enemies: &[enemies::Enemy], + position: Vector2D, + ) -> bool { + for enemy in enemies { + if enemy.collides_with_hat(position) { + return true; + } + } + false + } + + // returns the distance actually moved + fn update_position(&mut self, level: &Level) -> Vector2D { + let old_position = self.position; + let x_velocity = (self.velocity.x, 0.into()).into(); + if !self.collision_at_point(level, self.position + x_velocity) { + self.position += x_velocity; + } else { + self.position += self.binary_search_collision(level, (1, 0).into(), self.velocity.x); + } + + let y_velocity = (0.into(), self.velocity.y).into(); + if !self.collision_at_point(level, self.position + y_velocity) { + self.position += y_velocity; + } else { + self.position += self.binary_search_collision(level, (0, 1).into(), self.velocity.y); + } + + self.position - old_position + } + + fn update_position_with_enemy( + &mut self, + level: &Level, + enemies: &[enemies::Enemy], + ) -> (Vector2D, bool) { + let mut was_enemy_collision = false; + let old_position = self.position; + let x_velocity = (self.velocity.x, 0.into()).into(); + + if !(self.collision_at_point(level, self.position + x_velocity) + || self.enemy_collision_at_point(enemies, self.position + x_velocity)) + { + self.position += x_velocity; + } else if self.enemy_collision_at_point(enemies, self.position + x_velocity) { + self.position -= x_velocity; + was_enemy_collision = true; + } + + let y_velocity = (0.into(), self.velocity.y).into(); + if !(self.collision_at_point(level, self.position + y_velocity) + || self.enemy_collision_at_point(enemies, self.position + y_velocity)) + { + self.position += y_velocity; + } else if self.enemy_collision_at_point(enemies, self.position + y_velocity) { + self.position -= y_velocity; + was_enemy_collision = true; + } + + (self.position - old_position, was_enemy_collision) + } + + fn binary_search_collision( + &self, + level: &Level, + unit_vector: Vector2D, + initial: FixedNumberType, + ) -> Vector2D { + let mut low: FixedNumberType = 0.into(); + let mut high = initial; + + let one: FixedNumberType = 1.into(); + while (high - low).abs() > one / 8 { + let mid = (low + high) / 2; + let new_vel: Vector2D = unit_vector * mid; + + if self.collision_at_point(level, self.position + new_vel) { + high = mid; + } else { + low = mid; + } + } + + unit_vector * low + } + + fn commit_position(&mut self, offset: Vector2D) { + let position = (self.position - offset).floor(); + self.sprite.set_position(position - (8, 8).into()); + if position.x < -8 || position.x > WIDTH + 8 || position.y < -8 || position.y > HEIGHT + 8 { + self.sprite.hide(); + } else { + self.sprite.show(); + } + } +} + +struct Map<'a, 'b> { + background: &'a mut InfiniteScrolledMap<'b>, + foreground: &'a mut InfiniteScrolledMap<'b>, + position: Vector2D, + level: &'a Level, +} + +impl<'a, 'b> Map<'a, 'b> { + pub fn commit_position(&mut self, vram: &mut VRamManager) { + self.background.set_pos(vram, self.position.floor()); + self.foreground.set_pos(vram, self.position.floor()); + + self.background.commit(vram); + self.foreground.commit(vram); + } + + pub fn init_background(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus { + self.background.init_partial(vram, self.position.floor()) + } + + pub fn init_foreground(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus { + self.foreground.init_partial(vram, self.position.floor()) + } +} + +impl Level { + fn collides(&self, x: i32, y: i32) -> bool { + self.at_point(x, y, map_tiles::tilemap::COLLISION_TILE as u32) + } + + fn kills(&self, x: i32, y: i32) -> bool { + self.at_point(x, y, map_tiles::tilemap::KILL_TILE as u32) + } + + fn at_point(&self, x: i32, y: i32, tile: u32) -> bool { + if (x < 0 || x >= self.dimensions.x as i32) || (y < 0 || y >= self.dimensions.y as i32) { + return true; + } + let pos = (self.dimensions.x as i32 * y + x) as usize; + let tile_foreground = self.foreground[pos]; + let tile_background = self.background[pos]; + let foreground_tile_property = self.collision[tile_foreground as usize]; + let background_tile_property = self.collision[tile_background as usize]; + foreground_tile_property == tile || background_tile_property == tile + } + + fn wins(&self, x: i32, y: i32) -> bool { + self.at_point(x, y, map_tiles::tilemap::WIN_TILE as u32) + } +} + +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum HatState { + OnHead, + Thrown, + WizardTowards, +} + +struct Player<'a> { + wizard: Entity<'a>, + hat: Entity<'a>, + hat_state: HatState, + hat_left_range: bool, + hat_slow_counter: i32, + wizard_frame: u8, + num_recalls: i8, + is_on_ground: bool, + facing: input::Tri, +} + +fn ping_pong(i: i32, n: i32) -> i32 { + let cycle = 2 * (n - 1); + let i = i % cycle; + if i >= n { + cycle - i + } else { + i + } +} + +impl<'a> Player<'a> { + fn new(controller: &'a ObjectController, start_position: Vector2D) -> Self { + let mut wizard = Entity::new(controller, (6_u16, 14_u16).into()); + let mut hat = Entity::new(controller, (6_u16, 6_u16).into()); + + wizard + .sprite + .set_sprite(controller.sprite(HAT_SPIN_1.sprite(0))); + hat.sprite + .set_sprite(controller.sprite(HAT_SPIN_1.sprite(0))); + + wizard.sprite.show(); + hat.sprite.show(); + + hat.sprite.set_z(-1); + + wizard.position = start_position; + hat.position = start_position - (0, 10).into(); + + Player { + wizard, + hat, + hat_slow_counter: 0, + hat_state: HatState::OnHead, + hat_left_range: false, + wizard_frame: 0, + num_recalls: 0, + is_on_ground: true, + facing: input::Tri::Zero, + } + } + + fn update_frame( + &mut self, + input: &ButtonController, + controller: &'a ObjectController, + timer: i32, + level: &Level, + enemies: &[enemies::Enemy], + sfx_player: &mut sfx::SfxPlayer, + ) { + // throw or recall + if input.is_just_pressed(Button::A) { + if self.hat_state == HatState::OnHead { + let direction: Vector2D = { + let up_down = input.y_tri() as i32; + let left_right = if up_down == 0 { + self.facing as i32 + } else { + input.x_tri() as i32 + }; + (left_right, up_down).into() + }; + + if direction != (0, 0).into() { + let mut velocity = direction.normalise() * 5; + if velocity.y > 0.into() { + velocity.y *= FixedNumberType::new(4) / 3; + } + self.hat.velocity = velocity; + self.hat_state = HatState::Thrown; + + sfx_player.throw(); + } + } else if self.hat_state == HatState::Thrown { + self.num_recalls += 1; + if self.num_recalls < 3 { + self.hat.velocity = (0, 0).into(); + self.wizard.velocity = (0, 0).into(); + self.hat_state = HatState::WizardTowards; + } + } else if self.hat_state == HatState::WizardTowards { + self.hat_state = HatState::Thrown; + self.wizard.velocity /= 8; + } + } + + let was_on_ground = self.is_on_ground; + let is_on_ground = self + .wizard + .collision_at_point(level, self.wizard.position + (0, 1).into()); + + if is_on_ground && !was_on_ground && self.wizard.velocity.y > 1.into() { + sfx_player.land(); + } + self.is_on_ground = is_on_ground; + + if self.hat_state != HatState::WizardTowards { + if is_on_ground { + self.num_recalls = 0; + } + + if is_on_ground { + self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 16; + self.wizard.velocity = self.wizard.velocity * 54 / 64; + if input.is_just_pressed(Button::B) { + self.wizard.velocity.y = -FixedNumberType::new(3) / 2; + sfx_player.jump(); + } + } else { + self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 64; + self.wizard.velocity = self.wizard.velocity * 63 / 64; + let gravity: Vector2D = (0, 1).into(); + let gravity = gravity / 16; + self.wizard.velocity += gravity; + } + + self.wizard.velocity = self.wizard.update_position(level); + + if self.wizard.velocity.x.abs() > 0.into() { + let offset = (ping_pong(timer / 16, 4)) as usize; + self.wizard_frame = offset as u8; + + let frame = WALKING.animation_sprite(offset); + let sprite = controller.sprite(frame); + + self.wizard.sprite.set_sprite(sprite); + } + + if self.wizard.velocity.y < -FixedNumberType::new(1) / 16 { + // going up + self.wizard_frame = 5; + + let frame = JUMPING.animation_sprite(0); + let sprite = controller.sprite(frame); + + self.wizard.sprite.set_sprite(sprite); + } else if self.wizard.velocity.y > FixedNumberType::new(1) / 16 { + // going down + let offset = if self.wizard.velocity.y * 2 > 3.into() { + (timer / 4) as usize + } else { + // Don't flap beard unless going quickly + 0 + }; + + self.wizard_frame = 0; + + let frame = FALLING.animation_sprite(offset); + let sprite = controller.sprite(frame); + + self.wizard.sprite.set_sprite(sprite); + } + + if input.x_tri() != agb::input::Tri::Zero { + self.facing = input.x_tri(); + } + } + + let hat_base_tile = match self.num_recalls { + 0 => HAT_SPIN_1, + 1 => HAT_SPIN_2, + _ => HAT_SPIN_3, + }; + + let hat_resting_position = match self.wizard_frame { + 1 | 2 => (0, 9).into(), + 5 => (0, 10).into(), + _ => (0, 8).into(), + }; + + match self.facing { + agb::input::Tri::Negative => { + self.wizard.sprite.set_hflip(true); + self.hat + .sprite + .set_sprite(controller.sprite(hat_base_tile.sprite(5))); + } + agb::input::Tri::Positive => { + self.wizard.sprite.set_hflip(false); + self.hat + .sprite + .set_sprite(controller.sprite(hat_base_tile.sprite(0))); + } + _ => {} + } + + match self.hat_state { + HatState::Thrown => { + // hat is thrown, make hat move towards wizard + let distance_vector = + self.wizard.position - self.hat.position - hat_resting_position; + let distance = distance_vector.magnitude(); + let direction = if distance == 0.into() { + (0, 0).into() + } else { + distance_vector / distance + }; + + let hat_sprite_divider = match self.num_recalls { + 0 => 1, + 1 => 2, + _ => 4, + }; + + let hat_sprite_offset = (timer / hat_sprite_divider) as usize; + + self.hat.sprite.set_sprite( + controller.sprite(hat_base_tile.animation_sprite(hat_sprite_offset)), + ); + + if self.hat_slow_counter < 30 && self.hat.velocity.magnitude() < 2.into() { + self.hat.velocity = (0, 0).into(); + self.hat_slow_counter += 1; + } else { + self.hat.velocity += direction / 4; + } + let (new_velocity, enemy_collision) = + self.hat.update_position_with_enemy(level, enemies); + self.hat.velocity = new_velocity; + + if enemy_collision { + sfx_player.snail_hat_bounce(); + } + + if distance > 16.into() { + self.hat_left_range = true; + } + if self.hat_left_range && distance < 16.into() { + sfx_player.catch(); + self.hat_state = HatState::OnHead; + } + } + HatState::OnHead => { + // hat is on head, place hat on head + self.hat_slow_counter = 0; + self.hat_left_range = false; + self.hat.position = self.wizard.position - hat_resting_position; + } + HatState::WizardTowards => { + self.hat.sprite.set_sprite( + controller.sprite(hat_base_tile.animation_sprite(timer as usize / 2)), + ); + let distance_vector = + self.hat.position - self.wizard.position + hat_resting_position; + let distance = distance_vector.magnitude(); + if distance != 0.into() { + let v = self.wizard.velocity.magnitude() + 1; + self.wizard.velocity = distance_vector / distance * v; + } + self.wizard.velocity = self.wizard.update_position(level); + if distance < 16.into() { + self.wizard.velocity /= 8; + self.hat_state = HatState::OnHead; + sfx_player.catch(); + } + } + } + } +} + +struct PlayingLevel<'a, 'b> { + timer: i32, + background: Map<'a, 'b>, + input: ButtonController, + player: Player<'a>, + + enemies: [enemies::Enemy<'a>; 16], +} + +enum UpdateState { + Normal, + Dead, + Complete, +} + +impl<'a, 'b> PlayingLevel<'a, 'b> { + fn open_level( + level: &'a Level, + object_control: &'a ObjectController, + background: &'a mut InfiniteScrolledMap<'b>, + foreground: &'a mut InfiniteScrolledMap<'b>, + input: ButtonController, + ) -> Self { + let mut e: [enemies::Enemy<'a>; 16] = Default::default(); + let mut enemy_count = 0; + for &slime in level.slimes { + e[enemy_count] = enemies::Enemy::new_slime(object_control, slime.into()); + enemy_count += 1; + } + + for &snail in level.snails { + e[enemy_count] = enemies::Enemy::new_snail(object_control, snail.into()); + enemy_count += 1; + } + + let start_pos: Vector2D = level.start_pos.into(); + + let background_position = ( + (start_pos.x - WIDTH / 2) + .clamp(0.into(), ((level.dimensions.x * 8) as i32 - WIDTH).into()), + (start_pos.y - HEIGHT / 2) + .clamp(0.into(), ((level.dimensions.y * 8) as i32 - HEIGHT).into()), + ) + .into(); + + PlayingLevel { + timer: 0, + background: Map { + background, + foreground, + level, + position: background_position, + }, + player: Player::new(object_control, start_pos), + input, + enemies: e, + } + } + + fn show_backgrounds(&mut self) { + self.background.background.show(); + self.background.foreground.show(); + } + + fn hide_backgrounds(&mut self) { + self.background.background.hide(); + self.background.foreground.hide(); + } + + fn clear_backgrounds(&mut self, vram: &mut VRamManager) { + self.background.background.clear(vram); + self.background.foreground.clear(vram); + } + + fn dead_start(&mut self) { + self.player.wizard.velocity = (0, -1).into(); + self.player.wizard.sprite.set_priority(Priority::P0); + } + + fn dead_update(&mut self, controller: &'a ObjectController) -> bool { + self.timer += 1; + + let frame = PLAYER_DEATH.animation_sprite(self.timer as usize / 8); + let sprite = controller.sprite(frame); + + self.player.wizard.velocity += (0.into(), FixedNumberType::new(1) / 32).into(); + self.player.wizard.position += self.player.wizard.velocity; + self.player.wizard.sprite.set_sprite(sprite); + + self.player.wizard.commit_position(self.background.position); + + self.player.wizard.position.y - self.background.position.y < (HEIGHT + 8).into() + } + + fn update_frame( + &mut self, + sfx_player: &mut sfx::SfxPlayer, + vram: &mut VRamManager, + controller: &'a ObjectController, + ) -> UpdateState { + self.timer += 1; + self.input.update(); + + let mut player_dead = false; + + self.player.update_frame( + &self.input, + controller, + self.timer, + self.background.level, + &self.enemies, + sfx_player, + ); + + for enemy in self.enemies.iter_mut() { + match enemy.update( + controller, + self.background.level, + self.player.wizard.position, + self.player.hat_state, + self.timer, + sfx_player, + ) { + enemies::EnemyUpdateState::KillPlayer => player_dead = true, + enemies::EnemyUpdateState::None => {} + } + } + + self.background.position = self.get_next_map_position(); + self.background.commit_position(vram); + + self.player.wizard.commit_position(self.background.position); + self.player.hat.commit_position(self.background.position); + + for enemy in self.enemies.iter_mut() { + enemy.commit(self.background.position); + } + + player_dead |= self + .player + .wizard + .killision_at_point(self.background.level, self.player.wizard.position); + if player_dead { + UpdateState::Dead + } else if self + .player + .wizard + .completion_at_point(self.background.level, self.player.wizard.position) + { + UpdateState::Complete + } else { + UpdateState::Normal + } + } + + fn get_next_map_position(&self) -> Vector2D { + // want to ensure the player and the hat are visible if possible, so try to position the map + // so the centre is at the average position. But give the player some extra priority + let hat_pos = self.player.hat.position.floor(); + let player_pos = self.player.wizard.position.floor(); + + let new_target_position = (hat_pos + player_pos * 3) / 4; + + let screen: Vector2D = (WIDTH, HEIGHT).into(); + let half_screen = screen / 2; + let current_centre = self.background.position.floor() + half_screen; + + let mut target_position = ((current_centre * 3 + new_target_position) / 4) - half_screen; + + target_position.x = target_position.x.clamp( + 0, + (self.background.level.dimensions.x * 8 - (WIDTH as u32)) as i32, + ); + target_position.y = target_position.y.clamp( + 0, + (self.background.level.dimensions.y * 8 - (HEIGHT as u32)) as i32, + ); + + target_position.into() + } +} + +pub fn main(mut agb: agb::Gba) -> ! { + let (tiled, mut vram) = agb.display.video.tiled0(); + vram.set_background_palettes(tile_sheet::PALETTES); + let mut splash_screen = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32); + let mut world_display = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32); + + let tileset = TileSet::new(tile_sheet::background.tiles, TileFormat::FourBpp); + + for y in 0..32u16 { + for x in 0..32u16 { + world_display.set_tile( + &mut vram, + (x, y).into(), + &tileset, + TileSetting::from_raw(level_display::BLANK), + ); + } + } + + world_display.commit(&mut vram); + world_display.show(); + + splash_screen::show_splash_screen( + splash_screen::SplashScreen::Start, + None, + None, + &mut splash_screen, + &mut vram, + ); + + loop { + world_display.commit(&mut vram); + world_display.show(); + + vram.set_background_palettes(tile_sheet::PALETTES); + + let object = agb.display.object.get(); + let mut mixer = agb.mixer.mixer(Frequency::Hz10512); + + mixer.enable(); + let mut music_box = sfx::MusicBox::new(); + + let vblank = agb::interrupt::VBlank::get(); + let mut current_level = 0; + + loop { + if current_level == map_tiles::LEVELS.len() as u32 { + break; + } + + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + + level_display::write_level( + &mut world_display, + current_level / 8 + 1, + current_level % 8 + 1, + &tileset, + &mut vram, + ); + + world_display.commit(&mut vram); + world_display.show(); + + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + + let map_current_level = current_level; + let mut background = InfiniteScrolledMap::new( + tiled.background(Priority::P2, RegularBackgroundSize::Background32x64), + Box::new(|pos: Vector2D| { + let level = &map_tiles::LEVELS[map_current_level as usize]; + ( + &tileset, + TileSetting::from_raw( + *level + .background + .get((pos.y * level.dimensions.x as i32 + pos.x) as usize) + .unwrap_or(&0), + ), + ) + }), + ); + let mut foreground = InfiniteScrolledMap::new( + tiled.background(Priority::P0, RegularBackgroundSize::Background64x32), + Box::new(|pos: Vector2D| { + let level = &map_tiles::LEVELS[map_current_level as usize]; + ( + &tileset, + TileSetting::from_raw( + *level + .foreground + .get((pos.y * level.dimensions.x as i32 + pos.x) as usize) + .unwrap_or(&0), + ), + ) + }), + ); + + let mut level = PlayingLevel::open_level( + &map_tiles::LEVELS[current_level as usize], + &object, + &mut background, + &mut foreground, + agb::input::ButtonController::new(), + ); + + while level.background.init_background(&mut vram) != PartialUpdateStatus::Done { + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + } + + while level.background.init_foreground(&mut vram) != PartialUpdateStatus::Done { + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + } + + for _ in 0..20 { + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + } + + object.commit(); + + level.show_backgrounds(); + + world_display.hide(); + + loop { + match level.update_frame( + &mut sfx::SfxPlayer::new(&mut mixer, &music_box), + &mut vram, + &object, + ) { + UpdateState::Normal => {} + UpdateState::Dead => { + level.dead_start(); + while level.dead_update(&object) { + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + object.commit(); + } + break; + } + UpdateState::Complete => { + current_level += 1; + break; + } + } + + music_box.before_frame(&mut mixer); + mixer.frame(); + vblank.wait_for_vblank(); + mixer.after_vblank(); + object.commit(); + } + + level.hide_backgrounds(); + level.clear_backgrounds(&mut vram); + } + + object.commit(); + + splash_screen::show_splash_screen( + splash_screen::SplashScreen::End, + Some(&mut mixer), + Some(&mut music_box), + &mut splash_screen, + &mut vram, + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use agb::Gba; + + #[test_case] + fn test_ping_pong(_gba: &mut Gba) { + let test_cases = [ + [0, 2, 0], + [0, 7, 0], + [1, 2, 1], + [2, 2, 0], + [3, 2, 1], + [4, 2, 0], + ]; + + for test_case in test_cases { + assert_eq!( + ping_pong(test_case[0], test_case[1]), + test_case[2], + "Expected ping_pong({}, {}) to equal {}", + test_case[0], + test_case[1], + test_case[2], + ); + } + } +} diff --git a/examples/the-hat-chooses-the-wizard/src/main.rs b/examples/the-hat-chooses-the-wizard/src/main.rs index 8b646a5b..77118faf 100644 --- a/examples/the-hat-chooses-the-wizard/src/main.rs +++ b/examples/the-hat-chooses-the-wizard/src/main.rs @@ -4,996 +4,7 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] -extern crate alloc; - -use agb::{ - display::{ - object::{Graphics, Object, ObjectController, Tag, TagMap}, - tiled::{ - InfiniteScrolledMap, PartialUpdateStatus, RegularBackgroundSize, TileFormat, TileSet, - TileSetting, TiledMap, VRamManager, - }, - Priority, HEIGHT, WIDTH, - }, - fixnum::{FixedNum, Vector2D}, - input::{self, Button, ButtonController}, - sound::mixer::Frequency, -}; -use alloc::boxed::Box; - -mod enemies; -mod level_display; -mod sfx; -mod splash_screen; - -pub struct Level { - background: &'static [u16], - foreground: &'static [u16], - dimensions: Vector2D, - collision: &'static [u32], - - slimes: &'static [(i32, i32)], - snails: &'static [(i32, i32)], - enemy_stops: &'static [(i32, i32)], - start_pos: (i32, i32), -} - -mod map_tiles { - - use super::Level; - pub const LEVELS: &[Level] = &[ - l1_1::get_level(), - l1_2::get_level(), - l1_3::get_level(), - l1_4::get_level(), - l1_5::get_level(), - l1_7::get_level(), // these are intentionally this way round - l1_6::get_level(), - l1_8::get_level(), - l2_3::get_level(), // goes 2-3, 2-1 then 2-2 - l2_1::get_level(), - l2_2::get_level(), - l2_4::get_level(), - ]; - - pub mod l1_1 { - include!(concat!(env!("OUT_DIR"), "/1-1.json.rs")); - } - pub mod l1_2 { - include!(concat!(env!("OUT_DIR"), "/1-2.json.rs")); - } - pub mod l1_3 { - include!(concat!(env!("OUT_DIR"), "/1-3.json.rs")); - } - pub mod l1_4 { - include!(concat!(env!("OUT_DIR"), "/1-4.json.rs")); - } - pub mod l1_5 { - include!(concat!(env!("OUT_DIR"), "/1-5.json.rs")); - } - pub mod l1_6 { - include!(concat!(env!("OUT_DIR"), "/1-6.json.rs")); - } - pub mod l1_7 { - include!(concat!(env!("OUT_DIR"), "/1-7.json.rs")); - } - pub mod l2_1 { - include!(concat!(env!("OUT_DIR"), "/2-1.json.rs")); - } - - pub mod l1_8 { - include!(concat!(env!("OUT_DIR"), "/1-8.json.rs")); - } - pub mod l2_2 { - include!(concat!(env!("OUT_DIR"), "/2-2.json.rs")); - } - pub mod l2_3 { - include!(concat!(env!("OUT_DIR"), "/2-3.json.rs")); - } - - pub mod l2_4 { - include!(concat!(env!("OUT_DIR"), "/2-4.json.rs")); - } - - pub mod tilemap { - include!(concat!(env!("OUT_DIR"), "/tilemap.rs")); - } -} - -agb::include_gfx!("gfx/tile_sheet.toml"); - -const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite"); -const TAG_MAP: &TagMap = GRAPHICS.tags(); - -const WALKING: &Tag = TAG_MAP.get("Walking"); -const JUMPING: &Tag = TAG_MAP.get("Jumping"); -const FALLING: &Tag = TAG_MAP.get("Falling"); -const PLAYER_DEATH: &Tag = TAG_MAP.get("Player Death"); -const HAT_SPIN_1: &Tag = TAG_MAP.get("HatSpin"); -const HAT_SPIN_2: &Tag = TAG_MAP.get("HatSpin2"); -const HAT_SPIN_3: &Tag = TAG_MAP.get("HatSpin3"); - -type FixedNumberType = FixedNum<10>; - -pub struct Entity<'a> { - sprite: Object<'a>, - position: Vector2D, - velocity: Vector2D, - collision_mask: Vector2D, -} - -impl<'a> Entity<'a> { - pub fn new(object: &'a ObjectController, collision_mask: Vector2D) -> Self { - let dummy_sprite = object.sprite(WALKING.sprite(0)); - let mut sprite = object.object(dummy_sprite); - sprite.set_priority(Priority::P1); - Entity { - sprite, - collision_mask, - position: (0, 0).into(), - velocity: (0, 0).into(), - } - } - - fn something_at_point bool>( - &self, - position: Vector2D, - something_fn: T, - ) -> bool { - let left = (position.x - self.collision_mask.x as i32 / 2).floor() / 8; - let right = (position.x + self.collision_mask.x as i32 / 2 - 1).floor() / 8; - let top = (position.y - self.collision_mask.y as i32 / 2).floor() / 8; - let bottom = (position.y + self.collision_mask.y as i32 / 2 - 1).floor() / 8; - - for x in left..=right { - for y in top..=bottom { - if something_fn(x, y) { - return true; - } - } - } - false - } - - fn collision_at_point(&self, level: &Level, position: Vector2D) -> bool { - self.something_at_point(position, |x, y| level.collides(x, y)) - } - - fn killision_at_point(&self, level: &Level, position: Vector2D) -> bool { - self.something_at_point(position, |x, y| level.kills(x, y)) - } - - fn completion_at_point(&self, level: &Level, position: Vector2D) -> bool { - self.something_at_point(position, |x, y| level.wins(x, y)) - } - - fn enemy_collision_at_point( - &self, - enemies: &[enemies::Enemy], - position: Vector2D, - ) -> bool { - for enemy in enemies { - if enemy.collides_with_hat(position) { - return true; - } - } - false - } - - // returns the distance actually moved - fn update_position(&mut self, level: &Level) -> Vector2D { - let old_position = self.position; - let x_velocity = (self.velocity.x, 0.into()).into(); - if !self.collision_at_point(level, self.position + x_velocity) { - self.position += x_velocity; - } else { - self.position += self.binary_search_collision(level, (1, 0).into(), self.velocity.x); - } - - let y_velocity = (0.into(), self.velocity.y).into(); - if !self.collision_at_point(level, self.position + y_velocity) { - self.position += y_velocity; - } else { - self.position += self.binary_search_collision(level, (0, 1).into(), self.velocity.y); - } - - self.position - old_position - } - - fn update_position_with_enemy( - &mut self, - level: &Level, - enemies: &[enemies::Enemy], - ) -> (Vector2D, bool) { - let mut was_enemy_collision = false; - let old_position = self.position; - let x_velocity = (self.velocity.x, 0.into()).into(); - - if !(self.collision_at_point(level, self.position + x_velocity) - || self.enemy_collision_at_point(enemies, self.position + x_velocity)) - { - self.position += x_velocity; - } else if self.enemy_collision_at_point(enemies, self.position + x_velocity) { - self.position -= x_velocity; - was_enemy_collision = true; - } - - let y_velocity = (0.into(), self.velocity.y).into(); - if !(self.collision_at_point(level, self.position + y_velocity) - || self.enemy_collision_at_point(enemies, self.position + y_velocity)) - { - self.position += y_velocity; - } else if self.enemy_collision_at_point(enemies, self.position + y_velocity) { - self.position -= y_velocity; - was_enemy_collision = true; - } - - (self.position - old_position, was_enemy_collision) - } - - fn binary_search_collision( - &self, - level: &Level, - unit_vector: Vector2D, - initial: FixedNumberType, - ) -> Vector2D { - let mut low: FixedNumberType = 0.into(); - let mut high = initial; - - let one: FixedNumberType = 1.into(); - while (high - low).abs() > one / 8 { - let mid = (low + high) / 2; - let new_vel: Vector2D = unit_vector * mid; - - if self.collision_at_point(level, self.position + new_vel) { - high = mid; - } else { - low = mid; - } - } - - unit_vector * low - } - - fn commit_position(&mut self, offset: Vector2D) { - let position = (self.position - offset).floor(); - self.sprite.set_position(position - (8, 8).into()); - if position.x < -8 || position.x > WIDTH + 8 || position.y < -8 || position.y > HEIGHT + 8 { - self.sprite.hide(); - } else { - self.sprite.show(); - } - } -} - -struct Map<'a, 'b> { - background: &'a mut InfiniteScrolledMap<'b>, - foreground: &'a mut InfiniteScrolledMap<'b>, - position: Vector2D, - level: &'a Level, -} - -impl<'a, 'b> Map<'a, 'b> { - pub fn commit_position(&mut self, vram: &mut VRamManager) { - self.background.set_pos(vram, self.position.floor()); - self.foreground.set_pos(vram, self.position.floor()); - - self.background.commit(vram); - self.foreground.commit(vram); - } - - pub fn init_background(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus { - self.background.init_partial(vram, self.position.floor()) - } - - pub fn init_foreground(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus { - self.foreground.init_partial(vram, self.position.floor()) - } -} - -impl Level { - fn collides(&self, x: i32, y: i32) -> bool { - self.at_point(x, y, map_tiles::tilemap::COLLISION_TILE as u32) - } - - fn kills(&self, x: i32, y: i32) -> bool { - self.at_point(x, y, map_tiles::tilemap::KILL_TILE as u32) - } - - fn at_point(&self, x: i32, y: i32, tile: u32) -> bool { - if (x < 0 || x >= self.dimensions.x as i32) || (y < 0 || y >= self.dimensions.y as i32) { - return true; - } - let pos = (self.dimensions.x as i32 * y + x) as usize; - let tile_foreground = self.foreground[pos]; - let tile_background = self.background[pos]; - let foreground_tile_property = self.collision[tile_foreground as usize]; - let background_tile_property = self.collision[tile_background as usize]; - foreground_tile_property == tile || background_tile_property == tile - } - - fn wins(&self, x: i32, y: i32) -> bool { - self.at_point(x, y, map_tiles::tilemap::WIN_TILE as u32) - } -} - -#[derive(PartialEq, Eq, Copy, Clone)] -pub enum HatState { - OnHead, - Thrown, - WizardTowards, -} - -struct Player<'a> { - wizard: Entity<'a>, - hat: Entity<'a>, - hat_state: HatState, - hat_left_range: bool, - hat_slow_counter: i32, - wizard_frame: u8, - num_recalls: i8, - is_on_ground: bool, - facing: input::Tri, -} - -fn ping_pong(i: i32, n: i32) -> i32 { - let cycle = 2 * (n - 1); - let i = i % cycle; - if i >= n { - cycle - i - } else { - i - } -} - -impl<'a> Player<'a> { - fn new(controller: &'a ObjectController, start_position: Vector2D) -> Self { - let mut wizard = Entity::new(controller, (6_u16, 14_u16).into()); - let mut hat = Entity::new(controller, (6_u16, 6_u16).into()); - - wizard - .sprite - .set_sprite(controller.sprite(HAT_SPIN_1.sprite(0))); - hat.sprite - .set_sprite(controller.sprite(HAT_SPIN_1.sprite(0))); - - wizard.sprite.show(); - hat.sprite.show(); - - hat.sprite.set_z(-1); - - wizard.position = start_position; - hat.position = start_position - (0, 10).into(); - - Player { - wizard, - hat, - hat_slow_counter: 0, - hat_state: HatState::OnHead, - hat_left_range: false, - wizard_frame: 0, - num_recalls: 0, - is_on_ground: true, - facing: input::Tri::Zero, - } - } - - fn update_frame( - &mut self, - input: &ButtonController, - controller: &'a ObjectController, - timer: i32, - level: &Level, - enemies: &[enemies::Enemy], - sfx_player: &mut sfx::SfxPlayer, - ) { - // throw or recall - if input.is_just_pressed(Button::A) { - if self.hat_state == HatState::OnHead { - let direction: Vector2D = { - let up_down = input.y_tri() as i32; - let left_right = if up_down == 0 { - self.facing as i32 - } else { - input.x_tri() as i32 - }; - (left_right, up_down).into() - }; - - if direction != (0, 0).into() { - let mut velocity = direction.normalise() * 5; - if velocity.y > 0.into() { - velocity.y *= FixedNumberType::new(4) / 3; - } - self.hat.velocity = velocity; - self.hat_state = HatState::Thrown; - - sfx_player.throw(); - } - } else if self.hat_state == HatState::Thrown { - self.num_recalls += 1; - if self.num_recalls < 3 { - self.hat.velocity = (0, 0).into(); - self.wizard.velocity = (0, 0).into(); - self.hat_state = HatState::WizardTowards; - } - } else if self.hat_state == HatState::WizardTowards { - self.hat_state = HatState::Thrown; - self.wizard.velocity /= 8; - } - } - - let was_on_ground = self.is_on_ground; - let is_on_ground = self - .wizard - .collision_at_point(level, self.wizard.position + (0, 1).into()); - - if is_on_ground && !was_on_ground && self.wizard.velocity.y > 1.into() { - sfx_player.land(); - } - self.is_on_ground = is_on_ground; - - if self.hat_state != HatState::WizardTowards { - if is_on_ground { - self.num_recalls = 0; - } - - if is_on_ground { - self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 16; - self.wizard.velocity = self.wizard.velocity * 54 / 64; - if input.is_just_pressed(Button::B) { - self.wizard.velocity.y = -FixedNumberType::new(3) / 2; - sfx_player.jump(); - } - } else { - self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 64; - self.wizard.velocity = self.wizard.velocity * 63 / 64; - let gravity: Vector2D = (0, 1).into(); - let gravity = gravity / 16; - self.wizard.velocity += gravity; - } - - self.wizard.velocity = self.wizard.update_position(level); - - if self.wizard.velocity.x.abs() > 0.into() { - let offset = (ping_pong(timer / 16, 4)) as usize; - self.wizard_frame = offset as u8; - - let frame = WALKING.animation_sprite(offset); - let sprite = controller.sprite(frame); - - self.wizard.sprite.set_sprite(sprite); - } - - if self.wizard.velocity.y < -FixedNumberType::new(1) / 16 { - // going up - self.wizard_frame = 5; - - let frame = JUMPING.animation_sprite(0); - let sprite = controller.sprite(frame); - - self.wizard.sprite.set_sprite(sprite); - } else if self.wizard.velocity.y > FixedNumberType::new(1) / 16 { - // going down - let offset = if self.wizard.velocity.y * 2 > 3.into() { - (timer / 4) as usize - } else { - // Don't flap beard unless going quickly - 0 - }; - - self.wizard_frame = 0; - - let frame = FALLING.animation_sprite(offset); - let sprite = controller.sprite(frame); - - self.wizard.sprite.set_sprite(sprite); - } - - if input.x_tri() != agb::input::Tri::Zero { - self.facing = input.x_tri(); - } - } - - let hat_base_tile = match self.num_recalls { - 0 => HAT_SPIN_1, - 1 => HAT_SPIN_2, - _ => HAT_SPIN_3, - }; - - let hat_resting_position = match self.wizard_frame { - 1 | 2 => (0, 9).into(), - 5 => (0, 10).into(), - _ => (0, 8).into(), - }; - - match self.facing { - agb::input::Tri::Negative => { - self.wizard.sprite.set_hflip(true); - self.hat - .sprite - .set_sprite(controller.sprite(hat_base_tile.sprite(5))); - } - agb::input::Tri::Positive => { - self.wizard.sprite.set_hflip(false); - self.hat - .sprite - .set_sprite(controller.sprite(hat_base_tile.sprite(0))); - } - _ => {} - } - - match self.hat_state { - HatState::Thrown => { - // hat is thrown, make hat move towards wizard - let distance_vector = - self.wizard.position - self.hat.position - hat_resting_position; - let distance = distance_vector.magnitude(); - let direction = if distance == 0.into() { - (0, 0).into() - } else { - distance_vector / distance - }; - - let hat_sprite_divider = match self.num_recalls { - 0 => 1, - 1 => 2, - _ => 4, - }; - - let hat_sprite_offset = (timer / hat_sprite_divider) as usize; - - self.hat.sprite.set_sprite( - controller.sprite(hat_base_tile.animation_sprite(hat_sprite_offset)), - ); - - if self.hat_slow_counter < 30 && self.hat.velocity.magnitude() < 2.into() { - self.hat.velocity = (0, 0).into(); - self.hat_slow_counter += 1; - } else { - self.hat.velocity += direction / 4; - } - let (new_velocity, enemy_collision) = - self.hat.update_position_with_enemy(level, enemies); - self.hat.velocity = new_velocity; - - if enemy_collision { - sfx_player.snail_hat_bounce(); - } - - if distance > 16.into() { - self.hat_left_range = true; - } - if self.hat_left_range && distance < 16.into() { - sfx_player.catch(); - self.hat_state = HatState::OnHead; - } - } - HatState::OnHead => { - // hat is on head, place hat on head - self.hat_slow_counter = 0; - self.hat_left_range = false; - self.hat.position = self.wizard.position - hat_resting_position; - } - HatState::WizardTowards => { - self.hat.sprite.set_sprite( - controller.sprite(hat_base_tile.animation_sprite(timer as usize / 2)), - ); - let distance_vector = - self.hat.position - self.wizard.position + hat_resting_position; - let distance = distance_vector.magnitude(); - if distance != 0.into() { - let v = self.wizard.velocity.magnitude() + 1; - self.wizard.velocity = distance_vector / distance * v; - } - self.wizard.velocity = self.wizard.update_position(level); - if distance < 16.into() { - self.wizard.velocity /= 8; - self.hat_state = HatState::OnHead; - sfx_player.catch(); - } - } - } - } -} - -struct PlayingLevel<'a, 'b> { - timer: i32, - background: Map<'a, 'b>, - input: ButtonController, - player: Player<'a>, - - enemies: [enemies::Enemy<'a>; 16], -} - -enum UpdateState { - Normal, - Dead, - Complete, -} - -impl<'a, 'b> PlayingLevel<'a, 'b> { - fn open_level( - level: &'a Level, - object_control: &'a ObjectController, - background: &'a mut InfiniteScrolledMap<'b>, - foreground: &'a mut InfiniteScrolledMap<'b>, - input: ButtonController, - ) -> Self { - let mut e: [enemies::Enemy<'a>; 16] = Default::default(); - let mut enemy_count = 0; - for &slime in level.slimes { - e[enemy_count] = enemies::Enemy::new_slime(object_control, slime.into()); - enemy_count += 1; - } - - for &snail in level.snails { - e[enemy_count] = enemies::Enemy::new_snail(object_control, snail.into()); - enemy_count += 1; - } - - let start_pos: Vector2D = level.start_pos.into(); - - let background_position = ( - (start_pos.x - WIDTH / 2) - .clamp(0.into(), ((level.dimensions.x * 8) as i32 - WIDTH).into()), - (start_pos.y - HEIGHT / 2) - .clamp(0.into(), ((level.dimensions.y * 8) as i32 - HEIGHT).into()), - ) - .into(); - - PlayingLevel { - timer: 0, - background: Map { - background, - foreground, - level, - position: background_position, - }, - player: Player::new(object_control, start_pos), - input, - enemies: e, - } - } - - fn show_backgrounds(&mut self) { - self.background.background.show(); - self.background.foreground.show(); - } - - fn hide_backgrounds(&mut self) { - self.background.background.hide(); - self.background.foreground.hide(); - } - - fn clear_backgrounds(&mut self, vram: &mut VRamManager) { - self.background.background.clear(vram); - self.background.foreground.clear(vram); - } - - fn dead_start(&mut self) { - self.player.wizard.velocity = (0, -1).into(); - self.player.wizard.sprite.set_priority(Priority::P0); - } - - fn dead_update(&mut self, controller: &'a ObjectController) -> bool { - self.timer += 1; - - let frame = PLAYER_DEATH.animation_sprite(self.timer as usize / 8); - let sprite = controller.sprite(frame); - - self.player.wizard.velocity += (0.into(), FixedNumberType::new(1) / 32).into(); - self.player.wizard.position += self.player.wizard.velocity; - self.player.wizard.sprite.set_sprite(sprite); - - self.player.wizard.commit_position(self.background.position); - - self.player.wizard.position.y - self.background.position.y < (HEIGHT + 8).into() - } - - fn update_frame( - &mut self, - sfx_player: &mut sfx::SfxPlayer, - vram: &mut VRamManager, - controller: &'a ObjectController, - ) -> UpdateState { - self.timer += 1; - self.input.update(); - - let mut player_dead = false; - - self.player.update_frame( - &self.input, - controller, - self.timer, - self.background.level, - &self.enemies, - sfx_player, - ); - - for enemy in self.enemies.iter_mut() { - match enemy.update( - controller, - self.background.level, - self.player.wizard.position, - self.player.hat_state, - self.timer, - sfx_player, - ) { - enemies::EnemyUpdateState::KillPlayer => player_dead = true, - enemies::EnemyUpdateState::None => {} - } - } - - self.background.position = self.get_next_map_position(); - self.background.commit_position(vram); - - self.player.wizard.commit_position(self.background.position); - self.player.hat.commit_position(self.background.position); - - for enemy in self.enemies.iter_mut() { - enemy.commit(self.background.position); - } - - player_dead |= self - .player - .wizard - .killision_at_point(self.background.level, self.player.wizard.position); - if player_dead { - UpdateState::Dead - } else if self - .player - .wizard - .completion_at_point(self.background.level, self.player.wizard.position) - { - UpdateState::Complete - } else { - UpdateState::Normal - } - } - - fn get_next_map_position(&self) -> Vector2D { - // want to ensure the player and the hat are visible if possible, so try to position the map - // so the centre is at the average position. But give the player some extra priority - let hat_pos = self.player.hat.position.floor(); - let player_pos = self.player.wizard.position.floor(); - - let new_target_position = (hat_pos + player_pos * 3) / 4; - - let screen: Vector2D = (WIDTH, HEIGHT).into(); - let half_screen = screen / 2; - let current_centre = self.background.position.floor() + half_screen; - - let mut target_position = ((current_centre * 3 + new_target_position) / 4) - half_screen; - - target_position.x = target_position.x.clamp( - 0, - (self.background.level.dimensions.x * 8 - (WIDTH as u32)) as i32, - ); - target_position.y = target_position.y.clamp( - 0, - (self.background.level.dimensions.y * 8 - (HEIGHT as u32)) as i32, - ); - - target_position.into() - } -} - #[agb::entry] -fn agb_main(mut gba: agb::Gba) -> ! { - main(gba); -} - -pub fn main(mut agb: agb::Gba) -> ! { - let (tiled, mut vram) = agb.display.video.tiled0(); - vram.set_background_palettes(tile_sheet::PALETTES); - let mut splash_screen = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32); - let mut world_display = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32); - - let tileset = TileSet::new(tile_sheet::background.tiles, TileFormat::FourBpp); - - for y in 0..32u16 { - for x in 0..32u16 { - world_display.set_tile( - &mut vram, - (x, y).into(), - &tileset, - TileSetting::from_raw(level_display::BLANK), - ); - } - } - - world_display.commit(&mut vram); - world_display.show(); - - splash_screen::show_splash_screen( - splash_screen::SplashScreen::Start, - None, - None, - &mut splash_screen, - &mut vram, - ); - - loop { - world_display.commit(&mut vram); - world_display.show(); - - vram.set_background_palettes(tile_sheet::PALETTES); - - let object = agb.display.object.get(); - let mut mixer = agb.mixer.mixer(Frequency::Hz10512); - - mixer.enable(); - let mut music_box = sfx::MusicBox::new(); - - let vblank = agb::interrupt::VBlank::get(); - let mut current_level = 0; - - loop { - if current_level == map_tiles::LEVELS.len() as u32 { - break; - } - - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - - level_display::write_level( - &mut world_display, - current_level / 8 + 1, - current_level % 8 + 1, - &tileset, - &mut vram, - ); - - world_display.commit(&mut vram); - world_display.show(); - - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - - let map_current_level = current_level; - let mut background = InfiniteScrolledMap::new( - tiled.background(Priority::P2, RegularBackgroundSize::Background32x64), - Box::new(|pos: Vector2D| { - let level = &map_tiles::LEVELS[map_current_level as usize]; - ( - &tileset, - TileSetting::from_raw( - *level - .background - .get((pos.y * level.dimensions.x as i32 + pos.x) as usize) - .unwrap_or(&0), - ), - ) - }), - ); - let mut foreground = InfiniteScrolledMap::new( - tiled.background(Priority::P0, RegularBackgroundSize::Background64x32), - Box::new(|pos: Vector2D| { - let level = &map_tiles::LEVELS[map_current_level as usize]; - ( - &tileset, - TileSetting::from_raw( - *level - .foreground - .get((pos.y * level.dimensions.x as i32 + pos.x) as usize) - .unwrap_or(&0), - ), - ) - }), - ); - - let mut level = PlayingLevel::open_level( - &map_tiles::LEVELS[current_level as usize], - &object, - &mut background, - &mut foreground, - agb::input::ButtonController::new(), - ); - - while level.background.init_background(&mut vram) != PartialUpdateStatus::Done { - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - } - - while level.background.init_foreground(&mut vram) != PartialUpdateStatus::Done { - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - } - - for _ in 0..20 { - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - } - - object.commit(); - - level.show_backgrounds(); - - world_display.hide(); - - loop { - match level.update_frame( - &mut sfx::SfxPlayer::new(&mut mixer, &music_box), - &mut vram, - &object, - ) { - UpdateState::Normal => {} - UpdateState::Dead => { - level.dead_start(); - while level.dead_update(&object) { - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - object.commit(); - } - break; - } - UpdateState::Complete => { - current_level += 1; - break; - } - } - - music_box.before_frame(&mut mixer); - mixer.frame(); - vblank.wait_for_vblank(); - mixer.after_vblank(); - object.commit(); - } - - level.hide_backgrounds(); - level.clear_backgrounds(&mut vram); - } - - object.commit(); - - splash_screen::show_splash_screen( - splash_screen::SplashScreen::End, - Some(&mut mixer), - Some(&mut music_box), - &mut splash_screen, - &mut vram, - ); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use agb::Gba; - - #[test_case] - fn test_ping_pong(_gba: &mut Gba) { - let test_cases = [ - [0, 2, 0], - [0, 7, 0], - [1, 2, 1], - [2, 2, 0], - [3, 2, 1], - [4, 2, 0], - ]; - - for test_case in test_cases { - assert_eq!( - ping_pong(test_case[0], test_case[1]), - test_case[2], - "Expected ping_pong({}, {}) to equal {}", - test_case[0], - test_case[1], - test_case[2], - ); - } - } +fn entry(mut gba: agb::Gba) -> ! { + the_hat_chooses_the_wizard::main(gba); } diff --git a/examples/the-purple-night/src/lib.rs b/examples/the-purple-night/src/lib.rs new file mode 100644 index 00000000..1dc5513f --- /dev/null +++ b/examples/the-purple-night/src/lib.rs @@ -0,0 +1,2344 @@ +#![no_std] +#![no_main] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] +#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] + +extern crate alloc; + +mod sfx; + +use core::cmp::Ordering; + +use alloc::{boxed::Box, vec::Vec}; + +use agb::{ + display::{ + object::{Graphics, Object, ObjectController, Sprite, Tag, TagMap}, + tiled::{ + InfiniteScrolledMap, RegularBackgroundSize, TileFormat, TileSet, TileSetting, + VRamManager, + }, + Priority, HEIGHT, WIDTH, + }, + fixnum::{FixedNum, Rect, Vector2D}, + input::{Button, ButtonController, Tri}, + interrupt::VBlank, + rng, + sound::mixer::Frequency, +}; +use generational_arena::Arena; +use sfx::Sfx; + +const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/objects.aseprite", "gfx/boss.aseprite"); +const TAG_MAP: &TagMap = GRAPHICS.tags(); + +const LONG_SWORD_IDLE: &Tag = TAG_MAP.get("Idle - longsword"); +const LONG_SWORD_WALK: &Tag = TAG_MAP.get("Walk - longsword"); +const LONG_SWORD_JUMP: &Tag = TAG_MAP.get("Jump - longsword"); +const LONG_SWORD_ATTACK: &Tag = TAG_MAP.get("Attack - longsword"); +const LONG_SWORD_JUMP_ATTACK: &Tag = TAG_MAP.get("Jump attack - longsword"); + +const SHORT_SWORD_IDLE: &Tag = TAG_MAP.get("Idle - shortsword"); +const SHORT_SWORD_WALK: &Tag = TAG_MAP.get("Walk - shortsword"); +const SHORT_SWORD_JUMP: &Tag = TAG_MAP.get("jump - shortsword"); +const SHORT_SWORD_ATTACK: &Tag = TAG_MAP.get("attack - shortsword"); +const SHORT_SWORD_JUMP_ATTACK: &Tag = TAG_MAP.get("jump attack - shortsword"); + +const KNIFE_IDLE: &Tag = TAG_MAP.get("idle - knife"); +const KNIFE_WALK: &Tag = TAG_MAP.get("walk - knife"); +const KNIFE_JUMP: &Tag = TAG_MAP.get("jump - knife"); +const KNIFE_ATTACK: &Tag = TAG_MAP.get("attack - knife"); +const KNIFE_JUMP_ATTACK: &Tag = TAG_MAP.get("jump attack - knife"); + +const SWORDLESS_IDLE: &Tag = TAG_MAP.get("idle swordless"); +const SWORDLESS_WALK: &Tag = TAG_MAP.get("walk swordless"); +const SWORDLESS_JUMP: &Tag = TAG_MAP.get("jump swordless"); +const SWORDLESS_ATTACK: &Tag = KNIFE_ATTACK; +const SWORDLESS_JUMP_ATTACK: &Tag = KNIFE_JUMP_ATTACK; + +agb::include_gfx!("gfx/background.toml"); + +type Number = FixedNum<8>; + +struct Level<'a> { + background: InfiniteScrolledMap<'a>, + foreground: InfiniteScrolledMap<'a>, + clouds: InfiniteScrolledMap<'a>, + + slime_spawns: Vec<(u16, u16)>, + bat_spawns: Vec<(u16, u16)>, + emu_spawns: Vec<(u16, u16)>, +} + +impl<'a> Level<'a> { + fn load_level( + mut backdrop: InfiniteScrolledMap<'a>, + mut foreground: InfiniteScrolledMap<'a>, + mut clouds: InfiniteScrolledMap<'a>, + start_pos: Vector2D, + vram: &mut VRamManager, + sfx: &mut Sfx, + ) -> Self { + let vblank = VBlank::get(); + + let mut between_updates = || { + sfx.frame(); + vblank.wait_for_vblank(); + sfx.after_vblank(); + }; + + backdrop.init(vram, start_pos, &mut between_updates); + foreground.init(vram, start_pos, &mut between_updates); + clouds.init(vram, start_pos / 4, &mut between_updates); + + backdrop.commit(vram); + foreground.commit(vram); + clouds.commit(vram); + + backdrop.show(); + foreground.show(); + clouds.show(); + + let slime_spawns = tilemap::SLIME_SPAWNS_X + .iter() + .enumerate() + .map(|(i, x)| (*x, tilemap::SLIME_SPAWNS_Y[i])) + .collect(); + + let bat_spawns = tilemap::BAT_SPAWNS_X + .iter() + .enumerate() + .map(|(i, x)| (*x, tilemap::BAT_SPAWNS_Y[i])) + .collect(); + + let emu_spawns = tilemap::EMU_SPAWNS_X + .iter() + .enumerate() + .map(|(i, x)| (*x, tilemap::EMU_SPAWNS_Y[i])) + .collect(); + + Self { + background: backdrop, + foreground, + clouds, + + slime_spawns, + bat_spawns, + emu_spawns, + } + } + + fn collides(&self, v: Vector2D) -> Option> { + let factor: Number = Number::new(1) / Number::new(8); + let (x, y) = (v * factor).floor().get(); + + if !(0..=tilemap::WIDTH).contains(&x) || !(0..=tilemap::HEIGHT).contains(&y) { + return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())); + } + let position = tilemap::WIDTH as usize * y as usize + x as usize; + let tile_foreground = tilemap::FOREGROUND_MAP[position]; + let tile_background = tilemap::BACKGROUND_MAP[position]; + let tile_foreground_property = tilemap::TILE_TYPES[tile_foreground as usize]; + let tile_background_property = tilemap::TILE_TYPES[tile_background as usize]; + + if tile_foreground_property == 1 || tile_background_property == 1 { + Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())) + } else { + None + } + } + + fn clear(&mut self, vram: &mut VRamManager) { + self.background.clear(vram); + self.foreground.clear(vram); + self.clouds.clear(vram); + } +} + +struct Entity<'a> { + sprite: Object<'a>, + position: Vector2D, + velocity: Vector2D, + collision_mask: Rect, + visible: bool, +} + +impl<'a> Entity<'a> { + fn new(object_controller: &'a ObjectController, collision_mask: Rect) -> Self { + let s = object_controller.sprite(LONG_SWORD_IDLE.sprite(0)); + let mut sprite = object_controller.object(s); + sprite.set_priority(Priority::P1); + Entity { + sprite, + collision_mask, + position: (0, 0).into(), + velocity: (0, 0).into(), + visible: true, + } + } + + fn update_position(&mut self, level: &Level) -> Vector2D { + let initial_position = self.position; + + let y = self.velocity.y.to_raw().signum(); + if y != 0 { + let (delta, collided) = + self.collision_in_direction((0, y).into(), self.velocity.y.abs(), |v| { + level.collides(v) + }); + self.position += delta; + if collided { + self.velocity.y = 0.into(); + } + } + let x = self.velocity.x.to_raw().signum(); + if x != 0 { + let (delta, collided) = + self.collision_in_direction((x, 0).into(), self.velocity.x.abs(), |v| { + level.collides(v) + }); + self.position += delta; + if collided { + self.velocity.x = 0.into(); + } + } + + self.position - initial_position + } + + fn update_position_without_collision(&mut self) -> Vector2D { + self.position += self.velocity; + + self.velocity + } + + fn collider(&self) -> Rect { + let mut number_collision: Rect = Rect::new( + ( + self.collision_mask.position.x as i32, + self.collision_mask.position.y as i32, + ) + .into(), + ( + self.collision_mask.size.x as i32, + self.collision_mask.size.y as i32, + ) + .into(), + ); + number_collision.position = + self.position + number_collision.position - number_collision.size / 2; + number_collision + } + + fn collision_in_direction( + &mut self, + direction: Vector2D, + distance: Number, + collision: impl Fn(Vector2D) -> Option>, + ) -> (Vector2D, bool) { + let number_collision = self.collider(); + + let center_collision_point: Vector2D = number_collision.position + + number_collision.size / 2 + + number_collision.size.hadamard(direction) / 2; + + let direction_transpose: Vector2D = direction.swap(); + let small = direction_transpose * Number::new(4) / 64; + let triple_collider: [Vector2D; 2] = [ + center_collision_point + number_collision.size.hadamard(direction_transpose) / 2 + - small, + center_collision_point - number_collision.size.hadamard(direction_transpose) / 2 + + small, + ]; + + let original_distance = direction * distance; + let mut final_distance = original_distance; + + let mut has_collided = false; + + for edge_point in triple_collider { + let point = edge_point + original_distance; + if let Some(collider) = collision(point) { + let center = collider.position + collider.size / 2; + let edge = center - collider.size.hadamard(direction) / 2; + let new_distance = (edge - center_collision_point) + .hadamard((direction.x.abs(), direction.y.abs()).into()); + if final_distance.manhattan_distance() > new_distance.manhattan_distance() { + final_distance = new_distance; + } + has_collided = true; + } + } + + (final_distance, has_collided) + } + + fn commit_with_fudge(&mut self, offset: Vector2D, fudge: Vector2D) { + if !self.visible { + self.sprite.hide(); + } else { + let position = (self.position - offset).floor() + fudge; + self.sprite.set_position(position - (8, 8).into()); + if position.x < -8 + || position.x > WIDTH + 8 + || position.y < -8 + || position.y > HEIGHT + 8 + { + self.sprite.hide(); + } else { + self.sprite.show(); + } + } + } + + fn commit_with_size(&mut self, offset: Vector2D, size: Vector2D) { + if !self.visible { + self.sprite.hide(); + } else { + let position = (self.position - offset).floor(); + self.sprite.set_position(position - size / 2); + if position.x < -8 + || position.x > WIDTH + 8 + || position.y < -8 + || position.y > HEIGHT + 8 + { + self.sprite.hide(); + } else { + self.sprite.show(); + } + } + } +} + +#[derive(PartialEq, Eq)] +enum PlayerState { + OnGround, + InAir, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SwordState { + LongSword, + ShortSword, + Dagger, + Swordless, +} + +impl SwordState { + fn ground_walk_force(self) -> Number { + match self { + SwordState::LongSword => Number::new(4) / 16, + SwordState::ShortSword => Number::new(5) / 16, + SwordState::Dagger => Number::new(6) / 16, + SwordState::Swordless => Number::new(6) / 16, + } + } + fn jump_impulse(self) -> Number { + match self { + SwordState::LongSword => Number::new(32) / 16, + SwordState::ShortSword => Number::new(35) / 16, + SwordState::Dagger => Number::new(36) / 16, + SwordState::Swordless => Number::new(42) / 16, + } + } + fn air_move_force(self) -> Number { + match self { + SwordState::LongSword => Number::new(4) / 256, + SwordState::ShortSword => Number::new(5) / 256, + SwordState::Dagger => Number::new(6) / 256, + SwordState::Swordless => Number::new(6) / 256, + } + } + fn idle_animation(self, counter: u16) -> &'static Sprite { + let counter = counter as usize; + match self { + SwordState::LongSword => LONG_SWORD_IDLE.animation_sprite(counter / 8), + SwordState::ShortSword => SHORT_SWORD_IDLE.animation_sprite(counter / 8), + SwordState::Dagger => KNIFE_IDLE.animation_sprite(counter / 8), + SwordState::Swordless => SWORDLESS_IDLE.animation_sprite(counter / 8), + } + } + fn jump_tag(self) -> &'static Tag { + match self { + SwordState::LongSword => LONG_SWORD_JUMP, + SwordState::ShortSword => SHORT_SWORD_JUMP, + SwordState::Dagger => KNIFE_JUMP, + SwordState::Swordless => SWORDLESS_JUMP, + } + } + fn walk_animation(self, counter: u16) -> &'static Sprite { + let counter = counter as usize; + match self { + SwordState::LongSword => LONG_SWORD_WALK.animation_sprite(counter / 4), + SwordState::ShortSword => SHORT_SWORD_WALK.animation_sprite(counter / 4), + SwordState::Dagger => KNIFE_WALK.animation_sprite(counter / 4), + SwordState::Swordless => SWORDLESS_WALK.animation_sprite(counter / 4), + } + } + fn attack_duration(self) -> u16 { + match self { + SwordState::LongSword => 60, + SwordState::ShortSword => 40, + SwordState::Dagger => 20, + SwordState::Swordless => 0, + } + } + fn jump_attack_duration(self) -> u16 { + match self { + SwordState::LongSword => 34, + SwordState::ShortSword => 28, + SwordState::Dagger => 20, + SwordState::Swordless => 0, + } + } + fn attack_frame(self, timer: u16) -> u16 { + match self { + SwordState::LongSword => (self.attack_duration().saturating_sub(timer)) / 8, + SwordState::ShortSword => (self.attack_duration().saturating_sub(timer)) / 8, + SwordState::Dagger => (self.attack_duration().saturating_sub(timer)) / 8, + SwordState::Swordless => (self.attack_duration().saturating_sub(timer)) / 8, + } + } + fn jump_attack_tag(self) -> &'static Tag { + match self { + SwordState::LongSword => LONG_SWORD_JUMP_ATTACK, + SwordState::ShortSword => SHORT_SWORD_JUMP_ATTACK, + SwordState::Dagger => KNIFE_JUMP_ATTACK, + SwordState::Swordless => SWORDLESS_JUMP_ATTACK, + } + } + fn jump_attack_frame(self, timer: u16) -> u16 { + (self.jump_attack_duration().saturating_sub(timer)) / 8 + } + fn hold_frame(self) -> u16 { + 7 + } + + fn cooldown_time(self) -> u16 { + match self { + SwordState::LongSword => 20, + SwordState::ShortSword => 10, + SwordState::Dagger => 1, + SwordState::Swordless => 0, + } + } + fn attack_tag(self) -> &'static Tag { + match self { + SwordState::LongSword => LONG_SWORD_ATTACK, + SwordState::ShortSword => SHORT_SWORD_ATTACK, + SwordState::Dagger => KNIFE_ATTACK, + SwordState::Swordless => SWORDLESS_ATTACK, + } + } + + fn fudge(self, frame: u16) -> i32 { + match self { + SwordState::LongSword => long_sword_fudge(frame), + SwordState::ShortSword => short_sword_fudge(frame), + SwordState::Dagger => 0, + SwordState::Swordless => 0, + } + } + // origin at top left pre fudge boxes + fn ground_attack_hurtbox(self, frame: u16) -> Option> { + match self { + SwordState::LongSword => long_sword_hurtbox(frame), + SwordState::ShortSword => short_sword_hurtbox(frame), + SwordState::Dagger => dagger_hurtbox(frame), + SwordState::Swordless => None, + } + } + fn air_attack_hurtbox(self, _frame: u16) -> Option> { + Some(Rect::new((0, 0).into(), (16, 16).into())) + } +} + +fn dagger_hurtbox(_frame: u16) -> Option> { + Some(Rect::new((9, 5).into(), (7, 9).into())) +} + +fn long_sword_hurtbox(frame: u16) -> Option> { + match frame { + 0 => Some(Rect::new((1, 10).into(), (6, 3).into())), + 1 => Some(Rect::new((0, 9).into(), (7, 2).into())), + 2 => Some(Rect::new((0, 1).into(), (6, 8).into())), + 3 => Some(Rect::new((3, 0).into(), (6, 8).into())), + 4 => Some(Rect::new((6, 3).into(), (10, 8).into())), + 5 => Some(Rect::new((6, 5).into(), (10, 9).into())), + 6 => Some(Rect::new((6, 5).into(), (10, 9).into())), + 7 => Some(Rect::new((6, 5).into(), (10, 9).into())), + _ => None, + } +} + +fn short_sword_hurtbox(frame: u16) -> Option> { + match frame { + 0 => None, + 1 => Some(Rect::new((10, 5).into(), (3, 5).into())), + 2 => Some(Rect::new((8, 5).into(), (6, 6).into())), + 3 => Some(Rect::new((8, 6).into(), (8, 8).into())), + 4 => Some(Rect::new((8, 7).into(), (5, 7).into())), + 5 => Some(Rect::new((8, 7).into(), (7, 7).into())), + 6 => Some(Rect::new((8, 5).into(), (7, 8).into())), + 7 => Some(Rect::new((8, 4).into(), (4, 7).into())), + _ => None, + } +} + +fn short_sword_fudge(frame: u16) -> i32 { + match frame { + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 3, + 5 => 3, + 6 => 3, + 7 => 3, + _ => 0, + } +} + +fn long_sword_fudge(frame: u16) -> i32 { + match frame { + 0 => 0, + 1 => 0, + 2 => 1, + 3 => 4, + 4 => 5, + 5 => 5, + 6 => 5, + 7 => 4, + _ => 0, + } +} + +enum AttackTimer { + Idle, + Attack(u16), + Cooldown(u16), +} + +struct Player<'a> { + entity: Entity<'a>, + facing: Tri, + state: PlayerState, + sprite_offset: u16, + attack_timer: AttackTimer, + damage_cooldown: u16, + sword: SwordState, + fudge_factor: Vector2D, + hurtbox: Option>, + controllable: bool, +} + +impl<'a> Player<'a> { + fn new(object_controller: &'a ObjectController) -> Player { + let mut entity = Entity::new( + object_controller, + Rect::new((0_u16, 0_u16).into(), (4_u16, 12_u16).into()), + ); + let s = object_controller.sprite(LONG_SWORD_IDLE.sprite(0)); + entity.sprite.set_sprite(s); + entity.sprite.show(); + entity.position = (144, 0).into(); + + Player { + entity, + facing: Tri::Positive, + state: PlayerState::OnGround, + sword: SwordState::LongSword, + sprite_offset: 0, + attack_timer: AttackTimer::Idle, + fudge_factor: (0, 0).into(), + hurtbox: None, + damage_cooldown: 0, + controllable: true, + } + } + + fn update( + &mut self, + controller: &'a ObjectController, + buttons: &ButtonController, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + let mut instruction = UpdateInstruction::None; + + let x = if self.controllable { + buttons.x_tri() + } else { + Tri::Zero + }; + + let b_press = buttons.is_just_pressed(Button::B) && self.controllable; + let a_press = buttons.is_just_pressed(Button::A) && self.controllable; + + self.fudge_factor = (0, 0).into(); + let mut hurtbox = None; + + match self.state { + PlayerState::OnGround => { + self.entity.velocity.y = 0.into(); + self.entity.velocity.x = self.entity.velocity.x * 40 / 64; + + match &mut self.attack_timer { + AttackTimer::Idle => { + if x != Tri::Zero { + self.facing = x; + } + self.entity.sprite.set_hflip(self.facing == Tri::Negative); + self.entity.velocity.x += self.sword.ground_walk_force() * x as i32; + if self.entity.velocity.x.abs() > Number::new(1) / 10 { + let sprite = + controller.sprite(self.sword.walk_animation(self.sprite_offset)); + self.entity.sprite.set_sprite(sprite); + } else { + let sprite = + controller.sprite(self.sword.idle_animation(self.sprite_offset)); + self.entity.sprite.set_sprite(sprite); + } + + if b_press && self.sword != SwordState::Swordless { + self.attack_timer = AttackTimer::Attack(self.sword.attack_duration()); + sfx.sword(); + } else if a_press { + self.entity.velocity.y -= self.sword.jump_impulse(); + self.state = PlayerState::InAir; + self.sprite_offset = 0; + + sfx.jump(); + } + } + AttackTimer::Attack(a) => { + *a -= 1; + let frame = self.sword.attack_frame(*a); + self.fudge_factor.x = self.sword.fudge(frame) * self.facing as i32; + let tag = self.sword.attack_tag(); + let sprite = controller.sprite(tag.animation_sprite(frame as usize)); + self.entity.sprite.set_sprite(sprite); + + hurtbox = self.sword.ground_attack_hurtbox(frame); + + if *a == 0 { + self.attack_timer = AttackTimer::Cooldown(self.sword.cooldown_time()); + } + } + AttackTimer::Cooldown(a) => { + *a -= 1; + let frame = self.sword.hold_frame(); + self.fudge_factor.x = self.sword.fudge(frame) * self.facing as i32; + let tag = self.sword.attack_tag(); + let sprite = controller.sprite(tag.animation_sprite(frame as usize)); + self.entity.sprite.set_sprite(sprite); + if *a == 0 { + self.attack_timer = AttackTimer::Idle; + } + } + } + } + PlayerState::InAir => { + self.entity.velocity.x = self.entity.velocity.x * 63 / 64; + + match &mut self.attack_timer { + AttackTimer::Idle => { + let frame = if self.sprite_offset < 3 * 4 { + self.sprite_offset / 4 + } else if self.entity.velocity.y.abs() < Number::new(1) / 5 { + 3 + } else if self.entity.velocity.y > 1.into() { + 5 + } else if self.entity.velocity.y > 0.into() { + 4 + } else { + 2 + }; + let tag = self.sword.jump_tag(); + let sprite = controller.sprite(tag.animation_sprite(frame as usize)); + self.entity.sprite.set_sprite(sprite); + + if x != Tri::Zero { + self.facing = x; + } + self.entity.sprite.set_hflip(self.facing == Tri::Negative); + self.entity.velocity.x += self.sword.air_move_force() * x as i32; + + if b_press + && self.sword != SwordState::LongSword + && self.sword != SwordState::Swordless + { + sfx.sword(); + self.attack_timer = + AttackTimer::Attack(self.sword.jump_attack_duration()); + } + } + AttackTimer::Attack(a) => { + *a -= 1; + let frame = self.sword.jump_attack_frame(*a); + let tag = self.sword.jump_attack_tag(); + let sprite = controller.sprite(tag.animation_sprite(frame as usize)); + self.entity.sprite.set_sprite(sprite); + + hurtbox = self.sword.air_attack_hurtbox(frame); + + if *a == 0 { + self.attack_timer = AttackTimer::Idle; + } + } + AttackTimer::Cooldown(_) => { + self.attack_timer = AttackTimer::Idle; + } + } + } + } + let gravity: Number = 1.into(); + let gravity = gravity / 16; + self.entity.velocity.y += gravity; + + let fudge_number = (self.fudge_factor.x, self.fudge_factor.y).into(); + + // convert the hurtbox to a location in the game + self.hurtbox = hurtbox.map(|h| { + let mut b = Rect::new(h.position - (8, 8).into(), h.size); + if self.facing == Tri::Negative { + b.position.x = -b.position.x - b.size.x; + } + b.position += self.entity.position + fudge_number; + b + }); + + let prior_y_velocity = self.entity.velocity.y; + self.entity.update_position(level); + let (_, collided_down) = self + .entity + .collision_in_direction((0, 1).into(), 1.into(), |v| level.collides(v)); + + if collided_down { + if self.state == PlayerState::InAir && prior_y_velocity > 2.into() { + instruction = UpdateInstruction::CreateParticle( + ParticleData::new_dust(), + self.entity.position + (2 * self.facing as i32, 0).into(), + ); + + sfx.player_land(); + } + + self.state = PlayerState::OnGround; + } else { + self.state = PlayerState::InAir; + } + + if self.damage_cooldown > 0 { + self.damage_cooldown -= 1; + } + + self.sprite_offset += 1; + + instruction + } + + // returns true if the player is alive and false otherwise + fn damage(&mut self) -> (bool, bool) { + if self.damage_cooldown != 0 { + return (true, false); + } + + self.damage_cooldown = 120; + let new_sword = match self.sword { + SwordState::LongSword => Some(SwordState::ShortSword), + SwordState::ShortSword => Some(SwordState::Dagger), + SwordState::Dagger => None, + SwordState::Swordless => Some(SwordState::Swordless), + }; + if let Some(sword) = new_sword { + self.sword = sword; + (true, true) + } else { + (false, true) + } + } + + fn heal(&mut self) { + let new_sword = match self.sword { + SwordState::LongSword => None, + SwordState::ShortSword => Some(SwordState::LongSword), + SwordState::Dagger => Some(SwordState::ShortSword), + SwordState::Swordless => Some(SwordState::Swordless), + }; + + if let Some(sword) = new_sword { + self.sword = sword; + } + + self.damage_cooldown = 30; + } + + fn commit(&mut self, offset: Vector2D) { + self.entity.commit_with_fudge(offset, self.fudge_factor); + } +} + +enum EnemyData { + Slime(SlimeData), + Bat(BatData), + MiniFlame(MiniFlameData), + Emu(EmuData), +} + +struct BatData { + sprite_offset: u16, + bat_state: BatState, +} + +enum BatState { + Idle, + Chasing(u16), + Dead, +} + +struct SlimeData { + sprite_offset: u16, + slime_state: SlimeState, +} + +impl BatData { + fn new() -> Self { + Self { + sprite_offset: 0, + bat_state: BatState::Idle, + } + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + let mut instruction = UpdateInstruction::None; + let should_die = player + .hurtbox + .as_ref() + .map(|hurtbox| hurtbox.touches(entity.collider())) + .unwrap_or(false); + let should_damage = entity.collider().touches(player.entity.collider()); + + const BAT_IDLE: &Tag = TAG_MAP.get("bat"); + + match &mut self.bat_state { + BatState::Idle => { + self.sprite_offset += 1; + if self.sprite_offset >= 9 * 8 { + self.sprite_offset = 0; + } + + if self.sprite_offset == 8 * 5 { + sfx.bat_flap(); + } + + let sprite = BAT_IDLE.sprite(self.sprite_offset as usize / 8); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + if (entity.position - player.entity.position).manhattan_distance() < 50.into() { + self.bat_state = BatState::Chasing(300); + self.sprite_offset /= 4; + } + + if should_die { + self.bat_state = BatState::Dead; + sfx.bat_death(); + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + + entity.velocity *= Number::new(15) / 16; + entity.update_position(level); + } + BatState::Chasing(count) => { + self.sprite_offset += 1; + + let speed = Number::new(1) / Number::new(4); + let target_velocity = player.entity.position - entity.position; + if target_velocity.manhattan_distance() > 1.into() { + entity.velocity = target_velocity.normalise() * speed; + } else { + entity.velocity = (0, 0).into(); + } + + if self.sprite_offset >= 9 * 2 { + self.sprite_offset = 0; + } + + let sprite = BAT_IDLE.sprite(self.sprite_offset as usize / 2); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + if self.sprite_offset == 2 * 5 { + sfx.bat_flap(); + } + + entity.update_position(level); + + if *count == 0 { + self.bat_state = BatState::Idle; + self.sprite_offset *= 4; + } else { + *count -= 1; + } + + if should_die { + self.bat_state = BatState::Dead; + sfx.bat_death(); + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + } + BatState::Dead => { + const BAT_DEAD: &Tag = TAG_MAP.get("bat dead"); + let sprite = BAT_DEAD.sprite(0); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + let gravity: Number = 1.into(); + let gravity = gravity / 16; + entity.velocity.x = 0.into(); + + entity.velocity.y += gravity; + + let original_y_velocity = entity.velocity.y; + let move_amount = entity.update_position(level); + + let just_landed = move_amount.y != 0.into() && original_y_velocity != move_amount.y; + + if just_landed { + instruction = UpdateInstruction::CreateParticle( + ParticleData::new_health(), + entity.position, + ); + } + } + } + instruction + } +} + +enum SlimeState { + Idle, + Chasing(Tri), + Dead(u16), +} + +impl SlimeData { + fn new() -> Self { + Self { + sprite_offset: 0, + slime_state: SlimeState::Idle, + } + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + let mut instruction = UpdateInstruction::None; + + let should_die = player + .hurtbox + .as_ref() + .map(|h| h.touches(entity.collider())) + .unwrap_or(false); + let should_damage = entity.collider().touches(player.entity.collider()); + + match &mut self.slime_state { + SlimeState::Idle => { + self.sprite_offset += 1; + if self.sprite_offset >= 32 { + self.sprite_offset = 0; + } + + const IDLE: &Tag = TAG_MAP.get("slime idle"); + + let sprite = IDLE.sprite(self.sprite_offset as usize / 16); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + if (player.entity.position - entity.position).manhattan_distance() < 40.into() { + let direction = match player.entity.position.x.cmp(&entity.position.x) { + Ordering::Equal => Tri::Zero, + Ordering::Greater => Tri::Positive, + Ordering::Less => Tri::Negative, + }; + + self.slime_state = SlimeState::Chasing(direction); + self.sprite_offset = 0; + } + if should_die { + self.slime_state = SlimeState::Dead(0); + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer + } + + let gravity: Number = 1.into(); + let gravity = gravity / 16; + entity.velocity.y += gravity; + entity.velocity *= Number::new(15) / 16; + entity.update_position(level); + } + SlimeState::Chasing(direction) => { + self.sprite_offset += 1; + if self.sprite_offset >= 7 * 6 { + self.slime_state = SlimeState::Idle; + } else { + let frame = ping_pong(self.sprite_offset / 6, 5); + + if frame == 0 { + sfx.slime_boing(); + } + + const CHASE: &Tag = TAG_MAP.get("Slime jump"); + + let sprite = CHASE.sprite(frame as usize); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + entity.velocity.x = match frame { + 2 | 3 | 4 => (Number::new(1) / 5) * Number::new(*direction as i32), + _ => 0.into(), + }; + + let gravity: Number = 1.into(); + let gravity = gravity / 16; + entity.velocity.y += gravity; + + let updated_position = entity.update_position(level); + if updated_position.y > 0.into() && self.sprite_offset > 2 * 6 { + // we're falling + self.sprite_offset = 6 * 6; + } + } + if should_die { + self.slime_state = SlimeState::Dead(0); + sfx.slime_dead(); + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer + } + } + SlimeState::Dead(count) => { + if *count < 5 * 4 { + const DEATH: &Tag = TAG_MAP.get("Slime death"); + let sprite = DEATH.sprite(*count as usize / 4); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + *count += 1; + } else { + return UpdateInstruction::Remove; + } + } + } + instruction + } +} + +enum MiniFlameState { + Idle(u16), + Chasing(u16), + Dead, +} + +struct MiniFlameData { + state: MiniFlameState, + sprite_offset: u16, +} + +impl MiniFlameData { + fn new() -> Self { + Self { + state: MiniFlameState::Chasing(90), + sprite_offset: 0, + } + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + _level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + let mut instruction = UpdateInstruction::None; + + let should_die = player + .hurtbox + .as_ref() + .map(|h| h.touches(entity.collider())) + .unwrap_or(false); + let should_damage = entity.collider().touches(player.entity.collider()); + + self.sprite_offset += 1; + + const ANGRY: &Tag = TAG_MAP.get("angry boss"); + + match &mut self.state { + MiniFlameState::Idle(frames) => { + *frames -= 1; + + if *frames == 0 { + let resulting_direction = player.entity.position - entity.position; + if resulting_direction.manhattan_distance() < 1.into() { + self.state = MiniFlameState::Idle(30); + } else { + sfx.flame_charge(); + self.state = MiniFlameState::Chasing(90); + entity.velocity = resulting_direction.normalise() * Number::new(2); + } + } else { + let sprite = ANGRY.animation_sprite(self.sprite_offset as usize / 8); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + + entity.velocity = (0.into(), Number::new(-1) / Number::new(4)).into(); + } + + if should_die { + self.sprite_offset = 0; + self.state = MiniFlameState::Dead; + + if rng::gen() % 4 == 0 { + instruction = UpdateInstruction::CreateParticle( + ParticleData::new_health(), + entity.position, + ); + } + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + } + MiniFlameState::Chasing(frame) => { + entity.velocity *= Number::new(63) / Number::new(64); + + if *frame == 0 { + self.state = MiniFlameState::Idle(30); + } else { + *frame -= 1; + } + + if should_die { + self.sprite_offset = 0; + self.state = MiniFlameState::Dead; + + if rng::gen() % 4 == 0 { + instruction = UpdateInstruction::CreateParticle( + ParticleData::new_health(), + entity.position, + ); + } + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + + if entity.velocity.manhattan_distance() < Number::new(1) / Number::new(4) { + self.state = MiniFlameState::Idle(90); + } + + let sprite = ANGRY.animation_sprite(self.sprite_offset as usize / 2); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + } + MiniFlameState::Dead => { + entity.velocity = (0, 0).into(); + if self.sprite_offset >= 6 * 12 { + instruction = UpdateInstruction::Remove; + } + + const DEATH: &Tag = TAG_MAP.get("angry boss dead"); + + let sprite = DEATH.animation_sprite(self.sprite_offset as usize / 12); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + + self.sprite_offset += 1; + } + }; + + entity.update_position_without_collision(); + + instruction + } +} + +enum EmuState { + Idle, + Charging(Tri), + Knockback, + Dead, +} + +struct EmuData { + state: EmuState, + sprite_offset: u16, +} + +impl EmuData { + fn new() -> Self { + Self { + state: EmuState::Idle, + sprite_offset: 0, + } + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + let mut instruction = UpdateInstruction::None; + + let should_die = player + .hurtbox + .as_ref() + .map(|h| h.touches(entity.collider())) + .unwrap_or(false); + let should_damage = entity.collider().touches(player.entity.collider()); + + match &mut self.state { + EmuState::Idle => { + self.sprite_offset += 1; + + if self.sprite_offset >= 3 * 16 { + self.sprite_offset = 0; + } + + const IDLE: &Tag = TAG_MAP.get("emu - idle"); + + let sprite = IDLE.sprite(self.sprite_offset as usize / 16); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + + if (entity.position.y - player.entity.position.y).abs() < 10.into() { + let velocity = Number::new(1) + * (player.entity.position.x - entity.position.x) + .to_raw() + .signum(); + entity.velocity.x = velocity; + + match velocity.cmp(&0.into()) { + Ordering::Greater => { + entity.sprite.set_hflip(true); + self.state = EmuState::Charging(Tri::Positive); + } + Ordering::Less => { + self.state = EmuState::Charging(Tri::Negative); + entity.sprite.set_hflip(false); + } + Ordering::Equal => { + self.state = EmuState::Idle; + } + } + } + + if should_die { + self.sprite_offset = 0; + self.state = EmuState::Dead; + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + } + EmuState::Charging(direction) => { + let direction = Number::new(*direction as i32); + self.sprite_offset += 1; + + if self.sprite_offset >= 4 * 2 { + self.sprite_offset = 0; + } + + if self.sprite_offset == 2 * 2 { + sfx.emu_step(); + } + + const WALK: &Tag = TAG_MAP.get("emu-walk"); + + let sprite = WALK.sprite(self.sprite_offset as usize / 2); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + + let gravity: Number = 1.into(); + let gravity = gravity / 16; + entity.velocity.y += gravity; + + let distance_traveled = entity.update_position(level); + + if distance_traveled.x == 0.into() { + sfx.emu_crash(); + self.state = EmuState::Knockback; + entity.velocity = (-direction / 2, Number::new(-1)).into(); + } + + if should_die { + self.sprite_offset = 0; + self.state = EmuState::Dead; + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + } + EmuState::Knockback => { + let gravity: Number = 1.into(); + let gravity = gravity / 16; + entity.velocity.y += gravity; + + entity.update_position(level); + let (_, is_collision) = + entity.collision_in_direction((0, 1).into(), gravity, |x| level.collides(x)); + + if is_collision { + entity.velocity.x = 0.into(); + self.state = EmuState::Idle; + } + + if should_die { + self.sprite_offset = 0; + self.state = EmuState::Dead; + } else if should_damage { + instruction = UpdateInstruction::DamagePlayer; + } + } + EmuState::Dead => { + if self.sprite_offset == 0 { + sfx.emu_death(); + } + + if self.sprite_offset >= 8 * 4 { + instruction = UpdateInstruction::Remove; + } + + const DEATH: &Tag = TAG_MAP.get("emu - die"); + + let sprite = DEATH.animation_sprite(self.sprite_offset as usize / 4); + let sprite = controller.sprite(sprite); + entity.sprite.set_sprite(sprite); + + self.sprite_offset += 1; + } + } + + instruction + } +} + +enum UpdateInstruction { + None, + HealBossAndRemove, + HealPlayerAndRemove, + Remove, + DamagePlayer, + CreateParticle(ParticleData, Vector2D), +} + +impl EnemyData { + fn collision_mask(&self) -> Rect { + match self { + EnemyData::Slime(_) => Rect::new((0u16, 0u16).into(), (4u16, 11u16).into()), + EnemyData::Bat(_) => Rect::new((0u16, 0u16).into(), (12u16, 4u16).into()), + EnemyData::MiniFlame(_) => Rect::new((0u16, 0u16).into(), (12u16, 12u16).into()), + EnemyData::Emu(_) => Rect::new((0u16, 0u16).into(), (7u16, 11u16).into()), + } + } + + fn sprite(&self) -> &'static Sprite { + const SLIME: &Tag = TAG_MAP.get("slime idle"); + const BAT: &Tag = TAG_MAP.get("bat"); + const MINI_FLAME: &Tag = TAG_MAP.get("angry boss"); + const EMU: &Tag = TAG_MAP.get("emu - idle"); + match self { + EnemyData::Slime(_) => SLIME.sprite(0), + EnemyData::Bat(_) => BAT.sprite(0), + EnemyData::MiniFlame(_) => MINI_FLAME.sprite(0), + EnemyData::Emu(_) => EMU.sprite(0), + } + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + match self { + EnemyData::Slime(data) => data.update(controller, entity, player, level, sfx), + EnemyData::Bat(data) => data.update(controller, entity, player, level, sfx), + EnemyData::MiniFlame(data) => data.update(controller, entity, player, level, sfx), + EnemyData::Emu(data) => data.update(controller, entity, player, level, sfx), + } + } +} + +struct Enemy<'a> { + entity: Entity<'a>, + enemy_data: EnemyData, +} + +impl<'a> Enemy<'a> { + fn new(object_controller: &'a ObjectController, enemy_data: EnemyData) -> Self { + let mut entity = Entity::new(object_controller, enemy_data.collision_mask()); + + let sprite = enemy_data.sprite(); + let sprite = object_controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + entity.sprite.show(); + + Self { entity, enemy_data } + } + + fn update( + &mut self, + controller: &'a ObjectController, + player: &Player, + level: &Level, + sfx: &mut sfx::Sfx, + ) -> UpdateInstruction { + self.enemy_data + .update(controller, &mut self.entity, player, level, sfx) + } +} + +enum ParticleData { + Dust(u16), + Health(u16), + BossHealer(u16, Vector2D), +} + +impl ParticleData { + fn new_dust() -> Self { + Self::Dust(0) + } + + fn new_health() -> Self { + Self::Health(0) + } + + fn new_boss_healer(target: Vector2D) -> Self { + Self::BossHealer(0, target) + } + + fn update<'a>( + &mut self, + controller: &'a ObjectController, + entity: &mut Entity<'a>, + player: &Player, + _level: &Level, + ) -> UpdateInstruction { + match self { + ParticleData::Dust(frame) => { + if *frame == 8 * 3 { + return UpdateInstruction::Remove; + } + + const DUST: &Tag = TAG_MAP.get("dust"); + let sprite = DUST.sprite(*frame as usize / 3); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + *frame += 1; + UpdateInstruction::None + } + ParticleData::Health(frame) => { + if *frame > 8 * 3 * 6 { + return UpdateInstruction::Remove; // have played the animation 6 times + } + + const HEALTH: &Tag = TAG_MAP.get("Heath"); + let sprite = HEALTH.animation_sprite(*frame as usize / 3); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + if *frame < 8 * 3 * 3 { + entity.velocity.y = Number::new(-1) / 2; + } else { + let speed = Number::new(2); + let target_velocity = player.entity.position - entity.position; + + if target_velocity.manhattan_distance() < 5.into() { + return UpdateInstruction::HealPlayerAndRemove; + } + + entity.velocity = target_velocity.normalise() * speed; + } + + entity.update_position_without_collision(); + + *frame += 1; + + UpdateInstruction::None + } + ParticleData::BossHealer(frame, target) => { + const HEALTH: &Tag = TAG_MAP.get("Heath"); + let sprite = HEALTH.animation_sprite(*frame as usize / 3); + let sprite = controller.sprite(sprite); + + entity.sprite.set_sprite(sprite); + + if *frame < 8 * 3 * 3 { + entity.velocity.y = Number::new(-1) / 2; + } else if *frame < 8 * 3 * 6 { + entity.velocity = (0, 0).into(); + } else { + let speed = Number::new(4); + let target_velocity = *target - entity.position; + + if target_velocity.manhattan_distance() < 5.into() { + return UpdateInstruction::HealBossAndRemove; + } + + entity.velocity = target_velocity.normalise() * speed; + } + + entity.update_position_without_collision(); + + *frame += 1; + UpdateInstruction::None + } + } + } +} + +struct Particle<'a> { + entity: Entity<'a>, + particle_data: ParticleData, +} + +impl<'a> Particle<'a> { + fn new( + object_controller: &'a ObjectController, + particle_data: ParticleData, + position: Vector2D, + ) -> Self { + let mut entity = Entity::new( + object_controller, + Rect::new((0u16, 0u16).into(), (0u16, 0u16).into()), + ); + + entity.position = position; + + Self { + entity, + particle_data, + } + } + + fn update( + &mut self, + controller: &'a ObjectController, + player: &Player, + level: &Level, + ) -> UpdateInstruction { + self.entity.sprite.show(); + self.particle_data + .update(controller, &mut self.entity, player, level) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum GameStatus { + Continue, + Lost, + RespawnAtBoss, +} + +enum BossState<'a> { + NotSpawned, + Active(Boss<'a>), + Following(FollowingBoss<'a>), +} + +impl<'a> BossState<'a> { + fn update( + &mut self, + enemies: &mut Arena>, + object_controller: &'a ObjectController, + player: &Player, + sfx: &mut sfx::Sfx, + ) -> BossInstruction { + match self { + BossState::Active(boss) => boss.update(enemies, object_controller, player, sfx), + BossState::Following(boss) => { + boss.update(object_controller, player); + BossInstruction::None + } + BossState::NotSpawned => BossInstruction::None, + } + } + fn commit(&mut self, offset: Vector2D) { + match self { + BossState::Active(boss) => { + boss.commit(offset); + } + BossState::Following(boss) => { + boss.commit(offset); + } + BossState::NotSpawned => {} + } + } +} + +struct FollowingBoss<'a> { + entity: Entity<'a>, + following: bool, + to_hole: bool, + timer: u32, + gone: bool, +} + +impl<'a> FollowingBoss<'a> { + fn new(object_controller: &'a ObjectController, position: Vector2D) -> Self { + let mut entity = Entity::new( + object_controller, + Rect::new((0_u16, 0_u16).into(), (0_u16, 0_u16).into()), + ); + entity.position = position; + + Self { + entity, + following: true, + timer: 0, + to_hole: false, + gone: false, + } + } + fn update(&mut self, controller: &'a ObjectController, player: &Player) { + let difference = player.entity.position - self.entity.position; + self.timer += 1; + + let frame = if self.to_hole { + let target: Vector2D = (17 * 8, -3 * 8).into(); + let difference = target - self.entity.position; + if difference.manhattan_distance() < 1.into() { + self.gone = true; + } else { + self.entity.velocity = difference.normalise() * 2; + } + + self.timer / 8 + } else if self.timer < 120 { + self.timer / 20 + } else if self.following { + self.entity.velocity = difference / 16; + if difference.manhattan_distance() < 20.into() { + self.following = false; + } + self.timer / 8 + } else { + self.entity.velocity = (0, 0).into(); + if difference.manhattan_distance() > 60.into() { + self.following = true; + } + self.timer / 16 + }; + + const BOSS: &Tag = TAG_MAP.get("happy boss"); + + let sprite = BOSS.animation_sprite(frame as usize); + let sprite = controller.sprite(sprite); + + self.entity.sprite.set_sprite(sprite); + + self.entity.update_position_without_collision(); + } + + fn commit(&mut self, offset: Vector2D) { + self.entity.commit_with_fudge(offset, (0, 0).into()); + } +} + +enum BossActiveState { + Damaged(u8), + MovingToTarget, + WaitingUntilExplosion(u8), + WaitingUntilDamaged(u16), + WaitUntilKilled, +} + +struct Boss<'a> { + entity: Entity<'a>, + health: u8, + target_location: u8, + state: BossActiveState, + timer: u32, + screen_coords: Vector2D, + shake_magnitude: Number, +} + +enum BossInstruction { + None, + Dead, +} + +impl<'a> Boss<'a> { + fn new(object_controller: &'a ObjectController, screen_coords: Vector2D) -> Self { + let mut entity = Entity::new( + object_controller, + Rect::new((0_u16, 0_u16).into(), (28_u16, 28_u16).into()), + ); + entity.position = screen_coords + (144, 136).into(); + Self { + entity, + health: 5, + target_location: rng::gen().rem_euclid(5) as u8, + state: BossActiveState::Damaged(60), + timer: 0, + screen_coords, + shake_magnitude: 0.into(), + } + } + fn update( + &mut self, + enemies: &mut Arena>, + object_controller: &'a ObjectController, + player: &Player, + sfx: &mut sfx::Sfx, + ) -> BossInstruction { + let mut instruction = BossInstruction::None; + match &mut self.state { + BossActiveState::Damaged(time) => { + *time -= 1; + if *time == 0 { + self.target_location = self.get_next_target_location(); + self.state = BossActiveState::MovingToTarget; + sfx.boss_move(); + } + } + BossActiveState::MovingToTarget => { + let target = self.get_target_location() + self.screen_coords; + let difference = target - self.entity.position; + if difference.manhattan_distance() < 1.into() { + self.entity.velocity = (0, 0).into(); + self.state = BossActiveState::WaitingUntilExplosion(60); + } else { + self.entity.velocity = difference / 16; + } + } + BossActiveState::WaitingUntilExplosion(time) => { + *time -= 1; + if *time == 0 { + if self.health == 0 { + enemies.clear(); + instruction = BossInstruction::Dead; + self.state = BossActiveState::WaitUntilKilled; + } else { + sfx.burning(); + self.explode(enemies, object_controller); + self.state = BossActiveState::WaitingUntilDamaged(60 * 5); + } + } + } + BossActiveState::WaitingUntilDamaged(time) => { + *time -= 1; + if *time == 0 { + sfx.burning(); + self.explode(enemies, object_controller); + self.state = BossActiveState::WaitingUntilDamaged(60 * 5); + } + if let Some(hurt) = &player.hurtbox { + if hurt.touches(self.entity.collider()) { + self.health -= 1; + self.state = BossActiveState::Damaged(30); + } + } + } + BossActiveState::WaitUntilKilled => {} + } + let animation_rate = match self.state { + BossActiveState::Damaged(_) => 6, + BossActiveState::MovingToTarget => 4, + BossActiveState::WaitingUntilExplosion(_) => 3, + BossActiveState::WaitingUntilDamaged(_) => 8, + BossActiveState::WaitUntilKilled => 12, + }; + + self.shake_magnitude = match self.state { + BossActiveState::Damaged(_) => 1.into(), + BossActiveState::MovingToTarget => 0.into(), + BossActiveState::WaitingUntilExplosion(_) => 5.into(), + BossActiveState::WaitingUntilDamaged(time) => { + if time < 60 { + 5.into() + } else { + 0.into() + } + } + BossActiveState::WaitUntilKilled => 3.into(), + }; + self.timer += 1; + let frame = self.timer / animation_rate; + + const BOSS: &Tag = TAG_MAP.get("Boss"); + + let sprite = BOSS.animation_sprite(frame as usize); + let sprite = object_controller.sprite(sprite); + + self.entity.sprite.set_sprite(sprite); + + self.entity.update_position_without_collision(); + instruction + } + fn commit(&mut self, offset: Vector2D) { + let shake = if self.shake_magnitude != 0.into() { + ( + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) + - self.shake_magnitude / 2, + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) + - self.shake_magnitude / 2, + ) + .into() + } else { + (0, 0).into() + }; + + self.entity + .commit_with_size(offset + shake, (32, 32).into()); + } + fn explode(&self, enemies: &mut Arena>, object_controller: &'a ObjectController) { + for _ in 0..(6 - self.health) { + let x_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; + let y_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; + let mut flame = Enemy::new( + object_controller, + EnemyData::MiniFlame(MiniFlameData::new()), + ); + flame.entity.position = self.entity.position; + flame.entity.velocity = (x_offset, y_offset).into(); + enemies.insert(flame); + } + } + + fn get_next_target_location(&self) -> u8 { + loop { + let a = rng::gen().rem_euclid(5) as u8; + if a != self.target_location { + break a; + } + } + } + fn get_target_location(&self) -> Vector2D { + match self.target_location { + 0 => (240 / 4, 160 / 4).into(), + 1 => (3 * 240 / 4, 160 / 4).into(), + 2 => (240 / 4, 3 * 160 / 4).into(), + 3 => (3 * 240 / 4, 3 * 160 / 4).into(), + 4 => (240 / 2, 160 / 2).into(), + _ => unreachable!(), + } + } +} + +struct Game<'a> { + player: Player<'a>, + input: ButtonController, + frame_count: u32, + level: Level<'a>, + offset: Vector2D, + shake_time: u16, + sunrise_timer: u16, + + enemies: Arena>, + particles: Arena>, + slime_load: usize, + bat_load: usize, + emu_load: usize, + boss: BossState<'a>, + move_state: MoveState, + fade_count: u16, +} + +enum MoveState { + Advancing, + PinnedAtEnd, + FollowingPlayer, + Ending, +} + +impl<'a> Game<'a> { + fn has_just_reached_end(&self) -> bool { + match self.boss { + BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH * 8, + _ => false, + } + } + + fn clear(&mut self, vram: &mut VRamManager) { + self.level.clear(vram); + } + + fn advance_frame( + &mut self, + object_controller: &'a ObjectController, + vram: &mut VRamManager, + sfx: &mut sfx::Sfx, + ) -> GameStatus { + let mut state = GameStatus::Continue; + + match self.move_state { + MoveState::Advancing => { + self.offset += Into::>::into((1, 0)) / 8; + + if self.has_just_reached_end() { + sfx.boss(); + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); + self.move_state = MoveState::PinnedAtEnd; + self.boss = BossState::Active(Boss::new(object_controller, self.offset)) + } + } + MoveState::PinnedAtEnd => { + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); + } + MoveState::FollowingPlayer => { + Game::update_sunrise(vram, self.sunrise_timer); + if self.sunrise_timer < 120 { + self.sunrise_timer += 1; + } else { + let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2); + + self.offset.x += difference / 8; + if self.offset.x > (tilemap::WIDTH * 8 - 248).into() { + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); + } else if self.offset.x < 8.into() { + self.offset.x = 8.into(); + self.move_state = MoveState::Ending; + } + } + } + MoveState::Ending => { + self.player.controllable = false; + if let BossState::Following(boss) = &mut self.boss { + boss.to_hole = true; + if boss.gone { + self.fade_count += 1; + self.fade_count = self.fade_count.min(600); + Game::update_fade_out(vram, self.fade_count); + } + } + } + } + + match self + .boss + .update(&mut self.enemies, object_controller, &self.player, sfx) + { + BossInstruction::Dead => { + let boss = match &self.boss { + BossState::Active(b) => b, + _ => unreachable!(), + }; + let new_particle = Particle::new( + object_controller, + ParticleData::new_boss_healer(boss.entity.position), + self.player.entity.position, + ); + self.particles.insert(new_particle); + sfx.stop_music(); + self.player.sword = SwordState::Swordless; + } + BossInstruction::None => {} + } + + self.load_enemies(object_controller); + + if self.player.entity.position.x < self.offset.x - 8 { + let (alive, damaged) = self.player.damage(); + if !alive { + state = GameStatus::Lost; + } + if damaged { + sfx.player_hurt(); + self.shake_time += 20; + } + } + + let mut this_frame_offset = self.offset; + if self.shake_time > 0 { + let size = self.shake_time.min(4) as i32; + let offset: Vector2D = ( + Number::from_raw(rng::gen()) % size - Number::new(size) / 2, + Number::from_raw(rng::gen()) % size - Number::new(size) / 2, + ) + .into(); + this_frame_offset += offset; + self.shake_time -= 1; + } + + self.input.update(); + if let UpdateInstruction::CreateParticle(data, position) = + self.player + .update(object_controller, &self.input, &self.level, sfx) + { + let new_particle = Particle::new(object_controller, data, position); + + self.particles.insert(new_particle); + } + + let mut remove = Vec::with_capacity(10); + for (idx, enemy) in self.enemies.iter_mut() { + if enemy.entity.position.x < self.offset.x - 8 { + remove.push(idx); + continue; + } + + match enemy.update(object_controller, &self.player, &self.level, sfx) { + UpdateInstruction::Remove => { + remove.push(idx); + } + UpdateInstruction::HealPlayerAndRemove => { + self.player.heal(); + sfx.player_heal(); + remove.push(idx); + } + UpdateInstruction::HealBossAndRemove => {} + UpdateInstruction::DamagePlayer => { + let (alive, damaged) = self.player.damage(); + if !alive { + state = GameStatus::Lost; + } + if damaged { + sfx.player_hurt(); + self.shake_time += 20; + } + } + UpdateInstruction::CreateParticle(data, position) => { + let new_particle = Particle::new(object_controller, data, position); + self.particles.insert(new_particle); + } + UpdateInstruction::None => {} + } + enemy + .entity + .commit_with_fudge(this_frame_offset, (0, 0).into()); + } + + self.player.commit(this_frame_offset); + self.boss.commit(this_frame_offset); + + let background_offset = (this_frame_offset.floor().x, 8).into(); + + self.level.background.set_pos(vram, background_offset); + self.level.foreground.set_pos(vram, background_offset); + self.level.clouds.set_pos(vram, background_offset / 4); + + for i in remove { + self.enemies.remove(i); + } + + let mut remove = Vec::with_capacity(10); + + for (idx, particle) in self.particles.iter_mut() { + match particle.update(object_controller, &self.player, &self.level) { + UpdateInstruction::Remove => remove.push(idx), + UpdateInstruction::HealBossAndRemove => { + sfx.sunrise(); + let location = match &self.boss { + BossState::Active(b) => b.entity.position, + _ => unreachable!(), + }; + self.boss = + BossState::Following(FollowingBoss::new(object_controller, location)); + self.move_state = MoveState::FollowingPlayer; + remove.push(idx); + } + UpdateInstruction::HealPlayerAndRemove => { + self.player.heal(); + sfx.player_heal(); + remove.push(idx); + } + UpdateInstruction::DamagePlayer => { + let (alive, damaged) = self.player.damage(); + if !alive { + state = GameStatus::Lost; + } + if damaged { + sfx.player_hurt(); + self.shake_time += 20; + } + } + UpdateInstruction::CreateParticle(_, _) => {} + UpdateInstruction::None => {} + } + particle + .entity + .commit_with_fudge(this_frame_offset, (0, 0).into()); + } + + self.level.background.commit(vram); + self.level.foreground.commit(vram); + self.level.clouds.commit(vram); + + for i in remove { + self.particles.remove(i); + } + + self.frame_count += 1; + if let GameStatus::Lost = state { + match self.boss { + BossState::Active(_) => GameStatus::RespawnAtBoss, + _ => GameStatus::Lost, + } + } else { + state + } + } + + fn load_enemies(&mut self, object_controller: &'a ObjectController) { + if self.slime_load < self.level.slime_spawns.len() { + for (idx, slime_spawn) in self + .level + .slime_spawns + .iter() + .enumerate() + .skip(self.slime_load) + { + if slime_spawn.0 as i32 > self.offset.x.floor() + 300 { + break; + } + self.slime_load = idx + 1; + let mut slime = Enemy::new(object_controller, EnemyData::Slime(SlimeData::new())); + slime.entity.position = (slime_spawn.0 as i32, slime_spawn.1 as i32 - 7).into(); + self.enemies.insert(slime); + } + } + if self.bat_load < self.level.bat_spawns.len() { + for (idx, bat_spawn) in self.level.bat_spawns.iter().enumerate().skip(self.bat_load) { + if bat_spawn.0 as i32 > self.offset.x.floor() + 300 { + break; + } + self.bat_load = idx + 1; + let mut bat = Enemy::new(object_controller, EnemyData::Bat(BatData::new())); + bat.entity.position = (bat_spawn.0 as i32, bat_spawn.1 as i32).into(); + self.enemies.insert(bat); + } + } + if self.emu_load < self.level.emu_spawns.len() { + for (idx, emu_spawn) in self.level.emu_spawns.iter().enumerate().skip(self.emu_load) { + if emu_spawn.0 as i32 > self.offset.x.floor() + 300 { + break; + } + self.emu_load = idx + 1; + let mut emu = Enemy::new(object_controller, EnemyData::Emu(EmuData::new())); + emu.entity.position = (emu_spawn.0 as i32, emu_spawn.1 as i32 - 7).into(); + self.enemies.insert(emu); + } + } + } + + fn update_sunrise(vram: &mut VRamManager, time: u16) { + let mut modified_palette = background::PALETTES[0].clone(); + + let a = modified_palette.colour(0); + let b = modified_palette.colour(1); + + modified_palette.update_colour(0, interpolate_colour(a, 17982, time, 120)); + modified_palette.update_colour(1, interpolate_colour(b, 22427, time, 120)); + + let modified_palettes = [modified_palette]; + + vram.set_background_palettes(&modified_palettes); + } + + fn update_fade_out(vram: &mut VRamManager, time: u16) { + let mut modified_palette = background::PALETTES[0].clone(); + + let c = modified_palette.colour(2); + + modified_palette.update_colour(0, interpolate_colour(17982, 0x7FFF, time, 600)); + modified_palette.update_colour(1, interpolate_colour(22427, 0x7FFF, time, 600)); + modified_palette.update_colour(2, interpolate_colour(c, 0x7FFF, time, 600)); + + let modified_palettes = [modified_palette]; + + vram.set_background_palettes(&modified_palettes); + } + + fn new(object: &'a ObjectController, level: Level<'a>, start_at_boss: bool) -> Self { + let mut player = Player::new(object); + let mut offset = (8, 8).into(); + if start_at_boss { + player.entity.position = (133 * 8, 10 * 8).into(); + offset = (130 * 8, 8).into(); + } + Self { + player, + input: ButtonController::new(), + frame_count: 0, + level, + offset, + shake_time: 0, + + enemies: Arena::with_capacity(100), + slime_load: 0, + bat_load: 0, + emu_load: 0, + particles: Arena::with_capacity(30), + boss: BossState::NotSpawned, + move_state: MoveState::Advancing, + sunrise_timer: 0, + fade_count: 0, + } + } +} + +fn game_with_level(gba: &mut agb::Gba) { + let vblank = agb::interrupt::VBlank::get(); + vblank.wait_for_vblank(); + + let mut mixer = gba.mixer.mixer(Frequency::Hz18157); + mixer.enable(); + + let mut sfx = sfx::Sfx::new(&mut mixer); + sfx.purple_night(); + + let mut start_at_boss = false; + + loop { + let (background, mut vram) = gba.display.video.tiled0(); + + vram.set_background_palettes(background::PALETTES); + + let tileset = TileSet::new(background::background.tiles, TileFormat::FourBpp); + + let object = gba.display.object.get(); + + let backdrop = InfiniteScrolledMap::new( + background.background(Priority::P2, RegularBackgroundSize::Background32x32), + Box::new(|pos| { + ( + &tileset, + TileSetting::from_raw( + *tilemap::BACKGROUND_MAP + .get((pos.x + tilemap::WIDTH * pos.y) as usize) + .unwrap_or(&0), + ), + ) + }), + ); + + let foreground = InfiniteScrolledMap::new( + background.background(Priority::P0, RegularBackgroundSize::Background32x32), + Box::new(|pos| { + ( + &tileset, + TileSetting::from_raw( + *tilemap::FOREGROUND_MAP + .get((pos.x + tilemap::WIDTH * pos.y) as usize) + .unwrap_or(&0), + ), + ) + }), + ); + + let clouds = InfiniteScrolledMap::new( + background.background(Priority::P3, RegularBackgroundSize::Background32x32), + Box::new(|pos| { + ( + &tileset, + TileSetting::from_raw( + *tilemap::CLOUD_MAP + .get((pos.x + tilemap::WIDTH * pos.y) as usize) + .unwrap_or(&0), + ), + ) + }), + ); + + let start_pos = if start_at_boss { + (130 * 8, 8).into() + } else { + (8, 8).into() + }; + + let mut game = Game::new( + &object, + Level::load_level(backdrop, foreground, clouds, start_pos, &mut vram, &mut sfx), + start_at_boss, + ); + + start_at_boss = loop { + sfx.frame(); + vblank.wait_for_vblank(); + sfx.after_vblank(); + object.commit(); + match game.advance_frame(&object, &mut vram, &mut sfx) { + GameStatus::Continue => {} + GameStatus::Lost => { + break false; + } + GameStatus::RespawnAtBoss => { + break true; + } + } + + let _ = rng::gen(); // advance RNG to make it less predictable between runs + }; + + game.clear(&mut vram); + } +} + +mod tilemap { + include!(concat!(env!("OUT_DIR"), "/tilemap.rs")); +} + +pub fn main(mut gba: agb::Gba) -> ! { + loop { + game_with_level(&mut gba); + } +} + +fn ping_pong(i: u16, n: u16) -> u16 { + let cycle = 2 * (n - 1); + let i = i % cycle; + if i >= n { + cycle - i + } else { + i + } +} + +fn interpolate_colour(initial: u16, destination: u16, time_so_far: u16, total_time: u16) -> u16 { + const MASK: u16 = 0b11111; + fn to_components(c: u16) -> [u16; 3] { + [c & MASK, (c >> 5) & MASK, (c >> 10) & MASK] + } + + let initial_rgb = to_components(initial); + let destination_rgb = to_components(destination); + let mut colour = 0; + + for (i, c) in initial_rgb + .iter() + .zip(destination_rgb) + .map(|(a, b)| (b - a) * time_so_far / total_time + a) + .enumerate() + { + colour |= (c & MASK) << (i * 5); + } + colour +} diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index 2cda306e..b87ef11e 100644 --- a/examples/the-purple-night/src/main.rs +++ b/examples/the-purple-night/src/main.rs @@ -4,2342 +4,7 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] -extern crate alloc; - -mod sfx; - -use core::cmp::Ordering; - -use alloc::{boxed::Box, vec::Vec}; - -use agb::{ - display::{ - object::{Graphics, Object, ObjectController, Sprite, Tag, TagMap}, - tiled::{ - InfiniteScrolledMap, RegularBackgroundSize, TileFormat, TileSet, TileSetting, - VRamManager, - }, - Priority, HEIGHT, WIDTH, - }, - fixnum::{FixedNum, Rect, Vector2D}, - input::{Button, ButtonController, Tri}, - interrupt::VBlank, - rng, - sound::mixer::Frequency, -}; -use generational_arena::Arena; -use sfx::Sfx; - -const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/objects.aseprite", "gfx/boss.aseprite"); -const TAG_MAP: &TagMap = GRAPHICS.tags(); - -const LONG_SWORD_IDLE: &Tag = TAG_MAP.get("Idle - longsword"); -const LONG_SWORD_WALK: &Tag = TAG_MAP.get("Walk - longsword"); -const LONG_SWORD_JUMP: &Tag = TAG_MAP.get("Jump - longsword"); -const LONG_SWORD_ATTACK: &Tag = TAG_MAP.get("Attack - longsword"); -const LONG_SWORD_JUMP_ATTACK: &Tag = TAG_MAP.get("Jump attack - longsword"); - -const SHORT_SWORD_IDLE: &Tag = TAG_MAP.get("Idle - shortsword"); -const SHORT_SWORD_WALK: &Tag = TAG_MAP.get("Walk - shortsword"); -const SHORT_SWORD_JUMP: &Tag = TAG_MAP.get("jump - shortsword"); -const SHORT_SWORD_ATTACK: &Tag = TAG_MAP.get("attack - shortsword"); -const SHORT_SWORD_JUMP_ATTACK: &Tag = TAG_MAP.get("jump attack - shortsword"); - -const KNIFE_IDLE: &Tag = TAG_MAP.get("idle - knife"); -const KNIFE_WALK: &Tag = TAG_MAP.get("walk - knife"); -const KNIFE_JUMP: &Tag = TAG_MAP.get("jump - knife"); -const KNIFE_ATTACK: &Tag = TAG_MAP.get("attack - knife"); -const KNIFE_JUMP_ATTACK: &Tag = TAG_MAP.get("jump attack - knife"); - -const SWORDLESS_IDLE: &Tag = TAG_MAP.get("idle swordless"); -const SWORDLESS_WALK: &Tag = TAG_MAP.get("walk swordless"); -const SWORDLESS_JUMP: &Tag = TAG_MAP.get("jump swordless"); -const SWORDLESS_ATTACK: &Tag = KNIFE_ATTACK; -const SWORDLESS_JUMP_ATTACK: &Tag = KNIFE_JUMP_ATTACK; - -agb::include_gfx!("gfx/background.toml"); - -type Number = FixedNum<8>; - -struct Level<'a> { - background: InfiniteScrolledMap<'a>, - foreground: InfiniteScrolledMap<'a>, - clouds: InfiniteScrolledMap<'a>, - - slime_spawns: Vec<(u16, u16)>, - bat_spawns: Vec<(u16, u16)>, - emu_spawns: Vec<(u16, u16)>, -} - -impl<'a> Level<'a> { - fn load_level( - mut backdrop: InfiniteScrolledMap<'a>, - mut foreground: InfiniteScrolledMap<'a>, - mut clouds: InfiniteScrolledMap<'a>, - start_pos: Vector2D, - vram: &mut VRamManager, - sfx: &mut Sfx, - ) -> Self { - let vblank = VBlank::get(); - - let mut between_updates = || { - sfx.frame(); - vblank.wait_for_vblank(); - sfx.after_vblank(); - }; - - backdrop.init(vram, start_pos, &mut between_updates); - foreground.init(vram, start_pos, &mut between_updates); - clouds.init(vram, start_pos / 4, &mut between_updates); - - backdrop.commit(vram); - foreground.commit(vram); - clouds.commit(vram); - - backdrop.show(); - foreground.show(); - clouds.show(); - - let slime_spawns = tilemap::SLIME_SPAWNS_X - .iter() - .enumerate() - .map(|(i, x)| (*x, tilemap::SLIME_SPAWNS_Y[i])) - .collect(); - - let bat_spawns = tilemap::BAT_SPAWNS_X - .iter() - .enumerate() - .map(|(i, x)| (*x, tilemap::BAT_SPAWNS_Y[i])) - .collect(); - - let emu_spawns = tilemap::EMU_SPAWNS_X - .iter() - .enumerate() - .map(|(i, x)| (*x, tilemap::EMU_SPAWNS_Y[i])) - .collect(); - - Self { - background: backdrop, - foreground, - clouds, - - slime_spawns, - bat_spawns, - emu_spawns, - } - } - - fn collides(&self, v: Vector2D) -> Option> { - let factor: Number = Number::new(1) / Number::new(8); - let (x, y) = (v * factor).floor().get(); - - if !(0..=tilemap::WIDTH).contains(&x) || !(0..=tilemap::HEIGHT).contains(&y) { - return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())); - } - let position = tilemap::WIDTH as usize * y as usize + x as usize; - let tile_foreground = tilemap::FOREGROUND_MAP[position]; - let tile_background = tilemap::BACKGROUND_MAP[position]; - let tile_foreground_property = tilemap::TILE_TYPES[tile_foreground as usize]; - let tile_background_property = tilemap::TILE_TYPES[tile_background as usize]; - - if tile_foreground_property == 1 || tile_background_property == 1 { - Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())) - } else { - None - } - } - - fn clear(&mut self, vram: &mut VRamManager) { - self.background.clear(vram); - self.foreground.clear(vram); - self.clouds.clear(vram); - } -} - -struct Entity<'a> { - sprite: Object<'a>, - position: Vector2D, - velocity: Vector2D, - collision_mask: Rect, - visible: bool, -} - -impl<'a> Entity<'a> { - fn new(object_controller: &'a ObjectController, collision_mask: Rect) -> Self { - let s = object_controller.sprite(LONG_SWORD_IDLE.sprite(0)); - let mut sprite = object_controller.object(s); - sprite.set_priority(Priority::P1); - Entity { - sprite, - collision_mask, - position: (0, 0).into(), - velocity: (0, 0).into(), - visible: true, - } - } - - fn update_position(&mut self, level: &Level) -> Vector2D { - let initial_position = self.position; - - let y = self.velocity.y.to_raw().signum(); - if y != 0 { - let (delta, collided) = - self.collision_in_direction((0, y).into(), self.velocity.y.abs(), |v| { - level.collides(v) - }); - self.position += delta; - if collided { - self.velocity.y = 0.into(); - } - } - let x = self.velocity.x.to_raw().signum(); - if x != 0 { - let (delta, collided) = - self.collision_in_direction((x, 0).into(), self.velocity.x.abs(), |v| { - level.collides(v) - }); - self.position += delta; - if collided { - self.velocity.x = 0.into(); - } - } - - self.position - initial_position - } - - fn update_position_without_collision(&mut self) -> Vector2D { - self.position += self.velocity; - - self.velocity - } - - fn collider(&self) -> Rect { - let mut number_collision: Rect = Rect::new( - ( - self.collision_mask.position.x as i32, - self.collision_mask.position.y as i32, - ) - .into(), - ( - self.collision_mask.size.x as i32, - self.collision_mask.size.y as i32, - ) - .into(), - ); - number_collision.position = - self.position + number_collision.position - number_collision.size / 2; - number_collision - } - - fn collision_in_direction( - &mut self, - direction: Vector2D, - distance: Number, - collision: impl Fn(Vector2D) -> Option>, - ) -> (Vector2D, bool) { - let number_collision = self.collider(); - - let center_collision_point: Vector2D = number_collision.position - + number_collision.size / 2 - + number_collision.size.hadamard(direction) / 2; - - let direction_transpose: Vector2D = direction.swap(); - let small = direction_transpose * Number::new(4) / 64; - let triple_collider: [Vector2D; 2] = [ - center_collision_point + number_collision.size.hadamard(direction_transpose) / 2 - - small, - center_collision_point - number_collision.size.hadamard(direction_transpose) / 2 - + small, - ]; - - let original_distance = direction * distance; - let mut final_distance = original_distance; - - let mut has_collided = false; - - for edge_point in triple_collider { - let point = edge_point + original_distance; - if let Some(collider) = collision(point) { - let center = collider.position + collider.size / 2; - let edge = center - collider.size.hadamard(direction) / 2; - let new_distance = (edge - center_collision_point) - .hadamard((direction.x.abs(), direction.y.abs()).into()); - if final_distance.manhattan_distance() > new_distance.manhattan_distance() { - final_distance = new_distance; - } - has_collided = true; - } - } - - (final_distance, has_collided) - } - - fn commit_with_fudge(&mut self, offset: Vector2D, fudge: Vector2D) { - if !self.visible { - self.sprite.hide(); - } else { - let position = (self.position - offset).floor() + fudge; - self.sprite.set_position(position - (8, 8).into()); - if position.x < -8 - || position.x > WIDTH + 8 - || position.y < -8 - || position.y > HEIGHT + 8 - { - self.sprite.hide(); - } else { - self.sprite.show(); - } - } - } - - fn commit_with_size(&mut self, offset: Vector2D, size: Vector2D) { - if !self.visible { - self.sprite.hide(); - } else { - let position = (self.position - offset).floor(); - self.sprite.set_position(position - size / 2); - if position.x < -8 - || position.x > WIDTH + 8 - || position.y < -8 - || position.y > HEIGHT + 8 - { - self.sprite.hide(); - } else { - self.sprite.show(); - } - } - } -} - -#[derive(PartialEq, Eq)] -enum PlayerState { - OnGround, - InAir, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SwordState { - LongSword, - ShortSword, - Dagger, - Swordless, -} - -impl SwordState { - fn ground_walk_force(self) -> Number { - match self { - SwordState::LongSword => Number::new(4) / 16, - SwordState::ShortSword => Number::new(5) / 16, - SwordState::Dagger => Number::new(6) / 16, - SwordState::Swordless => Number::new(6) / 16, - } - } - fn jump_impulse(self) -> Number { - match self { - SwordState::LongSword => Number::new(32) / 16, - SwordState::ShortSword => Number::new(35) / 16, - SwordState::Dagger => Number::new(36) / 16, - SwordState::Swordless => Number::new(42) / 16, - } - } - fn air_move_force(self) -> Number { - match self { - SwordState::LongSword => Number::new(4) / 256, - SwordState::ShortSword => Number::new(5) / 256, - SwordState::Dagger => Number::new(6) / 256, - SwordState::Swordless => Number::new(6) / 256, - } - } - fn idle_animation(self, counter: u16) -> &'static Sprite { - let counter = counter as usize; - match self { - SwordState::LongSword => LONG_SWORD_IDLE.animation_sprite(counter / 8), - SwordState::ShortSword => SHORT_SWORD_IDLE.animation_sprite(counter / 8), - SwordState::Dagger => KNIFE_IDLE.animation_sprite(counter / 8), - SwordState::Swordless => SWORDLESS_IDLE.animation_sprite(counter / 8), - } - } - fn jump_tag(self) -> &'static Tag { - match self { - SwordState::LongSword => LONG_SWORD_JUMP, - SwordState::ShortSword => SHORT_SWORD_JUMP, - SwordState::Dagger => KNIFE_JUMP, - SwordState::Swordless => SWORDLESS_JUMP, - } - } - fn walk_animation(self, counter: u16) -> &'static Sprite { - let counter = counter as usize; - match self { - SwordState::LongSword => LONG_SWORD_WALK.animation_sprite(counter / 4), - SwordState::ShortSword => SHORT_SWORD_WALK.animation_sprite(counter / 4), - SwordState::Dagger => KNIFE_WALK.animation_sprite(counter / 4), - SwordState::Swordless => SWORDLESS_WALK.animation_sprite(counter / 4), - } - } - fn attack_duration(self) -> u16 { - match self { - SwordState::LongSword => 60, - SwordState::ShortSword => 40, - SwordState::Dagger => 20, - SwordState::Swordless => 0, - } - } - fn jump_attack_duration(self) -> u16 { - match self { - SwordState::LongSword => 34, - SwordState::ShortSword => 28, - SwordState::Dagger => 20, - SwordState::Swordless => 0, - } - } - fn attack_frame(self, timer: u16) -> u16 { - match self { - SwordState::LongSword => (self.attack_duration().saturating_sub(timer)) / 8, - SwordState::ShortSword => (self.attack_duration().saturating_sub(timer)) / 8, - SwordState::Dagger => (self.attack_duration().saturating_sub(timer)) / 8, - SwordState::Swordless => (self.attack_duration().saturating_sub(timer)) / 8, - } - } - fn jump_attack_tag(self) -> &'static Tag { - match self { - SwordState::LongSword => LONG_SWORD_JUMP_ATTACK, - SwordState::ShortSword => SHORT_SWORD_JUMP_ATTACK, - SwordState::Dagger => KNIFE_JUMP_ATTACK, - SwordState::Swordless => SWORDLESS_JUMP_ATTACK, - } - } - fn jump_attack_frame(self, timer: u16) -> u16 { - (self.jump_attack_duration().saturating_sub(timer)) / 8 - } - fn hold_frame(self) -> u16 { - 7 - } - - fn cooldown_time(self) -> u16 { - match self { - SwordState::LongSword => 20, - SwordState::ShortSword => 10, - SwordState::Dagger => 1, - SwordState::Swordless => 0, - } - } - fn attack_tag(self) -> &'static Tag { - match self { - SwordState::LongSword => LONG_SWORD_ATTACK, - SwordState::ShortSword => SHORT_SWORD_ATTACK, - SwordState::Dagger => KNIFE_ATTACK, - SwordState::Swordless => SWORDLESS_ATTACK, - } - } - - fn fudge(self, frame: u16) -> i32 { - match self { - SwordState::LongSword => long_sword_fudge(frame), - SwordState::ShortSword => short_sword_fudge(frame), - SwordState::Dagger => 0, - SwordState::Swordless => 0, - } - } - // origin at top left pre fudge boxes - fn ground_attack_hurtbox(self, frame: u16) -> Option> { - match self { - SwordState::LongSword => long_sword_hurtbox(frame), - SwordState::ShortSword => short_sword_hurtbox(frame), - SwordState::Dagger => dagger_hurtbox(frame), - SwordState::Swordless => None, - } - } - fn air_attack_hurtbox(self, _frame: u16) -> Option> { - Some(Rect::new((0, 0).into(), (16, 16).into())) - } -} - -fn dagger_hurtbox(_frame: u16) -> Option> { - Some(Rect::new((9, 5).into(), (7, 9).into())) -} - -fn long_sword_hurtbox(frame: u16) -> Option> { - match frame { - 0 => Some(Rect::new((1, 10).into(), (6, 3).into())), - 1 => Some(Rect::new((0, 9).into(), (7, 2).into())), - 2 => Some(Rect::new((0, 1).into(), (6, 8).into())), - 3 => Some(Rect::new((3, 0).into(), (6, 8).into())), - 4 => Some(Rect::new((6, 3).into(), (10, 8).into())), - 5 => Some(Rect::new((6, 5).into(), (10, 9).into())), - 6 => Some(Rect::new((6, 5).into(), (10, 9).into())), - 7 => Some(Rect::new((6, 5).into(), (10, 9).into())), - _ => None, - } -} - -fn short_sword_hurtbox(frame: u16) -> Option> { - match frame { - 0 => None, - 1 => Some(Rect::new((10, 5).into(), (3, 5).into())), - 2 => Some(Rect::new((8, 5).into(), (6, 6).into())), - 3 => Some(Rect::new((8, 6).into(), (8, 8).into())), - 4 => Some(Rect::new((8, 7).into(), (5, 7).into())), - 5 => Some(Rect::new((8, 7).into(), (7, 7).into())), - 6 => Some(Rect::new((8, 5).into(), (7, 8).into())), - 7 => Some(Rect::new((8, 4).into(), (4, 7).into())), - _ => None, - } -} - -fn short_sword_fudge(frame: u16) -> i32 { - match frame { - 0 => 0, - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 3, - 5 => 3, - 6 => 3, - 7 => 3, - _ => 0, - } -} - -fn long_sword_fudge(frame: u16) -> i32 { - match frame { - 0 => 0, - 1 => 0, - 2 => 1, - 3 => 4, - 4 => 5, - 5 => 5, - 6 => 5, - 7 => 4, - _ => 0, - } -} - -enum AttackTimer { - Idle, - Attack(u16), - Cooldown(u16), -} - -struct Player<'a> { - entity: Entity<'a>, - facing: Tri, - state: PlayerState, - sprite_offset: u16, - attack_timer: AttackTimer, - damage_cooldown: u16, - sword: SwordState, - fudge_factor: Vector2D, - hurtbox: Option>, - controllable: bool, -} - -impl<'a> Player<'a> { - fn new(object_controller: &'a ObjectController) -> Player { - let mut entity = Entity::new( - object_controller, - Rect::new((0_u16, 0_u16).into(), (4_u16, 12_u16).into()), - ); - let s = object_controller.sprite(LONG_SWORD_IDLE.sprite(0)); - entity.sprite.set_sprite(s); - entity.sprite.show(); - entity.position = (144, 0).into(); - - Player { - entity, - facing: Tri::Positive, - state: PlayerState::OnGround, - sword: SwordState::LongSword, - sprite_offset: 0, - attack_timer: AttackTimer::Idle, - fudge_factor: (0, 0).into(), - hurtbox: None, - damage_cooldown: 0, - controllable: true, - } - } - - fn update( - &mut self, - controller: &'a ObjectController, - buttons: &ButtonController, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - let mut instruction = UpdateInstruction::None; - - let x = if self.controllable { - buttons.x_tri() - } else { - Tri::Zero - }; - - let b_press = buttons.is_just_pressed(Button::B) && self.controllable; - let a_press = buttons.is_just_pressed(Button::A) && self.controllable; - - self.fudge_factor = (0, 0).into(); - let mut hurtbox = None; - - match self.state { - PlayerState::OnGround => { - self.entity.velocity.y = 0.into(); - self.entity.velocity.x = self.entity.velocity.x * 40 / 64; - - match &mut self.attack_timer { - AttackTimer::Idle => { - if x != Tri::Zero { - self.facing = x; - } - self.entity.sprite.set_hflip(self.facing == Tri::Negative); - self.entity.velocity.x += self.sword.ground_walk_force() * x as i32; - if self.entity.velocity.x.abs() > Number::new(1) / 10 { - let sprite = - controller.sprite(self.sword.walk_animation(self.sprite_offset)); - self.entity.sprite.set_sprite(sprite); - } else { - let sprite = - controller.sprite(self.sword.idle_animation(self.sprite_offset)); - self.entity.sprite.set_sprite(sprite); - } - - if b_press && self.sword != SwordState::Swordless { - self.attack_timer = AttackTimer::Attack(self.sword.attack_duration()); - sfx.sword(); - } else if a_press { - self.entity.velocity.y -= self.sword.jump_impulse(); - self.state = PlayerState::InAir; - self.sprite_offset = 0; - - sfx.jump(); - } - } - AttackTimer::Attack(a) => { - *a -= 1; - let frame = self.sword.attack_frame(*a); - self.fudge_factor.x = self.sword.fudge(frame) * self.facing as i32; - let tag = self.sword.attack_tag(); - let sprite = controller.sprite(tag.animation_sprite(frame as usize)); - self.entity.sprite.set_sprite(sprite); - - hurtbox = self.sword.ground_attack_hurtbox(frame); - - if *a == 0 { - self.attack_timer = AttackTimer::Cooldown(self.sword.cooldown_time()); - } - } - AttackTimer::Cooldown(a) => { - *a -= 1; - let frame = self.sword.hold_frame(); - self.fudge_factor.x = self.sword.fudge(frame) * self.facing as i32; - let tag = self.sword.attack_tag(); - let sprite = controller.sprite(tag.animation_sprite(frame as usize)); - self.entity.sprite.set_sprite(sprite); - if *a == 0 { - self.attack_timer = AttackTimer::Idle; - } - } - } - } - PlayerState::InAir => { - self.entity.velocity.x = self.entity.velocity.x * 63 / 64; - - match &mut self.attack_timer { - AttackTimer::Idle => { - let frame = if self.sprite_offset < 3 * 4 { - self.sprite_offset / 4 - } else if self.entity.velocity.y.abs() < Number::new(1) / 5 { - 3 - } else if self.entity.velocity.y > 1.into() { - 5 - } else if self.entity.velocity.y > 0.into() { - 4 - } else { - 2 - }; - let tag = self.sword.jump_tag(); - let sprite = controller.sprite(tag.animation_sprite(frame as usize)); - self.entity.sprite.set_sprite(sprite); - - if x != Tri::Zero { - self.facing = x; - } - self.entity.sprite.set_hflip(self.facing == Tri::Negative); - self.entity.velocity.x += self.sword.air_move_force() * x as i32; - - if b_press - && self.sword != SwordState::LongSword - && self.sword != SwordState::Swordless - { - sfx.sword(); - self.attack_timer = - AttackTimer::Attack(self.sword.jump_attack_duration()); - } - } - AttackTimer::Attack(a) => { - *a -= 1; - let frame = self.sword.jump_attack_frame(*a); - let tag = self.sword.jump_attack_tag(); - let sprite = controller.sprite(tag.animation_sprite(frame as usize)); - self.entity.sprite.set_sprite(sprite); - - hurtbox = self.sword.air_attack_hurtbox(frame); - - if *a == 0 { - self.attack_timer = AttackTimer::Idle; - } - } - AttackTimer::Cooldown(_) => { - self.attack_timer = AttackTimer::Idle; - } - } - } - } - let gravity: Number = 1.into(); - let gravity = gravity / 16; - self.entity.velocity.y += gravity; - - let fudge_number = (self.fudge_factor.x, self.fudge_factor.y).into(); - - // convert the hurtbox to a location in the game - self.hurtbox = hurtbox.map(|h| { - let mut b = Rect::new(h.position - (8, 8).into(), h.size); - if self.facing == Tri::Negative { - b.position.x = -b.position.x - b.size.x; - } - b.position += self.entity.position + fudge_number; - b - }); - - let prior_y_velocity = self.entity.velocity.y; - self.entity.update_position(level); - let (_, collided_down) = self - .entity - .collision_in_direction((0, 1).into(), 1.into(), |v| level.collides(v)); - - if collided_down { - if self.state == PlayerState::InAir && prior_y_velocity > 2.into() { - instruction = UpdateInstruction::CreateParticle( - ParticleData::new_dust(), - self.entity.position + (2 * self.facing as i32, 0).into(), - ); - - sfx.player_land(); - } - - self.state = PlayerState::OnGround; - } else { - self.state = PlayerState::InAir; - } - - if self.damage_cooldown > 0 { - self.damage_cooldown -= 1; - } - - self.sprite_offset += 1; - - instruction - } - - // returns true if the player is alive and false otherwise - fn damage(&mut self) -> (bool, bool) { - if self.damage_cooldown != 0 { - return (true, false); - } - - self.damage_cooldown = 120; - let new_sword = match self.sword { - SwordState::LongSword => Some(SwordState::ShortSword), - SwordState::ShortSword => Some(SwordState::Dagger), - SwordState::Dagger => None, - SwordState::Swordless => Some(SwordState::Swordless), - }; - if let Some(sword) = new_sword { - self.sword = sword; - (true, true) - } else { - (false, true) - } - } - - fn heal(&mut self) { - let new_sword = match self.sword { - SwordState::LongSword => None, - SwordState::ShortSword => Some(SwordState::LongSword), - SwordState::Dagger => Some(SwordState::ShortSword), - SwordState::Swordless => Some(SwordState::Swordless), - }; - - if let Some(sword) = new_sword { - self.sword = sword; - } - - self.damage_cooldown = 30; - } - - fn commit(&mut self, offset: Vector2D) { - self.entity.commit_with_fudge(offset, self.fudge_factor); - } -} - -enum EnemyData { - Slime(SlimeData), - Bat(BatData), - MiniFlame(MiniFlameData), - Emu(EmuData), -} - -struct BatData { - sprite_offset: u16, - bat_state: BatState, -} - -enum BatState { - Idle, - Chasing(u16), - Dead, -} - -struct SlimeData { - sprite_offset: u16, - slime_state: SlimeState, -} - -impl BatData { - fn new() -> Self { - Self { - sprite_offset: 0, - bat_state: BatState::Idle, - } - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - let mut instruction = UpdateInstruction::None; - let should_die = player - .hurtbox - .as_ref() - .map(|hurtbox| hurtbox.touches(entity.collider())) - .unwrap_or(false); - let should_damage = entity.collider().touches(player.entity.collider()); - - const BAT_IDLE: &Tag = TAG_MAP.get("bat"); - - match &mut self.bat_state { - BatState::Idle => { - self.sprite_offset += 1; - if self.sprite_offset >= 9 * 8 { - self.sprite_offset = 0; - } - - if self.sprite_offset == 8 * 5 { - sfx.bat_flap(); - } - - let sprite = BAT_IDLE.sprite(self.sprite_offset as usize / 8); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - if (entity.position - player.entity.position).manhattan_distance() < 50.into() { - self.bat_state = BatState::Chasing(300); - self.sprite_offset /= 4; - } - - if should_die { - self.bat_state = BatState::Dead; - sfx.bat_death(); - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - - entity.velocity *= Number::new(15) / 16; - entity.update_position(level); - } - BatState::Chasing(count) => { - self.sprite_offset += 1; - - let speed = Number::new(1) / Number::new(4); - let target_velocity = player.entity.position - entity.position; - if target_velocity.manhattan_distance() > 1.into() { - entity.velocity = target_velocity.normalise() * speed; - } else { - entity.velocity = (0, 0).into(); - } - - if self.sprite_offset >= 9 * 2 { - self.sprite_offset = 0; - } - - let sprite = BAT_IDLE.sprite(self.sprite_offset as usize / 2); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - if self.sprite_offset == 2 * 5 { - sfx.bat_flap(); - } - - entity.update_position(level); - - if *count == 0 { - self.bat_state = BatState::Idle; - self.sprite_offset *= 4; - } else { - *count -= 1; - } - - if should_die { - self.bat_state = BatState::Dead; - sfx.bat_death(); - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - } - BatState::Dead => { - const BAT_DEAD: &Tag = TAG_MAP.get("bat dead"); - let sprite = BAT_DEAD.sprite(0); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - let gravity: Number = 1.into(); - let gravity = gravity / 16; - entity.velocity.x = 0.into(); - - entity.velocity.y += gravity; - - let original_y_velocity = entity.velocity.y; - let move_amount = entity.update_position(level); - - let just_landed = move_amount.y != 0.into() && original_y_velocity != move_amount.y; - - if just_landed { - instruction = UpdateInstruction::CreateParticle( - ParticleData::new_health(), - entity.position, - ); - } - } - } - instruction - } -} - -enum SlimeState { - Idle, - Chasing(Tri), - Dead(u16), -} - -impl SlimeData { - fn new() -> Self { - Self { - sprite_offset: 0, - slime_state: SlimeState::Idle, - } - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - let mut instruction = UpdateInstruction::None; - - let should_die = player - .hurtbox - .as_ref() - .map(|h| h.touches(entity.collider())) - .unwrap_or(false); - let should_damage = entity.collider().touches(player.entity.collider()); - - match &mut self.slime_state { - SlimeState::Idle => { - self.sprite_offset += 1; - if self.sprite_offset >= 32 { - self.sprite_offset = 0; - } - - const IDLE: &Tag = TAG_MAP.get("slime idle"); - - let sprite = IDLE.sprite(self.sprite_offset as usize / 16); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - if (player.entity.position - entity.position).manhattan_distance() < 40.into() { - let direction = match player.entity.position.x.cmp(&entity.position.x) { - Ordering::Equal => Tri::Zero, - Ordering::Greater => Tri::Positive, - Ordering::Less => Tri::Negative, - }; - - self.slime_state = SlimeState::Chasing(direction); - self.sprite_offset = 0; - } - if should_die { - self.slime_state = SlimeState::Dead(0); - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer - } - - let gravity: Number = 1.into(); - let gravity = gravity / 16; - entity.velocity.y += gravity; - entity.velocity *= Number::new(15) / 16; - entity.update_position(level); - } - SlimeState::Chasing(direction) => { - self.sprite_offset += 1; - if self.sprite_offset >= 7 * 6 { - self.slime_state = SlimeState::Idle; - } else { - let frame = ping_pong(self.sprite_offset / 6, 5); - - if frame == 0 { - sfx.slime_boing(); - } - - const CHASE: &Tag = TAG_MAP.get("Slime jump"); - - let sprite = CHASE.sprite(frame as usize); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - entity.velocity.x = match frame { - 2 | 3 | 4 => (Number::new(1) / 5) * Number::new(*direction as i32), - _ => 0.into(), - }; - - let gravity: Number = 1.into(); - let gravity = gravity / 16; - entity.velocity.y += gravity; - - let updated_position = entity.update_position(level); - if updated_position.y > 0.into() && self.sprite_offset > 2 * 6 { - // we're falling - self.sprite_offset = 6 * 6; - } - } - if should_die { - self.slime_state = SlimeState::Dead(0); - sfx.slime_dead(); - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer - } - } - SlimeState::Dead(count) => { - if *count < 5 * 4 { - const DEATH: &Tag = TAG_MAP.get("Slime death"); - let sprite = DEATH.sprite(*count as usize / 4); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - *count += 1; - } else { - return UpdateInstruction::Remove; - } - } - } - instruction - } -} - -enum MiniFlameState { - Idle(u16), - Chasing(u16), - Dead, -} - -struct MiniFlameData { - state: MiniFlameState, - sprite_offset: u16, -} - -impl MiniFlameData { - fn new() -> Self { - Self { - state: MiniFlameState::Chasing(90), - sprite_offset: 0, - } - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - _level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - let mut instruction = UpdateInstruction::None; - - let should_die = player - .hurtbox - .as_ref() - .map(|h| h.touches(entity.collider())) - .unwrap_or(false); - let should_damage = entity.collider().touches(player.entity.collider()); - - self.sprite_offset += 1; - - const ANGRY: &Tag = TAG_MAP.get("angry boss"); - - match &mut self.state { - MiniFlameState::Idle(frames) => { - *frames -= 1; - - if *frames == 0 { - let resulting_direction = player.entity.position - entity.position; - if resulting_direction.manhattan_distance() < 1.into() { - self.state = MiniFlameState::Idle(30); - } else { - sfx.flame_charge(); - self.state = MiniFlameState::Chasing(90); - entity.velocity = resulting_direction.normalise() * Number::new(2); - } - } else { - let sprite = ANGRY.animation_sprite(self.sprite_offset as usize / 8); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - - entity.velocity = (0.into(), Number::new(-1) / Number::new(4)).into(); - } - - if should_die { - self.sprite_offset = 0; - self.state = MiniFlameState::Dead; - - if rng::gen() % 4 == 0 { - instruction = UpdateInstruction::CreateParticle( - ParticleData::new_health(), - entity.position, - ); - } - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - } - MiniFlameState::Chasing(frame) => { - entity.velocity *= Number::new(63) / Number::new(64); - - if *frame == 0 { - self.state = MiniFlameState::Idle(30); - } else { - *frame -= 1; - } - - if should_die { - self.sprite_offset = 0; - self.state = MiniFlameState::Dead; - - if rng::gen() % 4 == 0 { - instruction = UpdateInstruction::CreateParticle( - ParticleData::new_health(), - entity.position, - ); - } - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - - if entity.velocity.manhattan_distance() < Number::new(1) / Number::new(4) { - self.state = MiniFlameState::Idle(90); - } - - let sprite = ANGRY.animation_sprite(self.sprite_offset as usize / 2); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - } - MiniFlameState::Dead => { - entity.velocity = (0, 0).into(); - if self.sprite_offset >= 6 * 12 { - instruction = UpdateInstruction::Remove; - } - - const DEATH: &Tag = TAG_MAP.get("angry boss dead"); - - let sprite = DEATH.animation_sprite(self.sprite_offset as usize / 12); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - - self.sprite_offset += 1; - } - }; - - entity.update_position_without_collision(); - - instruction - } -} - -enum EmuState { - Idle, - Charging(Tri), - Knockback, - Dead, -} - -struct EmuData { - state: EmuState, - sprite_offset: u16, -} - -impl EmuData { - fn new() -> Self { - Self { - state: EmuState::Idle, - sprite_offset: 0, - } - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - let mut instruction = UpdateInstruction::None; - - let should_die = player - .hurtbox - .as_ref() - .map(|h| h.touches(entity.collider())) - .unwrap_or(false); - let should_damage = entity.collider().touches(player.entity.collider()); - - match &mut self.state { - EmuState::Idle => { - self.sprite_offset += 1; - - if self.sprite_offset >= 3 * 16 { - self.sprite_offset = 0; - } - - const IDLE: &Tag = TAG_MAP.get("emu - idle"); - - let sprite = IDLE.sprite(self.sprite_offset as usize / 16); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - - if (entity.position.y - player.entity.position.y).abs() < 10.into() { - let velocity = Number::new(1) - * (player.entity.position.x - entity.position.x) - .to_raw() - .signum(); - entity.velocity.x = velocity; - - match velocity.cmp(&0.into()) { - Ordering::Greater => { - entity.sprite.set_hflip(true); - self.state = EmuState::Charging(Tri::Positive); - } - Ordering::Less => { - self.state = EmuState::Charging(Tri::Negative); - entity.sprite.set_hflip(false); - } - Ordering::Equal => { - self.state = EmuState::Idle; - } - } - } - - if should_die { - self.sprite_offset = 0; - self.state = EmuState::Dead; - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - } - EmuState::Charging(direction) => { - let direction = Number::new(*direction as i32); - self.sprite_offset += 1; - - if self.sprite_offset >= 4 * 2 { - self.sprite_offset = 0; - } - - if self.sprite_offset == 2 * 2 { - sfx.emu_step(); - } - - const WALK: &Tag = TAG_MAP.get("emu-walk"); - - let sprite = WALK.sprite(self.sprite_offset as usize / 2); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - - let gravity: Number = 1.into(); - let gravity = gravity / 16; - entity.velocity.y += gravity; - - let distance_traveled = entity.update_position(level); - - if distance_traveled.x == 0.into() { - sfx.emu_crash(); - self.state = EmuState::Knockback; - entity.velocity = (-direction / 2, Number::new(-1)).into(); - } - - if should_die { - self.sprite_offset = 0; - self.state = EmuState::Dead; - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - } - EmuState::Knockback => { - let gravity: Number = 1.into(); - let gravity = gravity / 16; - entity.velocity.y += gravity; - - entity.update_position(level); - let (_, is_collision) = - entity.collision_in_direction((0, 1).into(), gravity, |x| level.collides(x)); - - if is_collision { - entity.velocity.x = 0.into(); - self.state = EmuState::Idle; - } - - if should_die { - self.sprite_offset = 0; - self.state = EmuState::Dead; - } else if should_damage { - instruction = UpdateInstruction::DamagePlayer; - } - } - EmuState::Dead => { - if self.sprite_offset == 0 { - sfx.emu_death(); - } - - if self.sprite_offset >= 8 * 4 { - instruction = UpdateInstruction::Remove; - } - - const DEATH: &Tag = TAG_MAP.get("emu - die"); - - let sprite = DEATH.animation_sprite(self.sprite_offset as usize / 4); - let sprite = controller.sprite(sprite); - entity.sprite.set_sprite(sprite); - - self.sprite_offset += 1; - } - } - - instruction - } -} - -enum UpdateInstruction { - None, - HealBossAndRemove, - HealPlayerAndRemove, - Remove, - DamagePlayer, - CreateParticle(ParticleData, Vector2D), -} - -impl EnemyData { - fn collision_mask(&self) -> Rect { - match self { - EnemyData::Slime(_) => Rect::new((0u16, 0u16).into(), (4u16, 11u16).into()), - EnemyData::Bat(_) => Rect::new((0u16, 0u16).into(), (12u16, 4u16).into()), - EnemyData::MiniFlame(_) => Rect::new((0u16, 0u16).into(), (12u16, 12u16).into()), - EnemyData::Emu(_) => Rect::new((0u16, 0u16).into(), (7u16, 11u16).into()), - } - } - - fn sprite(&self) -> &'static Sprite { - const SLIME: &Tag = TAG_MAP.get("slime idle"); - const BAT: &Tag = TAG_MAP.get("bat"); - const MINI_FLAME: &Tag = TAG_MAP.get("angry boss"); - const EMU: &Tag = TAG_MAP.get("emu - idle"); - match self { - EnemyData::Slime(_) => SLIME.sprite(0), - EnemyData::Bat(_) => BAT.sprite(0), - EnemyData::MiniFlame(_) => MINI_FLAME.sprite(0), - EnemyData::Emu(_) => EMU.sprite(0), - } - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - match self { - EnemyData::Slime(data) => data.update(controller, entity, player, level, sfx), - EnemyData::Bat(data) => data.update(controller, entity, player, level, sfx), - EnemyData::MiniFlame(data) => data.update(controller, entity, player, level, sfx), - EnemyData::Emu(data) => data.update(controller, entity, player, level, sfx), - } - } -} - -struct Enemy<'a> { - entity: Entity<'a>, - enemy_data: EnemyData, -} - -impl<'a> Enemy<'a> { - fn new(object_controller: &'a ObjectController, enemy_data: EnemyData) -> Self { - let mut entity = Entity::new(object_controller, enemy_data.collision_mask()); - - let sprite = enemy_data.sprite(); - let sprite = object_controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - entity.sprite.show(); - - Self { entity, enemy_data } - } - - fn update( - &mut self, - controller: &'a ObjectController, - player: &Player, - level: &Level, - sfx: &mut sfx::Sfx, - ) -> UpdateInstruction { - self.enemy_data - .update(controller, &mut self.entity, player, level, sfx) - } -} - -enum ParticleData { - Dust(u16), - Health(u16), - BossHealer(u16, Vector2D), -} - -impl ParticleData { - fn new_dust() -> Self { - Self::Dust(0) - } - - fn new_health() -> Self { - Self::Health(0) - } - - fn new_boss_healer(target: Vector2D) -> Self { - Self::BossHealer(0, target) - } - - fn update<'a>( - &mut self, - controller: &'a ObjectController, - entity: &mut Entity<'a>, - player: &Player, - _level: &Level, - ) -> UpdateInstruction { - match self { - ParticleData::Dust(frame) => { - if *frame == 8 * 3 { - return UpdateInstruction::Remove; - } - - const DUST: &Tag = TAG_MAP.get("dust"); - let sprite = DUST.sprite(*frame as usize / 3); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - *frame += 1; - UpdateInstruction::None - } - ParticleData::Health(frame) => { - if *frame > 8 * 3 * 6 { - return UpdateInstruction::Remove; // have played the animation 6 times - } - - const HEALTH: &Tag = TAG_MAP.get("Heath"); - let sprite = HEALTH.animation_sprite(*frame as usize / 3); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - if *frame < 8 * 3 * 3 { - entity.velocity.y = Number::new(-1) / 2; - } else { - let speed = Number::new(2); - let target_velocity = player.entity.position - entity.position; - - if target_velocity.manhattan_distance() < 5.into() { - return UpdateInstruction::HealPlayerAndRemove; - } - - entity.velocity = target_velocity.normalise() * speed; - } - - entity.update_position_without_collision(); - - *frame += 1; - - UpdateInstruction::None - } - ParticleData::BossHealer(frame, target) => { - const HEALTH: &Tag = TAG_MAP.get("Heath"); - let sprite = HEALTH.animation_sprite(*frame as usize / 3); - let sprite = controller.sprite(sprite); - - entity.sprite.set_sprite(sprite); - - if *frame < 8 * 3 * 3 { - entity.velocity.y = Number::new(-1) / 2; - } else if *frame < 8 * 3 * 6 { - entity.velocity = (0, 0).into(); - } else { - let speed = Number::new(4); - let target_velocity = *target - entity.position; - - if target_velocity.manhattan_distance() < 5.into() { - return UpdateInstruction::HealBossAndRemove; - } - - entity.velocity = target_velocity.normalise() * speed; - } - - entity.update_position_without_collision(); - - *frame += 1; - UpdateInstruction::None - } - } - } -} - -struct Particle<'a> { - entity: Entity<'a>, - particle_data: ParticleData, -} - -impl<'a> Particle<'a> { - fn new( - object_controller: &'a ObjectController, - particle_data: ParticleData, - position: Vector2D, - ) -> Self { - let mut entity = Entity::new( - object_controller, - Rect::new((0u16, 0u16).into(), (0u16, 0u16).into()), - ); - - entity.position = position; - - Self { - entity, - particle_data, - } - } - - fn update( - &mut self, - controller: &'a ObjectController, - player: &Player, - level: &Level, - ) -> UpdateInstruction { - self.entity.sprite.show(); - self.particle_data - .update(controller, &mut self.entity, player, level) - } -} - -#[derive(PartialEq, Eq, Clone, Copy)] -enum GameStatus { - Continue, - Lost, - RespawnAtBoss, -} - -enum BossState<'a> { - NotSpawned, - Active(Boss<'a>), - Following(FollowingBoss<'a>), -} - -impl<'a> BossState<'a> { - fn update( - &mut self, - enemies: &mut Arena>, - object_controller: &'a ObjectController, - player: &Player, - sfx: &mut sfx::Sfx, - ) -> BossInstruction { - match self { - BossState::Active(boss) => boss.update(enemies, object_controller, player, sfx), - BossState::Following(boss) => { - boss.update(object_controller, player); - BossInstruction::None - } - BossState::NotSpawned => BossInstruction::None, - } - } - fn commit(&mut self, offset: Vector2D) { - match self { - BossState::Active(boss) => { - boss.commit(offset); - } - BossState::Following(boss) => { - boss.commit(offset); - } - BossState::NotSpawned => {} - } - } -} - -struct FollowingBoss<'a> { - entity: Entity<'a>, - following: bool, - to_hole: bool, - timer: u32, - gone: bool, -} - -impl<'a> FollowingBoss<'a> { - fn new(object_controller: &'a ObjectController, position: Vector2D) -> Self { - let mut entity = Entity::new( - object_controller, - Rect::new((0_u16, 0_u16).into(), (0_u16, 0_u16).into()), - ); - entity.position = position; - - Self { - entity, - following: true, - timer: 0, - to_hole: false, - gone: false, - } - } - fn update(&mut self, controller: &'a ObjectController, player: &Player) { - let difference = player.entity.position - self.entity.position; - self.timer += 1; - - let frame = if self.to_hole { - let target: Vector2D = (17 * 8, -3 * 8).into(); - let difference = target - self.entity.position; - if difference.manhattan_distance() < 1.into() { - self.gone = true; - } else { - self.entity.velocity = difference.normalise() * 2; - } - - self.timer / 8 - } else if self.timer < 120 { - self.timer / 20 - } else if self.following { - self.entity.velocity = difference / 16; - if difference.manhattan_distance() < 20.into() { - self.following = false; - } - self.timer / 8 - } else { - self.entity.velocity = (0, 0).into(); - if difference.manhattan_distance() > 60.into() { - self.following = true; - } - self.timer / 16 - }; - - const BOSS: &Tag = TAG_MAP.get("happy boss"); - - let sprite = BOSS.animation_sprite(frame as usize); - let sprite = controller.sprite(sprite); - - self.entity.sprite.set_sprite(sprite); - - self.entity.update_position_without_collision(); - } - - fn commit(&mut self, offset: Vector2D) { - self.entity.commit_with_fudge(offset, (0, 0).into()); - } -} - -enum BossActiveState { - Damaged(u8), - MovingToTarget, - WaitingUntilExplosion(u8), - WaitingUntilDamaged(u16), - WaitUntilKilled, -} - -struct Boss<'a> { - entity: Entity<'a>, - health: u8, - target_location: u8, - state: BossActiveState, - timer: u32, - screen_coords: Vector2D, - shake_magnitude: Number, -} - -enum BossInstruction { - None, - Dead, -} - -impl<'a> Boss<'a> { - fn new(object_controller: &'a ObjectController, screen_coords: Vector2D) -> Self { - let mut entity = Entity::new( - object_controller, - Rect::new((0_u16, 0_u16).into(), (28_u16, 28_u16).into()), - ); - entity.position = screen_coords + (144, 136).into(); - Self { - entity, - health: 5, - target_location: rng::gen().rem_euclid(5) as u8, - state: BossActiveState::Damaged(60), - timer: 0, - screen_coords, - shake_magnitude: 0.into(), - } - } - fn update( - &mut self, - enemies: &mut Arena>, - object_controller: &'a ObjectController, - player: &Player, - sfx: &mut sfx::Sfx, - ) -> BossInstruction { - let mut instruction = BossInstruction::None; - match &mut self.state { - BossActiveState::Damaged(time) => { - *time -= 1; - if *time == 0 { - self.target_location = self.get_next_target_location(); - self.state = BossActiveState::MovingToTarget; - sfx.boss_move(); - } - } - BossActiveState::MovingToTarget => { - let target = self.get_target_location() + self.screen_coords; - let difference = target - self.entity.position; - if difference.manhattan_distance() < 1.into() { - self.entity.velocity = (0, 0).into(); - self.state = BossActiveState::WaitingUntilExplosion(60); - } else { - self.entity.velocity = difference / 16; - } - } - BossActiveState::WaitingUntilExplosion(time) => { - *time -= 1; - if *time == 0 { - if self.health == 0 { - enemies.clear(); - instruction = BossInstruction::Dead; - self.state = BossActiveState::WaitUntilKilled; - } else { - sfx.burning(); - self.explode(enemies, object_controller); - self.state = BossActiveState::WaitingUntilDamaged(60 * 5); - } - } - } - BossActiveState::WaitingUntilDamaged(time) => { - *time -= 1; - if *time == 0 { - sfx.burning(); - self.explode(enemies, object_controller); - self.state = BossActiveState::WaitingUntilDamaged(60 * 5); - } - if let Some(hurt) = &player.hurtbox { - if hurt.touches(self.entity.collider()) { - self.health -= 1; - self.state = BossActiveState::Damaged(30); - } - } - } - BossActiveState::WaitUntilKilled => {} - } - let animation_rate = match self.state { - BossActiveState::Damaged(_) => 6, - BossActiveState::MovingToTarget => 4, - BossActiveState::WaitingUntilExplosion(_) => 3, - BossActiveState::WaitingUntilDamaged(_) => 8, - BossActiveState::WaitUntilKilled => 12, - }; - - self.shake_magnitude = match self.state { - BossActiveState::Damaged(_) => 1.into(), - BossActiveState::MovingToTarget => 0.into(), - BossActiveState::WaitingUntilExplosion(_) => 5.into(), - BossActiveState::WaitingUntilDamaged(time) => { - if time < 60 { - 5.into() - } else { - 0.into() - } - } - BossActiveState::WaitUntilKilled => 3.into(), - }; - self.timer += 1; - let frame = self.timer / animation_rate; - - const BOSS: &Tag = TAG_MAP.get("Boss"); - - let sprite = BOSS.animation_sprite(frame as usize); - let sprite = object_controller.sprite(sprite); - - self.entity.sprite.set_sprite(sprite); - - self.entity.update_position_without_collision(); - instruction - } - fn commit(&mut self, offset: Vector2D) { - let shake = if self.shake_magnitude != 0.into() { - ( - Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - - self.shake_magnitude / 2, - Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - - self.shake_magnitude / 2, - ) - .into() - } else { - (0, 0).into() - }; - - self.entity - .commit_with_size(offset + shake, (32, 32).into()); - } - fn explode(&self, enemies: &mut Arena>, object_controller: &'a ObjectController) { - for _ in 0..(6 - self.health) { - let x_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; - let y_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; - let mut flame = Enemy::new( - object_controller, - EnemyData::MiniFlame(MiniFlameData::new()), - ); - flame.entity.position = self.entity.position; - flame.entity.velocity = (x_offset, y_offset).into(); - enemies.insert(flame); - } - } - - fn get_next_target_location(&self) -> u8 { - loop { - let a = rng::gen().rem_euclid(5) as u8; - if a != self.target_location { - break a; - } - } - } - fn get_target_location(&self) -> Vector2D { - match self.target_location { - 0 => (240 / 4, 160 / 4).into(), - 1 => (3 * 240 / 4, 160 / 4).into(), - 2 => (240 / 4, 3 * 160 / 4).into(), - 3 => (3 * 240 / 4, 3 * 160 / 4).into(), - 4 => (240 / 2, 160 / 2).into(), - _ => unreachable!(), - } - } -} - -struct Game<'a> { - player: Player<'a>, - input: ButtonController, - frame_count: u32, - level: Level<'a>, - offset: Vector2D, - shake_time: u16, - sunrise_timer: u16, - - enemies: Arena>, - particles: Arena>, - slime_load: usize, - bat_load: usize, - emu_load: usize, - boss: BossState<'a>, - move_state: MoveState, - fade_count: u16, -} - -enum MoveState { - Advancing, - PinnedAtEnd, - FollowingPlayer, - Ending, -} - -impl<'a> Game<'a> { - fn has_just_reached_end(&self) -> bool { - match self.boss { - BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH * 8, - _ => false, - } - } - - fn clear(&mut self, vram: &mut VRamManager) { - self.level.clear(vram); - } - - fn advance_frame( - &mut self, - object_controller: &'a ObjectController, - vram: &mut VRamManager, - sfx: &mut sfx::Sfx, - ) -> GameStatus { - let mut state = GameStatus::Continue; - - match self.move_state { - MoveState::Advancing => { - self.offset += Into::>::into((1, 0)) / 8; - - if self.has_just_reached_end() { - sfx.boss(); - self.offset.x = (tilemap::WIDTH * 8 - 248).into(); - self.move_state = MoveState::PinnedAtEnd; - self.boss = BossState::Active(Boss::new(object_controller, self.offset)) - } - } - MoveState::PinnedAtEnd => { - self.offset.x = (tilemap::WIDTH * 8 - 248).into(); - } - MoveState::FollowingPlayer => { - Game::update_sunrise(vram, self.sunrise_timer); - if self.sunrise_timer < 120 { - self.sunrise_timer += 1; - } else { - let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2); - - self.offset.x += difference / 8; - if self.offset.x > (tilemap::WIDTH * 8 - 248).into() { - self.offset.x = (tilemap::WIDTH * 8 - 248).into(); - } else if self.offset.x < 8.into() { - self.offset.x = 8.into(); - self.move_state = MoveState::Ending; - } - } - } - MoveState::Ending => { - self.player.controllable = false; - if let BossState::Following(boss) = &mut self.boss { - boss.to_hole = true; - if boss.gone { - self.fade_count += 1; - self.fade_count = self.fade_count.min(600); - Game::update_fade_out(vram, self.fade_count); - } - } - } - } - - match self - .boss - .update(&mut self.enemies, object_controller, &self.player, sfx) - { - BossInstruction::Dead => { - let boss = match &self.boss { - BossState::Active(b) => b, - _ => unreachable!(), - }; - let new_particle = Particle::new( - object_controller, - ParticleData::new_boss_healer(boss.entity.position), - self.player.entity.position, - ); - self.particles.insert(new_particle); - sfx.stop_music(); - self.player.sword = SwordState::Swordless; - } - BossInstruction::None => {} - } - - self.load_enemies(object_controller); - - if self.player.entity.position.x < self.offset.x - 8 { - let (alive, damaged) = self.player.damage(); - if !alive { - state = GameStatus::Lost; - } - if damaged { - sfx.player_hurt(); - self.shake_time += 20; - } - } - - let mut this_frame_offset = self.offset; - if self.shake_time > 0 { - let size = self.shake_time.min(4) as i32; - let offset: Vector2D = ( - Number::from_raw(rng::gen()) % size - Number::new(size) / 2, - Number::from_raw(rng::gen()) % size - Number::new(size) / 2, - ) - .into(); - this_frame_offset += offset; - self.shake_time -= 1; - } - - self.input.update(); - if let UpdateInstruction::CreateParticle(data, position) = - self.player - .update(object_controller, &self.input, &self.level, sfx) - { - let new_particle = Particle::new(object_controller, data, position); - - self.particles.insert(new_particle); - } - - let mut remove = Vec::with_capacity(10); - for (idx, enemy) in self.enemies.iter_mut() { - if enemy.entity.position.x < self.offset.x - 8 { - remove.push(idx); - continue; - } - - match enemy.update(object_controller, &self.player, &self.level, sfx) { - UpdateInstruction::Remove => { - remove.push(idx); - } - UpdateInstruction::HealPlayerAndRemove => { - self.player.heal(); - sfx.player_heal(); - remove.push(idx); - } - UpdateInstruction::HealBossAndRemove => {} - UpdateInstruction::DamagePlayer => { - let (alive, damaged) = self.player.damage(); - if !alive { - state = GameStatus::Lost; - } - if damaged { - sfx.player_hurt(); - self.shake_time += 20; - } - } - UpdateInstruction::CreateParticle(data, position) => { - let new_particle = Particle::new(object_controller, data, position); - self.particles.insert(new_particle); - } - UpdateInstruction::None => {} - } - enemy - .entity - .commit_with_fudge(this_frame_offset, (0, 0).into()); - } - - self.player.commit(this_frame_offset); - self.boss.commit(this_frame_offset); - - let background_offset = (this_frame_offset.floor().x, 8).into(); - - self.level.background.set_pos(vram, background_offset); - self.level.foreground.set_pos(vram, background_offset); - self.level.clouds.set_pos(vram, background_offset / 4); - - for i in remove { - self.enemies.remove(i); - } - - let mut remove = Vec::with_capacity(10); - - for (idx, particle) in self.particles.iter_mut() { - match particle.update(object_controller, &self.player, &self.level) { - UpdateInstruction::Remove => remove.push(idx), - UpdateInstruction::HealBossAndRemove => { - sfx.sunrise(); - let location = match &self.boss { - BossState::Active(b) => b.entity.position, - _ => unreachable!(), - }; - self.boss = - BossState::Following(FollowingBoss::new(object_controller, location)); - self.move_state = MoveState::FollowingPlayer; - remove.push(idx); - } - UpdateInstruction::HealPlayerAndRemove => { - self.player.heal(); - sfx.player_heal(); - remove.push(idx); - } - UpdateInstruction::DamagePlayer => { - let (alive, damaged) = self.player.damage(); - if !alive { - state = GameStatus::Lost; - } - if damaged { - sfx.player_hurt(); - self.shake_time += 20; - } - } - UpdateInstruction::CreateParticle(_, _) => {} - UpdateInstruction::None => {} - } - particle - .entity - .commit_with_fudge(this_frame_offset, (0, 0).into()); - } - - self.level.background.commit(vram); - self.level.foreground.commit(vram); - self.level.clouds.commit(vram); - - for i in remove { - self.particles.remove(i); - } - - self.frame_count += 1; - if let GameStatus::Lost = state { - match self.boss { - BossState::Active(_) => GameStatus::RespawnAtBoss, - _ => GameStatus::Lost, - } - } else { - state - } - } - - fn load_enemies(&mut self, object_controller: &'a ObjectController) { - if self.slime_load < self.level.slime_spawns.len() { - for (idx, slime_spawn) in self - .level - .slime_spawns - .iter() - .enumerate() - .skip(self.slime_load) - { - if slime_spawn.0 as i32 > self.offset.x.floor() + 300 { - break; - } - self.slime_load = idx + 1; - let mut slime = Enemy::new(object_controller, EnemyData::Slime(SlimeData::new())); - slime.entity.position = (slime_spawn.0 as i32, slime_spawn.1 as i32 - 7).into(); - self.enemies.insert(slime); - } - } - if self.bat_load < self.level.bat_spawns.len() { - for (idx, bat_spawn) in self.level.bat_spawns.iter().enumerate().skip(self.bat_load) { - if bat_spawn.0 as i32 > self.offset.x.floor() + 300 { - break; - } - self.bat_load = idx + 1; - let mut bat = Enemy::new(object_controller, EnemyData::Bat(BatData::new())); - bat.entity.position = (bat_spawn.0 as i32, bat_spawn.1 as i32).into(); - self.enemies.insert(bat); - } - } - if self.emu_load < self.level.emu_spawns.len() { - for (idx, emu_spawn) in self.level.emu_spawns.iter().enumerate().skip(self.emu_load) { - if emu_spawn.0 as i32 > self.offset.x.floor() + 300 { - break; - } - self.emu_load = idx + 1; - let mut emu = Enemy::new(object_controller, EnemyData::Emu(EmuData::new())); - emu.entity.position = (emu_spawn.0 as i32, emu_spawn.1 as i32 - 7).into(); - self.enemies.insert(emu); - } - } - } - - fn update_sunrise(vram: &mut VRamManager, time: u16) { - let mut modified_palette = background::PALETTES[0].clone(); - - let a = modified_palette.colour(0); - let b = modified_palette.colour(1); - - modified_palette.update_colour(0, interpolate_colour(a, 17982, time, 120)); - modified_palette.update_colour(1, interpolate_colour(b, 22427, time, 120)); - - let modified_palettes = [modified_palette]; - - vram.set_background_palettes(&modified_palettes); - } - - fn update_fade_out(vram: &mut VRamManager, time: u16) { - let mut modified_palette = background::PALETTES[0].clone(); - - let c = modified_palette.colour(2); - - modified_palette.update_colour(0, interpolate_colour(17982, 0x7FFF, time, 600)); - modified_palette.update_colour(1, interpolate_colour(22427, 0x7FFF, time, 600)); - modified_palette.update_colour(2, interpolate_colour(c, 0x7FFF, time, 600)); - - let modified_palettes = [modified_palette]; - - vram.set_background_palettes(&modified_palettes); - } - - fn new(object: &'a ObjectController, level: Level<'a>, start_at_boss: bool) -> Self { - let mut player = Player::new(object); - let mut offset = (8, 8).into(); - if start_at_boss { - player.entity.position = (133 * 8, 10 * 8).into(); - offset = (130 * 8, 8).into(); - } - Self { - player, - input: ButtonController::new(), - frame_count: 0, - level, - offset, - shake_time: 0, - - enemies: Arena::with_capacity(100), - slime_load: 0, - bat_load: 0, - emu_load: 0, - particles: Arena::with_capacity(30), - boss: BossState::NotSpawned, - move_state: MoveState::Advancing, - sunrise_timer: 0, - fade_count: 0, - } - } -} - -fn game_with_level(gba: &mut agb::Gba) { - let vblank = agb::interrupt::VBlank::get(); - vblank.wait_for_vblank(); - - let mut mixer = gba.mixer.mixer(Frequency::Hz18157); - mixer.enable(); - - let mut sfx = sfx::Sfx::new(&mut mixer); - sfx.purple_night(); - - let mut start_at_boss = false; - - loop { - let (background, mut vram) = gba.display.video.tiled0(); - - vram.set_background_palettes(background::PALETTES); - - let tileset = TileSet::new(background::background.tiles, TileFormat::FourBpp); - - let object = gba.display.object.get(); - - let backdrop = InfiniteScrolledMap::new( - background.background(Priority::P2, RegularBackgroundSize::Background32x32), - Box::new(|pos| { - ( - &tileset, - TileSetting::from_raw( - *tilemap::BACKGROUND_MAP - .get((pos.x + tilemap::WIDTH * pos.y) as usize) - .unwrap_or(&0), - ), - ) - }), - ); - - let foreground = InfiniteScrolledMap::new( - background.background(Priority::P0, RegularBackgroundSize::Background32x32), - Box::new(|pos| { - ( - &tileset, - TileSetting::from_raw( - *tilemap::FOREGROUND_MAP - .get((pos.x + tilemap::WIDTH * pos.y) as usize) - .unwrap_or(&0), - ), - ) - }), - ); - - let clouds = InfiniteScrolledMap::new( - background.background(Priority::P3, RegularBackgroundSize::Background32x32), - Box::new(|pos| { - ( - &tileset, - TileSetting::from_raw( - *tilemap::CLOUD_MAP - .get((pos.x + tilemap::WIDTH * pos.y) as usize) - .unwrap_or(&0), - ), - ) - }), - ); - - let start_pos = if start_at_boss { - (130 * 8, 8).into() - } else { - (8, 8).into() - }; - - let mut game = Game::new( - &object, - Level::load_level(backdrop, foreground, clouds, start_pos, &mut vram, &mut sfx), - start_at_boss, - ); - - start_at_boss = loop { - sfx.frame(); - vblank.wait_for_vblank(); - sfx.after_vblank(); - object.commit(); - match game.advance_frame(&object, &mut vram, &mut sfx) { - GameStatus::Continue => {} - GameStatus::Lost => { - break false; - } - GameStatus::RespawnAtBoss => { - break true; - } - } - - let _ = rng::gen(); // advance RNG to make it less predictable between runs - }; - - game.clear(&mut vram); - } -} - -mod tilemap { - include!(concat!(env!("OUT_DIR"), "/tilemap.rs")); -} - #[agb::entry] -fn main(mut gba: agb::Gba) -> ! { - loop { - game_with_level(&mut gba); - } -} - -fn ping_pong(i: u16, n: u16) -> u16 { - let cycle = 2 * (n - 1); - let i = i % cycle; - if i >= n { - cycle - i - } else { - i - } -} - -fn interpolate_colour(initial: u16, destination: u16, time_so_far: u16, total_time: u16) -> u16 { - const MASK: u16 = 0b11111; - fn to_components(c: u16) -> [u16; 3] { - [c & MASK, (c >> 5) & MASK, (c >> 10) & MASK] - } - - let initial_rgb = to_components(initial); - let destination_rgb = to_components(destination); - let mut colour = 0; - - for (i, c) in initial_rgb - .iter() - .zip(destination_rgb) - .map(|(a, b)| (b - a) * time_so_far / total_time + a) - .enumerate() - { - colour |= (c & MASK) << (i * 5); - } - colour +fn entry(mut gba: agb::Gba) -> ! { + the_purple_night::main(gba); }