diff --git a/examples/hyperspace-roll/.cargo/config.toml b/examples/hyperspace-roll/.cargo/config.toml new file mode 100644 index 00000000..27b7c08d --- /dev/null +++ b/examples/hyperspace-roll/.cargo/config.toml @@ -0,0 +1,11 @@ +[unstable] +build-std = ["core", "alloc"] +build-std-features = ["compiler-builtins-mem"] +sparse-registry = true + +[build] +target = "thumbv4t-none-eabi" + +[target.thumbv4t-none-eabi] +rustflags = ["-Clink-arg=-Tgba.ld"] +runner = "mgba-qt" diff --git a/examples/hyperspace-roll/Cargo.lock b/examples/hyperspace-roll/Cargo.lock new file mode 100644 index 00000000..385134cc --- /dev/null +++ b/examples/hyperspace-roll/Cargo.lock @@ -0,0 +1,439 @@ +# 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.9.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.2.1" +dependencies = [ + "agb_macros", +] + +[[package]] +name = "agb_image_converter" +version = "0.7.0" +dependencies = [ + "asefile", + "fontdue", + "image", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + +[[package]] +name = "agb_macros" +version = "0.2.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "agb_sound_converter" +version = "0.2.0" +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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[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 = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide 0.5.1", +] + +[[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 = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[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 = "libc" +version = "0.2.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[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-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[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" diff --git a/examples/hyperspace-roll/Cargo.toml b/examples/hyperspace-roll/Cargo.toml new file mode 100644 index 00000000..c924236d --- /dev/null +++ b/examples/hyperspace-roll/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hyperspace-roll" +version = "0.1.0" +authors = [""] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +agb = { version = "0.9.2", path = "../../agb", features = ["freq32768"] } +bare-metal = "1" + +[profile.dev] +opt-level = 2 +debug = true + + +[profile.release] +panic = "abort" +lto = true +debug = true diff --git a/examples/hyperspace-roll/build.rs b/examples/hyperspace-roll/build.rs new file mode 100644 index 00000000..e71fdf55 --- /dev/null +++ b/examples/hyperspace-roll/build.rs @@ -0,0 +1 @@ +fn main() {} \ No newline at end of file diff --git a/examples/hyperspace-roll/gba.ld b/examples/hyperspace-roll/gba.ld new file mode 100644 index 00000000..cd74016a --- /dev/null +++ b/examples/hyperspace-roll/gba.ld @@ -0,0 +1,112 @@ +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") +OUTPUT_ARCH(arm) + +ENTRY(__start) +EXTERN(__RUST_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 + + __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/hyperspace-roll/gba_mb.ld b/examples/hyperspace-roll/gba_mb.ld new file mode 100644 index 00000000..238b5c4c --- /dev/null +++ b/examples/hyperspace-roll/gba_mb.ld @@ -0,0 +1,110 @@ +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") +OUTPUT_ARCH(arm) + +ENTRY(__start) +EXTERN(__RUST_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 + + __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/hyperspace-roll/gfx/descriptions.aseprite b/examples/hyperspace-roll/gfx/descriptions.aseprite new file mode 100644 index 00000000..2f6545c0 Binary files /dev/null and b/examples/hyperspace-roll/gfx/descriptions.aseprite differ diff --git a/examples/hyperspace-roll/gfx/descriptions.toml b/examples/hyperspace-roll/gfx/descriptions.toml new file mode 100644 index 00000000..9ce038e0 --- /dev/null +++ b/examples/hyperspace-roll/gfx/descriptions.toml @@ -0,0 +1,11 @@ +version = "1.0" + +[image.descriptions1] +filename = "descriptions1.png" +tile_size = "8x8" +transparent_colour = "121105" + +[image.descriptions2] +filename = "descriptions2.png" +tile_size = "8x8" +transparent_colour = "121105" \ No newline at end of file diff --git a/examples/hyperspace-roll/gfx/descriptions1.png b/examples/hyperspace-roll/gfx/descriptions1.png new file mode 100644 index 00000000..079dbdb9 Binary files /dev/null and b/examples/hyperspace-roll/gfx/descriptions1.png differ diff --git a/examples/hyperspace-roll/gfx/descriptions2.png b/examples/hyperspace-roll/gfx/descriptions2.png new file mode 100644 index 00000000..429077d7 Binary files /dev/null and b/examples/hyperspace-roll/gfx/descriptions2.png differ diff --git a/examples/hyperspace-roll/gfx/dice-faces.aseprite b/examples/hyperspace-roll/gfx/dice-faces.aseprite new file mode 100644 index 00000000..68c2c389 Binary files /dev/null and b/examples/hyperspace-roll/gfx/dice-faces.aseprite differ diff --git a/examples/hyperspace-roll/gfx/fantasy-24-1x.png b/examples/hyperspace-roll/gfx/fantasy-24-1x.png new file mode 100644 index 00000000..610256de Binary files /dev/null and b/examples/hyperspace-roll/gfx/fantasy-24-1x.png differ diff --git a/examples/hyperspace-roll/gfx/font-license.txt b/examples/hyperspace-roll/gfx/font-license.txt new file mode 100644 index 00000000..12065c59 --- /dev/null +++ b/examples/hyperspace-roll/gfx/font-license.txt @@ -0,0 +1,4 @@ +The FontStruction “Pixelated” +(http://fontstruct.com/fontstructions/show/426637) by “Greenma201” is +licensed under a Creative Commons Attribution Share Alike license +(http://creativecommons.org/licenses/by-sa/3.0/). diff --git a/examples/hyperspace-roll/gfx/help-text.aseprite b/examples/hyperspace-roll/gfx/help-text.aseprite new file mode 100644 index 00000000..f2273146 Binary files /dev/null and b/examples/hyperspace-roll/gfx/help-text.aseprite differ diff --git a/examples/hyperspace-roll/gfx/help-text.png b/examples/hyperspace-roll/gfx/help-text.png new file mode 100644 index 00000000..de6db475 Binary files /dev/null and b/examples/hyperspace-roll/gfx/help-text.png differ diff --git a/examples/hyperspace-roll/gfx/help.toml b/examples/hyperspace-roll/gfx/help.toml new file mode 100644 index 00000000..149b68d9 --- /dev/null +++ b/examples/hyperspace-roll/gfx/help.toml @@ -0,0 +1,6 @@ +version = "1.0" + +[image.help] +filename = "help-text.png" +tile_size = "8x8" +transparent_colour = "121105" diff --git a/examples/hyperspace-roll/gfx/pixelated.ttf b/examples/hyperspace-roll/gfx/pixelated.ttf new file mode 100644 index 00000000..29c265a1 Binary files /dev/null and b/examples/hyperspace-roll/gfx/pixelated.ttf differ diff --git a/examples/hyperspace-roll/gfx/ships.aseprite b/examples/hyperspace-roll/gfx/ships.aseprite new file mode 100644 index 00000000..5051010c Binary files /dev/null and b/examples/hyperspace-roll/gfx/ships.aseprite differ diff --git a/examples/hyperspace-roll/gfx/small-sprites.aseprite b/examples/hyperspace-roll/gfx/small-sprites.aseprite new file mode 100644 index 00000000..918f0b83 Binary files /dev/null and b/examples/hyperspace-roll/gfx/small-sprites.aseprite differ diff --git a/examples/hyperspace-roll/gfx/stars.aseprite b/examples/hyperspace-roll/gfx/stars.aseprite new file mode 100644 index 00000000..afc4fca8 Binary files /dev/null and b/examples/hyperspace-roll/gfx/stars.aseprite differ diff --git a/examples/hyperspace-roll/gfx/stars.png b/examples/hyperspace-roll/gfx/stars.png new file mode 100644 index 00000000..65062d6e Binary files /dev/null and b/examples/hyperspace-roll/gfx/stars.png differ diff --git a/examples/hyperspace-roll/gfx/stars.toml b/examples/hyperspace-roll/gfx/stars.toml new file mode 100644 index 00000000..354d1df4 --- /dev/null +++ b/examples/hyperspace-roll/gfx/stars.toml @@ -0,0 +1,10 @@ +version = "1.0" + +[image.stars] +filename = "stars.png" +tile_size = "8x8" +transparent_colour = "121105" + +[image.title] +filename = "title-screen.png" +tile_size = "8x8" \ No newline at end of file diff --git a/examples/hyperspace-roll/gfx/title-screen-for-submission.aseprite b/examples/hyperspace-roll/gfx/title-screen-for-submission.aseprite new file mode 100644 index 00000000..3a6385d8 Binary files /dev/null and b/examples/hyperspace-roll/gfx/title-screen-for-submission.aseprite differ diff --git a/examples/hyperspace-roll/gfx/title-screen-for-submission.png b/examples/hyperspace-roll/gfx/title-screen-for-submission.png new file mode 100644 index 00000000..559d66f0 Binary files /dev/null and b/examples/hyperspace-roll/gfx/title-screen-for-submission.png differ diff --git a/examples/hyperspace-roll/gfx/title-screen.aseprite b/examples/hyperspace-roll/gfx/title-screen.aseprite new file mode 100644 index 00000000..ca96a661 Binary files /dev/null and b/examples/hyperspace-roll/gfx/title-screen.aseprite differ diff --git a/examples/hyperspace-roll/gfx/title-screen.png b/examples/hyperspace-roll/gfx/title-screen.png new file mode 100644 index 00000000..cc2c5240 Binary files /dev/null and b/examples/hyperspace-roll/gfx/title-screen.png differ diff --git a/examples/hyperspace-roll/rust-toolchain.toml b/examples/hyperspace-roll/rust-toolchain.toml new file mode 100644 index 00000000..06842486 --- /dev/null +++ b/examples/hyperspace-roll/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/examples/hyperspace-roll/sfx/BGM_Fight.wav b/examples/hyperspace-roll/sfx/BGM_Fight.wav new file mode 100644 index 00000000..3dc022dd Binary files /dev/null and b/examples/hyperspace-roll/sfx/BGM_Fight.wav differ diff --git a/examples/hyperspace-roll/sfx/BGM_Menu.wav b/examples/hyperspace-roll/sfx/BGM_Menu.wav new file mode 100644 index 00000000..1e463b30 Binary files /dev/null and b/examples/hyperspace-roll/sfx/BGM_Menu.wav differ diff --git a/examples/hyperspace-roll/sfx/BGM_Title.wav b/examples/hyperspace-roll/sfx/BGM_Title.wav new file mode 100644 index 00000000..c82d59c0 Binary files /dev/null and b/examples/hyperspace-roll/sfx/BGM_Title.wav differ diff --git a/examples/hyperspace-roll/sfx/MultiRoll_1.wav b/examples/hyperspace-roll/sfx/MultiRoll_1.wav new file mode 100644 index 00000000..a7444040 Binary files /dev/null and b/examples/hyperspace-roll/sfx/MultiRoll_1.wav differ diff --git a/examples/hyperspace-roll/sfx/MultiRoll_2.wav b/examples/hyperspace-roll/sfx/MultiRoll_2.wav new file mode 100644 index 00000000..3ec4b637 Binary files /dev/null and b/examples/hyperspace-roll/sfx/MultiRoll_2.wav differ diff --git a/examples/hyperspace-roll/sfx/MultiRoll_3.wav b/examples/hyperspace-roll/sfx/MultiRoll_3.wav new file mode 100644 index 00000000..3f89cd0b Binary files /dev/null and b/examples/hyperspace-roll/sfx/MultiRoll_3.wav differ diff --git a/examples/hyperspace-roll/sfx/MultiRoll_4.wav b/examples/hyperspace-roll/sfx/MultiRoll_4.wav new file mode 100644 index 00000000..87c3934d Binary files /dev/null and b/examples/hyperspace-roll/sfx/MultiRoll_4.wav differ diff --git a/examples/hyperspace-roll/sfx/MultiRoll_5.wav b/examples/hyperspace-roll/sfx/MultiRoll_5.wav new file mode 100644 index 00000000..1d284c02 Binary files /dev/null and b/examples/hyperspace-roll/sfx/MultiRoll_5.wav differ diff --git a/examples/hyperspace-roll/sfx/SingleRoll_1.wav b/examples/hyperspace-roll/sfx/SingleRoll_1.wav new file mode 100644 index 00000000..d3325698 Binary files /dev/null and b/examples/hyperspace-roll/sfx/SingleRoll_1.wav differ diff --git a/examples/hyperspace-roll/sfx/SingleRoll_2.wav b/examples/hyperspace-roll/sfx/SingleRoll_2.wav new file mode 100644 index 00000000..0968ccac Binary files /dev/null and b/examples/hyperspace-roll/sfx/SingleRoll_2.wav differ diff --git a/examples/hyperspace-roll/sfx/SingleRoll_3.wav b/examples/hyperspace-roll/sfx/SingleRoll_3.wav new file mode 100644 index 00000000..5fbc2d97 Binary files /dev/null and b/examples/hyperspace-roll/sfx/SingleRoll_3.wav differ diff --git a/examples/hyperspace-roll/sfx/SingleRoll_4.wav b/examples/hyperspace-roll/sfx/SingleRoll_4.wav new file mode 100644 index 00000000..a3dc1e25 Binary files /dev/null and b/examples/hyperspace-roll/sfx/SingleRoll_4.wav differ diff --git a/examples/hyperspace-roll/sfx/SingleRoll_5.wav b/examples/hyperspace-roll/sfx/SingleRoll_5.wav new file mode 100644 index 00000000..9eff0b5a Binary files /dev/null and b/examples/hyperspace-roll/sfx/SingleRoll_5.wav differ diff --git a/examples/hyperspace-roll/sfx/accept.wav b/examples/hyperspace-roll/sfx/accept.wav new file mode 100644 index 00000000..cb5c6f70 Binary files /dev/null and b/examples/hyperspace-roll/sfx/accept.wav differ diff --git a/examples/hyperspace-roll/sfx/back.wav b/examples/hyperspace-roll/sfx/back.wav new file mode 100644 index 00000000..604ff511 Binary files /dev/null and b/examples/hyperspace-roll/sfx/back.wav differ diff --git a/examples/hyperspace-roll/sfx/burst_shield_hit.wav b/examples/hyperspace-roll/sfx/burst_shield_hit.wav new file mode 100644 index 00000000..9e008ab7 Binary files /dev/null and b/examples/hyperspace-roll/sfx/burst_shield_hit.wav differ diff --git a/examples/hyperspace-roll/sfx/disrupt.wav b/examples/hyperspace-roll/sfx/disrupt.wav new file mode 100644 index 00000000..741c1736 Binary files /dev/null and b/examples/hyperspace-roll/sfx/disrupt.wav differ diff --git a/examples/hyperspace-roll/sfx/heal.wav b/examples/hyperspace-roll/sfx/heal.wav new file mode 100644 index 00000000..3b845e71 Binary files /dev/null and b/examples/hyperspace-roll/sfx/heal.wav differ diff --git a/examples/hyperspace-roll/sfx/move_cursor.wav b/examples/hyperspace-roll/sfx/move_cursor.wav new file mode 100644 index 00000000..09c5b705 Binary files /dev/null and b/examples/hyperspace-roll/sfx/move_cursor.wav differ diff --git a/examples/hyperspace-roll/sfx/select.wav b/examples/hyperspace-roll/sfx/select.wav new file mode 100644 index 00000000..0dac713d Binary files /dev/null and b/examples/hyperspace-roll/sfx/select.wav differ diff --git a/examples/hyperspace-roll/sfx/send_burst_shield.wav b/examples/hyperspace-roll/sfx/send_burst_shield.wav new file mode 100644 index 00000000..46fd3a79 Binary files /dev/null and b/examples/hyperspace-roll/sfx/send_burst_shield.wav differ diff --git a/examples/hyperspace-roll/sfx/shield_defend.wav b/examples/hyperspace-roll/sfx/shield_defend.wav new file mode 100644 index 00000000..5a8ca71f Binary files /dev/null and b/examples/hyperspace-roll/sfx/shield_defend.wav differ diff --git a/examples/hyperspace-roll/sfx/shield_down.wav b/examples/hyperspace-roll/sfx/shield_down.wav new file mode 100644 index 00000000..64e8fbc9 Binary files /dev/null and b/examples/hyperspace-roll/sfx/shield_down.wav differ diff --git a/examples/hyperspace-roll/sfx/shield_up.wav b/examples/hyperspace-roll/sfx/shield_up.wav new file mode 100644 index 00000000..563f84b6 Binary files /dev/null and b/examples/hyperspace-roll/sfx/shield_up.wav differ diff --git a/examples/hyperspace-roll/sfx/ship_explode.wav b/examples/hyperspace-roll/sfx/ship_explode.wav new file mode 100644 index 00000000..9119b984 Binary files /dev/null and b/examples/hyperspace-roll/sfx/ship_explode.wav differ diff --git a/examples/hyperspace-roll/sfx/shoot.wav b/examples/hyperspace-roll/sfx/shoot.wav new file mode 100644 index 00000000..b0a7a634 Binary files /dev/null and b/examples/hyperspace-roll/sfx/shoot.wav differ diff --git a/examples/hyperspace-roll/sfx/shot_hit.wav b/examples/hyperspace-roll/sfx/shot_hit.wav new file mode 100644 index 00000000..fe596647 Binary files /dev/null and b/examples/hyperspace-roll/sfx/shot_hit.wav differ diff --git a/examples/hyperspace-roll/src/background.rs b/examples/hyperspace-roll/src/background.rs new file mode 100644 index 00000000..91d16c95 --- /dev/null +++ b/examples/hyperspace-roll/src/background.rs @@ -0,0 +1,147 @@ +use agb::{ + display::tiled::{RegularMap, TileFormat, TileSet, TileSetting, VRamManager}, + include_gfx, rng, +}; + +use crate::sfx::Sfx; + +include_gfx!("gfx/stars.toml"); + +include_gfx!("gfx/help.toml"); + +pub fn load_palettes(vram: &mut VRamManager) { + vram.set_background_palettes(&[ + stars::stars.palettes[0].clone(), + crate::customise::DESCRIPTIONS_1_PALETTE.clone(), + crate::customise::DESCRIPTIONS_2_PALETTE.clone(), + help::help.palettes[0].clone(), + ]); +} + +pub(crate) fn load_help_text( + vram: &mut VRamManager, + background: &mut RegularMap, + help_text_line: u16, + at_tile: (u16, u16), +) { + let help_tileset = TileSet::new(help::help.tiles, agb::display::tiled::TileFormat::FourBpp); + + for x in 0..16 { + background.set_tile( + vram, + (x + at_tile.0, at_tile.1).into(), + &help_tileset, + TileSetting::new(help_text_line * 16 + x, false, false, 3), + ) + } +} + +// Expects a 64x32 map +fn create_background_map(map: &mut RegularMap, vram: &mut VRamManager, stars_tileset: &TileSet) { + for x in 0..64u16 { + for y in 0..32u16 { + let blank = rng::gen().rem_euclid(32) < 30; + + let tile_id = if blank { + (1 << 10) - 1 + } else { + rng::gen().rem_euclid(64) as u16 + }; + let tile_setting = TileSetting::new(tile_id, false, false, 0); + + map.set_tile(vram, (x, y).into(), stars_tileset, tile_setting); + } + } + + map.set_scroll_pos((0u16, rng::gen().rem_euclid(8) as u16).into()); +} + +pub fn show_title_screen(background: &mut RegularMap, vram: &mut VRamManager, sfx: &mut Sfx) { + background.set_scroll_pos((0_u16, 0_u16).into()); + vram.set_background_palettes(stars::title.palettes); + let tile_set = TileSet::new(stars::title.tiles, agb::display::tiled::TileFormat::FourBpp); + background.hide(); + + for x in 0..30u16 { + for y in 0..20u16 { + let tile_id = y * 30 + x; + background.set_tile( + vram, + (x, y).into(), + &tile_set, + TileSetting::new( + tile_id, + false, + false, + stars::title.palette_assignments[tile_id as usize], + ), + ); + } + + sfx.frame(); + } + + background.commit(vram); + sfx.frame(); + background.show(); +} + +pub struct StarBackground<'a> { + background1: &'a mut RegularMap, + background2: &'a mut RegularMap, + + background1_timer: u32, + background2_timer: u32, +} + +impl<'a> StarBackground<'a> { + pub fn new( + background1: &'a mut RegularMap, + background2: &'a mut RegularMap, + vram: &'_ mut VRamManager, + ) -> Self { + let stars_tileset = TileSet::new(stars::stars.tiles, TileFormat::FourBpp); + create_background_map(background1, vram, &stars_tileset); + create_background_map(background2, vram, &stars_tileset); + + Self { + background1, + background2, + + background1_timer: 0, + background2_timer: 0, + } + } + + pub fn update(&mut self) { + if self.background1_timer == 0 { + self.background1 + .set_scroll_pos(self.background1.scroll_pos() + (1u16, 0).into()); + self.background1_timer = 2; + } + + if self.background2_timer == 0 { + self.background2 + .set_scroll_pos(self.background2.scroll_pos() + (1u16, 0).into()); + self.background2_timer = 3; + } + + self.background1_timer -= 1; + self.background2_timer -= 1; + } + + pub fn commit(&mut self, vram: &mut VRamManager) { + self.background1.commit(vram); + self.background2.commit(vram); + } + + pub fn hide(&mut self) { + self.background1.hide(); + self.background2.hide(); + } + + pub fn show(&mut self) { + self.background1.show(); + self.background2.show(); + } +} diff --git a/examples/hyperspace-roll/src/battle.rs b/examples/hyperspace-roll/src/battle.rs new file mode 100644 index 00000000..3fd3b870 --- /dev/null +++ b/examples/hyperspace-roll/src/battle.rs @@ -0,0 +1,615 @@ +use crate::sfx::Sfx; +use crate::{ + graphics::SELECT_BOX, level_generation::generate_attack, Agb, EnemyAttackType, Face, PlayerDice, +}; +use agb::display::tiled::RegularMap; +use agb::{hash_map::HashMap, input::Button}; +use alloc::vec; +use alloc::vec::Vec; + +use self::display::BattleScreenDisplay; + +mod display; + +pub(super) const MALFUNCTION_COOLDOWN_FRAMES: u32 = 3 * 60; +const ROLL_TIME_FRAMES_ALL: u32 = 2 * 60; +const ROLL_TIME_FRAMES_ONE: u32 = 60 / 8; + +/// A face of the rolled die and it's cooldown (should it be a malfunction) +#[derive(Debug)] +struct RolledDie { + face: Face, + cooldown: u32, +} + +impl RolledDie { + fn new(face: Face) -> Self { + let cooldown = if face == Face::Malfunction { + MALFUNCTION_COOLDOWN_FRAMES + } else { + 0 + }; + + Self { face, cooldown } + } + + fn update(&mut self) { + self.cooldown = self.cooldown.saturating_sub(1); + } + + fn can_reroll(&self) -> bool { + self.face != Face::Malfunction || self.cooldown == 0 + } + + fn can_reroll_after_accept(&self) -> bool { + self.face != Face::Malfunction + } + + fn cooldown(&self) -> Option { + if self.face == Face::Malfunction && self.cooldown > 0 { + Some(self.cooldown) + } else { + None + } + } +} + +#[derive(Debug)] +enum DieState { + Rolling(u32, Face, Face), + Rolled(RolledDie), +} + +#[derive(Debug, Clone)] +pub enum Action { + PlayerActivateShield { amount: u32 }, + PlayerShoot { damage: u32, piercing: u32 }, + PlayerDisrupt { amount: u32 }, + PlayerHeal { amount: u32 }, + PlayerBurstShield { multiplier: u32 }, + PlayerSendBurstShield { damage: u32 }, + EnemyShoot { damage: u32 }, + EnemyShield { amount: u32 }, + EnemyHeal { amount: u32 }, +} + +#[derive(Debug)] +struct RolledDice { + rolls: Vec, +} + +impl RolledDice { + fn update(&mut self, player_dice: &PlayerDice) { + self.rolls + .iter_mut() + .zip(player_dice.dice.iter()) + .for_each(|(die_state, player_die)| match die_state { + DieState::Rolling(ref mut timeout, ref mut face, previous_face) => { + if *timeout == 0 { + let mut number_of_rolls = 0; + *die_state = DieState::Rolled(RolledDie::new(loop { + let next_face = player_die.roll(); + number_of_rolls += 1; + if *previous_face != Face::Malfunction + || next_face != *previous_face + || number_of_rolls > 16 + { + break next_face; + } + })); + } else { + if *timeout % 2 == 0 { + *face = player_die.roll(); + } + *timeout -= 1; + } + } + DieState::Rolled(ref mut rolled_die) => rolled_die.update(), + }); + } + + fn faces_for_accepting(&self) -> impl Iterator + '_ { + self.rolls.iter().filter_map(|state| match state { + DieState::Rolled(rolled_die) => Some(rolled_die.face), + _ => None, + }) + } + + fn faces_to_render(&self) -> impl Iterator)> + '_ { + self.rolls.iter().map(|rolled_die| match rolled_die { + DieState::Rolling(_, face, _previous_face) => (*face, None), + DieState::Rolled(rolled_die) => (rolled_die.face, rolled_die.cooldown()), + }) + } + + fn accept_rolls(&mut self, player_dice: &PlayerDice) -> Vec { + let mut actions = vec![]; + + let mut face_counts: HashMap = HashMap::new(); + let mut shield_multiplier = 1; + let mut shoot_multiplier = 1; + for face in self.faces_for_accepting() { + match face { + Face::DoubleShot => *face_counts.entry(Face::Shoot).or_default() += 2, + Face::TripleShot => *face_counts.entry(Face::Shoot).or_default() += 3, + Face::DoubleShield => *face_counts.entry(Face::Shield).or_default() += 2, + Face::TripleShield => *face_counts.entry(Face::Shield).or_default() += 3, + Face::DoubleShieldValue => shield_multiplier *= 2, + Face::DoubleShotValue => shoot_multiplier *= 2, + Face::TripleShotValue => shoot_multiplier *= 3, + other => *face_counts.entry(other).or_default() += 1, + } + } + + let invert = *face_counts.entry(Face::Invert).or_default() % 2 == 1; + + // shield + let mut shield_amount = *face_counts.entry(Face::Shield).or_default() * shield_multiplier; + + // shooting + let shoot = *face_counts.entry(Face::Shoot).or_default(); + let shoot_power = (shoot * (shoot + 1)) / 2; + + let malfunction_shots = *face_counts.entry(Face::MalfunctionShot).or_default(); + let malfunctions = *face_counts.entry(Face::Malfunction).or_default(); + + let malfunction_shoot = (malfunction_shots * (malfunction_shots + 1)) / 2 + * (malfunctions * (malfunctions + 1)) + / 2; + + if malfunction_shoot != 0 { + for roll in self.rolls.iter_mut().filter_map(|face| match face { + DieState::Rolled(rolled_die) if rolled_die.face == Face::Malfunction => { + Some(rolled_die) + } + _ => None, + }) { + roll.face = Face::Blank; + } + } + + let mut shoot_power = (shoot_power + malfunction_shoot) * shoot_multiplier; + + if invert { + (shoot_power, shield_amount) = (shield_amount, shoot_power); + } + + if shoot_power > 0 { + actions.push(Action::PlayerShoot { + damage: shoot_power, + piercing: *face_counts.entry(Face::Bypass).or_default(), + }); + } + + if shield_amount > 0 { + actions.push(Action::PlayerActivateShield { + amount: shield_amount.min(5), + }); + } + + // burst shield + if face_counts.contains_key(&Face::BurstShield) { + actions.push(Action::PlayerBurstShield { + multiplier: shoot_multiplier, + }); + } + + // disrupt + let disrupt = *face_counts.entry(Face::Disrupt).or_default(); + let disrupt_power = (disrupt * (disrupt + 1)) / 2; + + if disrupt_power > 0 { + actions.push(Action::PlayerDisrupt { + amount: disrupt_power, + }); + } + + let heal = *face_counts.entry(Face::Heal).or_default(); + if heal != 0 { + actions.push(Action::PlayerHeal { + amount: ((heal * (heal + 1)) / 2) as u32, + }); + } + + let mut malfunction_all = false; + + for roll in self.rolls.iter_mut().filter_map(|face| match face { + DieState::Rolled(rolled_die) => Some(rolled_die), + _ => None, + }) { + if roll.face == Face::DoubleShot + || roll.face == Face::DoubleShield + || roll.face == Face::DoubleShotValue + { + roll.cooldown = MALFUNCTION_COOLDOWN_FRAMES; + roll.face = Face::Malfunction; + } + if roll.face == Face::TripleShot + || roll.face == Face::TripleShield + || roll.face == Face::TripleShotValue + || roll.face == Face::BurstShield + { + malfunction_all = true; + } + } + + if malfunction_all { + for roll in self.rolls.iter_mut().filter_map(|face| match face { + DieState::Rolled(rolled_die) => Some(rolled_die), + _ => None, + }) { + roll.cooldown = MALFUNCTION_COOLDOWN_FRAMES; + roll.face = Face::Malfunction; + } + } + + // reroll non-malfunctions after accepting + for i in 0..player_dice.dice.len() { + self.roll_die(i, ROLL_TIME_FRAMES_ALL, true, player_dice); + } + + actions + } + + fn roll_die( + &mut self, + die_index: usize, + time: u32, + is_after_accept: bool, + player_dice: &PlayerDice, + ) { + if let DieState::Rolled(ref selected_rolled_die) = self.rolls[die_index] { + let can_reroll = if is_after_accept { + selected_rolled_die.can_reroll_after_accept() + } else { + selected_rolled_die.can_reroll() + }; + + if can_reroll { + self.rolls[die_index] = DieState::Rolling( + time, + player_dice.dice[die_index].roll(), + selected_rolled_die.face, + ); + } + } + } +} + +#[derive(Debug)] +struct PlayerState { + shield_count: u32, + health: u32, + max_health: u32, +} + +#[derive(Debug)] +pub enum EnemyAttack { + Shoot(u32), + Shield(u32), + Heal(u32), +} + +impl EnemyAttack { + fn apply_effect(&self) -> Action { + match self { + EnemyAttack::Shoot(damage) => Action::EnemyShoot { damage: *damage }, + EnemyAttack::Shield(shield) => Action::EnemyShield { amount: *shield }, + EnemyAttack::Heal(amount) => Action::EnemyHeal { amount: *amount }, + } + } +} + +#[derive(Debug)] +struct EnemyAttackState { + attack: EnemyAttack, + cooldown: u32, + max_cooldown: u32, +} + +impl EnemyAttackState { + fn attack_type(&self) -> EnemyAttackType { + match self.attack { + EnemyAttack::Shoot(_) => EnemyAttackType::Attack, + EnemyAttack::Shield(_) => EnemyAttackType::Shield, + EnemyAttack::Heal(_) => EnemyAttackType::Heal, + } + } + + fn value_to_show(&self) -> Option { + match self.attack { + EnemyAttack::Shoot(i) => Some(i), + EnemyAttack::Heal(i) => Some(i), + EnemyAttack::Shield(i) => Some(i), + } + } + + #[must_use] + fn update(&mut self) -> Option { + if self.cooldown == 0 { + return Some(self.attack.apply_effect()); + } + + self.cooldown -= 1; + + None + } +} + +#[derive(Debug)] +struct EnemyState { + shield_count: u32, + health: u32, + max_health: u32, +} + +#[derive(Debug)] +pub struct CurrentBattleState { + player: PlayerState, + enemy: EnemyState, + rolled_dice: RolledDice, + player_dice: PlayerDice, + attacks: [Option; 2], + current_level: u32, +} + +impl CurrentBattleState { + fn accept_rolls(&mut self) -> Vec { + self.rolled_dice.accept_rolls(&self.player_dice) + } + + fn roll_die(&mut self, die_index: usize, time: u32, is_after_accept: bool) { + self.rolled_dice + .roll_die(die_index, time, is_after_accept, &self.player_dice); + } + + fn update(&mut self) -> Vec { + let mut actions = vec![]; + + for attack in self.attacks.iter_mut() { + if let Some(attack_state) = attack { + if let Some(action) = attack_state.update() { + attack.take(); + actions.push(action); + } + } else if let Some(generated_attack) = generate_attack(self.current_level) { + attack.replace(EnemyAttackState { + attack: generated_attack.attack, + cooldown: generated_attack.cooldown, + max_cooldown: generated_attack.cooldown, + }); + } + } + + actions + } + + fn update_dice(&mut self) { + self.rolled_dice.update(&self.player_dice); + } + + fn apply_action(&mut self, action: Action, sfx: &mut Sfx) -> Option { + match action { + Action::PlayerActivateShield { amount } => { + if amount > self.player.shield_count { + sfx.shield_up(); + } + + self.player.shield_count = self.player.shield_count.max(amount); + None + } + Action::PlayerShoot { damage, piercing } => { + if self.enemy.shield_count <= piercing { + self.enemy.health = self.enemy.health.saturating_sub(damage); + sfx.shot_hit(); + } else if self.enemy.shield_count <= damage { + self.enemy.shield_count = 0; // TODO: Dispatch action of drop shield to animate that + sfx.shield_down(); + } else { + sfx.shield_defend(); + } + + None + } + Action::PlayerDisrupt { amount } => { + for attack in self.attacks.iter_mut().flatten() { + attack.cooldown += amount * 240; + attack.max_cooldown = attack.cooldown.max(attack.max_cooldown); + } + + sfx.disrupt(); + + None + } + Action::PlayerHeal { amount } => { + self.player.health = self.player.max_health.min(self.player.health + amount); + sfx.heal(); + None + } + Action::EnemyShoot { damage } => { + if self.player.shield_count == 0 { + self.player.health = self.player.health.saturating_sub(damage); + sfx.shot_hit(); + } else if self.player.shield_count <= damage { + self.player.shield_count = 0; // TODO: Dispatch action of drop shield to animate that + sfx.shield_down(); + } else { + sfx.shield_defend(); + } + + None + } + Action::EnemyShield { amount } => { + if amount > self.enemy.shield_count { + sfx.shield_up(); + } + + self.enemy.shield_count = self.enemy.shield_count.max(amount); + None + } + Action::EnemyHeal { amount } => { + self.enemy.health = self.enemy.max_health.min(self.enemy.health + amount); + sfx.heal(); + None + } + Action::PlayerBurstShield { multiplier } => { + let damage = + self.player.shield_count * (self.player.shield_count + 1) * multiplier / 2; + self.player.shield_count = 0; + sfx.send_burst_shield(); + + Some(Action::PlayerSendBurstShield { damage }) + } + Action::PlayerSendBurstShield { damage } => { + self.enemy.shield_count = 0; + self.enemy.health = self.enemy.health.saturating_sub(damage); + + sfx.burst_shield_hit(); + sfx.shield_down(); + + None + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum BattleResult { + Win, + Loss, +} + +pub(crate) fn battle_screen( + agb: &mut Agb, + player_dice: PlayerDice, + current_level: u32, + help_background: &mut RegularMap, +) -> BattleResult { + agb.sfx.battle(); + agb.sfx.frame(); + + help_background.set_scroll_pos((u16::MAX - 16, u16::MAX - 97).into()); + crate::background::load_help_text(&mut agb.vram, help_background, 1, (0, 0)); + crate::background::load_help_text(&mut agb.vram, help_background, 2, (0, 1)); + + let obj = &agb.obj; + + let mut select_box_obj = agb.obj.object(agb.obj.sprite(SELECT_BOX.sprite(0))); + select_box_obj.show(); + + let num_dice = player_dice.dice.len(); + + let enemy_health = 5 + current_level * agb::rng::gen().rem_euclid(4) as u32; + + let mut current_battle_state = CurrentBattleState { + player: PlayerState { + shield_count: 0, + health: 20, + max_health: 20, + }, + enemy: EnemyState { + shield_count: 0, + health: enemy_health, + max_health: enemy_health, + }, + rolled_dice: RolledDice { + rolls: player_dice + .dice + .iter() + .map(|die| DieState::Rolling(ROLL_TIME_FRAMES_ALL, die.roll(), Face::Blank)) + .collect(), + }, + player_dice: player_dice.clone(), + attacks: [None, None], + current_level, + }; + + let mut battle_screen_display = BattleScreenDisplay::new(obj, ¤t_battle_state); + agb.sfx.frame(); + + let mut selected_die = 0usize; + let mut input = agb::input::ButtonController::new(); + let mut counter = 0usize; + + loop { + counter = counter.wrapping_add(1); + + for action_to_apply in battle_screen_display.update(obj, ¤t_battle_state) { + if let Some(action_to_return) = + current_battle_state.apply_action(action_to_apply, &mut agb.sfx) + { + battle_screen_display.add_action(action_to_return, obj, &mut agb.sfx); + } + } + + for action in current_battle_state.update() { + battle_screen_display.add_action(action, obj, &mut agb.sfx); + } + + current_battle_state.update_dice(); + + input.update(); + + if input.is_just_pressed(Button::LEFT) { + if selected_die == 0 { + selected_die = num_dice - 1; + } else { + selected_die -= 1; + } + + agb.sfx.move_cursor(); + } + + if input.is_just_pressed(Button::RIGHT) { + if selected_die == num_dice - 1 { + selected_die = 0; + } else { + selected_die += 1; + } + + agb.sfx.move_cursor(); + } + + if input.is_just_pressed(Button::A) { + current_battle_state.roll_die(selected_die, ROLL_TIME_FRAMES_ONE, false); + agb.sfx.roll(); + } + + if input.is_just_pressed(Button::START) { + for action in current_battle_state.accept_rolls() { + battle_screen_display.add_action(action, obj, &mut agb.sfx); + } + agb.sfx.roll_multi(); + } + + select_box_obj + .set_y(120 - 4) + .set_x(selected_die as u16 * 40 + 28 - 4) + .set_sprite(agb.obj.sprite(SELECT_BOX.animation_sprite(counter / 10))); + + agb.star_background.update(); + agb.sfx.frame(); + agb.vblank.wait_for_vblank(); + help_background.commit(&mut agb.vram); + help_background.show(); + + if current_battle_state.enemy.health == 0 { + agb.sfx.ship_explode(); + help_background.hide(); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 0)); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 1)); + return BattleResult::Win; + } + + if current_battle_state.player.health == 0 { + agb.sfx.ship_explode(); + help_background.hide(); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 0)); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 1)); + return BattleResult::Loss; + } + + agb.obj.commit(); + agb.star_background.commit(&mut agb.vram); + } +} diff --git a/examples/hyperspace-roll/src/battle/display.rs b/examples/hyperspace-roll/src/battle/display.rs new file mode 100644 index 00000000..b1658233 --- /dev/null +++ b/examples/hyperspace-roll/src/battle/display.rs @@ -0,0 +1,500 @@ +use agb::display::object::{Object, ObjectController}; +use agb::rng; +use alloc::vec; +use alloc::vec::Vec; + +use crate::graphics::{BURST_BULLET, DISRUPT_BULLET, SHIELD}; +use crate::sfx::Sfx; +use crate::{ + graphics::{ + FractionDisplay, HealthBar, NumberDisplay, BULLET_SPRITE, ENEMY_ATTACK_SPRITES, + FACE_SPRITES, SHIP_SPRITES, + }, + EnemyAttackType, Ship, +}; + +use super::{Action, CurrentBattleState, EnemyAttackState, MALFUNCTION_COOLDOWN_FRAMES}; + +struct BattleScreenDisplayObjects<'a> { + dice: Vec>, + dice_cooldowns: Vec>, + player_shield: Vec>, + enemy_shield: Vec>, + + player_healthbar: HealthBar<'a>, + enemy_healthbar: HealthBar<'a>, + player_health: FractionDisplay<'a>, + enemy_health: FractionDisplay<'a>, + + enemy_attack_display: Vec>, +} + +pub struct BattleScreenDisplay<'a> { + objs: BattleScreenDisplayObjects<'a>, + animations: Vec>, + + _misc_sprites: Vec>, +} + +const HEALTH_BAR_WIDTH: usize = 48; + +impl<'a> BattleScreenDisplay<'a> { + pub fn new(obj: &'a ObjectController, current_battle_state: &CurrentBattleState) -> Self { + let mut misc_sprites = vec![]; + let player_x = 12; + let player_y = 8; + let enemy_x = 167; + + let player_sprite = SHIP_SPRITES.sprite_for_ship(Ship::Player); + let enemy_sprite = SHIP_SPRITES.sprite_for_ship(if rng::gen() % 2 == 0 { + Ship::Drone + } else { + Ship::PilotedShip + }); + + let mut player_obj = obj.object(obj.sprite(player_sprite)); + let mut enemy_obj = obj.object(obj.sprite(enemy_sprite)); + + player_obj.set_x(player_x).set_y(player_y).set_z(1).show(); + enemy_obj.set_x(enemy_x).set_y(player_y).set_z(1).show(); + + misc_sprites.push(player_obj); + misc_sprites.push(enemy_obj); + + let dice: Vec<_> = current_battle_state + .rolled_dice + .faces_to_render() + .enumerate() + .map(|(i, (face, _))| { + let mut die_obj = obj.object(obj.sprite(FACE_SPRITES.sprite_for_face(face))); + + die_obj.set_y(120).set_x(i as u16 * 40 + 28).show(); + + die_obj + }) + .collect(); + + let dice_cooldowns: Vec<_> = dice + .iter() + .enumerate() + .map(|(i, _)| { + let mut cooldown_bar = + HealthBar::new((i as i32 * 40 + 28, 120 - 8).into(), 24, obj); + cooldown_bar.hide(); + cooldown_bar + }) + .collect(); + + let shield_sprite = SHIP_SPRITES.sprite_for_ship(Ship::Shield); + + let player_shield: Vec<_> = (0..5) + .into_iter() + .map(|i| { + let mut shield_obj = obj.object(obj.sprite(shield_sprite)); + shield_obj + .set_x(player_x + 18 + 11 * i) + .set_y(player_y) + .hide(); + + shield_obj + }) + .collect(); + + let enemy_shield: Vec<_> = (0..5) + .into_iter() + .map(|i| { + let mut shield_obj = obj.object(obj.sprite(shield_sprite)); + shield_obj + .set_x(enemy_x - 16 - 11 * i) + .set_y(player_y) + .set_hflip(true) + .hide(); + + shield_obj + }) + .collect(); + + let player_healthbar_x = 18; + let enemy_healthbar_x = 180; + let player_healthbar = HealthBar::new( + (player_healthbar_x, player_y - 8).into(), + HEALTH_BAR_WIDTH, + obj, + ); + let enemy_healthbar = HealthBar::new( + (enemy_healthbar_x, player_y - 8).into(), + HEALTH_BAR_WIDTH, + obj, + ); + + let player_health_display = FractionDisplay::new( + ( + player_healthbar_x + HEALTH_BAR_WIDTH as u16 / 2 - 16, + player_y, + ) + .into(), + 3, + obj, + ); + let enemy_health_display = FractionDisplay::new( + ( + enemy_healthbar_x + HEALTH_BAR_WIDTH as u16 / 2 - 16, + player_y, + ) + .into(), + 3, + obj, + ); + + let enemy_attack_display = (0..2) + .into_iter() + .map(|i| { + let mut attack_obj = obj.object( + obj.sprite(ENEMY_ATTACK_SPRITES.sprite_for_attack(EnemyAttackType::Attack)), + ); + + let attack_obj_position = (120, 56 + 32 * i).into(); + attack_obj.set_position(attack_obj_position).hide(); + + let mut attack_cooldown = + HealthBar::new(attack_obj_position + (32, 8).into(), 48, obj); + attack_cooldown.hide(); + + let attack_number_display = + NumberDisplay::new(attack_obj_position - (8, -10).into()); + + EnemyAttackDisplay::new(attack_obj, attack_cooldown, attack_number_display) + }) + .collect(); + + let objs = BattleScreenDisplayObjects { + dice, + dice_cooldowns, + player_shield, + enemy_shield, + + player_healthbar, + enemy_healthbar, + player_health: player_health_display, + enemy_health: enemy_health_display, + + enemy_attack_display, + }; + + Self { + objs, + + animations: vec![], + + _misc_sprites: misc_sprites, + } + } + + pub fn update( + &mut self, + obj: &'a ObjectController, + current_battle_state: &CurrentBattleState, + ) -> Vec { + for (i, player_shield) in self.objs.player_shield.iter_mut().enumerate() { + if i < current_battle_state.player.shield_count as usize { + player_shield + .show() + .set_sprite(obj.sprite(SHIELD.sprite(0))); + } else { + player_shield.hide(); + } + } + + for (i, player_shield) in self.objs.enemy_shield.iter_mut().enumerate() { + if i < current_battle_state.enemy.shield_count as usize { + player_shield + .show() + .set_sprite(obj.sprite(SHIELD.sprite(0))); + } else { + player_shield.hide(); + } + } + + self.objs.player_healthbar.set_value( + ((current_battle_state.player.health * HEALTH_BAR_WIDTH as u32) + / current_battle_state.player.max_health) as usize, + obj, + ); + + self.objs.enemy_healthbar.set_value( + ((current_battle_state.enemy.health * HEALTH_BAR_WIDTH as u32) + / current_battle_state.enemy.max_health) as usize, + obj, + ); + + self.objs.player_health.set_value( + current_battle_state.player.health as usize, + current_battle_state.player.max_health as usize, + obj, + ); + + self.objs.enemy_health.set_value( + current_battle_state.enemy.health as usize, + current_battle_state.enemy.max_health as usize, + obj, + ); + + for (i, attack) in current_battle_state.attacks.iter().enumerate() { + self.objs.enemy_attack_display[i].update(attack, obj); + } + + let mut actions_to_apply = vec![]; + + // update the dice display to display the current values + for ((die_obj, (current_face, cooldown)), cooldown_healthbar) in self + .objs + .dice + .iter_mut() + .zip(current_battle_state.rolled_dice.faces_to_render()) + .zip(self.objs.dice_cooldowns.iter_mut()) + { + die_obj.set_sprite(obj.sprite(FACE_SPRITES.sprite_for_face(current_face))); + + if let Some(cooldown) = cooldown { + cooldown_healthbar + .set_value((cooldown * 24 / MALFUNCTION_COOLDOWN_FRAMES) as usize, obj); + cooldown_healthbar.show(); + } else { + cooldown_healthbar.hide(); + } + } + + let mut animations_to_remove = vec![]; + for (i, animation) in self.animations.iter_mut().enumerate() { + match animation.update(&mut self.objs, obj, current_battle_state) { + AnimationUpdateState::RemoveWithAction(a) => { + actions_to_apply.push(a); + animations_to_remove.push(i); + } + AnimationUpdateState::Continue => {} + } + } + + for &animation_to_remove in animations_to_remove.iter().rev() { + self.animations.swap_remove(animation_to_remove); + } + + actions_to_apply + } + + pub fn add_action(&mut self, action: Action, obj: &'a ObjectController, sfx: &mut Sfx) { + play_sound_for_action_start(&action, sfx); + + self.animations + .push(AnimationStateHolder::for_action(action, obj)); + } +} + +fn play_sound_for_action_start(action: &Action, sfx: &mut Sfx) { + match action { + Action::PlayerShoot { .. } | Action::EnemyShoot { .. } => sfx.shoot(), + _ => {} + } +} + +struct EnemyAttackDisplay<'a> { + face: Object<'a>, + cooldown: HealthBar<'a>, + number: NumberDisplay<'a>, +} + +impl<'a> EnemyAttackDisplay<'a> { + pub fn new(face: Object<'a>, cooldown: HealthBar<'a>, number: NumberDisplay<'a>) -> Self { + Self { + face, + cooldown, + number, + } + } + + pub fn update(&mut self, attack: &Option, obj: &'a ObjectController) { + if let Some(attack) = attack { + self.face.show().set_sprite( + obj.sprite(ENEMY_ATTACK_SPRITES.sprite_for_attack(attack.attack_type())), + ); + self.cooldown + .set_value((attack.cooldown * 48 / attack.max_cooldown) as usize, obj); + self.cooldown.show(); + + self.number.set_value(attack.value_to_show(), obj); + } else { + self.face.hide(); + self.cooldown.hide(); + self.number.set_value(None, obj); + } + } +} + +enum AnimationState<'a> { + PlayerShoot { bullet: Object<'a>, x: i32 }, + PlayerActivateShield { amount: u32, frame: usize }, + PlayerDisrupt { bullet: Object<'a>, x: i32 }, + PlayerBurstShield { frame: usize }, + PlayerSendBurstShield { bullet: Object<'a>, x: i32 }, + PlayerHeal {}, + EnemyShoot { bullet: Object<'a>, x: i32 }, + EnemyShield { amount: u32, frame: usize }, + EnemyHeal {}, +} + +struct AnimationStateHolder<'a> { + action: Action, + state: AnimationState<'a>, +} + +enum AnimationUpdateState { + RemoveWithAction(Action), + Continue, +} + +impl<'a> AnimationStateHolder<'a> { + fn for_action(a: Action, obj: &'a ObjectController) -> Self { + let state = match a { + Action::PlayerActivateShield { amount, .. } => { + AnimationState::PlayerActivateShield { amount, frame: 0 } + } + Action::PlayerShoot { .. } => AnimationState::PlayerShoot { + bullet: obj.object(obj.sprite(BULLET_SPRITE)), + x: 64, + }, + Action::PlayerDisrupt { .. } => AnimationState::PlayerDisrupt { + bullet: obj.object(obj.sprite(DISRUPT_BULLET)), + x: 64, + }, + Action::PlayerHeal { .. } => AnimationState::PlayerHeal {}, + Action::PlayerBurstShield { .. } => AnimationState::PlayerBurstShield { frame: 0 }, + Action::PlayerSendBurstShield { .. } => AnimationState::PlayerSendBurstShield { + bullet: obj.object(obj.sprite(BURST_BULLET)), + x: 64, + }, + Action::EnemyShoot { .. } => AnimationState::EnemyShoot { + bullet: obj.object(obj.sprite(BULLET_SPRITE)), + x: 175, + }, + Action::EnemyShield { amount, .. } => AnimationState::EnemyShield { amount, frame: 0 }, + Action::EnemyHeal { .. } => AnimationState::EnemyHeal {}, + }; + + Self { action: a, state } + } + + fn update( + &mut self, + objs: &mut BattleScreenDisplayObjects<'a>, + obj: &'a ObjectController, + current_battle_state: &CurrentBattleState, + ) -> AnimationUpdateState { + match &mut self.state { + AnimationState::PlayerShoot { bullet, x } => { + bullet.show().set_x(*x as u16).set_y(36); + *x += 4; + + if *x > 180 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + AnimationState::PlayerDisrupt { bullet, x } => { + bullet.show().set_x(*x as u16).set_y(36); + *x += 2; + + if *x > 180 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + AnimationState::PlayerActivateShield { amount, frame } => { + // find all the shields that need animating + let current_player_shields = current_battle_state.player.shield_count; + if current_player_shields < *amount { + for i in current_player_shields..*amount { + objs.player_shield[i as usize] + .show() + .set_sprite(obj.sprite(SHIELD.sprite(3 - *frame / 2))); + } + } else { + return AnimationUpdateState::RemoveWithAction(self.action.clone()); + } + + *frame += 1; + + if *frame >= 6 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + AnimationState::EnemyShoot { bullet, x } => { + bullet.show().set_hflip(true).set_x(*x as u16).set_y(36); + *x -= 4; + + if *x < 50 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + AnimationState::EnemyShield { amount, frame } => { + // find all the shields that need animating + let current_enemy_shields = current_battle_state.enemy.shield_count; + if current_enemy_shields < *amount { + for i in current_enemy_shields..*amount { + objs.enemy_shield[i as usize] + .show() + .set_sprite(obj.sprite(SHIELD.sprite(3 - *frame / 2))); + } + } else { + return AnimationUpdateState::RemoveWithAction(self.action.clone()); + } + + *frame += 1; + + if *frame > 6 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + AnimationState::EnemyHeal {} => { + AnimationUpdateState::RemoveWithAction(self.action.clone()) // TODO: Animation for healing + } + AnimationState::PlayerHeal {} => { + AnimationUpdateState::RemoveWithAction(self.action.clone()) // TODO: Animation for healing + } + AnimationState::PlayerBurstShield { frame } => { + if *frame < 10 { + for shield in objs.player_shield.iter_mut() { + shield.set_sprite(obj.sprite(SHIELD.sprite(*frame / 2))); + } + + *frame += 1; + + AnimationUpdateState::Continue + } else { + for shield in objs.player_shield.iter_mut() { + shield.set_sprite(obj.sprite(SHIELD.sprite(0))); + } + + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } + } + AnimationState::PlayerSendBurstShield { bullet, x } => { + bullet.show().set_x(*x as u16).set_y(36); + *x += 1; + + if *x > 180 { + AnimationUpdateState::RemoveWithAction(self.action.clone()) + } else { + AnimationUpdateState::Continue + } + } + } + } +} diff --git a/examples/hyperspace-roll/src/customise.rs b/examples/hyperspace-roll/src/customise.rs new file mode 100644 index 00000000..852ff5a5 --- /dev/null +++ b/examples/hyperspace-roll/src/customise.rs @@ -0,0 +1,402 @@ +use agb::{ + display::{ + object::{Object, ObjectController}, + palette16::Palette16, + tiled::{RegularMap, TileSet, TileSetting}, + HEIGHT, WIDTH, + }, + include_gfx, + input::{Button, Tri}, +}; + +use alloc::vec::Vec; + +use crate::{ + graphics::{FACE_SPRITES, MODIFIED_BOX, SELECTED_BOX, SELECT_BOX}, + Agb, Die, Face, PlayerDice, +}; + +include_gfx!("gfx/descriptions.toml"); + +pub const DESCRIPTIONS_1_PALETTE: &Palette16 = &descriptions::descriptions1.palettes[0]; +pub const DESCRIPTIONS_2_PALETTE: &Palette16 = &descriptions::descriptions2.palettes[0]; + +enum CustomiseState { + Dice, + Face, + Upgrade, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct Cursor { + dice: usize, + face: usize, + upgrade: usize, +} + +fn net_position_for_index(idx: usize) -> (u32, u32) { + if idx == 4 { + (1, 0) + } else if idx == 5 { + (1, 2) + } else { + (idx as u32, 1) + } +} + +fn screen_position_for_index(idx: usize) -> (u32, u32) { + let (x, y) = net_position_for_index(idx); + (x * 32 + 20, y * 32 + HEIGHT as u32 - 3 * 32) +} + +fn move_net_position_lr(idx: usize, direction: Tri) -> usize { + match direction { + Tri::Zero => idx, + Tri::Positive => { + if idx >= 4 { + 2 + } else { + (idx + 1) % 3 + } + } + Tri::Negative => { + if idx >= 4 { + 0 + } else { + idx.checked_sub(1).unwrap_or(2) + } + } + } +} + +fn move_net_position_ud(idx: usize, direction: Tri) -> usize { + match direction { + Tri::Zero => idx, + Tri::Negative => { + if idx < 4 { + 4 + } else if idx == 4 { + 5 + } else if idx == 5 { + 1 + } else { + unreachable!() + } + } + Tri::Positive => { + if idx < 4 { + 5 + } else if idx == 4 { + 1 + } else if idx == 5 { + 4 + } else { + unreachable!() + } + } + } +} + +fn create_dice_display<'a>(gfx: &'a ObjectController, dice: &'_ PlayerDice) -> Vec> { + let mut objects = Vec::new(); + for (idx, dice) in dice.dice.iter().enumerate() { + let mut obj = gfx.object(gfx.sprite(FACE_SPRITES.sprite_for_face(dice.faces[1]))); + obj.set_x((idx as i32 * 32 - 24 / 2 + 20) as u16); + obj.set_y(16 - 24 / 2); + + obj.show(); + + objects.push(obj); + } + objects +} + +fn create_net<'a>(gfx: &'a ObjectController, die: &'_ Die, modified: &[usize]) -> Vec> { + let mut objects = Vec::new(); + for (idx, &face) in die.faces.iter().enumerate() { + let mut obj = gfx.object(gfx.sprite(FACE_SPRITES.sprite_for_face(face))); + let (x, y) = screen_position_for_index(idx); + obj.set_x((x - 24 / 2) as u16); + obj.set_y((y - 24 / 2) as u16); + + obj.show(); + + objects.push(obj); + } + + for &m in modified.iter().chain(core::iter::once(&3)) { + let mut obj = gfx.object(gfx.sprite(MODIFIED_BOX)); + let (x, y) = screen_position_for_index(m); + obj.set_x((x - 32 / 2) as u16); + obj.set_y((y - 32 / 2) as u16); + + obj.show(); + + objects.push(obj); + } + + objects +} + +fn upgrade_position(idx: usize) -> (u32, u32) { + ( + (WIDTH - 80) as u32, + (idx * 32 + HEIGHT as usize - 3 * 32) as u32, + ) +} + +fn create_upgrade_objects<'a>(gfx: &'a ObjectController, upgrades: &[Face]) -> Vec> { + let mut objects = Vec::new(); + for (idx, &upgrade) in upgrades.iter().enumerate() { + let mut obj = gfx.object(gfx.sprite(FACE_SPRITES.sprite_for_face(upgrade))); + let (x, y) = upgrade_position(idx); + obj.set_x((x - 24 / 2) as u16); + obj.set_y((y - 24 / 2) as u16); + + obj.show(); + + objects.push(obj); + } + objects +} + +pub(crate) fn customise_screen( + agb: &mut Agb, + mut player_dice: PlayerDice, + descriptions_map: &mut RegularMap, + help_background: &mut RegularMap, + level: u32, +) -> PlayerDice { + agb.sfx.customise(); + agb.sfx.frame(); + descriptions_map.set_scroll_pos((u16::MAX - 174, u16::MAX - 52).into()); + + help_background.set_scroll_pos((u16::MAX - 148, u16::MAX - 34).into()); + crate::background::load_help_text(&mut agb.vram, help_background, 0, (0, 0)); + + let descriptions_1_tileset = TileSet::new( + descriptions::descriptions1.tiles, + agb::display::tiled::TileFormat::FourBpp, + ); + let descriptions_2_tileset = TileSet::new( + descriptions::descriptions2.tiles, + agb::display::tiled::TileFormat::FourBpp, + ); + + // create the dice + + let mut _net = create_net(&agb.obj, &player_dice.dice[0], &[]); + let mut _dice = create_dice_display(&agb.obj, &player_dice); + + agb.sfx.frame(); + + let mut upgrades = crate::level_generation::generate_upgrades(level); + let mut _upgrade_objects = create_upgrade_objects(&agb.obj, &upgrades); + + let mut input = agb::input::ButtonController::new(); + + let mut select_box = agb.obj.object(agb.obj.sprite(SELECT_BOX.sprite(0))); + + select_box.show(); + + let mut selected_dice = agb.obj.object(agb.obj.sprite(SELECTED_BOX)); + selected_dice.hide(); + let mut selected_face = agb.obj.object(agb.obj.sprite(SELECTED_BOX)); + selected_face.hide(); + agb.sfx.frame(); + + let mut counter = 0usize; + + let mut state = CustomiseState::Dice; + + let mut cursor = Cursor { + dice: 0, + face: 1, + upgrade: 0, + }; + + let mut modified: Vec = Vec::new(); + + loop { + counter = counter.wrapping_add(1); + input.update(); + let ud = ( + input.is_just_pressed(Button::UP), + input.is_just_pressed(Button::DOWN), + ) + .into(); + let lr = ( + input.is_just_pressed(Button::LEFT), + input.is_just_pressed(Button::RIGHT), + ) + .into(); + + if ud != Tri::Zero || lr != Tri::Zero { + agb.sfx.move_cursor(); + } + + match &mut state { + CustomiseState::Dice => { + selected_dice.hide(); + let new_dice = (cursor.dice as isize + lr as isize) + .rem_euclid(player_dice.dice.len() as isize) + as usize; + if new_dice != cursor.dice { + cursor.dice = new_dice; + _net = create_net( + &agb.obj, + &player_dice.dice[cursor.dice], + &modified + .iter() + .filter_map(|x| (x.dice == cursor.dice).then_some(x.face)) + .collect::>(), + ); + } + + select_box.set_x((cursor.dice as i32 * 32 - 32 / 2 + 20) as u16); + select_box.set_y(0); + + if input.is_just_pressed(Button::A) { + selected_dice.set_x((cursor.dice as i32 * 32 - 32 / 2 + 20) as u16); + selected_dice.set_y(0); + selected_dice.show(); + state = CustomiseState::Face; + agb.sfx.select(); + } + } + CustomiseState::Face => { + cursor.face = move_net_position_lr(cursor.face, lr); + cursor.face = move_net_position_ud(cursor.face, ud); + + let (x, y) = screen_position_for_index(cursor.face); + select_box.set_x((x - 32 / 2) as u16); + select_box.set_y((y - 32 / 2) as u16); + selected_face.hide(); + + if input.is_just_pressed(Button::B) { + state = CustomiseState::Dice; + agb.sfx.back(); + } else if input.is_just_pressed(Button::A) + && !upgrades.is_empty() + && !modified.contains(&Cursor { + dice: cursor.dice, + face: cursor.face, + upgrade: 0, + }) + { + selected_face.set_x((x - 32 / 2) as u16); + selected_face.set_y((y - 32 / 2) as u16); + selected_face.show(); + + cursor.upgrade += upgrades.len(); + + state = CustomiseState::Upgrade; + agb.sfx.select(); + } + } + CustomiseState::Upgrade => { + let old_updade = cursor.upgrade; + cursor.upgrade = (cursor.upgrade as isize + ud as isize) + .rem_euclid(upgrades.len() as isize) as usize; + + if (upgrades[cursor.upgrade] as u32) < 17 { + if cursor.upgrade != old_updade { + for y in 0..11 { + for x in 0..8 { + if (upgrades[cursor.upgrade] as usize) < 10 { + descriptions_map.set_tile( + &mut agb.vram, + (x, y).into(), + &descriptions_1_tileset, + TileSetting::new( + y * 8 + x + 8 * 11 * upgrades[cursor.upgrade] as u16, + false, + false, + 1, + ), + ) + } else { + descriptions_map.set_tile( + &mut agb.vram, + (x, y).into(), + &descriptions_2_tileset, + TileSetting::new( + y * 8 + + x + + 8 * 11 * (upgrades[cursor.upgrade] as u16 - 10), + false, + false, + 2, + ), + ) + } + } + } + } + descriptions_map.show(); + } else { + descriptions_map.hide(); + } + + let (x, y) = upgrade_position(cursor.upgrade); + select_box.set_x((x - 32 / 2) as u16); + select_box.set_y((y - 32 / 2) as u16); + + if input.is_just_pressed(Button::B) { + state = CustomiseState::Face; + agb.sfx.back(); + } else if input.is_just_pressed(Button::A) + && player_dice.dice[cursor.dice].faces[cursor.face] != upgrades[cursor.upgrade] + { + descriptions_map.hide(); + + modified.push(Cursor { + dice: cursor.dice, + face: cursor.face, + upgrade: 0, + }); + + player_dice.dice[cursor.dice].faces[cursor.face] = upgrades[cursor.upgrade]; + upgrades.remove(cursor.upgrade); + _upgrade_objects = create_upgrade_objects(&agb.obj, &upgrades); + + _net = create_net( + &agb.obj, + &player_dice.dice[cursor.dice], + &modified + .iter() + .filter_map(|x| (x.dice == cursor.dice).then_some(x.face)) + .collect::>(), + ); + _dice = create_dice_display(&agb.obj, &player_dice); + state = CustomiseState::Face; + agb.sfx.accept(); + } + } + } + + if upgrades.is_empty() { + break; + } + + select_box.set_sprite(agb.obj.sprite(SELECT_BOX.animation_sprite(counter / 10))); + + agb.star_background.update(); + let _ = agb::rng::gen(); + agb.sfx.frame(); + agb.vblank.wait_for_vblank(); + agb.obj.commit(); + descriptions_map.commit(&mut agb.vram); + help_background.commit(&mut agb.vram); + help_background.show(); + agb.star_background.commit(&mut agb.vram); + } + + descriptions_map.hide(); + help_background.hide(); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 0)); + crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 1)); + descriptions_map.clear(&mut agb.vram); + + player_dice +} diff --git a/examples/hyperspace-roll/src/graphics.rs b/examples/hyperspace-roll/src/graphics.rs new file mode 100644 index 00000000..bab60ae6 --- /dev/null +++ b/examples/hyperspace-roll/src/graphics.rs @@ -0,0 +1,300 @@ +use agb::{ + display::object::{Object, ObjectController, Sprite, Tag}, + fixnum::Vector2D, +}; +use alloc::vec::Vec; + +use crate::{EnemyAttackType, Face, Ship}; + +const SPRITES: &agb::display::object::Graphics = agb::include_aseprite!( + "gfx/dice-faces.aseprite", + "gfx/ships.aseprite", + "gfx/small-sprites.aseprite" +); +pub const FACE_SPRITES: &FaceSprites = &FaceSprites::load_face_sprites(); +pub const ENEMY_ATTACK_SPRITES: &EnemyAttackSprites = &EnemyAttackSprites::new(); +pub const SELECT_BOX: &Tag = SPRITES.tags().get("selection"); +pub const SELECTED_BOX: &Sprite = SPRITES.tags().get("selected").sprite(0); +pub const MODIFIED_BOX: &Sprite = SPRITES.tags().get("modified").sprite(0); + +pub const BULLET_SPRITE: &Sprite = SPRITES.tags().get("bullet").sprite(0); +pub const DISRUPT_BULLET: &Sprite = SPRITES.tags().get("disrupt bullet").sprite(0); +pub const BURST_BULLET: &Sprite = SPRITES.tags().get("burst shield bullet").sprite(0); +pub const SHIELD: &Tag = SPRITES.tags().get("ship shield"); + +pub const SHIP_SPRITES: &ShipSprites = &ShipSprites::load_ship_sprites(); + +pub const SMALL_SPRITES: &SmallSprites = &SmallSprites {}; + +pub struct FaceSprites { + sprites: [&'static Sprite; 17], +} + +impl FaceSprites { + const fn load_face_sprites() -> Self { + const S_SHOOT: &Sprite = SPRITES.tags().get("shoot").sprite(0); + const S_SHIELD: &Sprite = SPRITES.tags().get("shield").sprite(0); + const S_MALFUNCTION: &Sprite = SPRITES.tags().get("malfunction").sprite(0); + const S_HEAL: &Sprite = SPRITES.tags().get("player_heal").sprite(0); + const S_BYPASS: &Sprite = SPRITES.tags().get("shield bypass").sprite(0); + const S_DOUBLESHOT: &Sprite = SPRITES.tags().get("double shoot").sprite(0); + const S_TRIPLESHOT: &Sprite = SPRITES.tags().get("triple shoot").sprite(0); + const S_BLANK: &Sprite = SPRITES.tags().get("blank").sprite(0); + const S_DISRUPT: &Sprite = SPRITES.tags().get("disruption").sprite(0); + const S_MALFUNCTION_SHOOT: &Sprite = SPRITES.tags().get("malfunction shot").sprite(0); + const S_DOUBLE_SHIELD: &Sprite = SPRITES.tags().get("double shield").sprite(0); + const S_TRIPLE_SHIELD: &Sprite = SPRITES.tags().get("triple shield").sprite(0); + const S_DOUBLE_SHIELD_VALUE: &Sprite = SPRITES.tags().get("double shield value").sprite(0); + const S_DOUBLE_SHOT_VALUE: &Sprite = SPRITES.tags().get("double shoot power").sprite(0); + const S_TRIPLE_SHOT_VALUE: &Sprite = SPRITES.tags().get("triple shoot power").sprite(0); + const S_BURST_SHIELD: &Sprite = SPRITES.tags().get("burst shield").sprite(0); + const S_INVERT: &Sprite = SPRITES.tags().get("swap shield and shoot").sprite(0); + + Self { + sprites: [ + S_SHOOT, + S_SHIELD, + S_MALFUNCTION, + S_HEAL, + S_BYPASS, + S_DOUBLESHOT, + S_TRIPLESHOT, + S_BLANK, + S_DISRUPT, + S_MALFUNCTION_SHOOT, + S_DOUBLE_SHIELD, + S_TRIPLE_SHIELD, + S_DOUBLE_SHIELD_VALUE, + S_DOUBLE_SHOT_VALUE, + S_TRIPLE_SHOT_VALUE, + S_BURST_SHIELD, + S_INVERT, + ], + } + } + + pub fn sprite_for_face(&self, face: Face) -> &'static Sprite { + self.sprites[face as usize] + } +} + +pub struct ShipSprites { + sprites: [&'static Sprite; 4], +} + +impl ShipSprites { + const fn load_ship_sprites() -> Self { + const S_PLAYER: &Sprite = SPRITES.tags().get("player").sprite(0); + const S_DRONE: &Sprite = SPRITES.tags().get("drone").sprite(0); + const S_PILOTED_SHIP: &Sprite = SPRITES.tags().get("piloted ship").sprite(0); + const S_SHIELD: &Sprite = SPRITES.tags().get("ship shield").sprite(0); + + Self { + sprites: [S_PLAYER, S_DRONE, S_PILOTED_SHIP, S_SHIELD], + } + } + + pub fn sprite_for_ship(&self, ship: Ship) -> &'static Sprite { + self.sprites[ship as usize] + } +} + +pub struct SmallSprites; + +impl SmallSprites { + pub const fn number(&self, i: u32) -> &'static Sprite { + SPRITES.tags().get("numbers").sprite(i as usize) + } + + pub const fn slash(&self) -> &'static Sprite { + SPRITES.tags().get("numbers").sprite(10) + } + + pub const fn red_bar(&self, i: usize) -> &'static Sprite { + SPRITES.tags().get("red bar").sprite(i) + } +} + +pub struct EnemyAttackSprites { + sprites: [&'static Sprite; 3], +} + +impl EnemyAttackSprites { + const fn new() -> Self { + const S_SHOOT: &Sprite = SPRITES.tags().get("enemy shoot").sprite(0); + const S_SHIELD: &Sprite = SPRITES.tags().get("enemy shield").sprite(0); + const S_HEAL: &Sprite = SPRITES.tags().get("enemy heal").sprite(0); + + Self { + sprites: [S_SHOOT, S_SHIELD, S_HEAL], + } + } + + pub fn sprite_for_attack(&self, attack: EnemyAttackType) -> &'static Sprite { + self.sprites[attack as usize] + } +} + +pub struct HealthBar<'a> { + max: usize, + sprites: Vec>, +} + +impl<'a> HealthBar<'a> { + pub fn new(pos: Vector2D, max: usize, obj: &'a ObjectController) -> Self { + assert_eq!(max % 8, 0); + + let sprites = (0..(max / 8)) + .into_iter() + .map(|i| { + let health_sprite = obj.sprite(SMALL_SPRITES.red_bar(0)); + + let mut health_object = obj.object(health_sprite); + health_object + .set_position(pos + (i as i32 * 8, 0).into()) + .show(); + health_object + }) + .collect(); + + Self { max, sprites } + } + + pub fn set_value(&mut self, new_value: usize, obj: &'a ObjectController) { + assert!(new_value <= self.max); + + for (i, sprite) in self.sprites.iter_mut().enumerate() { + if (i + 1) * 8 < new_value { + sprite.set_sprite(obj.sprite(SMALL_SPRITES.red_bar(0))); + } else if i * 8 < new_value { + sprite.set_sprite(obj.sprite(SMALL_SPRITES.red_bar(8 - (new_value - i * 8)))); + } else { + sprite.set_sprite(obj.sprite(SMALL_SPRITES.red_bar(8))); + } + } + } + + pub fn show(&mut self) { + for obj in self.sprites.iter_mut() { + obj.show(); + } + } + + pub fn hide(&mut self) { + for obj in self.sprites.iter_mut() { + obj.hide(); + } + } +} + +pub struct FractionDisplay<'a> { + sprites: Vec>, + digits: usize, + + current_current: usize, + current_max: usize, +} + +impl<'a> FractionDisplay<'a> { + pub fn new(pos: Vector2D, digits: usize, obj: &'a ObjectController) -> Self { + let mut sprites = Vec::with_capacity(digits * 2 + 1); + + for i in 0..digits { + let mut left_digit = obj.object(obj.sprite(SMALL_SPRITES.number(0))); + left_digit.set_position(pos + (i as i32 * 4, 0).into()); + + sprites.push(left_digit); + + let mut right_digit = obj.object(obj.sprite(SMALL_SPRITES.number(0))); + right_digit.set_position(pos + (i as i32 * 4 + digits as i32 * 4 + 7, 0).into()); + + sprites.push(right_digit); + } + + let mut slash = obj.object(obj.sprite(SMALL_SPRITES.slash())); + slash.set_position(pos + (digits as i32 * 4 + 1, 0).into()); + sprites.push(slash); + + Self { + sprites, + digits, + current_current: 0, + current_max: 0, + } + } + + pub fn set_value(&mut self, current: usize, max: usize, obj: &'a ObjectController) { + if self.current_current == current && self.current_max == max { + return; + } + + let mut current = current; + let mut max = max; + + for i in 0..self.digits { + let current_value_digit = current % 10; + current /= 10; + let current_value_sprite = &mut self.sprites[(self.digits - i) * 2 - 2]; + current_value_sprite + .set_sprite(obj.sprite(SMALL_SPRITES.number(current_value_digit as u32))); + + let max_value_digit = max % 10; + max /= 10; + let max_value_sprite = &mut self.sprites[(self.digits - i) * 2 - 1]; + max_value_sprite.set_sprite(obj.sprite(SMALL_SPRITES.number(max_value_digit as u32))); + } + } +} + +pub struct NumberDisplay<'a> { + objects: Vec>, + value: Option, + position: Vector2D, +} + +impl<'a> NumberDisplay<'a> { + pub fn new(position: Vector2D) -> Self { + Self { + objects: Vec::new(), + value: None, + position, + } + } + + pub fn set_value(&mut self, new_value: Option, obj: &'a ObjectController) { + if self.value == new_value { + return; + } + + self.value = new_value; + + self.objects.clear(); + + if let Some(mut new_value) = new_value { + if new_value == 0 { + let mut zero_object = obj.object(obj.sprite(SMALL_SPRITES.number(0))); + zero_object.show().set_position(self.position); + + self.objects.push(zero_object); + return; + } + + let mut digit = 0; + while new_value != 0 { + let current_value_digit = new_value % 10; + new_value /= 10; + + let mut current_value_obj = + obj.object(obj.sprite(SMALL_SPRITES.number(current_value_digit))); + + current_value_obj + .show() + .set_position(self.position - (digit * 4, 0).into()); + + digit += 1; + + self.objects.push(current_value_obj); + } + } + } +} diff --git a/examples/hyperspace-roll/src/level_generation.rs b/examples/hyperspace-roll/src/level_generation.rs new file mode 100644 index 00000000..be1a87af --- /dev/null +++ b/examples/hyperspace-roll/src/level_generation.rs @@ -0,0 +1,97 @@ +use agb::{hash_map::HashMap, rng}; +use alloc::vec::Vec; + +use crate::{battle::EnemyAttack, Face}; + +pub struct GeneratedAttack { + pub attack: EnemyAttack, + pub cooldown: u32, +} + +pub fn generate_attack(current_level: u32) -> Option { + if (rng::gen().rem_euclid(1024) as u32) < current_level * 2 { + Some(GeneratedAttack { + attack: generate_enemy_attack(current_level), + cooldown: generate_cooldown(current_level), + }) + } else { + None + } +} + +fn generate_enemy_attack(current_level: u32) -> EnemyAttack { + let attack_id = rng::gen().rem_euclid(10) as u32; + + if attack_id < 7 { + EnemyAttack::Shoot(rng::gen().rem_euclid(((current_level + 2) / 3) as i32) as u32 + 1) + } else if attack_id < 9 { + EnemyAttack::Shield( + (rng::gen().rem_euclid(((current_level + 4) / 5) as i32) as u32 + 1).min(5), + ) + } else { + EnemyAttack::Heal(rng::gen().rem_euclid(((current_level + 1) / 2) as i32) as u32) + } +} + +fn generate_cooldown(current_level: u32) -> u32 { + rng::gen().rem_euclid((5 * 60 - current_level as i32 * 10).max(1)) as u32 + 2 * 60 +} + +pub fn generate_upgrades(level: u32) -> Vec { + let mut upgrade_values = HashMap::new(); + + upgrade_values.insert(Face::Shoot, 5); + upgrade_values.insert(Face::DoubleShot, 10); + upgrade_values.insert(Face::DoubleShotValue, 15); + upgrade_values.insert(Face::TripleShot, 20); + upgrade_values.insert(Face::TripleShotValue, 30); + upgrade_values.insert(Face::Shield, 5); + upgrade_values.insert(Face::DoubleShield, 10); + upgrade_values.insert(Face::TripleShield, 20); + upgrade_values.insert(Face::DoubleShieldValue, 25); + upgrade_values.insert(Face::Malfunction, -2); + upgrade_values.insert(Face::Bypass, 7); + upgrade_values.insert(Face::Disrupt, 10); + upgrade_values.insert(Face::MalfunctionShot, 15); + upgrade_values.insert(Face::Heal, 8); + upgrade_values.insert(Face::BurstShield, 30); + upgrade_values.insert(Face::Invert, 30); + + let potential_upgrades: Vec = upgrade_values.keys().cloned().collect(); + + let mut upgrades = Vec::new(); + + let upgrade_value = |upgrades: &[Face], potential_upgrade: Face| -> i32 { + upgrades + .iter() + .map(|x| upgrade_values.get(x).unwrap()) + .sum::() + + upgrade_values.get(&potential_upgrade).unwrap() + }; + + let max_upgrade_value = 15 + (rng::gen().rem_euclid(level as i32 * 5)); + let mut attempts = 0; + + while upgrades.len() != 3 { + attempts += 1; + let next = potential_upgrades[rng::gen() as usize % potential_upgrades.len()]; + let number_of_malfunctions = upgrades + .iter() + .chain(core::iter::once(&next)) + .filter(|&x| *x == Face::Malfunction) + .count(); + let maximum_number_of_malfunctions = if level < 5 { 0 } else { 1 }; + if upgrade_value(&upgrades, next) <= max_upgrade_value + && number_of_malfunctions <= maximum_number_of_malfunctions + { + upgrades.push(next); + attempts = 0; + } + + if attempts > 100 { + upgrades.clear(); + } + } + + upgrades +} diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs new file mode 100644 index 00000000..9acf3c04 --- /dev/null +++ b/examples/hyperspace-roll/src/main.rs @@ -0,0 +1,228 @@ +// 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] + +use agb::display; +use agb::display::object::ObjectController; +use agb::display::tiled::VRamManager; +use agb::display::Priority; +use agb::interrupt::VBlank; + +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(); + + if save::load_high_score() > 1000 { + save::save_high_score(0); + } + + 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(); + 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(current_level); + } + 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) +} diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs new file mode 100644 index 00000000..e5df03e8 --- /dev/null +++ b/examples/hyperspace-roll/src/save.rs @@ -0,0 +1,44 @@ +use agb::interrupt::free; +use bare_metal::Mutex; +use core::cell::RefCell; + +const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; +const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1); + +static HIGHSCORE: Mutex> = Mutex::new(RefCell::new(0)); + +pub fn init_save() { + if (unsafe { RAM_ADDRESS.read_volatile() } == !0) { + save_high_score(0); + unsafe { RAM_ADDRESS.write_volatile(0) }; + } + + let mut a = [0; 4]; + for (idx, a) in a.iter_mut().enumerate() { + *a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() }; + } + + let high_score = u32::from_le_bytes(a); + + free(|cs| { + if high_score > 100 { + HIGHSCORE.borrow(cs).replace(0); + } else { + HIGHSCORE.borrow(cs).replace(high_score); + } + }); +} + +pub fn load_high_score() -> u32 { + free(|cs| *HIGHSCORE.borrow(cs).borrow()) +} + +pub fn save_high_score(score: u32) { + let a = score.to_le_bytes(); + + for (idx, &a) in a.iter().enumerate() { + unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) }; + } + + free(|cs| HIGHSCORE.borrow(cs).replace(score)); +} diff --git a/examples/hyperspace-roll/src/sfx.rs b/examples/hyperspace-roll/src/sfx.rs new file mode 100644 index 00000000..d2d877c4 --- /dev/null +++ b/examples/hyperspace-roll/src/sfx.rs @@ -0,0 +1,203 @@ +use agb::fixnum::num; +use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; +use agb::{include_wav, rng}; + +const DICE_ROLLS: &[&[u8]] = &[ + include_wav!("sfx/SingleRoll_1.wav"), + include_wav!("sfx/SingleRoll_2.wav"), + include_wav!("sfx/SingleRoll_3.wav"), + include_wav!("sfx/SingleRoll_4.wav"), + include_wav!("sfx/SingleRoll_5.wav"), +]; + +const MULTI_ROLLS: &[&[u8]] = &[ + include_wav!("sfx/MultiRoll_1.wav"), + include_wav!("sfx/MultiRoll_2.wav"), + include_wav!("sfx/MultiRoll_3.wav"), + include_wav!("sfx/MultiRoll_4.wav"), + include_wav!("sfx/MultiRoll_5.wav"), +]; + +const MENU_BGM: &[u8] = include_wav!("sfx/BGM_Menu.wav"); +const BATTLE_BGM: &[u8] = include_wav!("sfx/BGM_Fight.wav"); +const TITLE_BGM: &[u8] = include_wav!("sfx/BGM_Title.wav"); + +const SHOOT: &[u8] = include_wav!("sfx/shoot.wav"); +const SHOT_HIT: &[u8] = include_wav!("sfx/shot_hit.wav"); +const SHIP_EXPLODE: &[u8] = include_wav!("sfx/ship_explode.wav"); +const MOVE_CURSOR: &[u8] = include_wav!("sfx/move_cursor.wav"); +const SELECT: &[u8] = include_wav!("sfx/select.wav"); +const BACK: &[u8] = include_wav!("sfx/back.wav"); +const ACCEPT: &[u8] = include_wav!("sfx/accept.wav"); +const SHIELD_DOWN: &[u8] = include_wav!("sfx/shield_down.wav"); +const SHIELD_UP: &[u8] = include_wav!("sfx/shield_up.wav"); +const SHIELD_DEFEND: &[u8] = include_wav!("sfx/shield_defend.wav"); +const DISRUPT: &[u8] = include_wav!("sfx/disrupt.wav"); +const HEAL: &[u8] = include_wav!("sfx/heal.wav"); +const SEND_BURST_SHIELD: &[u8] = include_wav!("sfx/send_burst_shield.wav"); +const BURST_SHIELD_HIT: &[u8] = include_wav!("sfx/burst_shield_hit.wav"); + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BattleOrMenu { + Battle, + Menu, + Title, +} + +pub struct Sfx<'a> { + mixer: &'a mut Mixer, + state: BattleOrMenu, + + current_bgm: ChannelId, +} + +impl<'a> Sfx<'a> { + pub fn new(mixer: &'a mut Mixer) -> Self { + let mut title_music = SoundChannel::new_high_priority(TITLE_BGM); + title_music.should_loop(); + let title_channel = mixer.play_sound(title_music).unwrap(); + + Self { + mixer, + state: BattleOrMenu::Title, + + current_bgm: title_channel, + } + } + + pub fn frame(&mut self) { + self.mixer.frame(); + } + + pub fn battle(&mut self) { + if self.state == BattleOrMenu::Battle { + return; + } + + self.state = BattleOrMenu::Battle; + + let current_channel = self.mixer.channel(&self.current_bgm).unwrap(); + let pos = current_channel.pos(); + current_channel.stop(); + + let mut battle_music = SoundChannel::new_high_priority(BATTLE_BGM); + battle_music.should_loop().set_pos(pos); + self.current_bgm = self.mixer.play_sound(battle_music).unwrap(); + } + + pub fn customise(&mut self) { + if self.state == BattleOrMenu::Menu { + return; + } + + let should_restart = self.state == BattleOrMenu::Title; + + self.state = BattleOrMenu::Menu; + let current_channel = self.mixer.channel(&self.current_bgm).unwrap(); + let pos = current_channel.pos(); + current_channel.stop(); + + let mut menu_music = SoundChannel::new_high_priority(MENU_BGM); + menu_music + .should_loop() + .set_pos(if should_restart { 0.into() } else { pos }); + self.current_bgm = self.mixer.play_sound(menu_music).unwrap(); + } + + pub fn title_screen(&mut self) { + if self.state == BattleOrMenu::Title { + return; + } + + self.state = BattleOrMenu::Title; + self.mixer.channel(&self.current_bgm).unwrap().stop(); + + let mut title_music = SoundChannel::new_high_priority(TITLE_BGM); + title_music.should_loop(); + self.current_bgm = self.mixer.play_sound(title_music).unwrap(); + } + + pub fn roll(&mut self) { + let roll_sound_to_use = rng::gen().rem_euclid(DICE_ROLLS.len() as i32); + let sound_channel = SoundChannel::new(DICE_ROLLS[roll_sound_to_use as usize]); + + self.mixer.play_sound(sound_channel); + } + + pub fn roll_multi(&mut self) { + let roll_sound_to_use = rng::gen().rem_euclid(MULTI_ROLLS.len() as i32); + let sound_channel = SoundChannel::new(MULTI_ROLLS[roll_sound_to_use as usize]); + + self.mixer.play_sound(sound_channel); + } + + pub fn shoot(&mut self) { + self.mixer.play_sound(SoundChannel::new(SHOOT)); + } + + pub fn shot_hit(&mut self) { + self.mixer.play_sound(SoundChannel::new(SHOT_HIT)); + } + + pub fn ship_explode(&mut self) { + self.mixer.play_sound(SoundChannel::new(SHIP_EXPLODE)); + } + + pub fn move_cursor(&mut self) { + let mut channel = SoundChannel::new(MOVE_CURSOR); + channel.volume(num!(0.5)); + + self.mixer.play_sound(channel); + } + + pub fn select(&mut self) { + let mut channel = SoundChannel::new(SELECT); + channel.volume(num!(0.75)); + + self.mixer.play_sound(channel); + } + + pub fn back(&mut self) { + let mut channel = SoundChannel::new(BACK); + channel.volume(num!(0.5)); + + self.mixer.play_sound(channel); + } + + pub fn accept(&mut self) { + let mut channel = SoundChannel::new(ACCEPT); + channel.volume(num!(0.5)); + + self.mixer.play_sound(channel); + } + + pub fn shield_down(&mut self) { + self.mixer.play_sound(SoundChannel::new(SHIELD_DOWN)); + } + + pub fn shield_up(&mut self) { + self.mixer.play_sound(SoundChannel::new(SHIELD_UP)); + } + + pub fn shield_defend(&mut self) { + let mut channel = SoundChannel::new(SHIELD_DEFEND); + channel.volume(num!(0.5)); + self.mixer.play_sound(channel); + } + + pub fn disrupt(&mut self) { + self.mixer.play_sound(SoundChannel::new(DISRUPT)); + } + + pub fn heal(&mut self) { + self.mixer.play_sound(SoundChannel::new(HEAL)); + } + + pub fn send_burst_shield(&mut self) { + self.mixer.play_sound(SoundChannel::new(SEND_BURST_SHIELD)); + } + + pub fn burst_shield_hit(&mut self) { + self.mixer.play_sound(SoundChannel::new(BURST_SHIELD_HIT)); + } +} diff --git a/justfile b/justfile index cdc3097a..aef7fbc8 100644 --- a/justfile +++ b/justfile @@ -42,6 +42,7 @@ ci: build-debug clippy test build-release test-release doctest-agb build-roms bu build-roms: just _build-rom "examples/the-purple-night" "PURPLENIGHT" just _build-rom "examples/the-hat-chooses-the-wizard" "HATWIZARD" + just _build-rom "examples/hyperspace-roll" "HYPERSPACE" just _build-rom "book/games/pong" "PONG"