Combo game rom (#355)

Adds a rom that is all the examples compiled together!

- [ ] Changelog updated / no changelog update needed
This commit is contained in:
Corwin 2022-12-09 20:51:07 +00:00 committed by GitHub
commit d65ca26bd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 4544 additions and 3552 deletions

View file

@ -0,0 +1,14 @@
[unstable]
build-std = ["core", "alloc"]
build-std-features = ["compiler-builtins-mem"]
[build]
target = "thumbv4t-none-eabi"
[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"]
runner = "mgba-qt"
[target.armv4t-none-eabi]
rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"]
runner = "mgba-qt"

554
examples/combo/Cargo.lock generated Normal file
View file

@ -0,0 +1,554 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "agb"
version = "0.12.2"
dependencies = [
"agb_fixnum",
"agb_image_converter",
"agb_macros",
"agb_sound_converter",
"bare-metal",
"bitflags",
"modular-bitfield",
"rustc-hash",
]
[[package]]
name = "agb_fixnum"
version = "0.12.2"
dependencies = [
"agb_macros",
]
[[package]]
name = "agb_image_converter"
version = "0.12.2"
dependencies = [
"asefile",
"fontdue",
"image",
"proc-macro2",
"quote",
"serde",
"syn",
"toml",
]
[[package]]
name = "agb_macros"
version = "0.12.2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "agb_sound_converter"
version = "0.12.2"
dependencies = [
"hound",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "asefile"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a71de7aecd2d0a76ec90fde2c443d12667c737d92de76bd187f101eca37891"
dependencies = [
"bitflags",
"byteorder",
"flate2",
"image",
"log",
"nohash",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bare-metal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytemuck"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combo"
version = "0.1.0"
dependencies = [
"agb",
"hyperspace-roll",
"the-hat-chooses-the-wizard",
"the-purple-night",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "flate2"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"crc32fast",
"miniz_oxide 0.6.2",
]
[[package]]
name = "fontdue"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a62391ecb864cf12ed06b2af4eda2e609b97657950d6a8f06841b17726ab253"
dependencies = [
"hashbrown",
"ttf-parser",
]
[[package]]
name = "generational-arena"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "hound"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
[[package]]
name = "hyperspace-roll"
version = "0.1.0"
dependencies = [
"agb",
"bare-metal",
]
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-iter",
"num-rational",
"num-traits",
"png",
]
[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "libc"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libflate"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd"
dependencies = [
"adler32",
"crc32fast",
"rle-decode-fast",
"take_mut",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "modular-bitfield"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74"
dependencies = [
"modular-bitfield-impl",
"static_assertions",
]
[[package]]
name = "modular-bitfield-impl"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "nohash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca"
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]]
name = "proc-macro2"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rle-decode-fast"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "serde"
version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "take_mut"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "the-hat-chooses-the-wizard"
version = "0.1.0"
dependencies = [
"agb",
"serde",
"serde_json",
]
[[package]]
name = "the-purple-night"
version = "0.1.0"
dependencies = [
"agb",
"generational-arena",
"quote",
"tiled",
]
[[package]]
name = "tiled"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d2c30aeea9d8159cb461a17dba23ad28980a2a9c217a6784a14e931e4979d6f"
dependencies = [
"base64",
"libflate",
"xml-rs",
]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]]
name = "ttf-parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"

22
examples/combo/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "combo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
agb = { version = "0.12.2", path = "../../agb" }
the-purple-night = { path = "../the-purple-night" }
the-hat-chooses-the-wizard = { path = "../the-hat-chooses-the-wizard" }
hyperspace-roll = { path = "../hyperspace-roll" }
[profile.dev]
opt-level = 2
debug = true
[profile.release]
panic = "abort"
lto = true
debug = true

113
examples/combo/gba.ld Normal file
View file

@ -0,0 +1,113 @@
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_end = ABSOLUTE(.);
} > iwram
__iwram_rom_length_bytes = __iwram_data_end - __iwram_data_start;
__iwram_rom_length_halfwords = (__iwram_rom_length_bytes + 1) / 2;
__ewram_rom_length_bytes = __ewram_data_end - __ewram_data_start;
__ewram_rom_length_halfwords = (__ewram_rom_length_bytes + 1) / 2;
/* debugging sections */
/* Stabs */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
.debug_ranges 0 : { *(.debug_ranges) }
/* discard anything not already mentioned */
/DISCARD/ : { *(*) }
}

111
examples/combo/gba_mb.ld Normal file
View file

@ -0,0 +1,111 @@
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_end = ABSOLUTE(.);
} > iwram
__iwram_rom_length_bytes = __iwram_data_end - __iwram_data_start;
__iwram_rom_length_halfwords = (__iwram_rom_length_bytes + 1) / 2;
__ewram_rom_length_bytes = __ewram_data_end - __ewram_data_start;
__ewram_rom_length_halfwords = (__ewram_rom_length_bytes + 1) / 2;
/* debugging sections */
/* Stabs */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
.debug_ranges 0 : { *(.debug_ranges) }
/* discard anything not already mentioned */
/DISCARD/ : { *(*) }
}

Binary file not shown.

View file

@ -0,0 +1,16 @@
version = "1.0"
[image.hat]
filename = "hat.png"
tile_size = "8x8"
transparent_colour = "121105"
[image.purple]
filename = "purple.png"
tile_size = "8x8"
transparent_colour = "121105"
[image.hyperspace]
filename = "hyperspace.png"
tile_size = "8x8"
transparent_colour = "121105"

BIN
examples/combo/gfx/hat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

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

131
examples/combo/src/lib.rs Normal file
View file

@ -0,0 +1,131 @@
#![no_std]
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
extern crate alloc;
use alloc::boxed::Box;
use agb::{
display::{
tiled::{InfiniteScrolledMap, RegularBackgroundSize, TileFormat, TileSet, TileSetting},
Priority,
},
fixnum::{Num, Vector2D},
include_gfx,
input::Button,
};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Game {
TheHatChoosesTheWizard,
ThePurpleNight,
HyperspaceRoll,
}
impl Game {
fn launch_game(self, gba: agb::Gba) -> ! {
match self {
Game::TheHatChoosesTheWizard => the_hat_chooses_the_wizard::main(gba),
Game::ThePurpleNight => the_purple_night::main(gba),
Game::HyperspaceRoll => hyperspace_roll::main(gba),
}
}
fn from_index(index: i32) -> Game {
match index.rem_euclid(3) {
0 => Game::TheHatChoosesTheWizard,
1 => Game::ThePurpleNight,
2 => Game::HyperspaceRoll,
_ => unreachable!("game out of index in an unreachable manner"),
}
}
}
include_gfx!("gfx/games.toml");
fn get_game(gba: &mut agb::Gba) -> Game {
let mut input = agb::input::ButtonController::new();
let vblank = agb::interrupt::VBlank::get();
let (tile, mut vram) = gba.display.video.tiled0();
let hat = TileSet::new(games::hat.tiles, TileFormat::FourBpp);
let purple = TileSet::new(games::purple.tiles, TileFormat::FourBpp);
let hyperspace = TileSet::new(games::hyperspace.tiles, TileFormat::FourBpp);
let tiles = [hat, purple, hyperspace];
let palette_assignments = &[
games::hat.palette_assignments,
games::purple.palette_assignments,
games::hyperspace.palette_assignments,
];
vram.set_background_palettes(games::PALETTES);
let mut bg = InfiniteScrolledMap::new(
tile.background(Priority::P0, RegularBackgroundSize::Background32x32),
Box::new(|pos| {
let y = pos.y.rem_euclid(20);
let x = pos.x.rem_euclid(30);
let game = (pos.x).rem_euclid(90) as usize / 30;
let tile_id = (y * 30 + x) as usize;
(
&tiles[game],
TileSetting::new(
tile_id as u16,
false,
false,
palette_assignments[game][tile_id],
),
)
}),
);
bg.init(&mut vram, (0, 0).into(), &mut || {});
bg.set_pos(&mut vram, (0, 0).into());
bg.commit(&mut vram);
bg.show();
let mut position: Vector2D<Num<i32, 8>> = (0, 0).into();
let mut game_idx = 0;
let game = loop {
let lr: agb::input::Tri = (
input.is_just_pressed(Button::LEFT),
input.is_just_pressed(Button::RIGHT),
)
.into();
game_idx += lr as i32;
if (position.x - game_idx * 30 * 8).abs() < Num::new(1) / 2 {
position.x = Num::new(game_idx * 30 * 8);
}
position.x +=
((Num::new(game_idx * 30 * 8) - position.x) / 8).clamp(-Num::new(8), Num::new(8));
bg.set_pos(&mut vram, position.floor());
vblank.wait_for_vblank();
bg.commit(&mut vram);
input.update();
if input.is_just_pressed(Button::A) {
break Game::from_index(game_idx);
}
};
bg.hide();
bg.clear(&mut vram);
bg.commit(&mut vram);
game
}
pub fn main(mut gba: agb::Gba) -> ! {
get_game(&mut gba).launch_game(gba)
}

