Merge pull request #261 from corwinkuiper/hyperspace-roll

Introduce Hyperspace Roll to examples
This commit is contained in:
Corwin 2022-07-25 21:34:35 +01:00 committed by GitHub
commit 79e78b7fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 3265 additions and 0 deletions

View file

@ -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"

439
examples/hyperspace-roll/Cargo.lock generated Normal file
View file

@ -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"

View file

@ -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

View file

@ -0,0 +1 @@
fn main() {}

View file

@ -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/ : { *(*) }
}

View file

@ -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/ : { *(*) }
}

Binary file not shown.

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

View file

@ -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/).

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

View file

@ -0,0 +1,6 @@
version = "1.0"
[image.help]
filename = "help-text.png"
tile_size = "8x8"
transparent_colour = "121105"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rust-src", "clippy"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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();
}
}

View file

@ -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<u32> {
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<DieState>,
}
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<Item = Face> + '_ {
self.rolls.iter().filter_map(|state| match state {
DieState::Rolled(rolled_die) => Some(rolled_die.face),
_ => None,
})
}
fn faces_to_render(&self) -> impl Iterator<Item = (Face, Option<u32>)> + '_ {
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<Action> {
let mut actions = vec![];
let mut face_counts: HashMap<Face, u32> = 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<u32> {
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<Action> {
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<EnemyAttackState>; 2],
current_level: u32,
}
impl CurrentBattleState {
fn accept_rolls(&mut self) -> Vec<Action> {
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<Action> {
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<Action> {
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, &current_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, &current_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);
}
}

View file

@ -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<Object<'a>>,
dice_cooldowns: Vec<HealthBar<'a>>,
player_shield: Vec<Object<'a>>,
enemy_shield: Vec<Object<'a>>,
player_healthbar: HealthBar<'a>,
enemy_healthbar: HealthBar<'a>,
player_health: FractionDisplay<'a>,
enemy_health: FractionDisplay<'a>,
enemy_attack_display: Vec<EnemyAttackDisplay<'a>>,
}
pub struct BattleScreenDisplay<'a> {
objs: BattleScreenDisplayObjects<'a>,
animations: Vec<AnimationStateHolder<'a>>,
_misc_sprites: Vec<Object<'a>>,
}
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<Action> {
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<EnemyAttackState>, 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
}
}
}
}
}

View file

@ -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<Object<'a>> {
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<Object<'a>> {
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<Object<'a>> {
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<Cursor> = 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::<Vec<usize>>(),
);
}
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::<Vec<usize>>(),
);
_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
}

View file

@ -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<Object<'a>>,
}
impl<'a> HealthBar<'a> {
pub fn new(pos: Vector2D<i32>, 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<Object<'a>>,
digits: usize,
current_current: usize,
current_max: usize,
}
impl<'a> FractionDisplay<'a> {
pub fn new(pos: Vector2D<i32>, 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<Object<'a>>,
value: Option<u32>,
position: Vector2D<i32>,
}
impl<'a> NumberDisplay<'a> {
pub fn new(position: Vector2D<i32>) -> Self {
Self {
objects: Vec::new(),
value: None,
position,
}
}
pub fn set_value(&mut self, new_value: Option<u32>, 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);
}
}
}
}

View file

@ -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<GeneratedAttack> {
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<Face> {
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<Face> = 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::<i32>()
+ 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
}

View file

@ -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<Die>,
}
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)
}

View file

@ -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<RefCell<u32>> = 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));
}

View file

@ -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));
}
}

View file

@ -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"