View file

@ -0,0 +1,10 @@
#![no_std]
#![no_main]
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
#[agb::entry]
fn entry(gba: agb::Gba) -> ! {
combo::main(gba);
}

View file

@ -64,7 +64,7 @@ pub(crate) fn load_description(
for y in 0..11 { for y in 0..11 {
for x in 0..8 { for x in 0..8 {
let tile_id = y * 8 + x + 8 * 11 * (face_id as u16 - 10); let tile_id = y * 8 + x + 8 * 11 * (face_id as u16 % 10);
descriptions_map.set_tile( descriptions_map.set_tile(
vram, vram,
(x, y).into(), (x, y).into(),

View file

@ -0,0 +1,226 @@
// Games made using `agb` are no_std which means you don't have access to the standard
// rust library. This is because the game boy advance doesn't really have an operating
// system, so most of the content of the standard library doesn't apply.
//
// Provided you haven't disabled it, agb does provide an allocator, so it is possible
// to use both the `core` and the `alloc` built in crates.
#![no_std]
// `agb` defines its own `main` function, so you must declare your game's main function
// using the #[agb::entry] proc macro. Failing to do so will cause failure in linking
// which won't be a particularly clear error message.
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
use agb::display::object::ObjectController;
use agb::display::tiled::{TiledMap, VRamManager};
use agb::display::Priority;
use agb::interrupt::VBlank;
use agb::{display, sound::mixer::Frequency};
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
mod background;
mod battle;
mod customise;
mod graphics;
mod level_generation;
mod save;
mod sfx;
use background::{show_title_screen, StarBackground};
use battle::BattleResult;
use graphics::NumberDisplay;
use sfx::Sfx;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Face {
Shoot,
Shield,
Malfunction,
Heal,
Bypass,
DoubleShot,
TripleShot,
Blank,
Disrupt,
MalfunctionShot,
DoubleShield,
TripleShield,
DoubleShieldValue,
DoubleShotValue,
TripleShotValue,
BurstShield,
Invert,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Ship {
Player,
Drone,
PilotedShip,
Shield,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum EnemyAttackType {
Attack,
Shield,
Heal,
}
#[derive(Debug, Clone)]
pub struct Die {
faces: [Face; 6],
}
impl Die {
/// roll this die (potentially using the custom probabilities, should we implement that) and return which face index is showing
fn roll(&self) -> Face {
let n = agb::rng::gen().rem_euclid(6);
self.faces[n as usize]
}
}
#[derive(Debug, Clone)]
pub struct PlayerDice {
dice: Vec<Die>,
}
struct Agb<'a> {
obj: ObjectController,
vblank: VBlank,
star_background: StarBackground<'a>,
vram: VRamManager,
sfx: Sfx<'a>,
}
pub fn main(mut gba: agb::Gba) -> ! {
save::init_save(&mut gba).expect("Could not initialize save game");
if save::load_high_score() > 1000 {
save::save_high_score(&mut gba, 0).expect("Could not reset high score");
}
let gfx = gba.display.object.get();
let vblank = agb::interrupt::VBlank::get();
let (tiled, mut vram) = gba.display.video.tiled0();
let mut background0 = tiled.background(
Priority::P0,
display::tiled::RegularBackgroundSize::Background64x32,
);
let mut background1 = tiled.background(
Priority::P0,
display::tiled::RegularBackgroundSize::Background64x32,
);
let mut card_descriptions = tiled.background(
Priority::P1,
display::tiled::RegularBackgroundSize::Background32x32,
);
let mut help_background = tiled.background(
Priority::P1,
display::tiled::RegularBackgroundSize::Background32x32,
);
let basic_die = Die {
faces: [
Face::Shoot,
Face::Shield,
Face::Blank,
Face::Malfunction,
Face::Blank,
Face::Blank,
],
};
let mut star_background = StarBackground::new(&mut background0, &mut background1, &mut vram);
star_background.commit(&mut vram);
let mut mixer = gba.mixer.mixer(Frequency::Hz32768);
mixer.enable();
let _interrupt_handler = mixer.setup_interrupt_handler();
let sfx = Sfx::new(&mut mixer);
let mut agb = Agb {
obj: gfx,
vblank,
star_background,
vram,
sfx,
};
loop {
let mut dice = PlayerDice {
dice: vec![basic_die.clone(); 2],
};
let mut current_level = 1;
agb.sfx.title_screen();
{
show_title_screen(&mut help_background, &mut agb.vram, &mut agb.sfx);
let mut score_display = NumberDisplay::new((216, 9).into());
score_display.set_value(Some(save::load_high_score()), &agb.obj);
agb.obj.commit();
agb.star_background.hide();
let mut input = agb::input::ButtonController::new();
loop {
let _ = agb::rng::gen();
input.update();
if input.is_just_pressed(agb::input::Button::all()) {
break;
}
agb.vblank.wait_for_vblank();
agb.sfx.frame();
}
}
agb.obj.commit();
help_background.hide();
help_background.clear(&mut agb.vram);
help_background.commit(&mut agb.vram);
agb.sfx.frame();
background::load_palettes(&mut agb.vram);
agb.star_background.show();
loop {
dice = customise::customise_screen(
&mut agb,
dice.clone(),
&mut card_descriptions,
&mut help_background,
current_level,
);
let result =
battle::battle_screen(&mut agb, dice.clone(), current_level, &mut help_background);
match result {
BattleResult::Win => {}
BattleResult::Loss => {
agb.obj.commit();
agb.sfx.customise();
if save::load_high_score() < current_level {
save::save_high_score(&mut gba, current_level)
.expect("Could not save high score");
}
break;
}
}
current_level += 1;
if current_level % 5 == 0 && dice.dice.len() < 5 {
dice.dice.push(basic_die.clone());
}
}
}
}

View file

@ -1,232 +1,10 @@
// Games made using `agb` are no_std which means you don't have access to the standard
// rust library. This is because the game boy advance doesn't really have an operating
// system, so most of the content of the standard library doesn't apply.
//
// Provided you haven't disabled it, agb does provide an allocator, so it is possible
// to use both the `core` and the `alloc` built in crates.
#![no_std] #![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] #![no_main]
#![cfg_attr(test, feature(custom_test_frameworks))] #![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
use agb::display::object::ObjectController;
use agb::display::tiled::{TiledMap, VRamManager};
use agb::display::Priority;
use agb::interrupt::VBlank;
use agb::{display, sound::mixer::Frequency};
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
mod background;
mod battle;
mod customise;
mod graphics;
mod level_generation;
mod save;
mod sfx;
use background::{show_title_screen, StarBackground};
use battle::BattleResult;
use graphics::NumberDisplay;
use sfx::Sfx;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Face {
Shoot,
Shield,
Malfunction,
Heal,
Bypass,
DoubleShot,
TripleShot,
Blank,
Disrupt,
MalfunctionShot,
DoubleShield,
TripleShield,
DoubleShieldValue,
DoubleShotValue,
TripleShotValue,
BurstShield,
Invert,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Ship {
Player,
Drone,
PilotedShip,
Shield,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum EnemyAttackType {
Attack,
Shield,
Heal,
}
#[derive(Debug, Clone)]
pub struct Die {
faces: [Face; 6],
}
impl Die {
/// roll this die (potentially using the custom probabilities, should we implement that) and return which face index is showing
fn roll(&self) -> Face {
let n = agb::rng::gen().rem_euclid(6);
self.faces[n as usize]
}
}
#[derive(Debug, Clone)]
pub struct PlayerDice {
dice: Vec<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(&mut gba).expect("Could not initialize save game");
if save::load_high_score() > 1000 {
save::save_high_score(&mut gba, 0).expect("Could not reset high score");
}
let gfx = gba.display.object.get();
let vblank = agb::interrupt::VBlank::get();
let (tiled, mut vram) = gba.display.video.tiled0();
let mut background0 = tiled.background(
Priority::P0,
display::tiled::RegularBackgroundSize::Background64x32,
);
let mut background1 = tiled.background(
Priority::P0,
display::tiled::RegularBackgroundSize::Background64x32,
);
let mut card_descriptions = tiled.background(
Priority::P1,
display::tiled::RegularBackgroundSize::Background32x32,
);
let mut help_background = tiled.background(
Priority::P1,
display::tiled::RegularBackgroundSize::Background32x32,
);
let basic_die = Die {
faces: [
Face::Shoot,
Face::Shield,
Face::Blank,
Face::Malfunction,
Face::Blank,
Face::Blank,
],
};
let mut star_background = StarBackground::new(&mut background0, &mut background1, &mut vram);
star_background.commit(&mut vram);
let mut mixer = gba.mixer.mixer(Frequency::Hz32768);
mixer.enable();
let _interrupt_handler = mixer.setup_interrupt_handler();
let sfx = Sfx::new(&mut mixer);
let mut agb = Agb {
obj: gfx,
vblank,
star_background,
vram,
sfx,
};
loop {
let mut dice = PlayerDice {
dice: vec![basic_die.clone(); 2],
};
let mut current_level = 1;
agb.sfx.title_screen();
{
show_title_screen(&mut help_background, &mut agb.vram, &mut agb.sfx);
let mut score_display = NumberDisplay::new((216, 9).into());
score_display.set_value(Some(save::load_high_score()), &agb.obj);
agb.obj.commit();
agb.star_background.hide();
let mut input = agb::input::ButtonController::new();
loop {
let _ = agb::rng::gen();
input.update();
if input.is_just_pressed(agb::input::Button::all()) {
break;
}
agb.vblank.wait_for_vblank();
agb.sfx.frame();
}
}
agb.obj.commit();
help_background.hide();
help_background.clear(&mut agb.vram);
help_background.commit(&mut agb.vram);
agb.sfx.frame();
background::load_palettes(&mut agb.vram);
agb.star_background.show();
loop {
dice = customise::customise_screen(
&mut agb,
dice.clone(),
&mut card_descriptions,
&mut help_background,
current_level,
);
let result =
battle::battle_screen(&mut agb, dice.clone(), current_level, &mut help_background);
match result {
BattleResult::Win => {}
BattleResult::Loss => {
agb.obj.commit();
agb.sfx.customise();
if save::load_high_score() < current_level {
save::save_high_score(&mut gba, current_level)
.expect("Could not save high score");
}
break;
}
}
current_level += 1;
if current_level % 5 == 0 && dice.dice.len() < 5 {
dice.dice.push(basic_die.clone());
}
}
}
}
#[agb::entry] #[agb::entry]
fn entry(mut gba: agb::Gba) -> ! { fn entry(mut gba: agb::Gba) -> ! {
main(gba) hyperspace_roll::main(gba)
} }

View file

@ -0,0 +1,994 @@
#![no_std]
#![no_main]
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
extern crate alloc;
use agb::{
display::{
object::{Graphics, Object, ObjectController, Tag, TagMap},
tiled::{
InfiniteScrolledMap, PartialUpdateStatus, RegularBackgroundSize, TileFormat, TileSet,
TileSetting, TiledMap, VRamManager,
},
Priority, HEIGHT, WIDTH,
},
fixnum::{FixedNum, Vector2D},
input::{self, Button, ButtonController},
sound::mixer::Frequency,
};
use alloc::boxed::Box;
mod enemies;
mod level_display;
mod sfx;
mod splash_screen;
pub struct Level {
background: &'static [u16],
foreground: &'static [u16],
dimensions: Vector2D<u32>,
collision: &'static [u32],
slimes: &'static [(i32, i32)],
snails: &'static [(i32, i32)],
enemy_stops: &'static [(i32, i32)],
start_pos: (i32, i32),
}
mod map_tiles {
use super::Level;
pub const LEVELS: &[Level] = &[
l1_1::get_level(),
l1_2::get_level(),
l1_3::get_level(),
l1_4::get_level(),
l1_5::get_level(),
l1_7::get_level(), // these are intentionally this way round
l1_6::get_level(),
l1_8::get_level(),
l2_3::get_level(), // goes 2-3, 2-1 then 2-2
l2_1::get_level(),
l2_2::get_level(),
l2_4::get_level(),
];
pub mod l1_1 {
include!(concat!(env!("OUT_DIR"), "/1-1.json.rs"));
}
pub mod l1_2 {
include!(concat!(env!("OUT_DIR"), "/1-2.json.rs"));
}
pub mod l1_3 {
include!(concat!(env!("OUT_DIR"), "/1-3.json.rs"));
}
pub mod l1_4 {
include!(concat!(env!("OUT_DIR"), "/1-4.json.rs"));
}
pub mod l1_5 {
include!(concat!(env!("OUT_DIR"), "/1-5.json.rs"));
}
pub mod l1_6 {
include!(concat!(env!("OUT_DIR"), "/1-6.json.rs"));
}
pub mod l1_7 {
include!(concat!(env!("OUT_DIR"), "/1-7.json.rs"));
}
pub mod l2_1 {
include!(concat!(env!("OUT_DIR"), "/2-1.json.rs"));
}
pub mod l1_8 {
include!(concat!(env!("OUT_DIR"), "/1-8.json.rs"));
}
pub mod l2_2 {
include!(concat!(env!("OUT_DIR"), "/2-2.json.rs"));
}
pub mod l2_3 {
include!(concat!(env!("OUT_DIR"), "/2-3.json.rs"));
}
pub mod l2_4 {
include!(concat!(env!("OUT_DIR"), "/2-4.json.rs"));
}
pub mod tilemap {
include!(concat!(env!("OUT_DIR"), "/tilemap.rs"));
}
}
agb::include_gfx!("gfx/tile_sheet.toml");
const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite");
const TAG_MAP: &TagMap = GRAPHICS.tags();
const WALKING: &Tag = TAG_MAP.get("Walking");
const JUMPING: &Tag = TAG_MAP.get("Jumping");
const FALLING: &Tag = TAG_MAP.get("Falling");
const PLAYER_DEATH: &Tag = TAG_MAP.get("Player Death");
const HAT_SPIN_1: &Tag = TAG_MAP.get("HatSpin");
const HAT_SPIN_2: &Tag = TAG_MAP.get("HatSpin2");
const HAT_SPIN_3: &Tag = TAG_MAP.get("HatSpin3");
type FixedNumberType = FixedNum<10>;
pub struct Entity<'a> {
sprite: Object<'a>,
position: Vector2D<FixedNumberType>,
velocity: Vector2D<FixedNumberType>,
collision_mask: Vector2D<u16>,
}
impl<'a> Entity<'a> {
pub fn new(object: &'a ObjectController, collision_mask: Vector2D<u16>) -> Self {
let dummy_sprite = object.sprite(WALKING.sprite(0));
let mut sprite = object.object(dummy_sprite);
sprite.set_priority(Priority::P1);
Entity {
sprite,
collision_mask,
position: (0, 0).into(),
velocity: (0, 0).into(),
}
}
fn something_at_point<T: Fn(i32, i32) -> bool>(
&self,
position: Vector2D<FixedNumberType>,
something_fn: T,
) -> bool {
let left = (position.x - self.collision_mask.x as i32 / 2).floor() / 8;
let right = (position.x + self.collision_mask.x as i32 / 2 - 1).floor() / 8;
let top = (position.y - self.collision_mask.y as i32 / 2).floor() / 8;
let bottom = (position.y + self.collision_mask.y as i32 / 2 - 1).floor() / 8;
for x in left..=right {
for y in top..=bottom {
if something_fn(x, y) {
return true;
}
}
}
false
}
fn collision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.collides(x, y))
}
fn killision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.kills(x, y))
}
fn completion_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.wins(x, y))
}
fn enemy_collision_at_point(
&self,
enemies: &[enemies::Enemy],
position: Vector2D<FixedNumberType>,
) -> bool {
for enemy in enemies {
if enemy.collides_with_hat(position) {
return true;
}
}
false
}
// returns the distance actually moved
fn update_position(&mut self, level: &Level) -> Vector2D<FixedNumberType> {
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !self.collision_at_point(level, self.position + x_velocity) {
self.position += x_velocity;
} else {
self.position += self.binary_search_collision(level, (1, 0).into(), self.velocity.x);
}
let y_velocity = (0.into(), self.velocity.y).into();
if !self.collision_at_point(level, self.position + y_velocity) {
self.position += y_velocity;
} else {
self.position += self.binary_search_collision(level, (0, 1).into(), self.velocity.y);
}
self.position - old_position
}
fn update_position_with_enemy(
&mut self,
level: &Level,
enemies: &[enemies::Enemy],
) -> (Vector2D<FixedNumberType>, bool) {
let mut was_enemy_collision = false;
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !(self.collision_at_point(level, self.position + x_velocity)
|| self.enemy_collision_at_point(enemies, self.position + x_velocity))
{
self.position += x_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + x_velocity) {
self.position -= x_velocity;
was_enemy_collision = true;
}
let y_velocity = (0.into(), self.velocity.y).into();
if !(self.collision_at_point(level, self.position + y_velocity)
|| self.enemy_collision_at_point(enemies, self.position + y_velocity))
{
self.position += y_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + y_velocity) {
self.position -= y_velocity;
was_enemy_collision = true;
}
(self.position - old_position, was_enemy_collision)
}
fn binary_search_collision(
&self,
level: &Level,
unit_vector: Vector2D<FixedNumberType>,
initial: FixedNumberType,
) -> Vector2D<FixedNumberType> {
let mut low: FixedNumberType = 0.into();
let mut high = initial;
let one: FixedNumberType = 1.into();
while (high - low).abs() > one / 8 {
let mid = (low + high) / 2;
let new_vel: Vector2D<FixedNumberType> = unit_vector * mid;
if self.collision_at_point(level, self.position + new_vel) {
high = mid;
} else {
low = mid;
}
}
unit_vector * low
}
fn commit_position(&mut self, offset: Vector2D<FixedNumberType>) {
let position = (self.position - offset).floor();
self.sprite.set_position(position - (8, 8).into());
if position.x < -8 || position.x > WIDTH + 8 || position.y < -8 || position.y > HEIGHT + 8 {
self.sprite.hide();
} else {
self.sprite.show();
}
}
}
struct Map<'a, 'b> {
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
position: Vector2D<FixedNumberType>,
level: &'a Level,
}
impl<'a, 'b> Map<'a, 'b> {
pub fn commit_position(&mut self, vram: &mut VRamManager) {
self.background.set_pos(vram, self.position.floor());
self.foreground.set_pos(vram, self.position.floor());
self.background.commit(vram);
self.foreground.commit(vram);
}
pub fn init_background(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.background.init_partial(vram, self.position.floor())
}
pub fn init_foreground(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.foreground.init_partial(vram, self.position.floor())
}
}
impl Level {
fn collides(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::COLLISION_TILE as u32)
}
fn kills(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::KILL_TILE as u32)
}
fn at_point(&self, x: i32, y: i32, tile: u32) -> bool {
if (x < 0 || x >= self.dimensions.x as i32) || (y < 0 || y >= self.dimensions.y as i32) {
return true;
}
let pos = (self.dimensions.x as i32 * y + x) as usize;
let tile_foreground = self.foreground[pos];
let tile_background = self.background[pos];
let foreground_tile_property = self.collision[tile_foreground as usize];
let background_tile_property = self.collision[tile_background as usize];
foreground_tile_property == tile || background_tile_property == tile
}
fn wins(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::WIN_TILE as u32)
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum HatState {
OnHead,
Thrown,
WizardTowards,
}
struct Player<'a> {
wizard: Entity<'a>,
hat: Entity<'a>,
hat_state: HatState,
hat_left_range: bool,
hat_slow_counter: i32,
wizard_frame: u8,
num_recalls: i8,
is_on_ground: bool,
facing: input::Tri,
}
fn ping_pong(i: i32, n: i32) -> i32 {
let cycle = 2 * (n - 1);
let i = i % cycle;
if i >= n {
cycle - i
} else {
i
}
}
impl<'a> Player<'a> {
fn new(controller: &'a ObjectController, start_position: Vector2D<FixedNumberType>) -> Self {
let mut wizard = Entity::new(controller, (6_u16, 14_u16).into());
let mut hat = Entity::new(controller, (6_u16, 6_u16).into());
wizard
.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
hat.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
wizard.sprite.show();
hat.sprite.show();
hat.sprite.set_z(-1);
wizard.position = start_position;
hat.position = start_position - (0, 10).into();
Player {
wizard,
hat,
hat_slow_counter: 0,
hat_state: HatState::OnHead,
hat_left_range: false,
wizard_frame: 0,
num_recalls: 0,
is_on_ground: true,
facing: input::Tri::Zero,
}
}
fn update_frame(
&mut self,
input: &ButtonController,
controller: &'a ObjectController,
timer: i32,
level: &Level,
enemies: &[enemies::Enemy],
sfx_player: &mut sfx::SfxPlayer,
) {
// throw or recall
if input.is_just_pressed(Button::A) {
if self.hat_state == HatState::OnHead {
let direction: Vector2D<FixedNumberType> = {
let up_down = input.y_tri() as i32;
let left_right = if up_down == 0 {
self.facing as i32
} else {
input.x_tri() as i32
};
(left_right, up_down).into()
};
if direction != (0, 0).into() {
let mut velocity = direction.normalise() * 5;
if velocity.y > 0.into() {
velocity.y *= FixedNumberType::new(4) / 3;
}
self.hat.velocity = velocity;
self.hat_state = HatState::Thrown;
sfx_player.throw();
}
} else if self.hat_state == HatState::Thrown {
self.num_recalls += 1;
if self.num_recalls < 3 {
self.hat.velocity = (0, 0).into();
self.wizard.velocity = (0, 0).into();
self.hat_state = HatState::WizardTowards;
}
} else if self.hat_state == HatState::WizardTowards {
self.hat_state = HatState::Thrown;
self.wizard.velocity /= 8;
}
}
let was_on_ground = self.is_on_ground;
let is_on_ground = self
.wizard
.collision_at_point(level, self.wizard.position + (0, 1).into());
if is_on_ground && !was_on_ground && self.wizard.velocity.y > 1.into() {
sfx_player.land();
}
self.is_on_ground = is_on_ground;
if self.hat_state != HatState::WizardTowards {
if is_on_ground {
self.num_recalls = 0;
}
if is_on_ground {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 16;
self.wizard.velocity = self.wizard.velocity * 54 / 64;
if input.is_just_pressed(Button::B) {
self.wizard.velocity.y = -FixedNumberType::new(3) / 2;
sfx_player.jump();
}
} else {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 64;
self.wizard.velocity = self.wizard.velocity * 63 / 64;
let gravity: Vector2D<FixedNumberType> = (0, 1).into();
let gravity = gravity / 16;
self.wizard.velocity += gravity;
}
self.wizard.velocity = self.wizard.update_position(level);
if self.wizard.velocity.x.abs() > 0.into() {
let offset = (ping_pong(timer / 16, 4)) as usize;
self.wizard_frame = offset as u8;
let frame = WALKING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if self.wizard.velocity.y < -FixedNumberType::new(1) / 16 {
// going up
self.wizard_frame = 5;
let frame = JUMPING.animation_sprite(0);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
} else if self.wizard.velocity.y > FixedNumberType::new(1) / 16 {
// going down
let offset = if self.wizard.velocity.y * 2 > 3.into() {
(timer / 4) as usize
} else {
// Don't flap beard unless going quickly
0
};
self.wizard_frame = 0;
let frame = FALLING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if input.x_tri() != agb::input::Tri::Zero {
self.facing = input.x_tri();
}
}
let hat_base_tile = match self.num_recalls {
0 => HAT_SPIN_1,
1 => HAT_SPIN_2,
_ => HAT_SPIN_3,
};
let hat_resting_position = match self.wizard_frame {
1 | 2 => (0, 9).into(),
5 => (0, 10).into(),
_ => (0, 8).into(),
};
match self.facing {
agb::input::Tri::Negative => {
self.wizard.sprite.set_hflip(true);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(5)));
}
agb::input::Tri::Positive => {
self.wizard.sprite.set_hflip(false);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(0)));
}
_ => {}
}
match self.hat_state {
HatState::Thrown => {
// hat is thrown, make hat move towards wizard
let distance_vector =
self.wizard.position - self.hat.position - hat_resting_position;
let distance = distance_vector.magnitude();
let direction = if distance == 0.into() {
(0, 0).into()
} else {
distance_vector / distance
};
let hat_sprite_divider = match self.num_recalls {
0 => 1,
1 => 2,
_ => 4,
};
let hat_sprite_offset = (timer / hat_sprite_divider) as usize;
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(hat_sprite_offset)),
);
if self.hat_slow_counter < 30 && self.hat.velocity.magnitude() < 2.into() {
self.hat.velocity = (0, 0).into();
self.hat_slow_counter += 1;
} else {
self.hat.velocity += direction / 4;
}
let (new_velocity, enemy_collision) =
self.hat.update_position_with_enemy(level, enemies);
self.hat.velocity = new_velocity;
if enemy_collision {
sfx_player.snail_hat_bounce();
}
if distance > 16.into() {
self.hat_left_range = true;
}
if self.hat_left_range && distance < 16.into() {
sfx_player.catch();
self.hat_state = HatState::OnHead;
}
}
HatState::OnHead => {
// hat is on head, place hat on head
self.hat_slow_counter = 0;
self.hat_left_range = false;
self.hat.position = self.wizard.position - hat_resting_position;
}
HatState::WizardTowards => {
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(timer as usize / 2)),
);
let distance_vector =
self.hat.position - self.wizard.position + hat_resting_position;
let distance = distance_vector.magnitude();
if distance != 0.into() {
let v = self.wizard.velocity.magnitude() + 1;
self.wizard.velocity = distance_vector / distance * v;
}
self.wizard.velocity = self.wizard.update_position(level);
if distance < 16.into() {
self.wizard.velocity /= 8;
self.hat_state = HatState::OnHead;
sfx_player.catch();
}
}
}
}
}
struct PlayingLevel<'a, 'b> {
timer: i32,
background: Map<'a, 'b>,
input: ButtonController,
player: Player<'a>,
enemies: [enemies::Enemy<'a>; 16],
}
enum UpdateState {
Normal,
Dead,
Complete,
}
impl<'a, 'b> PlayingLevel<'a, 'b> {
fn open_level(
level: &'a Level,
object_control: &'a ObjectController,
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
input: ButtonController,
) -> Self {
let mut e: [enemies::Enemy<'a>; 16] = Default::default();
let mut enemy_count = 0;
for &slime in level.slimes {
e[enemy_count] = enemies::Enemy::new_slime(object_control, slime.into());
enemy_count += 1;
}
for &snail in level.snails {
e[enemy_count] = enemies::Enemy::new_snail(object_control, snail.into());
enemy_count += 1;
}
let start_pos: Vector2D<FixedNumberType> = level.start_pos.into();
let background_position = (
(start_pos.x - WIDTH / 2)
.clamp(0.into(), ((level.dimensions.x * 8) as i32 - WIDTH).into()),
(start_pos.y - HEIGHT / 2)
.clamp(0.into(), ((level.dimensions.y * 8) as i32 - HEIGHT).into()),
)
.into();
PlayingLevel {
timer: 0,
background: Map {
background,
foreground,
level,
position: background_position,
},
player: Player::new(object_control, start_pos),
input,
enemies: e,
}
}
fn show_backgrounds(&mut self) {
self.background.background.show();
self.background.foreground.show();
}
fn hide_backgrounds(&mut self) {
self.background.background.hide();
self.background.foreground.hide();
}
fn clear_backgrounds(&mut self, vram: &mut VRamManager) {
self.background.background.clear(vram);
self.background.foreground.clear(vram);
}
fn dead_start(&mut self) {
self.player.wizard.velocity = (0, -1).into();
self.player.wizard.sprite.set_priority(Priority::P0);
}
fn dead_update(&mut self, controller: &'a ObjectController) -> bool {
self.timer += 1;
let frame = PLAYER_DEATH.animation_sprite(self.timer as usize / 8);
let sprite = controller.sprite(frame);
self.player.wizard.velocity += (0.into(), FixedNumberType::new(1) / 32).into();
self.player.wizard.position += self.player.wizard.velocity;
self.player.wizard.sprite.set_sprite(sprite);
self.player.wizard.commit_position(self.background.position);
self.player.wizard.position.y - self.background.position.y < (HEIGHT + 8).into()
}
fn update_frame(
&mut self,
sfx_player: &mut sfx::SfxPlayer,
vram: &mut VRamManager,
controller: &'a ObjectController,
) -> UpdateState {
self.timer += 1;
self.input.update();
let mut player_dead = false;
self.player.update_frame(
&self.input,
controller,
self.timer,
self.background.level,
&self.enemies,
sfx_player,
);
for enemy in self.enemies.iter_mut() {
match enemy.update(
controller,
self.background.level,
self.player.wizard.position,
self.player.hat_state,
self.timer,
sfx_player,
) {
enemies::EnemyUpdateState::KillPlayer => player_dead = true,
enemies::EnemyUpdateState::None => {}
}
}
self.background.position = self.get_next_map_position();
self.background.commit_position(vram);
self.player.wizard.commit_position(self.background.position);
self.player.hat.commit_position(self.background.position);
for enemy in self.enemies.iter_mut() {
enemy.commit(self.background.position);
}
player_dead |= self
.player
.wizard
.killision_at_point(self.background.level, self.player.wizard.position);
if player_dead {
UpdateState::Dead
} else if self
.player
.wizard
.completion_at_point(self.background.level, self.player.wizard.position)
{
UpdateState::Complete
} else {
UpdateState::Normal
}
}
fn get_next_map_position(&self) -> Vector2D<FixedNumberType> {
// want to ensure the player and the hat are visible if possible, so try to position the map
// so the centre is at the average position. But give the player some extra priority
let hat_pos = self.player.hat.position.floor();
let player_pos = self.player.wizard.position.floor();
let new_target_position = (hat_pos + player_pos * 3) / 4;
let screen: Vector2D<i32> = (WIDTH, HEIGHT).into();
let half_screen = screen / 2;
let current_centre = self.background.position.floor() + half_screen;
let mut target_position = ((current_centre * 3 + new_target_position) / 4) - half_screen;
target_position.x = target_position.x.clamp(
0,
(self.background.level.dimensions.x * 8 - (WIDTH as u32)) as i32,
);
target_position.y = target_position.y.clamp(
0,
(self.background.level.dimensions.y * 8 - (HEIGHT as u32)) as i32,
);
target_position.into()
}
}
pub fn main(mut agb: agb::Gba) -> ! {
let (tiled, mut vram) = agb.display.video.tiled0();
vram.set_background_palettes(tile_sheet::PALETTES);
let mut splash_screen = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let mut world_display = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let tileset = TileSet::new(tile_sheet::background.tiles, TileFormat::FourBpp);
for y in 0..32u16 {
for x in 0..32u16 {
world_display.set_tile(
&mut vram,
(x, y).into(),
&tileset,
TileSetting::from_raw(level_display::BLANK),
);
}
}
world_display.commit(&mut vram);
world_display.show();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::Start,
None,
None,
&mut splash_screen,
&mut vram,
);
loop {
world_display.commit(&mut vram);
world_display.show();
vram.set_background_palettes(tile_sheet::PALETTES);
let object = agb.display.object.get();
let mut mixer = agb.mixer.mixer(Frequency::Hz10512);
mixer.enable();
let mut music_box = sfx::MusicBox::new();
let vblank = agb::interrupt::VBlank::get();
let mut current_level = 0;
loop {
if current_level == map_tiles::LEVELS.len() as u32 {
break;
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
level_display::write_level(
&mut world_display,
current_level / 8 + 1,
current_level % 8 + 1,
&tileset,
&mut vram,
);
world_display.commit(&mut vram);
world_display.show();
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
let map_current_level = current_level;
let mut background = InfiniteScrolledMap::new(
tiled.background(Priority::P2, RegularBackgroundSize::Background32x64),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.background
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut foreground = InfiniteScrolledMap::new(
tiled.background(Priority::P0, RegularBackgroundSize::Background64x32),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.foreground
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut level = PlayingLevel::open_level(
&map_tiles::LEVELS[current_level as usize],
&object,
&mut background,
&mut foreground,
agb::input::ButtonController::new(),
);
while level.background.init_background(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
while level.background.init_foreground(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
for _ in 0..20 {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
object.commit();
level.show_backgrounds();
world_display.hide();
loop {
match level.update_frame(
&mut sfx::SfxPlayer::new(&mut mixer, &music_box),
&mut vram,
&object,
) {
UpdateState::Normal => {}
UpdateState::Dead => {
level.dead_start();
while level.dead_update(&object) {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
object.commit();
}
break;
}
UpdateState::Complete => {
current_level += 1;
break;
}
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
object.commit();
}
level.hide_backgrounds();
level.clear_backgrounds(&mut vram);
}
object.commit();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::End,
Some(&mut mixer),
Some(&mut music_box),
&mut splash_screen,
&mut vram,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use agb::Gba;
#[test_case]
fn test_ping_pong(_gba: &mut Gba) {
let test_cases = [
[0, 2, 0],
[0, 7, 0],
[1, 2, 1],
[2, 2, 0],
[3, 2, 1],
[4, 2, 0],
];
for test_case in test_cases {
assert_eq!(
ping_pong(test_case[0], test_case[1]),
test_case[2],
"Expected ping_pong({}, {}) to equal {}",
test_case[0],
test_case[1],
test_case[2],
);
}
}
}

View file

@ -4,996 +4,7 @@
#![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
extern crate alloc;
use agb::{
display::{
object::{Graphics, Object, ObjectController, Tag, TagMap},
tiled::{
InfiniteScrolledMap, PartialUpdateStatus, RegularBackgroundSize, TileFormat, TileSet,
TileSetting, TiledMap, VRamManager,
},
Priority, HEIGHT, WIDTH,
},
fixnum::{FixedNum, Vector2D},
input::{self, Button, ButtonController},
sound::mixer::Frequency,
};
use alloc::boxed::Box;
mod enemies;
mod level_display;
mod sfx;
mod splash_screen;
pub struct Level {
background: &'static [u16],
foreground: &'static [u16],
dimensions: Vector2D<u32>,
collision: &'static [u32],
slimes: &'static [(i32, i32)],
snails: &'static [(i32, i32)],
enemy_stops: &'static [(i32, i32)],
start_pos: (i32, i32),
}
mod map_tiles {
use super::Level;
pub const LEVELS: &[Level] = &[
l1_1::get_level(),
l1_2::get_level(),
l1_3::get_level(),
l1_4::get_level(),
l1_5::get_level(),
l1_7::get_level(), // these are intentionally this way round
l1_6::get_level(),
l1_8::get_level(),
l2_3::get_level(), // goes 2-3, 2-1 then 2-2
l2_1::get_level(),
l2_2::get_level(),
l2_4::get_level(),
];
pub mod l1_1 {
include!(concat!(env!("OUT_DIR"), "/1-1.json.rs"));
}
pub mod l1_2 {
include!(concat!(env!("OUT_DIR"), "/1-2.json.rs"));
}
pub mod l1_3 {
include!(concat!(env!("OUT_DIR"), "/1-3.json.rs"));
}
pub mod l1_4 {
include!(concat!(env!("OUT_DIR"), "/1-4.json.rs"));
}
pub mod l1_5 {
include!(concat!(env!("OUT_DIR"), "/1-5.json.rs"));
}
pub mod l1_6 {
include!(concat!(env!("OUT_DIR"), "/1-6.json.rs"));
}
pub mod l1_7 {
include!(concat!(env!("OUT_DIR"), "/1-7.json.rs"));
}
pub mod l2_1 {
include!(concat!(env!("OUT_DIR"), "/2-1.json.rs"));
}
pub mod l1_8 {
include!(concat!(env!("OUT_DIR"), "/1-8.json.rs"));
}
pub mod l2_2 {
include!(concat!(env!("OUT_DIR"), "/2-2.json.rs"));
}
pub mod l2_3 {
include!(concat!(env!("OUT_DIR"), "/2-3.json.rs"));
}
pub mod l2_4 {
include!(concat!(env!("OUT_DIR"), "/2-4.json.rs"));
}
pub mod tilemap {
include!(concat!(env!("OUT_DIR"), "/tilemap.rs"));
}
}
agb::include_gfx!("gfx/tile_sheet.toml");
const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite");
const TAG_MAP: &TagMap = GRAPHICS.tags();
const WALKING: &Tag = TAG_MAP.get("Walking");
const JUMPING: &Tag = TAG_MAP.get("Jumping");
const FALLING: &Tag = TAG_MAP.get("Falling");
const PLAYER_DEATH: &Tag = TAG_MAP.get("Player Death");
const HAT_SPIN_1: &Tag = TAG_MAP.get("HatSpin");
const HAT_SPIN_2: &Tag = TAG_MAP.get("HatSpin2");
const HAT_SPIN_3: &Tag = TAG_MAP.get("HatSpin3");
type FixedNumberType = FixedNum<10>;
pub struct Entity<'a> {
sprite: Object<'a>,
position: Vector2D<FixedNumberType>,
velocity: Vector2D<FixedNumberType>,
collision_mask: Vector2D<u16>,
}
impl<'a> Entity<'a> {
pub fn new(object: &'a ObjectController, collision_mask: Vector2D<u16>) -> Self {
let dummy_sprite = object.sprite(WALKING.sprite(0));
let mut sprite = object.object(dummy_sprite);
sprite.set_priority(Priority::P1);
Entity {
sprite,
collision_mask,
position: (0, 0).into(),
velocity: (0, 0).into(),
}
}
fn something_at_point<T: Fn(i32, i32) -> bool>(
&self,
position: Vector2D<FixedNumberType>,
something_fn: T,
) -> bool {
let left = (position.x - self.collision_mask.x as i32 / 2).floor() / 8;
let right = (position.x + self.collision_mask.x as i32 / 2 - 1).floor() / 8;
let top = (position.y - self.collision_mask.y as i32 / 2).floor() / 8;
let bottom = (position.y + self.collision_mask.y as i32 / 2 - 1).floor() / 8;
for x in left..=right {
for y in top..=bottom {
if something_fn(x, y) {
return true;
}
}
}
false
}
fn collision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.collides(x, y))
}
fn killision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.kills(x, y))
}
fn completion_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.wins(x, y))
}
fn enemy_collision_at_point(
&self,
enemies: &[enemies::Enemy],
position: Vector2D<FixedNumberType>,
) -> bool {
for enemy in enemies {
if enemy.collides_with_hat(position) {
return true;
}
}
false
}
// returns the distance actually moved
fn update_position(&mut self, level: &Level) -> Vector2D<FixedNumberType> {
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !self.collision_at_point(level, self.position + x_velocity) {
self.position += x_velocity;
} else {
self.position += self.binary_search_collision(level, (1, 0).into(), self.velocity.x);
}
let y_velocity = (0.into(), self.velocity.y).into();
if !self.collision_at_point(level, self.position + y_velocity) {
self.position += y_velocity;
} else {
self.position += self.binary_search_collision(level, (0, 1).into(), self.velocity.y);
}
self.position - old_position
}
fn update_position_with_enemy(
&mut self,
level: &Level,
enemies: &[enemies::Enemy],
) -> (Vector2D<FixedNumberType>, bool) {
let mut was_enemy_collision = false;
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !(self.collision_at_point(level, self.position + x_velocity)
|| self.enemy_collision_at_point(enemies, self.position + x_velocity))
{
self.position += x_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + x_velocity) {
self.position -= x_velocity;
was_enemy_collision = true;
}
let y_velocity = (0.into(), self.velocity.y).into();
if !(self.collision_at_point(level, self.position + y_velocity)
|| self.enemy_collision_at_point(enemies, self.position + y_velocity))
{
self.position += y_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + y_velocity) {
self.position -= y_velocity;
was_enemy_collision = true;
}
(self.position - old_position, was_enemy_collision)
}
fn binary_search_collision(
&self,
level: &Level,
unit_vector: Vector2D<FixedNumberType>,
initial: FixedNumberType,
) -> Vector2D<FixedNumberType> {
let mut low: FixedNumberType = 0.into();
let mut high = initial;
let one: FixedNumberType = 1.into();
while (high - low).abs() > one / 8 {
let mid = (low + high) / 2;
let new_vel: Vector2D<FixedNumberType> = unit_vector * mid;
if self.collision_at_point(level, self.position + new_vel) {
high = mid;
} else {
low = mid;
}
}
unit_vector * low
}
fn commit_position(&mut self, offset: Vector2D<FixedNumberType>) {
let position = (self.position - offset).floor();
self.sprite.set_position(position - (8, 8).into());
if position.x < -8 || position.x > WIDTH + 8 || position.y < -8 || position.y > HEIGHT + 8 {
self.sprite.hide();
} else {
self.sprite.show();
}
}
}
struct Map<'a, 'b> {
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
position: Vector2D<FixedNumberType>,
level: &'a Level,
}
impl<'a, 'b> Map<'a, 'b> {
pub fn commit_position(&mut self, vram: &mut VRamManager) {
self.background.set_pos(vram, self.position.floor());
self.foreground.set_pos(vram, self.position.floor());
self.background.commit(vram);
self.foreground.commit(vram);
}
pub fn init_background(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.background.init_partial(vram, self.position.floor())
}
pub fn init_foreground(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.foreground.init_partial(vram, self.position.floor())
}
}
impl Level {
fn collides(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::COLLISION_TILE as u32)
}
fn kills(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::KILL_TILE as u32)
}
fn at_point(&self, x: i32, y: i32, tile: u32) -> bool {
if (x < 0 || x >= self.dimensions.x as i32) || (y < 0 || y >= self.dimensions.y as i32) {
return true;
}
let pos = (self.dimensions.x as i32 * y + x) as usize;
let tile_foreground = self.foreground[pos];
let tile_background = self.background[pos];
let foreground_tile_property = self.collision[tile_foreground as usize];
let background_tile_property = self.collision[tile_background as usize];
foreground_tile_property == tile || background_tile_property == tile
}
fn wins(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::WIN_TILE as u32)
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum HatState {
OnHead,
Thrown,
WizardTowards,
}
struct Player<'a> {
wizard: Entity<'a>,
hat: Entity<'a>,
hat_state: HatState,
hat_left_range: bool,
hat_slow_counter: i32,
wizard_frame: u8,
num_recalls: i8,
is_on_ground: bool,
facing: input::Tri,
}
fn ping_pong(i: i32, n: i32) -> i32 {
let cycle = 2 * (n - 1);
let i = i % cycle;
if i >= n {
cycle - i
} else {
i
}
}
impl<'a> Player<'a> {
fn new(controller: &'a ObjectController, start_position: Vector2D<FixedNumberType>) -> Self {
let mut wizard = Entity::new(controller, (6_u16, 14_u16).into());
let mut hat = Entity::new(controller, (6_u16, 6_u16).into());
wizard
.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
hat.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
wizard.sprite.show();
hat.sprite.show();
hat.sprite.set_z(-1);
wizard.position = start_position;
hat.position = start_position - (0, 10).into();
Player {
wizard,
hat,
hat_slow_counter: 0,
hat_state: HatState::OnHead,
hat_left_range: false,
wizard_frame: 0,
num_recalls: 0,
is_on_ground: true,
facing: input::Tri::Zero,
}
}
fn update_frame(
&mut self,
input: &ButtonController,
controller: &'a ObjectController,
timer: i32,
level: &Level,
enemies: &[enemies::Enemy],
sfx_player: &mut sfx::SfxPlayer,
) {
// throw or recall
if input.is_just_pressed(Button::A) {
if self.hat_state == HatState::OnHead {
let direction: Vector2D<FixedNumberType> = {
let up_down = input.y_tri() as i32;
let left_right = if up_down == 0 {
self.facing as i32
} else {
input.x_tri() as i32
};
(left_right, up_down).into()
};
if direction != (0, 0).into() {
let mut velocity = direction.normalise() * 5;
if velocity.y > 0.into() {
velocity.y *= FixedNumberType::new(4) / 3;
}
self.hat.velocity = velocity;
self.hat_state = HatState::Thrown;
sfx_player.throw();
}
} else if self.hat_state == HatState::Thrown {
self.num_recalls += 1;
if self.num_recalls < 3 {
self.hat.velocity = (0, 0).into();
self.wizard.velocity = (0, 0).into();
self.hat_state = HatState::WizardTowards;
}
} else if self.hat_state == HatState::WizardTowards {
self.hat_state = HatState::Thrown;
self.wizard.velocity /= 8;
}
}
let was_on_ground = self.is_on_ground;
let is_on_ground = self
.wizard
.collision_at_point(level, self.wizard.position + (0, 1).into());
if is_on_ground && !was_on_ground && self.wizard.velocity.y > 1.into() {
sfx_player.land();
}
self.is_on_ground = is_on_ground;
if self.hat_state != HatState::WizardTowards {
if is_on_ground {
self.num_recalls = 0;
}
if is_on_ground {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 16;
self.wizard.velocity = self.wizard.velocity * 54 / 64;
if input.is_just_pressed(Button::B) {
self.wizard.velocity.y = -FixedNumberType::new(3) / 2;
sfx_player.jump();
}
} else {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 64;
self.wizard.velocity = self.wizard.velocity * 63 / 64;
let gravity: Vector2D<FixedNumberType> = (0, 1).into();
let gravity = gravity / 16;
self.wizard.velocity += gravity;
}
self.wizard.velocity = self.wizard.update_position(level);
if self.wizard.velocity.x.abs() > 0.into() {
let offset = (ping_pong(timer / 16, 4)) as usize;
self.wizard_frame = offset as u8;
let frame = WALKING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if self.wizard.velocity.y < -FixedNumberType::new(1) / 16 {
// going up
self.wizard_frame = 5;
let frame = JUMPING.animation_sprite(0);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
} else if self.wizard.velocity.y > FixedNumberType::new(1) / 16 {
// going down
let offset = if self.wizard.velocity.y * 2 > 3.into() {
(timer / 4) as usize
} else {
// Don't flap beard unless going quickly
0
};
self.wizard_frame = 0;
let frame = FALLING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if input.x_tri() != agb::input::Tri::Zero {
self.facing = input.x_tri();
}
}
let hat_base_tile = match self.num_recalls {
0 => HAT_SPIN_1,
1 => HAT_SPIN_2,
_ => HAT_SPIN_3,
};
let hat_resting_position = match self.wizard_frame {
1 | 2 => (0, 9).into(),
5 => (0, 10).into(),
_ => (0, 8).into(),
};
match self.facing {
agb::input::Tri::Negative => {
self.wizard.sprite.set_hflip(true);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(5)));
}
agb::input::Tri::Positive => {
self.wizard.sprite.set_hflip(false);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(0)));
}
_ => {}
}
match self.hat_state {
HatState::Thrown => {
// hat is thrown, make hat move towards wizard
let distance_vector =
self.wizard.position - self.hat.position - hat_resting_position;
let distance = distance_vector.magnitude();
let direction = if distance == 0.into() {
(0, 0).into()
} else {
distance_vector / distance
};
let hat_sprite_divider = match self.num_recalls {
0 => 1,
1 => 2,
_ => 4,
};
let hat_sprite_offset = (timer / hat_sprite_divider) as usize;
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(hat_sprite_offset)),
);
if self.hat_slow_counter < 30 && self.hat.velocity.magnitude() < 2.into() {
self.hat.velocity = (0, 0).into();
self.hat_slow_counter += 1;
} else {
self.hat.velocity += direction / 4;
}
let (new_velocity, enemy_collision) =
self.hat.update_position_with_enemy(level, enemies);
self.hat.velocity = new_velocity;
if enemy_collision {
sfx_player.snail_hat_bounce();
}
if distance > 16.into() {
self.hat_left_range = true;
}
if self.hat_left_range && distance < 16.into() {
sfx_player.catch();
self.hat_state = HatState::OnHead;
}
}
HatState::OnHead => {
// hat is on head, place hat on head
self.hat_slow_counter = 0;
self.hat_left_range = false;
self.hat.position = self.wizard.position - hat_resting_position;
}
HatState::WizardTowards => {
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(timer as usize / 2)),
);
let distance_vector =
self.hat.position - self.wizard.position + hat_resting_position;
let distance = distance_vector.magnitude();
if distance != 0.into() {
let v = self.wizard.velocity.magnitude() + 1;
self.wizard.velocity = distance_vector / distance * v;
}
self.wizard.velocity = self.wizard.update_position(level);
if distance < 16.into() {
self.wizard.velocity /= 8;
self.hat_state = HatState::OnHead;
sfx_player.catch();
}
}
}
}
}
struct PlayingLevel<'a, 'b> {
timer: i32,
background: Map<'a, 'b>,
input: ButtonController,
player: Player<'a>,
enemies: [enemies::Enemy<'a>; 16],
}
enum UpdateState {
Normal,
Dead,
Complete,
}
impl<'a, 'b> PlayingLevel<'a, 'b> {
fn open_level(
level: &'a Level,
object_control: &'a ObjectController,
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
input: ButtonController,
) -> Self {
let mut e: [enemies::Enemy<'a>; 16] = Default::default();
let mut enemy_count = 0;
for &slime in level.slimes {
e[enemy_count] = enemies::Enemy::new_slime(object_control, slime.into());
enemy_count += 1;
}
for &snail in level.snails {
e[enemy_count] = enemies::Enemy::new_snail(object_control, snail.into());
enemy_count += 1;
}
let start_pos: Vector2D<FixedNumberType> = level.start_pos.into();
let background_position = (
(start_pos.x - WIDTH / 2)
.clamp(0.into(), ((level.dimensions.x * 8) as i32 - WIDTH).into()),
(start_pos.y - HEIGHT / 2)
.clamp(0.into(), ((level.dimensions.y * 8) as i32 - HEIGHT).into()),
)
.into();
PlayingLevel {
timer: 0,
background: Map {
background,
foreground,
level,
position: background_position,
},
player: Player::new(object_control, start_pos),
input,
enemies: e,
}
}
fn show_backgrounds(&mut self) {
self.background.background.show();
self.background.foreground.show();
}
fn hide_backgrounds(&mut self) {
self.background.background.hide();
self.background.foreground.hide();
}
fn clear_backgrounds(&mut self, vram: &mut VRamManager) {
self.background.background.clear(vram);
self.background.foreground.clear(vram);
}
fn dead_start(&mut self) {
self.player.wizard.velocity = (0, -1).into();
self.player.wizard.sprite.set_priority(Priority::P0);
}
fn dead_update(&mut self, controller: &'a ObjectController) -> bool {
self.timer += 1;
let frame = PLAYER_DEATH.animation_sprite(self.timer as usize / 8);
let sprite = controller.sprite(frame);
self.player.wizard.velocity += (0.into(), FixedNumberType::new(1) / 32).into();
self.player.wizard.position += self.player.wizard.velocity;
self.player.wizard.sprite.set_sprite(sprite);
self.player.wizard.commit_position(self.background.position);
self.player.wizard.position.y - self.background.position.y < (HEIGHT + 8).into()
}
fn update_frame(
&mut self,
sfx_player: &mut sfx::SfxPlayer,
vram: &mut VRamManager,
controller: &'a ObjectController,
) -> UpdateState {
self.timer += 1;
self.input.update();
let mut player_dead = false;
self.player.update_frame(
&self.input,
controller,
self.timer,
self.background.level,
&self.enemies,
sfx_player,
);
for enemy in self.enemies.iter_mut() {
match enemy.update(
controller,
self.background.level,
self.player.wizard.position,
self.player.hat_state,
self.timer,
sfx_player,
) {
enemies::EnemyUpdateState::KillPlayer => player_dead = true,
enemies::EnemyUpdateState::None => {}
}
}
self.background.position = self.get_next_map_position();
self.background.commit_position(vram);
self.player.wizard.commit_position(self.background.position);
self.player.hat.commit_position(self.background.position);
for enemy in self.enemies.iter_mut() {
enemy.commit(self.background.position);
}
player_dead |= self
.player
.wizard
.killision_at_point(self.background.level, self.player.wizard.position);
if player_dead {
UpdateState::Dead
} else if self
.player
.wizard
.completion_at_point(self.background.level, self.player.wizard.position)
{
UpdateState::Complete
} else {
UpdateState::Normal
}
}
fn get_next_map_position(&self) -> Vector2D<FixedNumberType> {
// want to ensure the player and the hat are visible if possible, so try to position the map
// so the centre is at the average position. But give the player some extra priority
let hat_pos = self.player.hat.position.floor();
let player_pos = self.player.wizard.position.floor();
let new_target_position = (hat_pos + player_pos * 3) / 4;
let screen: Vector2D<i32> = (WIDTH, HEIGHT).into();
let half_screen = screen / 2;
let current_centre = self.background.position.floor() + half_screen;
let mut target_position = ((current_centre * 3 + new_target_position) / 4) - half_screen;
target_position.x = target_position.x.clamp(
0,
(self.background.level.dimensions.x * 8 - (WIDTH as u32)) as i32,
);
target_position.y = target_position.y.clamp(
0,
(self.background.level.dimensions.y * 8 - (HEIGHT as u32)) as i32,
);
target_position.into()
}
}
#[agb::entry] #[agb::entry]
fn agb_main(mut gba: agb::Gba) -> ! { fn entry(mut gba: agb::Gba) -> ! {
main(gba); the_hat_chooses_the_wizard::main(gba);
}
pub fn main(mut agb: agb::Gba) -> ! {
let (tiled, mut vram) = agb.display.video.tiled0();
vram.set_background_palettes(tile_sheet::PALETTES);
let mut splash_screen = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let mut world_display = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let tileset = TileSet::new(tile_sheet::background.tiles, TileFormat::FourBpp);
for y in 0..32u16 {
for x in 0..32u16 {
world_display.set_tile(
&mut vram,
(x, y).into(),
&tileset,
TileSetting::from_raw(level_display::BLANK),
);
}
}
world_display.commit(&mut vram);
world_display.show();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::Start,
None,
None,
&mut splash_screen,
&mut vram,
);
loop {
world_display.commit(&mut vram);
world_display.show();
vram.set_background_palettes(tile_sheet::PALETTES);
let object = agb.display.object.get();
let mut mixer = agb.mixer.mixer(Frequency::Hz10512);
mixer.enable();
let mut music_box = sfx::MusicBox::new();
let vblank = agb::interrupt::VBlank::get();
let mut current_level = 0;
loop {
if current_level == map_tiles::LEVELS.len() as u32 {
break;
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
level_display::write_level(
&mut world_display,
current_level / 8 + 1,
current_level % 8 + 1,
&tileset,
&mut vram,
);
world_display.commit(&mut vram);
world_display.show();
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
let map_current_level = current_level;
let mut background = InfiniteScrolledMap::new(
tiled.background(Priority::P2, RegularBackgroundSize::Background32x64),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.background
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut foreground = InfiniteScrolledMap::new(
tiled.background(Priority::P0, RegularBackgroundSize::Background64x32),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.foreground
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut level = PlayingLevel::open_level(
&map_tiles::LEVELS[current_level as usize],
&object,
&mut background,
&mut foreground,
agb::input::ButtonController::new(),
);
while level.background.init_background(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
while level.background.init_foreground(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
for _ in 0..20 {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
}
object.commit();
level.show_backgrounds();
world_display.hide();
loop {
match level.update_frame(
&mut sfx::SfxPlayer::new(&mut mixer, &music_box),
&mut vram,
&object,
) {
UpdateState::Normal => {}
UpdateState::Dead => {
level.dead_start();
while level.dead_update(&object) {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
object.commit();
}
break;
}
UpdateState::Complete => {
current_level += 1;
break;
}
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
mixer.after_vblank();
object.commit();
}
level.hide_backgrounds();
level.clear_backgrounds(&mut vram);
}
object.commit();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::End,
Some(&mut mixer),
Some(&mut music_box),
&mut splash_screen,
&mut vram,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use agb::Gba;
#[test_case]
fn test_ping_pong(_gba: &mut Gba) {
let test_cases = [
[0, 2, 0],
[0, 7, 0],
[1, 2, 1],
[2, 2, 0],
[3, 2, 1],
[4, 2, 0],
];
for test_case in test_cases {
assert_eq!(
ping_pong(test_case[0], test_case[1]),
test_case[2],
"Expected ping_pong({}, {}) to equal {}",
test_case[0],
test_case[1],
test_case[2],
);
}
}
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff